@ferris1225/pi-subagents 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/dispatch.ts CHANGED
@@ -1,12 +1,11 @@
1
1
  /**
2
- * The `subagent` tool: dispatches explore/worker/reviewer agents as isolated pi
2
+ * The `subagent` tool: dispatches explore/worker/cleaner/reviewer agents as isolated pi
3
3
  * child processes, single or parallel. Owns the dispatch pipeline: config load,
4
- * per-agent model-pool resolution, per-run status tracking, the auto-fix chain
4
+ * per-agent selected→main model routing, per-run status tracking, the auto-fix chain
5
5
  * (REVIEW_FAIL → worker → re-review), and completion delivery.
6
6
  *
7
- * Vision: a task flagged `vision: true` uses the configured vision model as an
8
- * explicit primary, then the agent's configured backup and the current
9
- * main-window model. Stale refs remain in the pool and fail normally at runtime.
7
+ * Vision: a task flagged `vision: true` uses the configured vision model, then
8
+ * hands directly to the current main-window model on model/provider failure.
10
9
  */
11
10
 
12
11
  import { StringEnum } from "@earendil-works/pi-ai";
@@ -21,7 +20,12 @@ import {
21
20
  completionTriggersTurn,
22
21
  type CompletionMessageItem,
23
22
  } from "./completion.ts";
24
- import { loadConfig, type SubagentsConfig } from "./config.ts";
23
+ import {
24
+ DEFAULT_THINKING_LEVEL,
25
+ loadConfig,
26
+ type SubagentsConfig,
27
+ type ThinkingLevel,
28
+ } from "./config.ts";
25
29
  import {
26
30
  dispatchFailedResult,
27
31
  failedStartResult,
@@ -35,10 +39,16 @@ import {
35
39
  buildReReviewBrief,
36
40
  formatChainSummary,
37
41
  shouldTriggerFixLoop,
38
- summarizeChainResult,
39
42
  type ChainStep,
40
43
  } from "./fixloop.ts";
41
- import { currentModelRef, resolveAgentModelPool } from "./models.ts";
44
+ import {
45
+ availableModelsInScope,
46
+ currentModelRef,
47
+ findModelByRef,
48
+ modelRef,
49
+ resolveAgentModelRoute,
50
+ resolveThinkingLevel,
51
+ } from "./models.ts";
42
52
  import {
43
53
  formatTaskSummary,
44
54
  formatToolActivity,
@@ -50,19 +60,17 @@ import type { SubagentRuntime, SubagentThread, ThreadState } from "./runtime.ts"
50
60
  import { persistRecoveryRecords, recoveryRecordFromFinalization } from "./recovery.ts";
51
61
  import { forkRetainedSession } from "./session-fork.ts";
52
62
  import {
53
- buildFallbackResumeReason,
54
63
  buildResumePrompt,
55
64
  RpcRunControl,
56
65
  getResultOutput,
57
66
  isFailedResult,
58
67
  isModelLevelFailure,
59
68
  reviewVerdict,
60
- runSingleAgentWithModelFallback,
69
+ runSingleAgentWithMainFallback,
61
70
  type SingleResult,
62
71
  type SubagentDetails,
63
72
  type SubagentLiveEvent,
64
73
  } from "./spawn.ts";
65
- import { trajectoryStore, summarizeToolArgs } from "./trajectory.ts";
66
74
  import {
67
75
  createWorktreeIsolation,
68
76
  resolveWorktreeTarget,
@@ -74,13 +82,9 @@ import {
74
82
  const NON_BLANK_TASK_OPTIONS = { minLength: 1, pattern: "\\S" } as const;
75
83
  export const FORK_CONTINUATION_PROMPT =
76
84
  "Continue from the retained context above. Review the prior work, then take the most useful next step toward completing the existing objective without repeating completed work.";
77
- export const WORKTREE_ISOLATION_INSTRUCTIONS =
85
+ const WORKTREE_ISOLATION_INSTRUCTIONS =
78
86
  "You are running in a temporary detached Git worktree. Work only in the current cwd; do not create another worktree or manually copy/apply changes to the original checkout. The parent dispatcher will integrate your tracked, deleted, and untracked changes when this thread finally settles.";
79
87
 
80
- export function buildWorktreeTaskPrompt(task: string): string {
81
- return `${WORKTREE_ISOLATION_INSTRUCTIONS}\n\nTask: ${task}`;
82
- }
83
-
84
88
  function withWorktreeSystemPrompt(agent: AgentConfig): AgentConfig {
85
89
  return {
86
90
  ...agent,
@@ -92,14 +96,13 @@ interface DispatchEnvironment {
92
96
  ctx: ExtensionContext;
93
97
  config: SubagentsConfig;
94
98
  agents: AgentConfig[];
95
- sessionRef?: string;
96
99
  }
97
100
 
98
101
  const VISION_DESCRIPTION =
99
- "Set true when the task may require viewing images (screenshots, mockups, designs) — the configured vision model becomes primary, followed by the agent backup and current main-window model";
102
+ "Set true when the task may require viewing images (screenshots, mockups, designs) — the configured vision model is used first, then model-level failures hand directly to the current main-window model";
100
103
 
101
104
  const ISOLATION_DESCRIPTION =
102
- "Filesystem isolation: shared uses the caller's working tree; worktree creates a detached temporary Git worktree (write-capable agents only)";
105
+ "Filesystem isolation: shared uses the caller's working tree; worktree creates a detached temporary Git worktree (write-capable agents, including worker and cleaner, only)";
103
106
 
104
107
  const IsolationSchema = Type.Optional(
105
108
  StringEnum(["shared", "worktree"] as const, { description: ISOLATION_DESCRIPTION }),
@@ -181,21 +184,39 @@ function serializeAutoFixChain(
181
184
  };
182
185
  }
183
186
 
184
- function resolveDispatchModelPool(
187
+ interface DispatchModelRoute {
188
+ agent: AgentConfig;
189
+ mainFallbackRef?: string;
190
+ thinkingLevel: ThinkingLevel;
191
+ thinkingLevelForModel: (ref?: string) => ThinkingLevel;
192
+ }
193
+
194
+ function resolveDispatchModelRoute(
185
195
  agent: AgentConfig,
186
196
  config: SubagentsConfig,
187
- mainRef: string | undefined,
197
+ ctx: ExtensionContext,
188
198
  vision: boolean,
189
- ): { agent: AgentConfig; fallbackModelRefs: string[] } {
190
- const pool = resolveAgentModelPool({
191
- primaryRef: vision ? config.visionModel : config.agentModels[agent.name],
192
- backupRef: config.agentBackupModels[agent.name],
199
+ ): DispatchModelRoute {
200
+ const availableModels = availableModelsInScope(ctx);
201
+ const mainRef = currentModelRef(ctx);
202
+ const route = resolveAgentModelRoute({
203
+ selectedRef: vision ? config.visionModel : config.agentModels[agent.name],
193
204
  mainRef,
194
205
  declaredDefaultRef: agent.model,
206
+ availableRefs: availableModels.map(modelRef),
195
207
  });
208
+ const preferred = config.agentThinkingLevels[agent.name] ?? agent.thinking ?? DEFAULT_THINKING_LEVEL;
209
+ const thinkingLevelForModel = (ref?: string): ThinkingLevel => {
210
+ const model = ref === mainRef && ctx.model
211
+ ? ctx.model
212
+ : findModelByRef(availableModels, ref);
213
+ return resolveThinkingLevel(model, preferred);
214
+ };
196
215
  return {
197
- agent: { ...agent, model: pool.primaryRef },
198
- fallbackModelRefs: pool.fallbackModelRefs,
216
+ agent: { ...agent, model: route.primaryRef },
217
+ mainFallbackRef: route.mainFallbackRef,
218
+ thinkingLevel: thinkingLevelForModel(route.primaryRef),
219
+ thinkingLevelForModel,
199
220
  };
200
221
  }
201
222
 
@@ -205,33 +226,36 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
205
226
  label: "Subagent",
206
227
  description: [
207
228
  "Delegate a discrete, self-contained task to a specialized sub-agent running in an ISOLATED context window.",
208
- "Agents: explore (read-only codebase recon), worker (implement/fix/refactor/test, full tools), reviewer (adversarial pre-commit review, read-only).",
229
+ "Built-in roles (only configured enabled agents can dispatch): explore (read-only codebase recon), worker (implement/fix/refactor/test, full tools), cleaner (explicit evidence-first cleanup, full/write tools), reviewer (adversarial pre-commit gate, read-only).",
230
+ "When cleaner is enabled, route explicit cleanup intent in any language (for example dead code, redundancy, simplification, or over-engineering), including a requested periodic cleanup pass. Audit/find/inspect/report is read-only evidence, while explicit remove/clean/simplify/refactor permits verified edits. Generic code review goes to reviewer. Never route cleaner by PR count or as the pre-commit gate; reviewer reviews its edits.",
209
231
  "Modes: single ({agent, task}) or parallel ({tasks: [{agent, task}, ...]}).",
210
- "Isolation: single tasks default to shared; parallel worker tasks default to detached Git worktrees unless isolation: shared is explicit. explore/reviewer cannot use worktree isolation.",
232
+ "Isolation: single tasks default to shared; parallel worker tasks default to detached Git worktrees unless isolation: shared is explicit. Cleaner is write-capable and can opt into worktree isolation; explore/reviewer cannot use it.",
211
233
  "Use subagent_control to steer, retarget, park, resume, or fork a thread by its stable run id.",
212
234
  "It starts agents in the background and immediately returns control to the main window; completion messages automatically wake the main agent to continue.",
213
235
  "Each agent has no memory of this conversation — brief it fully (goal, exact paths, constraints, expected output).",
214
236
  "Results arrive as wake-up messages automatically — you do NOT need to wait. If you must get a result in-turn, subagent_wait is a non-blocking lookup by default (pass timeoutMs to block).",
215
- "Vision: set vision: true when the task may require viewing images (screenshots, mockups, design files — e.g. frontend work) — the configured vision model is primary, followed by that agent's backup and the current main-window model.",
237
+ "Vision: set vision: true when the task may require viewing images (screenshots, mockups, design files — e.g. frontend work) — the configured vision model is used first, then model-level failures hand directly to the current main-window model.",
216
238
  ].join(" "),
217
239
  promptSnippet:
218
- "Start background subagents: explore (read-only search), worker (implement), reviewer (adversarial review); completion automatically resumes the main agent. Simple tasks: use direct tools, not subagents.",
240
+ "Start background subagents: explore (read-only search), worker (implement), cleaner (explicit evidence-first cleanup), reviewer (pre-commit review); completion automatically resumes the main agent. Simple tasks: use direct tools, not subagents.",
219
241
  promptGuidelines: [
220
- "Delegate only when an isolated context genuinely pays: broad exploration, a self-contained implementation, or a review gate. Handle simple lookups and one-line edits inline with direct tools — never spawn a sub-agent for them.",
242
+ "Delegate only when an isolated context genuinely pays: broad exploration, a self-contained implementation, explicit evidence-first cleanup, or a review gate. Handle simple lookups and one-line edits inline with direct tools — never spawn a sub-agent for them.",
221
243
  "Use subagent with agent 'explore' for broad or open-ended code search before large changes; a targeted 'where is X' is a direct grep/read.",
244
+ "Treat explore output as a retrieval index, not authority: re-read load-bearing files before editing or deciding deletion, security, compatibility, persistence, or dynamic reachability. The cheapest model can cost more through rework on complex dynamic, concurrent, migration, or security-sensitive code; choose a stronger model or specialist there.",
222
245
  "Use subagent with agent 'worker' for a self-contained implementation task worth a separate context; it plans internally.",
223
- "Use subagent with agent 'reviewer' for a fresh read-only review before reporting work done or committing.",
246
+ "When cleaner is enabled, use subagent with agent 'cleaner' only for explicit cleanup intent in any language (for example dead code, redundancy, simplification, or over-engineering) or a requested periodic cleanup pass. Audit/find/inspect/report means read-only ranked evidence; apply only for explicit remove/clean/simplify/refactor wording. Generic code review goes to reviewer. Never trigger cleaner from PR count or as a pre-commit gate; send non-trivial cleaner edits to reviewer.",
247
+ "Use subagent with agent 'reviewer' for the fresh read-only gate before reporting non-trivial work done or committing, including after cleaner edits.",
224
248
  "subagent launches work in the background and ends the current turn; when a result arrives, the main agent is automatically resumed with it.",
225
249
  "Run independent tasks in parallel by passing a tasks array to subagent; parallel worker items default to isolation: worktree so their edits are integrated independently. Pass isolation: shared only when workers intentionally need the caller's live uncommitted tree.",
226
- "Use isolation: worktree only for worker/write-capable agents and only inside a Git repository with a committed HEAD; setup or integration failures never silently fall back to shared.",
250
+ "Use isolation: worktree only for worker, cleaner, or another write-capable agent and only inside a Git repository with a committed HEAD; parallel worker tasks default to worktree, while cleaner must opt in. Setup or integration failures never silently fall back to shared.",
227
251
  "NEVER sleep or poll, and do NOT call subagent_wait to hold the turn — subagent ends the turn immediately and the result arrives as a message that wakes you automatically (even mid-turn). Ending your turn is the default and the only correct way to wait.",
228
252
  "If you must keep the turn for a result, call subagent_wait with an explicit timeoutMs (non-blocking by default) — never bash sleep/timeout to wait for a sub-agent.",
229
- "When a delegated task may require viewing images (frontend screenshots, mockups, design comparisons), pass vision: true and give the sub-agent the exact image paths — it reads them with its read tool. The configured vision model becomes primary; model-level failures continue through the agent's backup pool and current main-window model.",
253
+ "When a delegated task may require viewing images (frontend screenshots, mockups, design comparisons), pass vision: true and give the sub-agent the exact image paths — it reads them with its read tool. The configured vision model is used first; model-level failures hand directly to the current main-window model.",
230
254
  "When a sub-agent result arrives it is already shown to the user — do NOT restate, paraphrase, or summarize it; reply with only your own conclusion or next action (often just one line), since duplicating the result wastes tokens for nothing.",
231
255
  ],
232
256
  parameters: SubagentParams,
233
257
 
234
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
258
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
235
259
  monitor.beginTurn();
236
260
  const config = await loadConfig(runtime.configPath);
237
261
  // Pick up concurrency changes from /subagents-setup without a restart.
@@ -242,29 +266,25 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
242
266
  const finishRun = (
243
267
  runId: number,
244
268
  status: "done" | "failed",
245
- opts?: { silent?: boolean; retain?: boolean },
269
+ opts?: { silent?: boolean },
246
270
  ): void => {
247
271
  monitor.setStatus(runId, status); // stamps endedAt for the elapsed time
248
- const run = opts?.retain ? monitor.findRun(runId) : monitor.removeRun(runId);
272
+ const run = monitor.removeRun(runId);
249
273
  if (!run) return; // already finished — stay idempotent
250
- if (opts?.retain) monitor.setRetained(runId, true);
251
274
  if (opts?.silent || !runtime.sessionActive) return;
252
275
  const icon = status === "done" ? "✓" : "✗";
253
276
  ctx.ui.notify(`${icon} ${monitor.summarize(run)}`, status === "done" ? "info" : "error");
254
277
  };
255
278
 
256
279
  // Live sub-agent activity → concise one-line status ("thinking",
257
- // "read src/index.ts", ...), never a raw args blob. In parallel, every
258
- // live event is appended to the thread's append-only trajectory (status,
259
- // model-candidate changes, usage, tool starts/ends with a redacted
260
- // args summary). The live handler only updates monitor state; finishing
261
- // (removeRun + notify) is
262
- // owned by the queue task / launchInLoop. That keeps a startup retry —
280
+ // "read src/index.ts", ...), never a raw args blob. The live handler
281
+ // only updates monitor state; finishing (removeRun + notify) is owned
282
+ // by the queue task / launchInLoop. That keeps a startup retry —
263
283
  // which fires a transient "failed" status before relaunching — from
264
284
  // ripping the row out early, and lets the queue task decide between
265
285
  // delivering a reviewer's result and starting an auto-fix chain.
266
286
  const makeLiveHandler =
267
- (runId: number, threadId?: number, generation?: number) =>
287
+ (runId: number, generation?: number) =>
268
288
  (e: SubagentLiveEvent): void => {
269
289
  if (generation !== undefined && runtime.threads.get(runId)?.generation !== generation) return;
270
290
  switch (e.kind) {
@@ -277,6 +297,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
277
297
  break;
278
298
  case "model":
279
299
  monitor.setModel(runId, e.model, e.fallbackFrom);
300
+ monitor.setThinking(runId, e.thinking);
280
301
  break;
281
302
  case "usage":
282
303
  monitor.setUsage(runId, e.usage, e.model);
@@ -295,31 +316,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
295
316
  monitor.setActivity(runId, "responding");
296
317
  break;
297
318
  }
298
- if (threadId !== undefined) {
299
- const trajectory = trajectoryStore.get(threadId).trajectory;
300
- switch (e.kind) {
301
- case "status":
302
- trajectory.append({ kind: "status", status: e.status });
303
- break;
304
- case "model":
305
- trajectory.append({ kind: "candidate", model: e.model, fallbackFrom: e.fallbackFrom });
306
- break;
307
- case "usage":
308
- trajectory.append({ kind: "usage", usage: { ...e.usage }, model: e.model });
309
- break;
310
- case "tool_start":
311
- trajectory.append({
312
- kind: "tool_start",
313
- tool: e.toolName,
314
- toolCallId: e.toolCallId,
315
- summary: summarizeToolArgs(e.args),
316
- });
317
- break;
318
- case "tool_end":
319
- trajectory.append({ kind: "tool_end", tool: e.toolName, toolCallId: e.toolCallId, isError: e.isError });
320
- break;
321
- }
322
- }
323
319
  };
324
320
 
325
321
  const discovery = discoverAgents(ctx.cwd, {
@@ -327,7 +323,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
327
323
  enabledNames: config.enabledAgents,
328
324
  projectTrusted: ctx.isProjectTrusted?.() === true,
329
325
  });
330
- const sessionRef = currentModelRef(ctx);
331
326
  const agents = discovery.agents;
332
327
 
333
328
  const hasTasks = (params.tasks?.length ?? 0) > 0;
@@ -391,70 +386,48 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
391
386
  ): Promise<{ runId?: number; result: SingleResult }> => {
392
387
  const agent = agents.find((candidate) => candidate.name === agentName);
393
388
  if (!agent) return { result: failedStartResult(agentName, task, `Unknown agent: "${agentName}".`) };
394
- // Vision chains keep the vision override as each round's primary while
395
- // retaining that worker/reviewer's own configured backup pool.
396
- const pool = resolveDispatchModelPool(agent, config, sessionRef, vision);
397
- const thinkingLevel = config.agentThinkingLevels[agent.name] ?? agent.thinking ?? config.thinkingLevel;
398
- const runId = monitor.addRun(agent.name, task, pool.agent.model, thinkingLevel, meta);
399
- // Chain rounds keep their own lifecycle trajectory.
400
- const chainState = trajectoryStore.get(runId);
401
- chainState.trajectory.append({
402
- kind: "dispatch",
403
- agent: agent.name,
404
- task,
405
- model: pool.agent.model,
406
- thinking: thinkingLevel,
407
- pool: pool.fallbackModelRefs,
408
- vision,
409
- isolation: "shared",
410
- originalCwd: executionCwd,
411
- isolationCwd: executionCwd,
412
- });
413
- const onLive = makeLiveHandler(runId, runId);
389
+ // Vision chains use the vision override first; every model-level failure
390
+ // hands directly to the current main model with re-clamped thinking.
391
+ const route = resolveDispatchModelRoute(agent, config, ctx, vision);
392
+ const thinkingLevel = route.thinkingLevel;
393
+ const runId = monitor.addRun(agent.name, task, route.agent.model, thinkingLevel, meta);
394
+ const onLive = makeLiveHandler(runId);
414
395
  try {
415
- const result = await runSingleAgentWithModelFallback(
396
+ const result = await runSingleAgentWithMainFallback(
416
397
  {
417
398
  defaultCwd: executionCwd,
418
399
  cwd: executionCwd,
419
- agent: pool.agent,
400
+ agent: route.agent,
420
401
  agentName,
421
402
  task,
422
403
  thinkingLevel,
404
+ thinkingLevelForModel: route.thinkingLevelForModel,
423
405
  signal,
424
406
  onLive,
425
407
  makeDetails: makeDetails("single", true),
426
408
  idleTimeoutMs: config.idleTimeoutSec * 1000,
427
409
  },
428
- pool.fallbackModelRefs,
410
+ route.mainFallbackRef,
429
411
  );
430
412
  result.runId = runId;
413
+ result.projectCwd = executionCwd;
431
414
  result.isolation = "shared";
432
- result.originalCwd = executionCwd;
433
- result.isolationCwd = executionCwd;
434
415
  runtime.retainSession(result);
435
416
  monitor.setModel(runId, result.model, result.modelFallbackFrom);
436
- chainState.trajectory.append({
437
- kind: "settled",
438
- status: isFailedResult(result) ? "failed" : "done",
439
- model: result.model,
440
- });
441
- // Keep the finished round in status state while the chain is
442
- // still running, with a one-line summary of what it did; the whole
443
- // group is dropped when the chain resolves (see removeChainGroup).
444
- monitor.setSummary(runId, summarizeChainResult(result));
445
- finishRun(runId, isFailedResult(result) ? "failed" : "done", { retain: true });
417
+ monitor.setThinking(runId, result.thinking);
418
+ // The parent row represents the chain. Internal rounds leave live
419
+ // status as soon as they settle; their reports remain addressable by id.
420
+ finishRun(runId, isFailedResult(result) ? "failed" : "done", { silent: true });
446
421
  runtime.registerRunResult(runId, result);
447
422
  return { runId, result };
448
423
  } catch (error) {
449
- finishRun(runId, "failed", { retain: true });
450
- chainState.trajectory.append({ kind: "settled", status: "failed", model: pool.agent.model });
424
+ finishRun(runId, "failed", { silent: true });
451
425
  const errorMessage = error instanceof Error ? error.message : String(error);
452
426
  const crashed: SingleResult = {
453
- ...queuedResult(pool.agent, task, thinkingLevel),
427
+ ...queuedResult(route.agent, task, thinkingLevel),
454
428
  runId,
429
+ projectCwd: executionCwd,
455
430
  isolation: "shared",
456
- originalCwd: executionCwd,
457
- isolationCwd: executionCwd,
458
431
  exitCode: 1,
459
432
  stderr: errorMessage,
460
433
  stopReason: signal.aborted ? "aborted" : "error",
@@ -473,7 +446,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
473
446
  * Failures short-circuit: a crashed worker skips its re-review and delivers.
474
447
  * The triggering reviewer stays in monitor state until the chain resolves.
475
448
  */
476
- /** Drop every monitor row belonging to an auto-fix chain; the retained
449
+ /** Drop any in-flight monitor row belonging to an auto-fix chain; the
477
450
  * parent is removed separately (it does not carry the groupId). */
478
451
  const removeChainGroup = (groupId: string): void => {
479
452
  for (const run of [...monitor.getRuns()]) {
@@ -514,10 +487,20 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
514
487
  };
515
488
  fixController = runtime.backgroundQueue.enqueue(
516
489
  serializeAutoFixChain(executionCwd, async (signal) => {
490
+ if (!ownsParent()) return;
491
+ // The parent id belongs to the stable logical thread and will point at
492
+ // the chain outcome. Archive the triggering review under its own id so
493
+ // every id advertised by the chain summary resolves to that exact step.
494
+ const initialStepRunId = monitor.reserveRunId();
495
+ const initialStepResult: SingleResult = {
496
+ ...initialReviewerResult,
497
+ runId: initialStepRunId,
498
+ };
499
+ runtime.registerRunResult(initialStepRunId, initialStepResult);
517
500
  const chain: ChainStep[] = [
518
- { runId: parentRunId, result: initialReviewerResult, relation: "initial review" },
501
+ { runId: initialStepRunId, result: initialStepResult, relation: "initial review" },
519
502
  ];
520
- let lastReviewer = initialReviewerResult;
503
+ let lastReviewer = initialStepResult;
521
504
  for (let round = 1; round <= config.maxFixRounds; round++) {
522
505
  if (!runtime.sessionActive) break;
523
506
  const fixBrief = buildFixTaskBrief(lastReviewer, round, config.maxFixRounds);
@@ -579,37 +562,34 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
579
562
  return;
580
563
  }
581
564
  // Parking an auto-fix chain aborts its in-flight child but preserves the
582
- // parent's retained checkpoint and suppresses an aborted chain delivery.
565
+ // parent's checkpoint and suppresses an aborted chain delivery.
583
566
  if (controlledParent.state === "parked") {
584
567
  clearOwnedController();
585
568
  removeChainGroup(parentGroupId);
586
- monitor.setRetained(parentRunId, false);
587
569
  monitor.setStatus(parentRunId, "parked");
588
570
  return;
589
571
  }
590
- // The chain is done (success, exhaustion, or abort): drop the retained
591
- // parent row and its retained round rows, then deliver one condensed
572
+ // The chain is done (success, exhaustion, or abort): drop its monitor
573
+ // rows, then deliver one condensed
592
574
  // summary. Register the parent's final state (the last chain result)
593
- // before removal so subagent_wait can resolve it.
575
+ // before removal so subagent_wait can resolve it. Clone instead of
576
+ // mutating: the internal step remains addressable under its own run id.
594
577
  const last = chain[chain.length - 1];
595
- runtime.registerRunResult(parentRunId, last.result);
578
+ const parentResult: SingleResult = {
579
+ ...last.result,
580
+ runId: parentRunId,
581
+ };
582
+ runtime.registerRunResult(parentRunId, parentResult);
596
583
  removeChainGroup(parentGroupId);
597
584
  monitor.removeRun(parentRunId);
598
- runtime.retainSession(last.result);
585
+ runtime.retainSession(parentResult);
599
586
  const parentThread = parentThreadAtStart;
587
+ parentThread.lastResult = parentResult;
600
588
  parentThread.agentName = last.result.agent;
601
589
  parentThread.task = last.result.task;
602
590
  parentThread.sessionId = last.result.sessionId;
603
591
  parentThread.sessionDir = last.result.sessionDir;
604
592
  parentThread.state = isFailedResult(last.result) ? "failed" : "completed";
605
- // The chain outcome settles the parent thread's trajectory: the
606
- // last chain step is its final state.
607
- const parentTrajectory = trajectoryStore.get(parentRunId);
608
- parentTrajectory.trajectory.append({
609
- kind: "settled",
610
- status: parentThread.state === "failed" ? "failed" : "done",
611
- model: last.result.model,
612
- });
613
593
  if (!runtime.sessionActive) {
614
594
  clearOwnedController();
615
595
  return;
@@ -642,7 +622,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
642
622
  clearOwnedController();
643
623
  removeChainGroup(parentGroupId);
644
624
  if (controlledParent.state === "parked") {
645
- monitor.setRetained(parentRunId, false);
646
625
  monitor.setStatus(parentRunId, "parked");
647
626
  return;
648
627
  }
@@ -701,8 +680,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
701
680
  worktree?: WorktreeIsolation;
702
681
  forkedFromRunId?: number;
703
682
  forkObjective?: string;
704
- modelPool?: string[];
705
- thinkingLevel?: SubagentThread["thinkingLevel"];
706
683
  }
707
684
 
708
685
  interface ResumeReservation {
@@ -758,12 +735,11 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
758
735
  const runCtx = environment?.ctx ?? ctx;
759
736
  const runConfig = environment?.config ?? config;
760
737
  const runAgents = environment?.agents ?? agents;
761
- const runSessionRef = environment?.sessionRef ?? sessionRef;
762
738
  const agent = runAgents.find((candidate) => candidate.name === agentName);
763
739
  if (!agent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
764
740
  if (isolation === "worktree" && !isWorktreeCapableAgent(agent)) {
765
741
  return {
766
- ...failedStartResult(agentName, task, `Agent "${agentName}" is read-only; worktree isolation is available only to worker/write-capable agents.`),
742
+ ...failedStartResult(agentName, task, `Agent "${agentName}" is read-only; worktree isolation is available only to write-capable agents such as worker or cleaner.`),
767
743
  isolation,
768
744
  };
769
745
  }
@@ -776,7 +752,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
776
752
  return {
777
753
  ...failedStartResult(agentName, task, `Run #${existingThread?.id ?? "?"} has no active continuation worktree.`),
778
754
  isolation,
779
- originalCwd,
780
755
  integrationStatus: worktree.state === "finalizing" ? "pending" : worktree.state,
781
756
  };
782
757
  }
@@ -787,65 +762,49 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
787
762
  return {
788
763
  ...failedStartResult(agentName, task, error instanceof Error ? error.message : String(error)),
789
764
  isolation,
790
- originalCwd,
791
765
  };
792
766
  }
793
767
  }
794
768
  }
795
769
  const executionCwd = worktree?.cwd ?? originalCwd;
796
- const resolvedPool = resolveDispatchModelPool(agent, runConfig, runSessionRef, vision);
797
- const inheritedPool = seed?.modelPool?.filter((ref) => ref.trim().length > 0) ?? [];
798
- const rawPool = inheritedPool.length > 0
799
- ? {
800
- agent: { ...agent, model: inheritedPool[0] },
801
- fallbackModelRefs: inheritedPool.slice(1),
802
- }
803
- : resolvedPool;
770
+ const resolvedRoute = resolveDispatchModelRoute(agent, runConfig, runCtx, vision);
804
771
  // Isolation is a persistent system-level invariant, not a one-shot task
805
- // prefix: queued retargets, live retargets, resumes, and model fallbacks
806
- // all keep the same worktree boundary.
807
- const pool = isolation === "worktree"
808
- ? { ...rawPool, agent: withWorktreeSystemPrompt(rawPool.agent) }
809
- : rawPool;
810
- const thinkingLevel = seed?.thinkingLevel ?? runConfig.agentThinkingLevels[agent.name] ?? agent.thinking ?? runConfig.thinkingLevel;
811
- const modelPool = [pool.agent.model, ...pool.fallbackModelRefs].filter((ref): ref is string => Boolean(ref));
772
+ // prefix: queued retargets, live retargets, resumes, and main-model
773
+ // handoffs all keep the same worktree boundary.
774
+ const route = isolation === "worktree"
775
+ ? { ...resolvedRoute, agent: withWorktreeSystemPrompt(resolvedRoute.agent) }
776
+ : resolvedRoute;
777
+ const thinkingLevel = route.thinkingLevel;
812
778
  const priorTask = existingThread?.task;
813
779
  const priorSessionId = seed?.sessionId ?? existingThread?.sessionId;
814
780
  const priorSessionDir = seed?.sessionDir ?? existingThread?.sessionDir;
815
781
  if (existingThread && resumeReservation && !ownsResumeReservation(existingThread, resumeReservation)) {
816
782
  return failedStartResult(agentName, task, `Run #${existingThread.id} changed while resume was preparing; no new generation was started.`);
817
783
  }
818
- const runId = existingThread?.id ?? monitor.addRun(agent.name, task, pool.agent.model, thinkingLevel, {
784
+ const runId = existingThread?.id ?? monitor.addRun(agent.name, task, route.agent.model, thinkingLevel, {
819
785
  isolation,
820
786
  ...(seed?.forkedFromRunId !== undefined ? { forkedFromRunId: seed.forkedFromRunId } : {}),
821
787
  });
822
788
  const generation = (existingThread?.generation ?? 0) + 1;
823
789
  const pending: SingleResult = {
824
- ...queuedResult(pool.agent, task, thinkingLevel),
790
+ ...queuedResult(route.agent, task, thinkingLevel),
825
791
  runId,
792
+ projectCwd: originalCwd,
826
793
  isolation,
827
- originalCwd,
828
- isolationCwd: executionCwd,
829
794
  ...(isolation === "worktree" ? { integrationStatus: "pending" as const } : {}),
830
795
  ...(seed?.sessionId && seed.sessionDir
831
- ? { sessionId: seed.sessionId, sessionDir: seed.sessionDir, resumed: true }
796
+ ? { sessionId: seed.sessionId, sessionDir: seed.sessionDir }
832
797
  : {}),
833
798
  ...(seed?.forkedFromRunId !== undefined ? { forkedFromRunId: seed.forkedFromRunId } : {}),
834
799
  };
835
800
  if (existingThread) {
836
- monitor.restartRun(runId, agent.name, task, pool.agent.model, thinkingLevel, isolation);
801
+ monitor.restartRun(runId, agent.name, task, route.agent.model, thinkingLevel, isolation);
837
802
  runtime.settledRuns.delete(runId);
838
803
  }
839
804
 
840
805
  let thread!: SubagentThread;
841
806
  const control = new RpcRunControl(task, generation, (phase) => {
842
807
  if (runtime.threads.get(runId)?.generation !== generation || phase === "settled") return;
843
- // Orchestration transitions are part of the trajectory (retrying →
844
- // retry event, park/stop → terminal control events).
845
- const trajectory = trajectoryStore.get(runId).trajectory;
846
- if (phase === "retrying") trajectory.append({ kind: "retry", reason: "retrying" });
847
- else if (phase === "parked") trajectory.append({ kind: "park" });
848
- else if (phase === "stopped") trajectory.append({ kind: "stop", reason: control.getStopMessage() });
849
808
  const state: ThreadState =
850
809
  phase === "queued" || phase === "starting"
851
810
  ? "queued"
@@ -866,45 +825,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
866
825
  else if (state === "running") monitor.setStatus(runId, "running");
867
826
  });
868
827
 
869
- // Restart bumps the generation while preserving append-only history.
870
- const trajectoryState = trajectoryStore.get(runId);
871
- if (existingThread) {
872
- trajectoryState.trajectory.restart();
873
- trajectoryState.trajectory.append({
874
- kind: "resume",
875
- objective: newObjectiveOnResume ? task : undefined,
876
- });
877
- }
878
- if (seed?.forkedFromRunId !== undefined) {
879
- trajectoryState.trajectory.append({
880
- kind: "fork",
881
- sourceRunId: seed.forkedFromRunId,
882
- childRunId: runId,
883
- objective: seed.forkObjective,
884
- });
885
- }
886
- trajectoryState.trajectory.append({
887
- kind: "dispatch",
888
- agent: agent.name,
889
- task,
890
- model: pool.agent.model,
891
- thinking: thinkingLevel,
892
- pool: pool.fallbackModelRefs,
893
- vision,
894
- resumed: existingThread !== undefined || seed !== undefined,
895
- isolation,
896
- originalCwd,
897
- isolationCwd: executionCwd,
898
- });
899
- if (worktree && worktree !== previousWorktree) {
900
- trajectoryState.trajectory.append({
901
- kind: "worktree",
902
- status: "created",
903
- originalCwd,
904
- isolationCwd: executionCwd,
905
- worktreePath: worktree.worktreePath,
906
- });
907
- }
908
828
 
909
829
  if (existingThread) {
910
830
  thread = existingThread;
@@ -914,7 +834,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
914
834
  thread.cwd = originalCwd;
915
835
  thread.executionCwd = executionCwd;
916
836
  thread.vision = vision;
917
- thread.modelPool = modelPool;
918
837
  thread.thinkingLevel = thinkingLevel;
919
838
  thread.isolation = isolation;
920
839
  thread.worktree = worktree;
@@ -939,7 +858,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
939
858
  cwd: originalCwd,
940
859
  executionCwd,
941
860
  vision,
942
- modelPool,
943
861
  thinkingLevel,
944
862
  isolation,
945
863
  worktree,
@@ -963,7 +881,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
963
881
  thread.notifyIsolationFailure = (finalization) => {
964
882
  const paths = [finalization.worktreePath, finalization.patchPath].filter(Boolean).join(" · ");
965
883
  runCtx.ui.notify(
966
- `✗ worker worktree ${finalization.integrated ? "cleanup" : "integration"} failed${paths ? ` · retained ${paths}` : ""}: ${finalization.error ?? "unknown Git integration error"}`,
884
+ `✗ ${agent.name} worktree ${finalization.integrated ? "cleanup" : "integration"} failed${paths ? ` · retained ${paths}` : ""}: ${finalization.error ?? "unknown Git integration error"}`,
967
885
  "error",
968
886
  );
969
887
  };
@@ -975,21 +893,9 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
975
893
  if (thread.generation !== expectedGeneration) return undefined;
976
894
  const finalization = await thread.worktree.finalize();
977
895
  monitor.setIsolation(runId, "worktree", finalization.status);
978
- trajectoryState.trajectory.append({
979
- kind: "worktree",
980
- status: finalization.status,
981
- originalCwd: thread.cwd,
982
- isolationCwd: thread.executionCwd,
983
- worktreePath: finalization.worktreePath,
984
- patchPath: finalization.patchPath,
985
- integrated: finalization.integrated,
986
- error: finalization.error,
987
- });
988
896
  if (result) {
989
897
  result.runId = runId;
990
898
  result.isolation = "worktree";
991
- result.originalCwd = thread.cwd;
992
- result.isolationCwd = thread.executionCwd;
993
899
  result.integrationStatus = finalization.status;
994
900
  result.integrationApplied = finalization.integrated;
995
901
  result.integrationError = finalization.error;
@@ -1014,7 +920,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1014
920
  }
1015
921
  }
1016
922
  if (finalization.status === "retained") {
1017
- runtime.retainWorktreeArtifacts(finalization);
1018
923
  if (!thread.isolationFailureNotified) {
1019
924
  thread.isolationFailureNotified = true;
1020
925
  try {
@@ -1048,13 +953,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1048
953
  const discardUnusedWorktree = async (candidate: WorktreeIsolation | undefined): Promise<void> => {
1049
954
  if (!candidate) return;
1050
955
  try {
1051
- if (candidate.discard) {
1052
- await candidate.discard();
1053
- return;
1054
- }
1055
- // Compatibility for externally supplied/test handles. Production handles
1056
- // expose discard(), so this fallback never integrates a seeded worktree.
1057
- if (candidate.state === "active") await candidate.finalize();
956
+ await candidate.discard();
1058
957
  } catch (error) {
1059
958
  const retainedPath = existsSync(candidate.worktreePath)
1060
959
  ? candidate.worktreePath
@@ -1069,7 +968,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1069
968
  ...(existsSync(candidate.patchPath) ? { patchPath: candidate.patchPath } : {}),
1070
969
  error: `Discarding unused continuation failed: ${error instanceof Error ? error.message : String(error)}`,
1071
970
  };
1072
- runtime.retainWorktreeArtifacts(finalization);
1073
971
  await persistRecoveryRecords(runtime.configPath, [
1074
972
  recoveryRecordFromFinalization(runId, finalization),
1075
973
  ]).catch(() => undefined);
@@ -1246,7 +1144,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1246
1144
  ctx: currentCtx,
1247
1145
  config: currentConfig,
1248
1146
  agents: currentAgents,
1249
- sessionRef: currentModelRef(currentCtx),
1250
1147
  },
1251
1148
  seed,
1252
1149
  reservation,
@@ -1409,7 +1306,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1409
1306
  ctx: currentCtx,
1410
1307
  config: currentConfig,
1411
1308
  agents: currentAgents,
1412
- sessionRef: currentModelRef(currentCtx),
1413
1309
  },
1414
1310
  {
1415
1311
  sessionId: forkedSession.sessionId,
@@ -1418,8 +1314,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1418
1314
  worktree: childWorktree,
1419
1315
  forkedFromRunId: runId,
1420
1316
  forkObjective,
1421
- modelPool: [...thread.modelPool],
1422
- thinkingLevel: thread.thinkingLevel,
1423
1317
  },
1424
1318
  );
1425
1319
  if (child.exitCode !== -1 || child.runId === undefined) {
@@ -1439,12 +1333,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1439
1333
  const childThread = runtime.threads.get(childRunId);
1440
1334
  if (childThread) childThread.forkedFromRunId = runId;
1441
1335
  monitor.setForkRelation(runId, childRunId);
1442
- trajectoryStore.get(runId).trajectory.append({
1443
- kind: "fork",
1444
- sourceRunId: runId,
1445
- childRunId,
1446
- objective: forkObjective,
1447
- });
1448
1336
  const sourceResult = runtime.settledRuns.get(runId) ?? thread.lastResult;
1449
1337
  if (sourceResult) sourceResult.forkChildRunIds = [...thread.forkChildRunIds];
1450
1338
  return child;
@@ -1469,20 +1357,21 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1469
1357
  }
1470
1358
  };
1471
1359
 
1472
- const onLive = makeLiveHandler(runId, runId, generation);
1360
+ const onLive = makeLiveHandler(runId, generation);
1473
1361
  const queueController = runtime.backgroundQueue.enqueue(
1474
1362
  async (backgroundSignal) => {
1475
1363
  if (runtime.threads.get(runId)?.generation !== generation) return;
1476
1364
  let result: SingleResult;
1477
1365
  try {
1478
- result = await runSingleAgentWithModelFallback(
1366
+ result = await runSingleAgentWithMainFallback(
1479
1367
  {
1480
1368
  defaultCwd: executionCwd,
1481
- agent: pool.agent,
1369
+ agent: route.agent,
1482
1370
  agentName,
1483
1371
  task,
1484
1372
  cwd: executionCwd,
1485
1373
  thinkingLevel,
1374
+ thinkingLevelForModel: route.thinkingLevelForModel,
1486
1375
  signal: backgroundSignal,
1487
1376
  onLive,
1488
1377
  control,
@@ -1494,11 +1383,11 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1494
1383
  sessionDir: priorSessionDir,
1495
1384
  stdinText: seed?.prompt ?? (newObjectiveOnResume
1496
1385
  ? task
1497
- : buildResumePrompt(priorTask ?? task, buildFallbackResumeReason())),
1386
+ : buildResumePrompt(priorTask ?? task, "the retained thread was resumed")),
1498
1387
  }
1499
1388
  : {}),
1500
1389
  },
1501
- pool.fallbackModelRefs,
1390
+ route.mainFallbackRef,
1502
1391
  );
1503
1392
  } catch (error) {
1504
1393
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -1517,9 +1406,8 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1517
1406
  // no monitor mutation, result registration, or completion delivery.
1518
1407
  if (runtime.threads.get(runId)?.generation !== generation) return;
1519
1408
  result.runId = runId;
1409
+ result.projectCwd = originalCwd;
1520
1410
  result.isolation = isolation;
1521
- result.originalCwd = originalCwd;
1522
- result.isolationCwd = executionCwd;
1523
1411
  result.forkedFromRunId = thread.forkedFromRunId;
1524
1412
  result.forkChildRunIds = [...thread.forkChildRunIds];
1525
1413
  thread.queueController = undefined;
@@ -1530,6 +1418,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1530
1418
  thread.lastResult = result;
1531
1419
  runtime.retainSession(result);
1532
1420
  monitor.setModel(runId, result.model, result.modelFallbackFrom);
1421
+ monitor.setThinking(runId, result.thinking);
1533
1422
 
1534
1423
  // Destructive stop owns publication once it has synchronously claimed
1535
1424
  // the lifecycle. Leave the partial result/session on the thread; the
@@ -1548,8 +1437,10 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1548
1437
  const wantsFixLoop = shouldTriggerFixLoop(result, runConfig);
1549
1438
  if (wantsFixLoop && isolation === "shared" && runtime.sessionActive) {
1550
1439
  thread.state = "running";
1551
- finishRun(runId, "done", { silent: true, retain: true });
1552
- monitor.setAnnotation(runId, "auto-fix chain running");
1440
+ // The review being done does not mean the logical run is over:
1441
+ // the same row now represents the chain until it resolves.
1442
+ monitor.setStatus(runId, "running");
1443
+ monitor.setActivity(runId, "auto-fix chain running");
1553
1444
  startFixLoop(result, `fix-${runId}`, runId, thread.executionCwd, vision);
1554
1445
  return;
1555
1446
  }
@@ -1578,15 +1469,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1578
1469
  // Stamp the terminal monitor state before projecting it. This gives every
1579
1470
  // path a fixed endedAt even when the row is removed immediately.
1580
1471
  monitor.setStatus(runId, failed ? "failed" : "done");
1581
- trajectoryState.trajectory.append({
1582
- kind: "settled",
1583
- status: failed ? "failed" : "done",
1584
- model: result.model,
1585
- isolation,
1586
- ...(result.integrationStatus && result.integrationStatus !== "pending"
1587
- ? { integrationStatus: result.integrationStatus }
1588
- : {}),
1589
- });
1590
1472
  if (!runtime.sessionActive || !ownsSettlement()) return;
1591
1473
 
1592
1474
  const modelLevel = failed && isModelLevelFailure(result);
@@ -1596,8 +1478,8 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1596
1478
  const completion: CompletionMessageItem = {
1597
1479
  agent: result.agent,
1598
1480
  block: modelLevel
1599
- ? `${formatCompletionBlock(result, runConfig.maxResultLines, runCtx.cwd)}\n\n${modelLevelTakeoverNote(result, { runId })}`
1600
- : formatCompletionBlock(result, runConfig.maxResultLines, runCtx.cwd),
1481
+ ? `${formatCompletionBlock(result, runConfig.maxResultLines, result.projectCwd ?? originalCwd)}\n\n${modelLevelTakeoverNote(result, { runId })}`
1482
+ : formatCompletionBlock(result, runConfig.maxResultLines, result.projectCwd ?? originalCwd),
1601
1483
  triggerTurn: completionTriggersTurn(result, runConfig.notifyOnReviewPass),
1602
1484
  };
1603
1485
  if (modelLevel) {
@@ -1618,8 +1500,8 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1618
1500
  () => {
1619
1501
  if (runtime.threads.get(runId)?.generation !== generation) return;
1620
1502
  // Queued park/stop owns publication and may still be finalizing an
1621
- // isolated worktree. Do not expose a terminal monitor/trajectory state
1622
- // before that owner records the checkpoint or aborted result.
1503
+ // isolated worktree. Do not expose a terminal monitor state before
1504
+ // that owner records the checkpoint or aborted result.
1623
1505
  if (thread.lifecycleOperation === "park" || thread.lifecycleOperation === "stop") return;
1624
1506
  runtime.runControllers.delete(runId);
1625
1507
  thread.queueController = undefined;
@@ -1629,7 +1511,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1629
1511
  }
1630
1512
  thread.state = "stopped";
1631
1513
  monitor.setStatus(runId, "failed");
1632
- trajectoryState.trajectory.append({ kind: "settled", status: "stopped", model: monitor.findRun(runId)?.model, isolation });
1633
1514
  if (!runtime.sessionActive) {
1634
1515
  monitor.removeRun(runId);
1635
1516
  return;
@@ -1652,26 +1533,15 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1652
1533
  !thread.retired;
1653
1534
  try {
1654
1535
  const crashed: SingleResult = {
1655
- ...dispatchFailedResult(pool.agent, control.getObjective(), error, thinkingLevel),
1536
+ ...dispatchFailedResult(route.agent, control.getObjective(), error, thinkingLevel),
1656
1537
  runId,
1657
1538
  isolation,
1658
- originalCwd,
1659
- isolationCwd: executionCwd,
1660
1539
  forkedFromRunId: thread.forkedFromRunId,
1661
1540
  };
1662
1541
  await thread.finalizeIsolation(generation, crashed);
1663
1542
  if (!ownsSettlement()) return;
1664
1543
  thread.state = "failed";
1665
1544
  monitor.setStatus(runId, "failed");
1666
- trajectoryState.trajectory.append({
1667
- kind: "settled",
1668
- status: "failed",
1669
- model: crashed.model,
1670
- isolation,
1671
- ...(crashed.integrationStatus && crashed.integrationStatus !== "pending"
1672
- ? { integrationStatus: crashed.integrationStatus }
1673
- : {}),
1674
- });
1675
1545
  finishRun(runId, "failed", { silent: true });
1676
1546
  runtime.registerRunResult(runId, crashed);
1677
1547
  runtime.runControllers.delete(runId);
@@ -1682,7 +1552,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1682
1552
  runtime.sendCompletionGroup([
1683
1553
  {
1684
1554
  agent: agent.name,
1685
- block: formatCompletionBlock(crashed, runConfig.maxResultLines, runCtx.cwd),
1555
+ block: formatCompletionBlock(crashed, runConfig.maxResultLines, crashed.projectCwd ?? originalCwd),
1686
1556
  triggerTurn: true,
1687
1557
  },
1688
1558
  ]);
@@ -1807,7 +1677,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1807
1677
  const pending = r.exitCode === -1;
1808
1678
  const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
1809
1679
  const usage = formatUsage(r.usage);
1810
- const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (pool fallback from ${r.modelFallbackFrom})` : ""}`;
1680
+ const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (main after ${r.modelFallbackFrom} failed)` : ""}`;
1811
1681
  const isolation = r.isolation === "worktree" ? ` · worktree ${r.integrationStatus ?? "active"}` : "";
1812
1682
  const runId = r.runId === undefined ? "" : `${theme.fg("dim", `#${r.runId}`)} `;
1813
1683
  const line = `${theme.fg("toolTitle", theme.bold("subagent "))}${icon} ${runId}${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${r.thinking ? ` · thinking ${r.thinking}` : ""}${isolation}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`;
@@ -1822,7 +1692,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1822
1692
  const pending = r.exitCode === -1;
1823
1693
  const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
1824
1694
  const usage = formatUsage(r.usage);
1825
- const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (pool fallback from ${r.modelFallbackFrom})` : ""}`;
1695
+ const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (main after ${r.modelFallbackFrom} failed)` : ""}`;
1826
1696
  const isolation = r.isolation === "worktree" ? ` · worktree ${r.integrationStatus ?? "active"}` : "";
1827
1697
  const runId = r.runId === undefined ? "" : `${theme.fg("dim", `#${r.runId}`)} `;
1828
1698
  lines.push(` ${icon} ${runId}${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${r.thinking ? ` · thinking ${r.thinking}` : ""}${isolation}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`);