pi-subagents 0.52.1 → 0.54.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +4 -0
  3. package/docs/configuration.md +12 -2
  4. package/docs/extension-api.md +3 -1
  5. package/docs/models.md +17 -3
  6. package/docs/workflows.md +2 -0
  7. package/index.ts +10 -1
  8. package/package.json +2 -1
  9. package/prompts/council.md +51 -0
  10. package/skills/council-mode/SKILL.md +231 -0
  11. package/skills/pi-subagents/SKILL.md +5 -1
  12. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -0
  13. package/skills/pi-subagents/references/execution-controls.md +13 -0
  14. package/skills/pi-subagents/references/prompting-and-roles.md +7 -0
  15. package/src/agents/agent-management.ts +107 -8
  16. package/src/agents/agent-serializer.ts +2 -0
  17. package/src/agents/agents.ts +96 -37
  18. package/src/agents/builtin-names.ts +9 -0
  19. package/src/agents/runtime-agent-registry.ts +418 -0
  20. package/src/api/agents.ts +7 -0
  21. package/src/api/preflight.ts +8 -3
  22. package/src/extension/config.ts +3 -0
  23. package/src/extension/doctor.ts +11 -0
  24. package/src/extension/fanout-child.ts +3 -2
  25. package/src/extension/index.ts +20 -4
  26. package/src/extension/public-execution.ts +1 -1
  27. package/src/extension/rpc.ts +41 -1
  28. package/src/extension/schemas.ts +6 -3
  29. package/src/extension/tool-description.ts +2 -2
  30. package/src/extension/tool-result.ts +19 -0
  31. package/src/inspectors/herdr/client.ts +3 -3
  32. package/src/runs/background/async-execution.ts +12 -6
  33. package/src/runs/background/async-job-tracker.ts +4 -3
  34. package/src/runs/background/async-resume.ts +2 -1
  35. package/src/runs/background/async-retention.ts +1 -1
  36. package/src/runs/background/async-status-snapshot.ts +14 -5
  37. package/src/runs/background/auto-drain.ts +1 -0
  38. package/src/runs/background/chain-root-attachment.ts +5 -0
  39. package/src/runs/background/result-watcher.ts +9 -0
  40. package/src/runs/background/stale-run-reconciler.ts +3 -0
  41. package/src/runs/background/subagent-runner.ts +32 -5
  42. package/src/runs/background/subagent-wait.ts +9 -5
  43. package/src/runs/background/terminal-run-index.ts +15 -6
  44. package/src/runs/background/wait-completions.ts +2 -0
  45. package/src/runs/background/wait-tool.ts +5 -3
  46. package/src/runs/foreground/execution.ts +34 -2
  47. package/src/runs/foreground/subagent-executor.ts +196 -52
  48. package/src/runs/foreground/workflow-detach-reconcile.ts +83 -15
  49. package/src/runs/shared/acceptance.ts +44 -1
  50. package/src/runs/shared/model-exclusions.ts +242 -0
  51. package/src/runs/shared/model-fallback.ts +72 -16
  52. package/src/runs/shared/model-scope.ts +106 -39
  53. package/src/runs/shared/pi-args.ts +34 -1
  54. package/src/runs/shared/subagent-control.ts +25 -3
  55. package/src/runs/shared/subagent-prompt-runtime.ts +36 -9
  56. package/src/shared/fork-context.ts +17 -1
  57. package/src/shared/model-info.ts +20 -0
  58. package/src/shared/settings.ts +2 -2
  59. package/src/shared/types.ts +47 -2
  60. package/src/slash/slash-commands.ts +20 -6
  61. package/src/slash/slash-live-state.ts +3 -3
  62. package/src/tui/fleet-status.ts +86 -1
  63. package/src/tui/fleet.ts +55 -2
  64. package/src/tui/render.ts +73 -3
  65. package/src/watchdog/permission-arbiter.ts +59 -51
  66. package/src/workflows/scripted-workflow.ts +100 -12
  67. package/src/workflows/workflow-receipt.ts +140 -0
@@ -27,7 +27,7 @@ export const SUBAGENT_RPC_REQUEST_EVENT = "subagents:rpc:v1:request";
27
27
  export const SUBAGENT_RPC_READY_EVENT = "subagents:rpc:v1:ready";
28
28
  export const SUBAGENT_RPC_REPLY_EVENT_PREFIX = "subagents:rpc:v1:reply:";
29
29
 
30
- export const SUBAGENT_RPC_METHODS = ["ping", "status", "spawn", "steer", "interrupt", "stop", "resume"] as const;
30
+ export const SUBAGENT_RPC_METHODS = ["ping", "status", "manage", "spawn", "steer", "interrupt", "stop", "resume"] as const;
31
31
  export type SubagentRpcMethod = typeof SUBAGENT_RPC_METHODS[number];
32
32
 
33
33
  export interface SubagentRpcRequestEnvelope {
@@ -58,6 +58,18 @@ export type SubagentRpcReplyEnvelope<T = unknown> = {
58
58
  };
59
59
  };
60
60
 
61
+ export const SUBAGENT_RPC_MANAGEMENT_ACTIONS = [
62
+ "schedule.list",
63
+ "schedule.show",
64
+ "schedule.history",
65
+ "schedule.pause",
66
+ "schedule.resume",
67
+ "schedule.run",
68
+ "schedule.delete",
69
+ ] as const;
70
+
71
+ type SubagentRpcManagementAction = typeof SUBAGENT_RPC_MANAGEMENT_ACTIONS[number];
72
+
61
73
  type SubagentRpcErrorCode =
62
74
  | "invalid_request"
63
75
  | "invalid_params"
@@ -375,6 +387,7 @@ function pingData(ctx: ExtensionContext | null) {
375
387
  methods: [...SUBAGENT_RPC_METHODS],
376
388
  capabilities: {
377
389
  status: true,
390
+ managementActions: [...SUBAGENT_RPC_MANAGEMENT_ACTIONS],
378
391
  fleetStatus: { version: 1 },
379
392
  asyncStatusSnapshot: { kind: ASYNC_STATUS_SNAPSHOT_KIND, version: ASYNC_STATUS_SNAPSHOT_VERSION },
380
393
  asyncSpawn: true,
@@ -412,6 +425,30 @@ async function executeChecked(
412
425
  return dataFromToolResult(result);
413
426
  }
414
427
 
428
+ function manageParams(params: unknown): SubagentParamsLike {
429
+ const input = assertRecordParams(params, "manage");
430
+ if (typeof input.action !== "string" || !(SUBAGENT_RPC_MANAGEMENT_ACTIONS as readonly string[]).includes(input.action)) {
431
+ throw new SubagentRpcError(
432
+ "invalid_params",
433
+ `RPC manage action must be one of: ${SUBAGENT_RPC_MANAGEMENT_ACTIONS.join(", ")}.`,
434
+ );
435
+ }
436
+ if (input.id !== undefined && (typeof input.id !== "string" || !input.id.trim())) {
437
+ throw new SubagentRpcError("invalid_params", "RPC manage id must be a non-empty string.");
438
+ }
439
+ const action = input.action as SubagentRpcManagementAction;
440
+ const requiresId = action !== "schedule.list";
441
+ if (requiresId && typeof input.id !== "string") {
442
+ throw new SubagentRpcError("invalid_params", `RPC manage ${action} requires id.`);
443
+ }
444
+ const output: SubagentParamsLike = {
445
+ action,
446
+ ...(typeof input.id === "string" ? { id: input.id.trim() } : {}),
447
+ };
448
+ assertSubagentParams(output, "RPC manage params");
449
+ return output;
450
+ }
451
+
415
452
  function spawnParams(params: unknown): SubagentParamsLike {
416
453
  const input = assertRecordParams(params, "spawn");
417
454
  const normalized = normalizePublicSubagentExecution(input);
@@ -535,6 +572,9 @@ async function handleRequest(
535
572
  if (request.method === "ping") return pingData(ctx);
536
573
  if (!ctx) throw new SubagentRpcError("no_active_session", "No active extension context for subagent RPC.");
537
574
 
575
+ if (request.method === "manage") {
576
+ return executeChecked(options, ctx, request.requestId, request.method, manageParams(request.params));
577
+ }
538
578
  if (request.method === "spawn") {
539
579
  return executeChecked(options, ctx, request.requestId, request.method, spawnParams(request.params));
540
580
  }
@@ -307,13 +307,13 @@ const SubagentParamProperties = {
307
307
  ],
308
308
  description: "Agent config for create/update. Object or JSON string."
309
309
  })),
310
- workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Trusted inline JavaScript statement body. Normally async unless asyncByDefault:false; set async:true when async matters. Use async:false only when the parent must block until completion, never for reviews or gates. Use explicit return for output. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. Use await runs.run(key, {agent, task, worktree?, gate?}) or runs.run(key, {resume, task}), runs.all([...]), await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}), runs.status(id), runs.ref(s), emit(value), console, and return. For ordinary parallel fanout, use await runs.all([{key, agent, task}, ...]); do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout, and each must later be observed with direct await, Promise.race, or Promise.all. runs.steer targets a prior stable child key, never a raw run id, and must be awaited or returned. Mission workflows also have async state.get(key) and state.set(key, JSONValue). Compose sequential and parallel phases dynamically. Set worktree:true at workflow or child level for a separate managed worktree; child fields override workflow defaults. gate is one host-run command and cannot be combined with acceptance. runs.run accepts one child only. No filesystem, shell, Pi tools, or host globals." })),
310
+ workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Trusted inline JavaScript statement body. Normally async unless asyncByDefault:false; set async:true when async matters. Use async:false only when the parent must block until completion, never for reviews or gates. Use explicit return for output. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. Use await runs.run(key, {agent, task, worktree?, gate?}) or runs.run(key, {resume, task}), where resume is a retained run id or {workflowRunId,key,latest:true} from a durable async workflow receipt. Use runs.all([...]), await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}), runs.status(id), runs.ref(s), emit(value), console, and return. For ordinary parallel fanout, use await runs.all([{key, agent, task}, ...]); do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout, and each must later be observed with direct await, Promise.race, or Promise.all. runs.steer targets a prior stable child key, never a raw run id, and must be awaited or returned. Mission workflows also have async state.get(key) and state.set(key, JSONValue). Compose sequential and parallel phases dynamically. Set worktree:true at workflow or child level for a separate managed worktree; child fields override workflow defaults. gate is one host-run command and cannot be combined with acceptance. runs.run accepts one child only. No filesystem, shell, Pi tools, or host globals." })),
311
311
  chatProgress: Type.Optional(Type.String({ enum: ["auto", "off", "live-card"], description: "WorkflowScript chat progress projection. auto shows a live in-chat card only for watched foreground workflows in the same Git repository; it is off otherwise. Explicit live-card requires same-repository async:false; async workflows should omit chatProgress or use auto/off." })),
312
312
  isolation: Type.Optional(Type.String({ enum: ["none", "worktree"], description: "Workflow child isolation. none runs in the shared cwd; worktree requires managed git worktree isolation." })),
313
313
  worktree: Type.Optional(Type.Boolean({ description: "Managed child isolation. true gives each workflow child a separate git worktree; an individual runs.run/runs.all item can override a workflow default with worktree:false." })),
314
314
  context: Type.Optional(Type.String({
315
- enum: ["fresh", "fork"],
316
- description: "'fresh' or 'fork' to branch from parent session. Explicit context overrides every child. If omitted, config defaultSubagentContext wins over each agent defaultContext; implicit fork needs a persisted parent session and leaf, else fresh.",
315
+ enum: ["fresh", "fork", "profile"],
316
+ description: "'fresh' or 'fork' to branch from parent session, or 'profile' to require the selected agent's declared defaultContext. Explicit fresh/fork overrides every child; profile ignores config defaultSubagentContext and fails when an agent has no defaultContext. If omitted, config defaultSubagentContext wins over each agent defaultContext; implicit fork needs a persisted parent session and leaf, else fresh.",
317
317
  })),
318
318
  async: Type.Optional(Type.Boolean({ description: "Run in background unless asyncByDefault:false. Set false only when the parent must block until completion." })),
319
319
  timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Timeout. Foreground and single async runs use config timeoutMs, else 30m; async composites have no default parent deadline. Alias maxRuntimeMs." })),
@@ -370,6 +370,9 @@ const SubagentWaitParamsSchema = Type.Object({
370
370
  minimum: 1,
371
371
  description: "Give up waiting after this many milliseconds (the runs keep going regardless). Defaults to 1800000 (30 minutes).",
372
372
  })),
373
+ stopOnAttention: Type.Optional(Type.Boolean({
374
+ description: "Blocking waits stop when a run needs attention by default. Set false to keep waiting through idle or long-thinking attention; supervisor/contact requests still stop the wait.",
375
+ })),
373
376
  });
374
377
 
375
378
  export const SubagentWaitParams = keepTopLevelParameterDescriptions(SubagentWaitParamsSchema);
@@ -36,7 +36,7 @@ EXECUTION:
36
36
  • WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel children; do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. Ordinary JavaScript provides sequence, branching, filtering, retries, and aggregation. workflowScript is an ordinary JavaScript statement body, so use an explicit return for a useful result. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. Pass async:false only when the parent must block until completion, never for final reviews or gates. Same-repo blocking workflows default to a live in-chat card; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Use {action:"children.list"} to list recent retained workflow children with resumable/not-resumable reasons. Resume only rows reported resumable. For a simple follow-up or implementation challenge, use {action:"resume", id:"run-id", message:"..."}. Resume keeps the stored agent/model/tool contract. If no resumable child is listed, launch a same-role fallback challenge and label it as fallback. Inside workflowScript, continue one with runs.run(key, {resume:"run-id", task:"follow-up"}); workflow resumes wait for completed output, and loops must continue from each latest returned runId. Await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}) to guide a prior keyed child without exposing its run id; receipts are queued, delivered, missed, or failed. Always await or return runs.steer. For repository mutation lanes, set worktree:true on the workflow or individual runs.run/runs.all item for managed isolation; each parallel child gets a separate worktree and handoff artifact. A workflow usageBudget is enforced once across the workflow. Available globals are runs.run, runs.all, runs.steer, runs.status, runs.ref/refs, emit, console, and standard JavaScript only. Workflows get async state.get(key) and state.set(key, JSONValue) through their automatic or explicit mission; mission:false workflows do not have a state global. Scripts cannot access filesystem, shell, arbitrary Pi tools, or host globals.
37
37
  • Sequential example: { workflowScript: "const a = await runs.run('analyze', {agent:'agent-a', task:'Analyze the request'}); return (await runs.run('plan', {agent:'agent-b', task:'Plan from: '+a.output})).output" }
38
38
  • Parallel example: { workflowScript: "const [a,b] = await runs.all([{key:'correctness',agent:'agent-a',task:'Review correctness'},{key:'tests',agent:'agent-b',task:'Review tests'}]); return {correctness:a.output,tests:b.output}" }
39
- • Optional context is "fresh" or "fork". Explicit context wins. When omitted, config defaultSubagentContext wins over agent defaultContext. timeoutMs/maxRuntimeMs apply to foreground and async workflows; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls; evidence levels end at verified, and acceptance.review.required requests independent writer review.
39
+ • Optional context is "fresh", "fork", or "profile". profile requires the selected agent's declared defaultContext and ignores config defaultSubagentContext. Explicit fresh/fork wins. When omitted, config defaultSubagentContext wins over agent defaultContext. timeoutMs/maxRuntimeMs apply to foreground and async workflows; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls; evidence levels end at verified, and acceptance.review.required requests independent writer review.
40
40
  • Durable mission attachment is automatic by default. Use missionId to attach an existing mission, mission:{...} to override auto-create, or mission:false for ephemeral work. A mission object needs exactly one non-empty title or summary; objective and labels are optional. goal may only be true and requires budget:{tokens}.
41
41
 
42
42
  MANAGEMENT / CONTROL (use action; omit execution fields):
@@ -53,7 +53,7 @@ EXECUTE:
53
53
  • SINGLE {agent:"worker",task:"..."} starts exactly one child through the workflow runtime. Workflow-level fields remain child defaults. Do not combine agent/task with action or workflowScript.
54
54
  • SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel work; do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. Await runs.steer(key,message,options?) to guide a prior keyed child; it returns queued, delivered, missed, or failed and never accepts a raw run id. Always await or return steering calls. Use {action:"children.list"} for recent retained workflow children and resume only rows reported resumable. Use {action:"resume",id:"run-id",message:"..."} for a simple follow-up or challenge; resume keeps the stored agent/model/tool contract. If none is resumable, launch a same-role fallback challenge and label it as fallback. Inside workflowScript use runs.run(key,{resume:"run-id",task:"follow-up"}) when the script must wait for completion and continue from the latest returned runId. Workflows get async state.get/state.set through their automatic or explicit mission; mission:false does not. Scripts are ordinary JavaScript statement bodies; use explicit return for a useful result. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Use JavaScript for sequence, branching, retries, and aggregation. For repository mutation lanes, use worktree:true on the workflow or runs.run/runs.all item for managed isolation. Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. async:false blocks the parent until completion and auto-enables a same-repo live chat card unless chatProgress is off; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off.
55
55
  • Example: {workflowScript:"const [a,b]=await runs.all([{key:'a',agent:'agent-a',task:'Implement A',worktree:true},{key:'b',agent:'agent-b',task:'Implement B',worktree:true}]); return [a.output,b.output]"}
56
- • context can be fresh or fork. Explicit context wins; omitted context follows defaultSubagentContext before agent defaultContext. timeoutMs/maxRuntimeMs apply to foreground and async workflows; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls.
56
+ • context can be fresh, fork, or profile. profile requires the selected agent's declared defaultContext and ignores defaultSubagentContext. Explicit fresh/fork wins; omitted context follows defaultSubagentContext before agent defaultContext. timeoutMs/maxRuntimeMs apply to foreground and async workflows; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls.
57
57
 
58
58
  MANAGE / CONTROL:
59
59
  • Use action without execution fields for list/get/models/guide/authoring, refine/refine.show/refine.rollback, mission, watchdog, status, interrupt, stop, resume, steer, script-only scheduling, diagnostics, and other management actions. guide reads shipped current-version docs by topic.
@@ -0,0 +1,19 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+
3
+ /**
4
+ * Convert pi-subagents' internal logical-error result into the rejection Pi's
5
+ * public tool boundary uses to emit a canonical errored ToolResult.
6
+ *
7
+ * Keep this at registered tool boundaries. Internal workflows intentionally
8
+ * retain their return-based error handling.
9
+ */
10
+ export function finalizeToolResult<T>(result: AgentToolResult<T>): AgentToolResult<T> {
11
+ if (result.isError !== true) return result;
12
+
13
+ const message = result.content
14
+ .flatMap((item) => item.type === "text" && typeof item.text === "string" ? [item.text] : [])
15
+ .join("\n")
16
+ .trim();
17
+
18
+ throw new Error(message || "pi-subagents reported a logical tool failure.");
19
+ }
@@ -1,4 +1,4 @@
1
- import { spawn, type ChildProcess } from "node:child_process";
1
+ import { spawn } from "node:child_process";
2
2
 
3
3
  export type HerdrErrorCode =
4
4
  | "HERDR_UNAVAILABLE"
@@ -16,7 +16,7 @@ export interface HerdrClient {
16
16
  run<T = unknown>(args: string[], options?: { timeoutMs?: number; signal?: AbortSignal; textOk?: boolean }): Promise<HerdrResult<T>>;
17
17
  }
18
18
 
19
- type SpawnHerdr = (command: string, args: readonly string[], options: { shell: false; windowsHide: true; env: NodeJS.ProcessEnv }) => ChildProcess;
19
+ type SpawnHerdr = (command: string, args: readonly string[], options: { shell: false; windowsHide: true; env: NodeJS.ProcessEnv }) => ReturnType<typeof spawn>;
20
20
 
21
21
  function error(code: HerdrErrorCode, message: string, details?: unknown): HerdrResult<never> {
22
22
  return { ok: false, error: { code, message, ...(details !== undefined ? { details } : {}) } };
@@ -46,7 +46,7 @@ export function createHerdrClient(options: { bin?: string; spawn?: SpawnHerdr }
46
46
  return {
47
47
  run<T>(args: string[], runOptions: { timeoutMs?: number; signal?: AbortSignal; textOk?: boolean } = {}): Promise<HerdrResult<T>> {
48
48
  return new Promise((resolve) => {
49
- let child: ChildProcess;
49
+ let child: ReturnType<typeof spawn>;
50
50
  try {
51
51
  child = spawnImpl(bin, args, { shell: false, windowsHide: true, env: process.env });
52
52
  } catch (cause) {
@@ -26,7 +26,7 @@ import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
26
26
  import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, PROMPT_REDACTED, resolveChildCwd } from "../../shared/utils.ts";
27
27
  import { buildModelCandidates, inheritsParentModel, resolveEffectiveSubagentModel, resolveModelCandidate, resolveSubagentModelOverride, type AvailableModelInfo, type ParentModel } from "../shared/model-fallback.ts";
28
28
  import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
29
- import type { ModelScopeConfig } from "../shared/model-scope.ts";
29
+ import { resolveModelScopesForAgent, type ModelScopeConfig } from "../shared/model-scope.ts";
30
30
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
31
31
  import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
32
32
  import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
@@ -791,18 +791,20 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
791
791
  const taskText = `${readInstructions.prefix}${taskTemplate}${progressInstructions.suffix}`;
792
792
  const task = namespaceOutputPath ? taskText : injectSingleOutputInstruction(taskText, outputPath, a);
793
793
 
794
+ const modelScopes = resolveModelScopesForAgent(ctx.modelScope, a.name, ctx.currentModel);
794
795
  const primaryModel = externalRunner ? undefined : resolveEffectiveSubagentModel(
795
796
  s.model,
796
797
  a.model,
797
798
  ctx.currentModel,
798
799
  availableModels,
799
800
  ctx.currentModelProvider,
800
- { scope: ctx.modelScope },
801
+ { scope: modelScopes },
801
802
  );
802
803
  const thinkingOverride = flatIndex === undefined ? undefined : thinkingOverridesByFlatIndex?.[flatIndex];
803
804
  const effectiveThinking = externalRunner ? undefined : thinkingOverride ?? a.thinking;
804
805
  const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinking, thinkingOverride !== undefined);
805
806
  const agentContract = s.agentContract ?? params.agentContract;
807
+ const permissionRules = resolvePermissionRules(ctx.permissions, a.permissions);
806
808
  const toolPlan = resolvePiLaunchToolPlan({
807
809
  tools: a.tools,
808
810
  extensions: a.extensions,
@@ -814,9 +816,9 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
814
816
  capabilityCeiling: params.capabilityCeiling,
815
817
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
816
818
  agentName: a.name,
819
+ permissionRules,
817
820
  });
818
821
  const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
819
- const permissionRules = resolvePermissionRules(ctx.permissions, a.permissions);
820
822
  if (externalRunner && permissionRules) {
821
823
  throw new AsyncStartValidationError(`Agent '${a.name}' uses runner.type='${externalRunnerType}', which cannot enforce native Pi child permission rules.`);
822
824
  }
@@ -839,7 +841,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
839
841
  thinking: resolveEffectiveThinking(model, effectiveThinking),
840
842
  launchResolvedExtensions,
841
843
  modelCandidates: externalRunner ? undefined : buildModelCandidates(primaryModel, a.fallbackModels, availableModels, ctx.currentModelProvider, {
842
- scope: ctx.modelScope,
844
+ scope: modelScopes,
843
845
  primaryModelFromParent: inheritsParentModel(s.model, a.model, ctx.currentModel),
844
846
  }).map((candidate) =>
845
847
  applyThinkingSuffix(candidate, effectiveThinking, thinkingOverride !== undefined),
@@ -1406,7 +1408,7 @@ export function executeAsyncSingle(
1406
1408
  const effectiveOutput = normalizeSingleOutputOverride(params.output, agentConfig.output);
1407
1409
  const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, instructionCwd, params.outputBaseDir ?? (artifactsDir ? path.join(artifactsDir, "outputs", id) : undefined));
1408
1410
  systemPrompt = injectOutputPathSystemPrompt(systemPrompt, outputPath, agentConfig);
1409
- const outputMode = params.outputMode ?? "inline";
1411
+ const outputMode = params.outputMode ?? agentConfig.outputMode ?? "inline";
1410
1412
  const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
1411
1413
  if (validationError) return formatAsyncStartError("single", validationError);
1412
1414
  const taskWithOutputInstruction = injectSingleOutputInstruction(task, outputPath, agentConfig);
@@ -1418,6 +1420,7 @@ export function executeAsyncSingle(
1418
1420
  ? `[Read from: ${readPaths.join(", ")}]\n\n`
1419
1421
  : "";
1420
1422
  const taskText = readsInstruction + taskWithOutputInstruction;
1423
+ const modelScopes = resolveModelScopesForAgent(ctx.modelScope, agentConfig.name, ctx.currentModel);
1421
1424
  const primaryModel = externalRunner ? undefined : params.modelOverrideFromParent
1422
1425
  ? params.modelOverride
1423
1426
  : resolveSubagentModelOverride(
@@ -1425,6 +1428,7 @@ export function executeAsyncSingle(
1425
1428
  ctx.currentModel,
1426
1429
  availableModels,
1427
1430
  ctx.currentModelProvider,
1431
+ { scope: modelScopes },
1428
1432
  );
1429
1433
  const effectiveThinking = externalRunner ? undefined : params.thinkingOverride ?? agentConfig.thinking;
1430
1434
  const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinking, params.thinkingOverride !== undefined);
@@ -1453,7 +1457,7 @@ export function executeAsyncSingle(
1453
1457
  const modelCandidates = externalRunner
1454
1458
  ? []
1455
1459
  : buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, {
1456
- scope: ctx.modelScope,
1460
+ scope: modelScopes,
1457
1461
  primaryModelFromParent: params.modelOverrideFromParent,
1458
1462
  })
1459
1463
  .flatMap((candidate) => {
@@ -1472,6 +1476,7 @@ export function executeAsyncSingle(
1472
1476
  capabilityCeiling,
1473
1477
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
1474
1478
  agentName: agentConfig.name,
1479
+ permissionRules: resolvePermissionRules(ctx.permissions, agentConfig.permissions),
1475
1480
  });
1476
1481
  const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
1477
1482
  const launchContractDigest = launchBindingDigest({
@@ -1534,6 +1539,7 @@ export function executeAsyncSingle(
1534
1539
  ...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
1535
1540
  ...(params.acceptance !== undefined ? { acceptance: params.acceptance } : {}),
1536
1541
  ...(controlConfig ? { controlConfig } : {}),
1542
+ ...(params.context ? { context: params.context } : {}),
1537
1543
  ...(params.intercomBridge !== undefined ? { intercomBridge: params.intercomBridge } : {}),
1538
1544
  ...(deadlineAt !== undefined ? { absoluteDeadlineAt: deadlineAt } : {}),
1539
1545
  ...(initialTurnBudget ? { initialTurnBudget: { maxTurns: initialTurnBudget.maxTurns, graceTurns: initialTurnBudget.graceTurns } } : {}),
@@ -324,7 +324,8 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
324
324
  };
325
325
 
326
326
  const refreshJob = (job: AsyncJobState): boolean => {
327
- const widgetStateBefore = widgetRenderKey(job);
327
+ const widgetExpanded = state.lastUiContext?.hasUI ? state.lastUiContext.ui.getToolsExpanded?.() ?? false : false;
328
+ const widgetStateBefore = widgetRenderKey(job, widgetExpanded);
328
329
  let nestedRefreshFailed = false;
329
330
  const refreshNestedProjection = () => {
330
331
  try {
@@ -422,7 +423,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
422
423
  scheduleCleanup(job.asyncId);
423
424
  }
424
425
  }
425
- return widgetRenderKey(job) !== widgetStateBefore;
426
+ return widgetRenderKey(job, widgetExpanded) !== widgetStateBefore;
426
427
  }
427
428
  if (job.status === "queued") {
428
429
  job.status = "running";
@@ -439,7 +440,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
439
440
  rememberFleetJob(state, job);
440
441
  if (!hasLiveNestedDescendants(job.nestedChildren) && !state.cleanupTimers.has(job.asyncId)) scheduleCleanup(job.asyncId);
441
442
  }
442
- return widgetRenderKey(job) !== widgetStateBefore;
443
+ return widgetRenderKey(job, widgetExpanded) !== widgetStateBefore;
443
444
  };
444
445
 
445
446
  const scheduleJobRefresh = (asyncId: string, delayMs = EVENT_REFRESH_DEBOUNCE_MS) => {
@@ -321,7 +321,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
321
321
  "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelOverrideFromParent", "fallbackModels", "thinking", "tools", "extensions",
322
322
  "subagentOnlyExtensions", "mcpDirectTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritSkills", "skills",
323
323
  "skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
324
- "artifactsDir", "maxOutput", "controlConfig", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
324
+ "artifactsDir", "maxOutput", "controlConfig", "context", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
325
325
  "launchResolvedExtensions", "runFanoutBudget",
326
326
  ]);
327
327
  for (const field of Object.keys(parsed)) {
@@ -345,6 +345,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
345
345
  }
346
346
  if (parsed.systemPromptMode !== "append" && parsed.systemPromptMode !== "replace") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': systemPromptMode is invalid.`);
347
347
  if (parsed.outputMode !== "inline" && parsed.outputMode !== "file-only") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': outputMode is invalid.`);
348
+ if (parsed.context !== undefined && parsed.context !== "fresh" && parsed.context !== "fork") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': context is invalid.`);
348
349
  if (parsed.modelOverrideFromParent !== undefined && typeof parsed.modelOverrideFromParent !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': modelOverrideFromParent must be a boolean.`);
349
350
  for (const field of ["inheritProjectContext", "inheritSkills", "share"] as const) {
350
351
  if (typeof parsed[field] !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a boolean.`);
@@ -600,7 +600,7 @@ function runRetentionDiscovery(input: {
600
600
  };
601
601
  const onAbort = (): void => fail(new RetentionCancelledError("Retention discovery was cancelled."));
602
602
  input.signal?.addEventListener("abort", onAbort, { once: true });
603
- worker.once("error", (error) => fail(error));
603
+ worker.once("error", (error) => fail(error instanceof Error ? error : new Error(String(error))));
604
604
  worker.once("exit", (code) => {
605
605
  if (!settled) fail(new Error(`Retention discovery worker exited before replying (${code}).`));
606
606
  });
@@ -227,12 +227,21 @@ function snapshotBytes(snapshot: AsyncStatusSnapshotV1): number {
227
227
  }
228
228
 
229
229
  function enforceByteLimit(snapshot: AsyncStatusSnapshotV1): void {
230
- while (snapshot.runs.length > 0 && snapshotBytes(snapshot) > snapshot.caps.maxSerializedBytes) {
231
- snapshot.runs.pop();
232
- snapshot.omitted.runs += 1;
233
- snapshot.omitted.byteLimitExceeded = true;
230
+ if (snapshotBytes(snapshot) <= snapshot.caps.maxSerializedBytes) return;
231
+ snapshot.omitted.byteLimitExceeded = true;
232
+ const runs = snapshot.runs;
233
+ const initialOmittedRuns = snapshot.omitted.runs;
234
+ let lower = 0;
235
+ let upper = Math.max(0, runs.length - 1);
236
+ while (lower < upper) {
237
+ const retained = Math.ceil((lower + upper) / 2);
238
+ snapshot.runs = runs.slice(0, retained);
239
+ snapshot.omitted.runs = initialOmittedRuns + runs.length - retained;
240
+ if (snapshotBytes(snapshot) <= snapshot.caps.maxSerializedBytes) lower = retained;
241
+ else upper = retained - 1;
234
242
  }
235
- if (snapshotBytes(snapshot) > snapshot.caps.maxSerializedBytes) snapshot.omitted.byteLimitExceeded = true;
243
+ snapshot.runs = runs.slice(0, lower);
244
+ snapshot.omitted.runs = initialOmittedRuns + runs.length - lower;
236
245
  }
237
246
 
238
247
  export function buildAsyncStatusSnapshot(jobs: Iterable<AsyncJobState>, options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshotV1 {
@@ -58,6 +58,7 @@ export async function drainOutstandingWork(deps: AutoDrainDeps): Promise<void> {
58
58
  now,
59
59
  stopOnAttention: false,
60
60
  failOnFailedRuns: true,
61
+ failOnAttention: true,
61
62
  },
62
63
  );
63
64
  if (waitResult.isError) {
@@ -21,6 +21,7 @@ export interface ImportedAsyncRootResult {
21
21
  model?: string;
22
22
  attemptedModels?: string[];
23
23
  modelAttempts?: ModelAttempt[];
24
+ contextOverflow?: boolean;
24
25
  totalCost?: CostSummary;
25
26
  structuredOutput?: unknown;
26
27
  structuredOutputPath?: string;
@@ -49,6 +50,7 @@ interface AsyncResultFile {
49
50
  model?: string;
50
51
  attemptedModels?: string[];
51
52
  modelAttempts?: ModelAttempt[];
53
+ contextOverflow?: boolean;
52
54
  totalCost?: CostSummary;
53
55
  structuredOutput?: unknown;
54
56
  structuredOutputPath?: string;
@@ -110,6 +112,7 @@ function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus,
110
112
  ...(step?.model ? { model: step.model } : {}),
111
113
  ...(step?.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
112
114
  ...(step?.modelAttempts ? { modelAttempts: step.modelAttempts } : {}),
115
+ ...(step?.contextOverflow ? { contextOverflow: true } : {}),
113
116
  ...(step?.totalCost ? { totalCost: step.totalCost } : {}),
114
117
  ...(step?.structuredOutput !== undefined ? { structuredOutput: step.structuredOutput } : {}),
115
118
  ...(step?.structuredOutputPath ? { structuredOutputPath: step.structuredOutputPath } : {}),
@@ -131,6 +134,7 @@ function outputFromTimeout(root: ImportedAsyncRoot, status: AsyncStatus | null,
131
134
  ...(step?.model ? { model: step.model } : {}),
132
135
  ...(step?.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
133
136
  ...(step?.modelAttempts ? { modelAttempts: step.modelAttempts } : {}),
137
+ ...(step?.contextOverflow ? { contextOverflow: true } : {}),
134
138
  ...(step?.totalCost ? { totalCost: step.totalCost } : {}),
135
139
  };
136
140
  }
@@ -158,6 +162,7 @@ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null
158
162
  ...(child?.model ?? step?.model ? { model: child?.model ?? step?.model } : {}),
159
163
  ...(child?.attemptedModels ?? step?.attemptedModels ? { attemptedModels: child?.attemptedModels ?? step?.attemptedModels } : {}),
160
164
  ...(child?.modelAttempts ?? step?.modelAttempts ? { modelAttempts: child?.modelAttempts ?? step?.modelAttempts } : {}),
165
+ ...(child?.contextOverflow || step?.contextOverflow ? { contextOverflow: true } : {}),
161
166
  ...(child?.totalCost ?? step?.totalCost ? { totalCost: child?.totalCost ?? step?.totalCost } : {}),
162
167
  ...(child?.structuredOutput !== undefined ? { structuredOutput: child.structuredOutput } : step?.structuredOutput !== undefined ? { structuredOutput: step.structuredOutput } : {}),
163
168
  ...(child?.structuredOutputPath ?? step?.structuredOutputPath ? { structuredOutputPath: child?.structuredOutputPath ?? step?.structuredOutputPath } : {}),
@@ -57,6 +57,8 @@ type ResultWatcherDeps = {
57
57
  coalesceDelayMs?: number;
58
58
  /** Returns true while a durable completion source needs periodic delivery checks. */
59
59
  hasDeliveryDemand?: () => boolean;
60
+ /** Control how slow result-index scans are logged. Defaults to \"activity\". */
61
+ resultScanLogging?: "all" | "activity" | "off";
60
62
  platform?: NodeJS.Platform;
61
63
  };
62
64
 
@@ -604,6 +606,13 @@ export function createResultWatcher(
604
606
  const logScanStats = (stats: ResultScanStats) => {
605
607
  const elapsed = Date.now() - stats.startedAt;
606
608
  if (elapsed < SLOW_RESULT_SCAN_MS) return;
609
+ const resultScanLogging = deps.resultScanLogging ?? "activity";
610
+ if (resultScanLogging === "off") return;
611
+ // A scan that inspected and scheduled nothing is a quiet no-op (e.g. the
612
+ // healthy periodic rescan while no async runs are pending). Under
613
+ // "activity", skip it so empty scans do not burn context tokens in the
614
+ // session transcript.
615
+ if (resultScanLogging === "activity" && stats.files === 0 && stats.scheduled === 0) return;
607
616
  console.error(`Subagent result scan inspected ${stats.files} indexed result file(s), scheduled ${stats.scheduled} in ${elapsed}ms (${resultsDir}).`);
608
617
  };
609
618
  const indexedResultCandidates = (observed: ReadonlySet<string>): string[] => {
@@ -97,6 +97,7 @@ interface ResultChildOutcome {
97
97
  thinking?: string;
98
98
  attemptedModels?: string[];
99
99
  modelAttempts?: NonNullable<AsyncStatus["steps"]>[number]["modelAttempts"];
100
+ contextOverflow?: boolean;
100
101
  }
101
102
 
102
103
  interface ResultRepairData {
@@ -163,6 +164,7 @@ function terminalStatusFromResult(status: AsyncStatus, resultPath: string, now:
163
164
  thinking,
164
165
  attemptedModels: child?.attemptedModels ?? step.attemptedModels,
165
166
  modelAttempts: child?.modelAttempts ?? step.modelAttempts,
167
+ contextOverflow: child?.contextOverflow ?? step.contextOverflow,
166
168
  };
167
169
  });
168
170
  const terminalStatus: AsyncStatus = {
@@ -258,6 +260,7 @@ function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, r
258
260
  model: step.model,
259
261
  attemptedModels: step.attemptedModels,
260
262
  modelAttempts: step.modelAttempts,
263
+ contextOverflow: step.contextOverflow,
261
264
  sessionFile: step.sessionFile,
262
265
  })),
263
266
  exitCode: 1,
@@ -85,7 +85,7 @@ import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
85
85
  import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
86
86
  import { claimRunFanoutBatch, getRunFanoutBudgetSnapshot } from "../shared/run-fanout-budget.ts";
87
87
  import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
88
- import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
88
+ import { formatModelAttemptNote, isContextOverflow, isRetryableModelFailure, recordRetryableModelFailure } from "../shared/model-fallback.ts";
89
89
  import {
90
90
  SUBAGENT_STARTUP_RETRY_DELAYS_MS,
91
91
  formatSubagentExtensionConflictError,
@@ -236,6 +236,8 @@ interface StepResult {
236
236
  model?: string;
237
237
  attemptedModels?: string[];
238
238
  modelAttempts?: ModelAttempt[];
239
+ /** True when the dispatch failed because the input exceeded the model's context window. */
240
+ contextOverflow?: boolean;
239
241
  totalCost?: CostSummary;
240
242
  artifactPaths?: ArtifactPaths;
241
243
  outputSaveError?: string;
@@ -1257,6 +1259,7 @@ async function runSingleStepInner(
1257
1259
  model: imported.model,
1258
1260
  attemptedModels: imported.attemptedModels,
1259
1261
  modelAttempts: imported.modelAttempts,
1262
+ contextOverflow: imported.contextOverflow,
1260
1263
  totalCost: imported.totalCost,
1261
1264
  structuredOutput: timedOut || stopped ? undefined : imported.structuredOutput,
1262
1265
  structuredOutputPath: timedOut || stopped ? undefined : imported.structuredOutputPath,
@@ -1432,8 +1435,8 @@ async function runSingleStepInner(
1432
1435
  });
1433
1436
  }
1434
1437
 
1435
- const candidates = step.modelCandidates && step.modelCandidates.length > 0
1436
- ? step.modelCandidates
1438
+ const candidates = step.modelCandidates !== undefined
1439
+ ? step.modelCandidates.length > 0 ? step.modelCandidates : [undefined]
1437
1440
  : step.model
1438
1441
  ? [step.model]
1439
1442
  : [undefined];
@@ -1458,6 +1461,8 @@ async function runSingleStepInner(
1458
1461
  // Escalated to "file" after an unexplained zero-activity startup failure so
1459
1462
  // retries keep the task text out of argv (endpoint pre-exec scans may deny it).
1460
1463
  let taskDeliveryOverride: SubagentTaskDelivery | undefined;
1464
+ let contextOverflow = false;
1465
+ let launchWarningsEmitted = false;
1461
1466
  modelAttemptsLoop: while (modelIndex < candidates.length) {
1462
1467
  if (ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
1463
1468
  const candidate = candidates[modelIndex];
@@ -1479,7 +1484,7 @@ async function runSingleStepInner(
1479
1484
  childIndex: ctx.flatIndex,
1480
1485
  })
1481
1486
  : undefined;
1482
- const { args, env, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit: attemptCapabilityAudit } = buildPiArgs(omitUndefinedProperties({
1487
+ const { args, env, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit: attemptCapabilityAudit, warnings } = buildPiArgs(omitUndefinedProperties({
1483
1488
  parentSessionId: step.parentSessionId,
1484
1489
  baseArgs: ["--mode", "json", "-p"],
1485
1490
  task,
@@ -1525,6 +1530,10 @@ async function runSingleStepInner(
1525
1530
  childWatchdog,
1526
1531
  waitToolEnabled: step.waitToolEnabled,
1527
1532
  }));
1533
+ if (!launchWarningsEmitted && warnings.length > 0) {
1534
+ for (const warning of warnings) console.warn(`[pi-subagents] ${warning}`);
1535
+ launchWarningsEmitted = true;
1536
+ }
1528
1537
  if (step.definitionDigest) {
1529
1538
  const toolPlan = resolvePiLaunchToolPlan(omitUndefinedProperties({
1530
1539
  tools: step.tools,
@@ -1536,6 +1545,7 @@ async function runSingleStepInner(
1536
1545
  structuredOutput: Boolean(effectiveStructuredOutput),
1537
1546
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1538
1547
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
1548
+ permissionRules: step.permissionRules,
1539
1549
  }));
1540
1550
  launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
1541
1551
  actualLaunchContractDigest = launchBindingDigest(omitUndefinedProperties({
@@ -1769,7 +1779,14 @@ async function runSingleStepInner(
1769
1779
  finalResult.finalOutput = startupError;
1770
1780
  break modelAttemptsLoop;
1771
1781
  }
1772
- if (!isRetryableModelFailure(error) || modelIndex === candidates.length - 1) break modelAttemptsLoop;
1782
+ const retryableModelFailure = isRetryableModelFailure(error);
1783
+ if (retryableModelFailure) recordRetryableModelFailure(candidate ?? run.model ?? step.model, error);
1784
+ if (isContextOverflow(error)) {
1785
+ contextOverflow = true;
1786
+ attemptNotes.push(`[fallback] ${attempt.model} failed: context overflow — the input exceeds this model's context window. Reduce the task input or use a model with a larger context window.`);
1787
+ break modelAttemptsLoop;
1788
+ }
1789
+ if (!retryableModelFailure || modelIndex === candidates.length - 1) break modelAttemptsLoop;
1773
1790
  attemptNotes.push(formatModelAttemptNote(attempt, candidates[modelIndex + 1]));
1774
1791
  modelIndex += 1;
1775
1792
  startupAttemptIndex = 0;
@@ -1906,6 +1923,7 @@ async function runSingleStepInner(
1906
1923
  model: finalResult?.model,
1907
1924
  attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
1908
1925
  modelAttempts,
1926
+ contextOverflow: contextOverflow || undefined,
1909
1927
  totalCost: costSummaryFromAttempts(modelAttempts),
1910
1928
  artifactPaths,
1911
1929
  outputSaveError: artifactErrors.outputSaveError,
@@ -2518,6 +2536,7 @@ async function runSubagent(
2518
2536
  model: step.model,
2519
2537
  attemptedModels: step.attemptedModels,
2520
2538
  modelAttempts: step.modelAttempts,
2539
+ contextOverflow: step.contextOverflow,
2521
2540
  })),
2522
2541
  exitCode: state === "complete" || state === "paused" ? 0 : 1,
2523
2542
  timestamp: now,
@@ -3330,6 +3349,7 @@ async function runSubagent(
3330
3349
  startedAt: step.startedAt ?? overallStartTime,
3331
3350
  lastActivityAt,
3332
3351
  currentTool: step.currentTool,
3352
+ thinking: step.thinking,
3333
3353
  now,
3334
3354
  }));
3335
3355
  if (idleState === "needs_attention") {
@@ -3866,6 +3886,7 @@ async function runSubagent(
3866
3886
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, fi).thinking));
3867
3887
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "attemptedModels", singleResult.attemptedModels);
3868
3888
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "modelAttempts", singleResult.modelAttempts);
3889
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "contextOverflow", singleResult.contextOverflow);
3869
3890
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "totalCost", singleResult.totalCost);
3870
3891
  if (singleResult.totalCost) {
3871
3892
  pendingParallelUsageCost = {
@@ -3935,6 +3956,7 @@ async function runSubagent(
3935
3956
  model: pr.model,
3936
3957
  attemptedModels: pr.attemptedModels,
3937
3958
  modelAttempts: pr.modelAttempts,
3959
+ contextOverflow: pr.contextOverflow,
3938
3960
  totalCost: pr.totalCost,
3939
3961
  artifactPaths: pr.artifactPaths,
3940
3962
  transcriptPath: pr.transcriptPath,
@@ -4260,6 +4282,7 @@ async function runSubagent(
4260
4282
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, fi).thinking));
4261
4283
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "attemptedModels", singleResult.attemptedModels);
4262
4284
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "modelAttempts", singleResult.modelAttempts);
4285
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "contextOverflow", singleResult.contextOverflow);
4263
4286
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "totalCost", singleResult.totalCost);
4264
4287
  if (singleResult.totalCost) {
4265
4288
  pendingParallelUsageCost = {
@@ -4363,6 +4386,7 @@ async function runSubagent(
4363
4386
  model: pr.model,
4364
4387
  attemptedModels: pr.attemptedModels,
4365
4388
  modelAttempts: pr.modelAttempts,
4389
+ contextOverflow: pr.contextOverflow,
4366
4390
  totalCost: pr.totalCost,
4367
4391
  artifactPaths: pr.artifactPaths,
4368
4392
  transcriptPath: pr.transcriptPath,
@@ -4580,6 +4604,7 @@ async function runSubagent(
4580
4604
  model: singleResult.model,
4581
4605
  attemptedModels: singleResult.attemptedModels,
4582
4606
  modelAttempts: singleResult.modelAttempts,
4607
+ contextOverflow: singleResult.contextOverflow,
4583
4608
  totalCost: singleResult.totalCost,
4584
4609
  artifactPaths: singleResult.artifactPaths,
4585
4610
  transcriptPath: singleResult.transcriptPath,
@@ -4658,6 +4683,7 @@ async function runSubagent(
4658
4683
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, flatIndex).thinking));
4659
4684
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "attemptedModels", singleResult.attemptedModels);
4660
4685
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "modelAttempts", singleResult.modelAttempts);
4686
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "contextOverflow", singleResult.contextOverflow);
4661
4687
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "totalCost", singleResult.totalCost);
4662
4688
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "error", stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error);
4663
4689
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "transcriptPath", singleResult.transcriptPath ?? requiredStatusStep(statusPayload, flatIndex).transcriptPath);
@@ -4932,6 +4958,7 @@ async function runSubagent(
4932
4958
  model: r.model,
4933
4959
  attemptedModels: r.attemptedModels,
4934
4960
  modelAttempts: r.modelAttempts,
4961
+ contextOverflow: r.contextOverflow,
4935
4962
  totalCost: r.totalCost,
4936
4963
  artifactPaths: r.artifactPaths,
4937
4964
  outputSaveError: r.outputSaveError,