@ferris1225/pi-subagents 1.0.1 → 2.0.1

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/config.ts CHANGED
@@ -17,10 +17,10 @@ import { dirname, join } from "node:path";
17
17
  import { getAgentDir, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
18
18
 
19
19
  /** Full catalog of agents shipped with the package (selectable in /subagents-setup). */
20
- export const BUILTIN_AGENT_NAMES = ["explore", "worker", "reviewer"] as const;
20
+ export const BUILTIN_AGENT_NAMES = ["explore", "worker", "cleaner", "reviewer"] as const;
21
21
 
22
- /** Agents enabled out of the box. */
23
- export const DEFAULT_ENABLED_AGENTS: readonly string[] = ["explore", "worker", "reviewer"];
22
+ /** Agents enabled out of the box on a fresh install. Explicit configured lists are preserved. */
23
+ export const DEFAULT_ENABLED_AGENTS: readonly string[] = ["explore", "worker", "cleaner", "reviewer"];
24
24
 
25
25
  export const AGENT_SCOPE_VALUES = ["user", "project", "both"] as const;
26
26
  export type AgentScope = (typeof AGENT_SCOPE_VALUES)[number];
@@ -52,24 +52,20 @@ export const MAX_FIX_ROUNDS_LIMIT = 5;
52
52
 
53
53
  /**
54
54
  * Default idle timeout in seconds: a sub-agent whose stdout (JSON event stream)
55
- * goes silent for this long is terminated and may be retried with the fallback
56
- * model. 0 disables the idle watchdog. Default: 90.
55
+ * goes silent for this long is terminated; a selected model then hands the
56
+ * retained session to current main. 0 disables the watchdog. Default: 90.
57
57
  */
58
58
  export const DEFAULT_IDLE_TIMEOUT_SEC = 90;
59
59
  /** Upper bound accepted for idleTimeoutSec (defensive clamp). 0 disables. */
60
60
  export const IDLE_TIMEOUT_SEC_LIMIT = 600;
61
61
 
62
62
  export interface SubagentsConfig {
63
- /** Agent names that are discoverable and injected. Default: explore, worker, reviewer. */
63
+ /** Agent names that are discoverable and injected. Fresh-install default: explore, worker, cleaner, reviewer. */
64
64
  enabledAgents: string[];
65
- /** Per-agent primary model override, keyed by agent name, as "provider/model-id". */
65
+ /** Per-agent model override, keyed by agent name, as "provider/model-id". */
66
66
  agentModels: Record<string, string>;
67
- /** Optional per-agent backup model, tried after the primary and before the current main-window model. */
68
- agentBackupModels: Record<string, string>;
69
- /** Per-agent thinking-level override, keyed by agent name. */
67
+ /** Optional per-agent thinking preference. Runtime clamps it to the effective model's supported levels. */
70
68
  agentThinkingLevels: Record<string, ThinkingLevel>;
71
- /** Thinking level for sub-agents without a per-agent override or frontmatter default. Default: "high". */
72
- thinkingLevel: ThinkingLevel;
73
69
  /**
74
70
  * When a review passes (REVIEW_PASS verdict), deliver it without waking the
75
71
  * main agent. Disabled by default so passing reviews still resume orchestration.
@@ -98,8 +94,8 @@ export interface SubagentsConfig {
98
94
  maxFixRounds: number;
99
95
  /**
100
96
  * Idle timeout in seconds: a sub-agent whose stdout (JSON event stream) goes
101
- * silent for this long is terminated and may be retried with the fallback
102
- * model. 0 disables the idle watchdog. Default: 90.
97
+ * silent for this long is terminated; a configured agent model then hands
98
+ * off to the current main model. 0 disables the idle watchdog. Default: 90.
103
99
  */
104
100
  idleTimeoutSec: number;
105
101
  /**
@@ -119,9 +115,7 @@ export interface SubagentsConfig {
119
115
  export const DEFAULT_CONFIG: SubagentsConfig = {
120
116
  enabledAgents: [...DEFAULT_ENABLED_AGENTS],
121
117
  agentModels: {},
122
- agentBackupModels: {},
123
118
  agentThinkingLevels: {},
124
- thinkingLevel: DEFAULT_THINKING_LEVEL,
125
119
  notifyOnReviewPass: false,
126
120
  maxResultLines: DEFAULT_MAX_RESULT_LINES,
127
121
  proactiveInjection: true,
@@ -179,12 +173,6 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
179
173
  }
180
174
  }
181
175
 
182
- if (isRecord(raw.agentBackupModels)) {
183
- for (const [key, value] of Object.entries(raw.agentBackupModels)) {
184
- if (isModelReference(value)) config.agentBackupModels[key.trim()] = value.trim();
185
- }
186
- }
187
-
188
176
  if (isRecord(raw.agentThinkingLevels)) {
189
177
  for (const [key, value] of Object.entries(raw.agentThinkingLevels)) {
190
178
  if (
@@ -196,10 +184,6 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
196
184
  }
197
185
  }
198
186
 
199
- if (typeof raw.thinkingLevel === "string" && (THINKING_LEVEL_VALUES as readonly string[]).includes(raw.thinkingLevel)) {
200
- config.thinkingLevel = raw.thinkingLevel as ThinkingLevel;
201
- }
202
-
203
187
  if (typeof raw.notifyOnReviewPass === "boolean") {
204
188
  config.notifyOnReviewPass = raw.notifyOnReviewPass;
205
189
  }
@@ -246,7 +230,6 @@ function defaultConfig(): SubagentsConfig {
246
230
  ...DEFAULT_CONFIG,
247
231
  enabledAgents: [...DEFAULT_CONFIG.enabledAgents],
248
232
  agentModels: {},
249
- agentBackupModels: {},
250
233
  agentThinkingLevels: {},
251
234
  announcedFeatures: [],
252
235
  };
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,
@@ -37,7 +41,14 @@ import {
37
41
  shouldTriggerFixLoop,
38
42
  type ChainStep,
39
43
  } from "./fixloop.ts";
40
- import { currentModelRef, resolveAgentModelPool } from "./models.ts";
44
+ import {
45
+ availableModelsInScope,
46
+ currentModelRef,
47
+ findModelByRef,
48
+ modelRef,
49
+ resolveAgentModelRoute,
50
+ resolveThinkingLevel,
51
+ } from "./models.ts";
41
52
  import {
42
53
  formatTaskSummary,
43
54
  formatToolActivity,
@@ -49,14 +60,13 @@ import type { SubagentRuntime, SubagentThread, ThreadState } from "./runtime.ts"
49
60
  import { persistRecoveryRecords, recoveryRecordFromFinalization } from "./recovery.ts";
50
61
  import { forkRetainedSession } from "./session-fork.ts";
51
62
  import {
52
- buildFallbackResumeReason,
53
63
  buildResumePrompt,
54
64
  RpcRunControl,
55
65
  getResultOutput,
56
66
  isFailedResult,
57
67
  isModelLevelFailure,
58
68
  reviewVerdict,
59
- runSingleAgentWithModelFallback,
69
+ runSingleAgentWithMainFallback,
60
70
  type SingleResult,
61
71
  type SubagentDetails,
62
72
  type SubagentLiveEvent,
@@ -86,14 +96,13 @@ interface DispatchEnvironment {
86
96
  ctx: ExtensionContext;
87
97
  config: SubagentsConfig;
88
98
  agents: AgentConfig[];
89
- sessionRef?: string;
90
99
  }
91
100
 
92
101
  const VISION_DESCRIPTION =
93
- "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";
94
103
 
95
104
  const ISOLATION_DESCRIPTION =
96
- "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)";
97
106
 
98
107
  const IsolationSchema = Type.Optional(
99
108
  StringEnum(["shared", "worktree"] as const, { description: ISOLATION_DESCRIPTION }),
@@ -175,21 +184,39 @@ function serializeAutoFixChain(
175
184
  };
176
185
  }
177
186
 
178
- function resolveDispatchModelPool(
187
+ interface DispatchModelRoute {
188
+ agent: AgentConfig;
189
+ mainFallbackRef?: string;
190
+ thinkingLevel: ThinkingLevel;
191
+ thinkingLevelForModel: (ref?: string) => ThinkingLevel;
192
+ }
193
+
194
+ function resolveDispatchModelRoute(
179
195
  agent: AgentConfig,
180
196
  config: SubagentsConfig,
181
- mainRef: string | undefined,
197
+ ctx: ExtensionContext,
182
198
  vision: boolean,
183
- ): { agent: AgentConfig; fallbackModelRefs: string[] } {
184
- const pool = resolveAgentModelPool({
185
- primaryRef: vision ? config.visionModel : config.agentModels[agent.name],
186
- 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],
187
204
  mainRef,
188
205
  declaredDefaultRef: agent.model,
206
+ availableRefs: availableModels.map(modelRef),
189
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
+ };
190
215
  return {
191
- agent: { ...agent, model: pool.primaryRef },
192
- fallbackModelRefs: pool.fallbackModelRefs,
216
+ agent: { ...agent, model: route.primaryRef },
217
+ mainFallbackRef: route.mainFallbackRef,
218
+ thinkingLevel: thinkingLevelForModel(route.primaryRef),
219
+ thinkingLevelForModel,
193
220
  };
194
221
  }
195
222
 
@@ -199,28 +226,31 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
199
226
  label: "Subagent",
200
227
  description: [
201
228
  "Delegate a discrete, self-contained task to a specialized sub-agent running in an ISOLATED context window.",
202
- "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.",
203
231
  "Modes: single ({agent, task}) or parallel ({tasks: [{agent, task}, ...]}).",
204
- "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.",
205
233
  "Use subagent_control to steer, retarget, park, resume, or fork a thread by its stable run id.",
206
234
  "It starts agents in the background and immediately returns control to the main window; completion messages automatically wake the main agent to continue.",
207
235
  "Each agent has no memory of this conversation — brief it fully (goal, exact paths, constraints, expected output).",
208
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).",
209
- "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.",
210
238
  ].join(" "),
211
239
  promptSnippet:
212
- "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.",
213
241
  promptGuidelines: [
214
- "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.",
215
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.",
216
245
  "Use subagent with agent 'worker' for a self-contained implementation task worth a separate context; it plans internally.",
217
- "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.",
218
248
  "subagent launches work in the background and ends the current turn; when a result arrives, the main agent is automatically resumed with it.",
219
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.",
220
- "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.",
221
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.",
222
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.",
223
- "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.",
224
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.",
225
255
  ],
226
256
  parameters: SubagentParams,
@@ -267,6 +297,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
267
297
  break;
268
298
  case "model":
269
299
  monitor.setModel(runId, e.model, e.fallbackFrom);
300
+ monitor.setThinking(runId, e.thinking);
270
301
  break;
271
302
  case "usage":
272
303
  monitor.setUsage(runId, e.usage, e.model);
@@ -292,7 +323,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
292
323
  enabledNames: config.enabledAgents,
293
324
  projectTrusted: ctx.isProjectTrusted?.() === true,
294
325
  });
295
- const sessionRef = currentModelRef(ctx);
296
326
  const agents = discovery.agents;
297
327
 
298
328
  const hasTasks = (params.tasks?.length ?? 0) > 0;
@@ -356,32 +386,35 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
356
386
  ): Promise<{ runId?: number; result: SingleResult }> => {
357
387
  const agent = agents.find((candidate) => candidate.name === agentName);
358
388
  if (!agent) return { result: failedStartResult(agentName, task, `Unknown agent: "${agentName}".`) };
359
- // Vision chains keep the vision override as each round's primary while
360
- // retaining that worker/reviewer's own configured backup pool.
361
- const pool = resolveDispatchModelPool(agent, config, sessionRef, vision);
362
- const thinkingLevel = config.agentThinkingLevels[agent.name] ?? agent.thinking ?? config.thinkingLevel;
363
- const runId = monitor.addRun(agent.name, task, pool.agent.model, thinkingLevel, meta);
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);
364
394
  const onLive = makeLiveHandler(runId);
365
395
  try {
366
- const result = await runSingleAgentWithModelFallback(
396
+ const result = await runSingleAgentWithMainFallback(
367
397
  {
368
398
  defaultCwd: executionCwd,
369
399
  cwd: executionCwd,
370
- agent: pool.agent,
400
+ agent: route.agent,
371
401
  agentName,
372
402
  task,
373
403
  thinkingLevel,
404
+ thinkingLevelForModel: route.thinkingLevelForModel,
374
405
  signal,
375
406
  onLive,
376
407
  makeDetails: makeDetails("single", true),
377
408
  idleTimeoutMs: config.idleTimeoutSec * 1000,
378
409
  },
379
- pool.fallbackModelRefs,
410
+ route.mainFallbackRef,
380
411
  );
381
412
  result.runId = runId;
413
+ result.projectCwd = executionCwd;
382
414
  result.isolation = "shared";
383
415
  runtime.retainSession(result);
384
416
  monitor.setModel(runId, result.model, result.modelFallbackFrom);
417
+ monitor.setThinking(runId, result.thinking);
385
418
  // The parent row represents the chain. Internal rounds leave live
386
419
  // status as soon as they settle; their reports remain addressable by id.
387
420
  finishRun(runId, isFailedResult(result) ? "failed" : "done", { silent: true });
@@ -391,8 +424,9 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
391
424
  finishRun(runId, "failed", { silent: true });
392
425
  const errorMessage = error instanceof Error ? error.message : String(error);
393
426
  const crashed: SingleResult = {
394
- ...queuedResult(pool.agent, task, thinkingLevel),
427
+ ...queuedResult(route.agent, task, thinkingLevel),
395
428
  runId,
429
+ projectCwd: executionCwd,
396
430
  isolation: "shared",
397
431
  exitCode: 1,
398
432
  stderr: errorMessage,
@@ -646,8 +680,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
646
680
  worktree?: WorktreeIsolation;
647
681
  forkedFromRunId?: number;
648
682
  forkObjective?: string;
649
- modelPool?: string[];
650
- thinkingLevel?: SubagentThread["thinkingLevel"];
651
683
  }
652
684
 
653
685
  interface ResumeReservation {
@@ -703,12 +735,11 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
703
735
  const runCtx = environment?.ctx ?? ctx;
704
736
  const runConfig = environment?.config ?? config;
705
737
  const runAgents = environment?.agents ?? agents;
706
- const runSessionRef = environment?.sessionRef ?? sessionRef;
707
738
  const agent = runAgents.find((candidate) => candidate.name === agentName);
708
739
  if (!agent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
709
740
  if (isolation === "worktree" && !isWorktreeCapableAgent(agent)) {
710
741
  return {
711
- ...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.`),
712
743
  isolation,
713
744
  };
714
745
  }
@@ -736,36 +767,29 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
736
767
  }
737
768
  }
738
769
  const executionCwd = worktree?.cwd ?? originalCwd;
739
- const resolvedPool = resolveDispatchModelPool(agent, runConfig, runSessionRef, vision);
740
- const inheritedPool = seed?.modelPool?.filter((ref) => ref.trim().length > 0) ?? [];
741
- const rawPool = inheritedPool.length > 0
742
- ? {
743
- agent: { ...agent, model: inheritedPool[0] },
744
- fallbackModelRefs: inheritedPool.slice(1),
745
- }
746
- : resolvedPool;
770
+ const resolvedRoute = resolveDispatchModelRoute(agent, runConfig, runCtx, vision);
747
771
  // Isolation is a persistent system-level invariant, not a one-shot task
748
- // prefix: queued retargets, live retargets, resumes, and model fallbacks
749
- // all keep the same worktree boundary.
750
- const pool = isolation === "worktree"
751
- ? { ...rawPool, agent: withWorktreeSystemPrompt(rawPool.agent) }
752
- : rawPool;
753
- const thinkingLevel = seed?.thinkingLevel ?? runConfig.agentThinkingLevels[agent.name] ?? agent.thinking ?? runConfig.thinkingLevel;
754
- 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;
755
778
  const priorTask = existingThread?.task;
756
779
  const priorSessionId = seed?.sessionId ?? existingThread?.sessionId;
757
780
  const priorSessionDir = seed?.sessionDir ?? existingThread?.sessionDir;
758
781
  if (existingThread && resumeReservation && !ownsResumeReservation(existingThread, resumeReservation)) {
759
782
  return failedStartResult(agentName, task, `Run #${existingThread.id} changed while resume was preparing; no new generation was started.`);
760
783
  }
761
- 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, {
762
785
  isolation,
763
786
  ...(seed?.forkedFromRunId !== undefined ? { forkedFromRunId: seed.forkedFromRunId } : {}),
764
787
  });
765
788
  const generation = (existingThread?.generation ?? 0) + 1;
766
789
  const pending: SingleResult = {
767
- ...queuedResult(pool.agent, task, thinkingLevel),
790
+ ...queuedResult(route.agent, task, thinkingLevel),
768
791
  runId,
792
+ projectCwd: originalCwd,
769
793
  isolation,
770
794
  ...(isolation === "worktree" ? { integrationStatus: "pending" as const } : {}),
771
795
  ...(seed?.sessionId && seed.sessionDir
@@ -774,7 +798,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
774
798
  ...(seed?.forkedFromRunId !== undefined ? { forkedFromRunId: seed.forkedFromRunId } : {}),
775
799
  };
776
800
  if (existingThread) {
777
- monitor.restartRun(runId, agent.name, task, pool.agent.model, thinkingLevel, isolation);
801
+ monitor.restartRun(runId, agent.name, task, route.agent.model, thinkingLevel, isolation);
778
802
  runtime.settledRuns.delete(runId);
779
803
  }
780
804
 
@@ -810,7 +834,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
810
834
  thread.cwd = originalCwd;
811
835
  thread.executionCwd = executionCwd;
812
836
  thread.vision = vision;
813
- thread.modelPool = modelPool;
814
837
  thread.thinkingLevel = thinkingLevel;
815
838
  thread.isolation = isolation;
816
839
  thread.worktree = worktree;
@@ -835,7 +858,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
835
858
  cwd: originalCwd,
836
859
  executionCwd,
837
860
  vision,
838
- modelPool,
839
861
  thinkingLevel,
840
862
  isolation,
841
863
  worktree,
@@ -859,7 +881,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
859
881
  thread.notifyIsolationFailure = (finalization) => {
860
882
  const paths = [finalization.worktreePath, finalization.patchPath].filter(Boolean).join(" · ");
861
883
  runCtx.ui.notify(
862
- `✗ 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"}`,
863
885
  "error",
864
886
  );
865
887
  };
@@ -1122,7 +1144,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1122
1144
  ctx: currentCtx,
1123
1145
  config: currentConfig,
1124
1146
  agents: currentAgents,
1125
- sessionRef: currentModelRef(currentCtx),
1126
1147
  },
1127
1148
  seed,
1128
1149
  reservation,
@@ -1285,7 +1306,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1285
1306
  ctx: currentCtx,
1286
1307
  config: currentConfig,
1287
1308
  agents: currentAgents,
1288
- sessionRef: currentModelRef(currentCtx),
1289
1309
  },
1290
1310
  {
1291
1311
  sessionId: forkedSession.sessionId,
@@ -1294,8 +1314,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1294
1314
  worktree: childWorktree,
1295
1315
  forkedFromRunId: runId,
1296
1316
  forkObjective,
1297
- modelPool: [...thread.modelPool],
1298
- thinkingLevel: thread.thinkingLevel,
1299
1317
  },
1300
1318
  );
1301
1319
  if (child.exitCode !== -1 || child.runId === undefined) {
@@ -1345,14 +1363,15 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1345
1363
  if (runtime.threads.get(runId)?.generation !== generation) return;
1346
1364
  let result: SingleResult;
1347
1365
  try {
1348
- result = await runSingleAgentWithModelFallback(
1366
+ result = await runSingleAgentWithMainFallback(
1349
1367
  {
1350
1368
  defaultCwd: executionCwd,
1351
- agent: pool.agent,
1369
+ agent: route.agent,
1352
1370
  agentName,
1353
1371
  task,
1354
1372
  cwd: executionCwd,
1355
1373
  thinkingLevel,
1374
+ thinkingLevelForModel: route.thinkingLevelForModel,
1356
1375
  signal: backgroundSignal,
1357
1376
  onLive,
1358
1377
  control,
@@ -1364,11 +1383,11 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1364
1383
  sessionDir: priorSessionDir,
1365
1384
  stdinText: seed?.prompt ?? (newObjectiveOnResume
1366
1385
  ? task
1367
- : buildResumePrompt(priorTask ?? task, buildFallbackResumeReason())),
1386
+ : buildResumePrompt(priorTask ?? task, "the retained thread was resumed")),
1368
1387
  }
1369
1388
  : {}),
1370
1389
  },
1371
- pool.fallbackModelRefs,
1390
+ route.mainFallbackRef,
1372
1391
  );
1373
1392
  } catch (error) {
1374
1393
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -1387,6 +1406,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1387
1406
  // no monitor mutation, result registration, or completion delivery.
1388
1407
  if (runtime.threads.get(runId)?.generation !== generation) return;
1389
1408
  result.runId = runId;
1409
+ result.projectCwd = originalCwd;
1390
1410
  result.isolation = isolation;
1391
1411
  result.forkedFromRunId = thread.forkedFromRunId;
1392
1412
  result.forkChildRunIds = [...thread.forkChildRunIds];
@@ -1398,6 +1418,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1398
1418
  thread.lastResult = result;
1399
1419
  runtime.retainSession(result);
1400
1420
  monitor.setModel(runId, result.model, result.modelFallbackFrom);
1421
+ monitor.setThinking(runId, result.thinking);
1401
1422
 
1402
1423
  // Destructive stop owns publication once it has synchronously claimed
1403
1424
  // the lifecycle. Leave the partial result/session on the thread; the
@@ -1457,8 +1478,8 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1457
1478
  const completion: CompletionMessageItem = {
1458
1479
  agent: result.agent,
1459
1480
  block: modelLevel
1460
- ? `${formatCompletionBlock(result, runConfig.maxResultLines, runCtx.cwd)}\n\n${modelLevelTakeoverNote(result, { runId })}`
1461
- : 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),
1462
1483
  triggerTurn: completionTriggersTurn(result, runConfig.notifyOnReviewPass),
1463
1484
  };
1464
1485
  if (modelLevel) {
@@ -1512,7 +1533,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1512
1533
  !thread.retired;
1513
1534
  try {
1514
1535
  const crashed: SingleResult = {
1515
- ...dispatchFailedResult(pool.agent, control.getObjective(), error, thinkingLevel),
1536
+ ...dispatchFailedResult(route.agent, control.getObjective(), error, thinkingLevel),
1516
1537
  runId,
1517
1538
  isolation,
1518
1539
  forkedFromRunId: thread.forkedFromRunId,
@@ -1531,7 +1552,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1531
1552
  runtime.sendCompletionGroup([
1532
1553
  {
1533
1554
  agent: agent.name,
1534
- block: formatCompletionBlock(crashed, runConfig.maxResultLines, runCtx.cwd),
1555
+ block: formatCompletionBlock(crashed, runConfig.maxResultLines, crashed.projectCwd ?? originalCwd),
1535
1556
  triggerTurn: true,
1536
1557
  },
1537
1558
  ]);
@@ -1656,7 +1677,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1656
1677
  const pending = r.exitCode === -1;
1657
1678
  const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
1658
1679
  const usage = formatUsage(r.usage);
1659
- const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (pool fallback from ${r.modelFallbackFrom})` : ""}`;
1680
+ const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (main after ${r.modelFallbackFrom} failed)` : ""}`;
1660
1681
  const isolation = r.isolation === "worktree" ? ` · worktree ${r.integrationStatus ?? "active"}` : "";
1661
1682
  const runId = r.runId === undefined ? "" : `${theme.fg("dim", `#${r.runId}`)} `;
1662
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}` : ""}`)}`;
@@ -1671,7 +1692,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
1671
1692
  const pending = r.exitCode === -1;
1672
1693
  const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
1673
1694
  const usage = formatUsage(r.usage);
1674
- const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (pool fallback from ${r.modelFallbackFrom})` : ""}`;
1695
+ const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (main after ${r.modelFallbackFrom} failed)` : ""}`;
1675
1696
  const isolation = r.isolation === "worktree" ? ` · worktree ${r.integrationStatus ?? "active"}` : "";
1676
1697
  const runId = r.runId === undefined ? "" : `${theme.fg("dim", `#${r.runId}`)} `;
1677
1698
  lines.push(` ${icon} ${runId}${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${r.thinking ? ` · thinking ${r.thinking}` : ""}${isolation}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`);