Merge claudedo/1f4f59b14e4d481d97e843b6b2014af5

This commit is contained in:
mika kuns
2026-08-06 13:48:37 +02:00
11 changed files with 1084 additions and 28 deletions
@@ -104,12 +104,30 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
// The model itself is deliberately NOT forced here — the user can still switch it in the TUI.
var effort = EffortFor(globalSettings, task.Model ?? listConfig?.Model);
// Resume an existing session as-is; for a fresh session, seed the interactive TUI with
// the task's brief (title + description) via a file, never as a positional CLI argument --
// see BuildFreshTaskArgsAsync for why.
var args = run?.SessionId is { Length: > 0 } sessionId
? WithEffort(WindowsTerminalLauncher.BuildResumeArgs(sessionId), effort)
: await BuildFreshTaskArgsAsync(task, effort, ct);
// Resume this task's own last interactive conversation when it has one -- it takes
// precedence over the latest autonomous run's session, since an interactive session is a
// distinct conversation from an autonomous run even against the same worktree. Fall back
// to the autonomous run's session so opening a task interactively for the first time still
// picks up prior context. Neither survives a freshly (re)created worktree (isFreshWorktree
// already forced `run` to null above).
var resumeSessionId = isFreshWorktree ? null : task.InteractiveSessionId ?? run?.SessionId;
// For a fresh session, seed the interactive TUI with the task's brief (title +
// description) via a file, never as a positional CLI argument -- see
// BuildFreshTaskArgsAsync for why. The session id claude will run under is generated and
// persisted HERE, before launch, so a closed/aborted session -- even one that never got
// past startup -- still leaves an id the next open can resume.
IReadOnlyList<string> args;
if (resumeSessionId is { Length: > 0 })
{
args = WithEffort(WindowsTerminalLauncher.BuildResumeArgs(resumeSessionId), effort);
}
else
{
var sessionId = Guid.NewGuid().ToString();
await new TaskRepository(ctx).SetInteractiveSessionIdAsync(taskId, sessionId, ct);
args = await BuildFreshTaskArgsAsync(task, effort, sessionId, ct);
}
// Same run environment variable ClaudeProcess sets for every headless run: the
// AskUser MCP tool call and wait_for_task_change cap at 60s unless raised, and lifting
@@ -445,11 +463,15 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
// Read; --effort (single-value) must sit directly before the positional kickoff so the
// preceding variadic --add-dir doesn't swallow the kickoff as another directory.
// No brief (task has neither a title nor a description) -> no positional arg at all.
private static async Task<IReadOnlyList<string>> BuildFreshTaskArgsAsync(TaskEntity task, string effort, CancellationToken ct)
// `--session-id` pre-assigns the claude session id the caller already persisted (see
// BuildForTaskAsync) so this fresh conversation is resumable from its very first turn --
// it's a single-value flag, so it may sit directly before the positional kickoff.
private static async Task<IReadOnlyList<string>> BuildFreshTaskArgsAsync(
TaskEntity task, string effort, string sessionId, CancellationToken ct)
{
var brief = BuildTaskBrief(task);
if (string.IsNullOrEmpty(brief))
return new[] { "--effort", effort };
return new[] { "--effort", effort, "--session-id", sessionId };
var sessionDir = Path.Combine(Paths.AppDataRoot(), "task-sessions", task.Id);
Directory.CreateDirectory(sessionDir);
@@ -460,6 +482,7 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
{
"--add-dir", sessionDir,
"--effort", effort,
"--session-id", sessionId,
$"Read the file {briefPath} first. It contains the task you must work on. " +
"After reading it, begin the session as your instructions describe.",
};
@@ -17,9 +17,12 @@ public interface IInteractiveLaunchSpecService
/// Throws KeyNotFoundException if the task doesn't exist, InvalidOperationException
/// if it's Running/Queued. If the task has no usable worktree yet, one is created on
/// demand (same mechanism as an autonomous run) provided the task's list has a working
/// directory pointing at a git repo -- otherwise throws InvalidOperationException. A task
/// that has never run, or whose worktree was just created fresh, gets a fresh-start spec
/// (no --resume); an existing worktree with a persisted SessionId gets --resume.</summary>
/// directory pointing at a git repo -- otherwise throws InvalidOperationException. Resumes
/// (--resume) this task's own last interactive session (TaskEntity.InteractiveSessionId) if
/// it has one, else the latest autonomous run's session; a task that has never run either
/// way, or whose worktree was just created fresh, gets a fresh-start spec instead -- pre-
/// assigned a new session id via --session-id and persisted to InteractiveSessionId before
/// launch, so a closed/aborted session can be resumed next time.</summary>
Task<LaunchSpec> BuildForTaskAsync(string taskId, CancellationToken ct);
/// <summary>Builds a LaunchSpec for an ad-hoc interactive session in an arbitrary directory --