@oh-my-pi/pi-coding-agent 17.2.8 → 17.2.10

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 (246) hide show
  1. package/CHANGELOG.md +94 -1
  2. package/dist/{CHANGELOG-0b0w17se.md → CHANGELOG-rhwzpexa.md} +94 -1
  3. package/dist/cli.js +13216 -15614
  4. package/dist/types/async/job-manager.d.ts +12 -0
  5. package/dist/types/cli/args.d.ts +1 -0
  6. package/dist/types/config/model-resolver.d.ts +13 -0
  7. package/dist/types/discovery/agents.d.ts +11 -0
  8. package/dist/types/extensibility/custom-commands/types.d.ts +5 -5
  9. package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
  10. package/dist/types/extensibility/extensions/loader.d.ts +2 -2
  11. package/dist/types/extensibility/extensions/types.d.ts +34 -8
  12. package/dist/types/extensibility/hooks/types.d.ts +5 -5
  13. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +10 -0
  14. package/dist/types/hindsight/state.d.ts +0 -1
  15. package/dist/types/index.d.ts +2 -2
  16. package/dist/types/lsp/utils.d.ts +8 -0
  17. package/dist/types/main.d.ts +1 -1
  18. package/dist/types/modes/acp/acp-agent.d.ts +1 -1
  19. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
  20. package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -1
  21. package/dist/types/modes/acp/acp-mode.d.ts +1 -1
  22. package/dist/types/modes/components/agent-hub-projection.d.ts +38 -0
  23. package/dist/types/modes/components/agent-hub-renderer.d.ts +41 -0
  24. package/dist/types/modes/components/agent-hub.d.ts +12 -5
  25. package/dist/types/modes/components/model-browser.d.ts +9 -1
  26. package/dist/types/modes/interactive-mode.d.ts +8 -1
  27. package/dist/types/modes/session-observer-registry.d.ts +2 -0
  28. package/dist/types/registry/agent-lifecycle.d.ts +1 -1
  29. package/dist/types/registry/agent-registry.d.ts +47 -0
  30. package/dist/types/registry/persisted-agents.d.ts +3 -1
  31. package/dist/types/session/agent-session-types.d.ts +7 -0
  32. package/dist/types/session/agent-session.d.ts +8 -4
  33. package/dist/types/session/session-entries.d.ts +10 -0
  34. package/dist/types/session/session-loader.d.ts +11 -1
  35. package/dist/types/session/session-manager.d.ts +9 -1
  36. package/dist/types/session/session-tools.d.ts +10 -1
  37. package/dist/types/session/turn-recovery.d.ts +6 -2
  38. package/dist/types/slash-commands/acp-builtins.d.ts +1 -1
  39. package/dist/types/slash-commands/available-commands.d.ts +1 -1
  40. package/dist/types/task/executor.d.ts +10 -0
  41. package/dist/types/task/index.d.ts +2 -3
  42. package/dist/types/task/read-only-policy.d.ts +3 -0
  43. package/dist/types/task/structured-subagent.d.ts +2 -0
  44. package/dist/types/task/types.d.ts +4 -0
  45. package/dist/types/tools/bash.d.ts +3 -3
  46. package/dist/types/tools/browser/launch.d.ts +3 -0
  47. package/dist/types/tools/browser/tab-worker.d.ts +2 -2
  48. package/dist/types/tools/path-utils.d.ts +8 -0
  49. package/dist/types/tools/read.d.ts +3 -5
  50. package/dist/types/tools/run-scope.d.ts +27 -1
  51. package/dist/types/tools/shell-tokenize.d.ts +21 -3
  52. package/dist/types/tools/terminal-output.d.ts +1 -1
  53. package/dist/types/utils/changelog.d.ts +3 -2
  54. package/dist/types/utils/late-cleanup.d.ts +2 -0
  55. package/dist/types/utils/turndown.d.ts +1 -1
  56. package/dist/types/vibe/runtime.d.ts +4 -3
  57. package/dist/types/web/scrapers/readthedocs.d.ts +0 -3
  58. package/dist/types/web/search/providers/browser-headers.d.ts +2 -3
  59. package/examples/custom-tools/README.md +1 -1
  60. package/examples/extensions/README.md +1 -1
  61. package/examples/extensions/api-demo.ts +5 -6
  62. package/examples/extensions/chalk-logger.ts +1 -1
  63. package/examples/extensions/hello.ts +2 -2
  64. package/examples/extensions/reload-runtime.ts +1 -1
  65. package/examples/extensions/tools.ts +2 -2
  66. package/examples/extensions/with-deps/index.ts +1 -1
  67. package/examples/sdk/06-extensions.ts +1 -1
  68. package/package.json +15 -31
  69. package/scripts/bundle-dist.ts +1 -11
  70. package/scripts/omp +11 -2
  71. package/src/advisor/runtime.ts +54 -0
  72. package/src/async/job-manager.ts +26 -0
  73. package/src/cli/agents-cli.ts +1 -1
  74. package/src/cli/args.ts +24 -2
  75. package/src/cli/auth-broker-cli.ts +1 -1
  76. package/src/cli/auth-gateway-cli.ts +1 -1
  77. package/src/cli/bench-cli.ts +1 -1
  78. package/src/cli/claude-trace-cli.ts +1 -1
  79. package/src/cli/config-cli.ts +1 -1
  80. package/src/cli/dry-balance-cli.ts +1 -1
  81. package/src/cli/file-processor.ts +1 -1
  82. package/src/cli/flag-tables.ts +4 -0
  83. package/src/cli/grep-cli.ts +1 -1
  84. package/src/cli/grievances-cli.ts +1 -1
  85. package/src/cli/help-extra.ts +1 -1
  86. package/src/cli/models-cli.ts +1 -1
  87. package/src/cli/plugin-cli.ts +1 -1
  88. package/src/cli/read-cli.ts +1 -1
  89. package/src/cli/setup-cli.ts +1 -1
  90. package/src/cli/shell-cli.ts +1 -1
  91. package/src/cli/ssh-cli.ts +1 -1
  92. package/src/cli/stats-cli.ts +4 -4
  93. package/src/cli/tiny-models-cli.ts +1 -1
  94. package/src/cli/ttsr-cli.ts +1 -1
  95. package/src/cli/update-cli.ts +3 -57
  96. package/src/cli/usage-cli.ts +1 -1
  97. package/src/cli/web-search-cli.ts +1 -1
  98. package/src/cli/worktree-cli.ts +1 -1
  99. package/src/commands/say.ts +1 -1
  100. package/src/commands/token.ts +1 -1
  101. package/src/commit/agentic/agent.ts +1 -1
  102. package/src/commit/cli.ts +1 -1
  103. package/src/config/config-file.ts +2 -2
  104. package/src/config/keybindings.ts +3 -3
  105. package/src/config/model-discovery.ts +36 -2
  106. package/src/config/model-registry.ts +4 -1
  107. package/src/config/model-resolver.ts +54 -7
  108. package/src/discovery/agents.ts +73 -3
  109. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +5 -4
  110. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -1
  111. package/src/discovery/claude.ts +8 -5
  112. package/src/discovery/cursor.ts +8 -5
  113. package/src/discovery/gemini.ts +11 -8
  114. package/src/discovery/vscode.ts +1 -0
  115. package/src/discovery/windsurf.ts +4 -2
  116. package/src/edit/hashline/filesystem.ts +7 -7
  117. package/src/eval/agent-bridge.ts +1 -3
  118. package/src/eval/jl/prelude.jl +4 -4
  119. package/src/eval/js/shared/prelude.txt +2 -2
  120. package/src/eval/py/prelude.py +0 -3
  121. package/src/eval/py/runner.py +38 -1
  122. package/src/eval/rb/prelude.rb +1 -2
  123. package/src/extensibility/custom-commands/loader.ts +4 -4
  124. package/src/extensibility/custom-commands/types.ts +5 -5
  125. package/src/extensibility/custom-tools/loader.ts +4 -4
  126. package/src/extensibility/custom-tools/types.ts +1 -1
  127. package/src/extensibility/extensions/loader.ts +7 -6
  128. package/src/extensibility/extensions/types.ts +39 -8
  129. package/src/extensibility/hooks/loader.ts +4 -5
  130. package/src/extensibility/hooks/types.ts +5 -5
  131. package/src/extensibility/legacy-pi-coding-agent-shim.ts +23 -0
  132. package/src/extensibility/plugins/legacy-pi-compat.ts +53 -0
  133. package/src/hindsight/state.ts +2 -22
  134. package/src/index.ts +2 -2
  135. package/src/internal-urls/memory-protocol.ts +33 -0
  136. package/src/launch/terminal-output.ts +1 -1
  137. package/src/lsp/client.ts +2 -2
  138. package/src/lsp/utils.ts +29 -0
  139. package/src/main.ts +85 -21
  140. package/src/markit/converters/docx.ts +1 -1
  141. package/src/markit/converters/epub.ts +1 -1
  142. package/src/markit/converters/pptx.ts +1 -1
  143. package/src/markit/converters/xlsx.ts +1 -1
  144. package/src/modes/acp/acp-agent.ts +7 -6
  145. package/src/modes/acp/acp-client-bridge.ts +1 -1
  146. package/src/modes/acp/acp-event-mapper.ts +1 -1
  147. package/src/modes/acp/acp-mode.ts +1 -1
  148. package/src/modes/components/agent-hub-projection.ts +248 -0
  149. package/src/modes/components/agent-hub-renderer.ts +194 -0
  150. package/src/modes/components/agent-hub.ts +670 -192
  151. package/src/modes/components/agent-transcript-viewer.ts +1 -3
  152. package/src/modes/components/assistant-message.ts +1 -1
  153. package/src/modes/components/bordered-loader.ts +1 -1
  154. package/src/modes/components/custom-editor.ts +2 -2
  155. package/src/modes/components/extensions/inspector-panel.ts +11 -6
  156. package/src/modes/components/footer.ts +1 -3
  157. package/src/modes/components/hook-input.ts +1 -1
  158. package/src/modes/components/hook-selector.ts +2 -2
  159. package/src/modes/components/model-browser.ts +11 -3
  160. package/src/modes/components/model-hub.ts +4 -1
  161. package/src/modes/components/session-selector.ts +2 -2
  162. package/src/modes/components/status-line/segments.ts +4 -3
  163. package/src/modes/controllers/event-controller.ts +188 -7
  164. package/src/modes/controllers/extension-ui-controller.ts +4 -3
  165. package/src/modes/controllers/mcp-command-controller.ts +81 -20
  166. package/src/modes/controllers/selector-controller.ts +40 -49
  167. package/src/modes/controllers/streaming-reveal.ts +1 -1
  168. package/src/modes/interactive-mode.ts +13 -2
  169. package/src/modes/print-mode.ts +29 -12
  170. package/src/modes/rpc/rpc-mode.ts +2 -2
  171. package/src/modes/runtime-init.ts +1 -1
  172. package/src/modes/session-observer-registry.ts +5 -0
  173. package/src/modes/theme/theme.ts +2 -2
  174. package/src/prompts/tools/computer.md +1 -1
  175. package/src/registry/agent-lifecycle.ts +35 -10
  176. package/src/registry/agent-registry.ts +65 -2
  177. package/src/registry/persisted-agents.ts +341 -16
  178. package/src/sdk.ts +11 -5
  179. package/src/session/agent-session-types.ts +8 -0
  180. package/src/session/agent-session.ts +177 -188
  181. package/src/session/session-entries.ts +10 -0
  182. package/src/session/session-listing.ts +1 -1
  183. package/src/session/session-loader.ts +74 -8
  184. package/src/session/session-manager.ts +22 -2
  185. package/src/session/session-paths.ts +105 -110
  186. package/src/session/session-tools.ts +28 -1
  187. package/src/session/settings-stream-fn.ts +7 -3
  188. package/src/session/turn-recovery.ts +236 -18
  189. package/src/session/unexpected-stop-classifier.ts +12 -3
  190. package/src/slash-commands/acp-builtins.ts +1 -1
  191. package/src/slash-commands/available-commands.ts +1 -1
  192. package/src/slash-commands/helpers/stats-dashboard.ts +2 -1
  193. package/src/task/agents.ts +1 -1
  194. package/src/task/executor.ts +173 -47
  195. package/src/task/index.ts +9 -28
  196. package/src/task/isolation-runner.ts +43 -12
  197. package/src/task/persisted-revive.ts +12 -5
  198. package/src/task/read-only-policy.ts +27 -0
  199. package/src/task/structured-subagent.ts +31 -6
  200. package/src/task/types.ts +4 -0
  201. package/src/tiny/worker.ts +1 -1
  202. package/src/tools/auto-generated-guard.ts +1 -1
  203. package/src/tools/bash-interactive.ts +4 -4
  204. package/src/tools/bash-interceptor.ts +8 -4
  205. package/src/tools/bash.ts +6 -6
  206. package/src/tools/browser/cmux/cmux-tab.ts +66 -18
  207. package/src/tools/browser/launch.ts +76 -19
  208. package/src/tools/browser/readable.ts +9 -9
  209. package/src/tools/browser/tab-supervisor.ts +12 -3
  210. package/src/tools/browser/tab-worker-entry.ts +1 -1
  211. package/src/tools/browser/tab-worker.ts +101 -12
  212. package/src/tools/fetch.ts +1 -1
  213. package/src/tools/glob.ts +4 -2
  214. package/src/tools/path-utils.ts +22 -5
  215. package/src/tools/read.ts +22 -5
  216. package/src/tools/run-scope.ts +290 -4
  217. package/src/tools/shell-tokenize.ts +38 -9
  218. package/src/tools/terminal-output.ts +1 -1
  219. package/src/tools/todo.ts +1 -1
  220. package/src/utils/changelog.ts +5 -4
  221. package/src/utils/clipboard.ts +10 -2
  222. package/src/utils/external-editor.ts +4 -2
  223. package/src/utils/jj.ts +1 -1
  224. package/src/utils/late-cleanup.ts +17 -0
  225. package/src/utils/turndown.ts +1 -2
  226. package/src/vibe/runtime.ts +202 -58
  227. package/src/web/scrapers/arxiv.ts +1 -1
  228. package/src/web/scrapers/go-pkg.ts +1 -1
  229. package/src/web/scrapers/hackage.ts +1 -12
  230. package/src/web/scrapers/iacr.ts +1 -1
  231. package/src/web/scrapers/readthedocs.ts +2 -1
  232. package/src/web/scrapers/twitter.ts +2 -2
  233. package/src/web/scrapers/types.ts +1 -1
  234. package/src/web/scrapers/wikipedia.ts +1 -1
  235. package/src/web/search/providers/browser-headers.ts +12 -39
  236. package/src/web/search/providers/codex.ts +3 -26
  237. package/src/web/search/providers/duckduckgo.ts +1 -1
  238. package/src/web/search/providers/ecosia.ts +2 -2
  239. package/src/web/search/providers/exa.ts +1 -1
  240. package/src/web/search/providers/firecrawl.ts +1 -1
  241. package/src/web/search/providers/google.ts +1 -1
  242. package/src/web/search/providers/mojeek.ts +2 -2
  243. package/src/web/search/providers/perplexity.ts +1 -1
  244. package/src/web/search/providers/public.ts +1 -1
  245. package/src/web/search/providers/startpage.ts +2 -2
  246. package/src/markit/converters/mammoth.d.ts +0 -24
@@ -17,6 +17,7 @@ import {
17
17
  formatModelStringWithRouting,
18
18
  resolveAgentPrewalkPattern,
19
19
  resolveConfiguredModelPatterns,
20
+ resolveExplicitModelRole,
20
21
  resolveModelOverride,
21
22
  resolveModelOverrideWithAuthFallback,
22
23
  } from "../config/model-resolver";
@@ -54,10 +55,12 @@ import { normalizeSchema } from "../tools/jtd-to-json-schema";
54
55
  import { buildOutputValidator, summarizeValidationFailure } from "../tools/output-schema-validator";
55
56
  import { ToolAbortError } from "../tools/tool-errors";
56
57
  import type { EventBus } from "../utils/event-bus";
58
+ import { trackLateCleanup } from "../utils/late-cleanup";
57
59
  import { buildNamedToolChoice } from "../utils/tool-choice";
58
60
  import type { WorkspaceTree } from "../workspace-tree";
59
61
  import { generateTaskLabel } from "./label";
60
62
  import { resolveAgentPrewalkDefault } from "./prewalk";
63
+ import { isReadOnlyAgent } from "./read-only-policy";
61
64
  import { subprocessToolRegistry } from "./subprocess-tool-registry";
62
65
  import {
63
66
  type AgentDefinition,
@@ -79,6 +82,7 @@ import { arrayValuedLabels, assembleYieldResult } from "./yield-assembly";
79
82
  export type { YieldItem } from "./types";
80
83
 
81
84
  const MCP_CALL_TIMEOUT_MS = 60_000;
85
+ const TASK_ABORT_CLEANUP_GRACE_MS = 10_000;
82
86
 
83
87
  /**
84
88
  * Soft per-agent request budgets (assistant requests per run). Crossing the
@@ -166,9 +170,11 @@ function resolveSubagentRetryFallbackCandidates(
166
170
  ): SubagentRetryFallbackCandidate[] {
167
171
  const candidates: SubagentRetryFallbackCandidate[] = [];
168
172
  const seen = new Set<string>();
173
+ const disabledProviders = new Set(settings.get("disabledProviders"));
169
174
  for (const pattern of modelPatterns) {
170
175
  const resolved = resolveModelOverride([pattern], modelRegistry, settings);
171
176
  if (!resolved.model) continue;
177
+ if (disabledProviders.has(resolved.model.provider)) continue;
172
178
  const selector = resolved.explicitThinkingLevel
173
179
  ? formatModelSelectorValue(formatModelStringWithRouting(resolved.model), resolved.thinkingLevel)
174
180
  : formatModelStringWithRouting(resolved.model);
@@ -179,8 +185,26 @@ function resolveSubagentRetryFallbackCandidates(
179
185
  return candidates;
180
186
  }
181
187
 
182
- function resolveSubagentDefaultRetryFallbackChain(settings: Settings): string[] | undefined {
183
- const fallbackChain = settings.get("retry.fallbackChains")?.default;
188
+ /**
189
+ * Chain a single-model subagent inherits when its own model patterns supply no
190
+ * fallbacks of their own. The child is pinned to a `subagent:<id>` role whose
191
+ * chain shadows every configured role chain (see
192
+ * {@link installSubagentRetryFallbackChain}), so a role-alias request (`@smol`)
193
+ * MUST inherit that role's chain — otherwise the pin silently re-routes the
194
+ * child onto the `default` role's chain. Explicit model selectors keep
195
+ * inheriting `default`: they carry no role identity, and a role that happens to
196
+ * be assigned the same model must not capture the child's fallback routing.
197
+ */
198
+ function resolveSubagentInheritedRetryFallbackChain(
199
+ settings: Settings,
200
+ modelRegistry: ModelRegistry,
201
+ modelPatterns: string[],
202
+ ): string[] | undefined {
203
+ const configuredChains = settings.get("retry.fallbackChains");
204
+ const role = resolveExplicitModelRole(modelPatterns, settings);
205
+ // An explicitly emptied role chain means "no fallbacks", not "inherit
206
+ // default" — mirrors expandDefaultRetryFallbackChains.
207
+ const fallbackChain = (role !== undefined ? configuredChains?.[role] : undefined) ?? configuredChains?.default;
184
208
  if (
185
209
  !Array.isArray(fallbackChain) ||
186
210
  fallbackChain.length === 0 ||
@@ -188,18 +212,22 @@ function resolveSubagentDefaultRetryFallbackChain(settings: Settings): string[]
188
212
  ) {
189
213
  return undefined;
190
214
  }
191
- return fallbackChain;
215
+ const disabledProviders = new Set(settings.get("disabledProviders"));
216
+ return fallbackChain.filter(entry => {
217
+ const resolved = resolveModelOverride([entry], modelRegistry, settings);
218
+ return !resolved.model || !disabledProviders.has(resolved.model.provider);
219
+ });
192
220
  }
193
221
 
194
222
  function installSubagentRetryFallbackChain(args: {
195
223
  settings: Settings;
196
224
  id: string;
197
225
  candidates: SubagentRetryFallbackCandidate[];
198
- defaultFallbackChain: string[] | undefined;
226
+ inheritedFallbackChain: string[] | undefined;
199
227
  model: Model<Api> | undefined;
200
228
  authFallbackUsed: boolean;
201
229
  }): string | undefined {
202
- const { settings, id, candidates, defaultFallbackChain, model, authFallbackUsed } = args;
230
+ const { settings, id, candidates, inheritedFallbackChain, model, authFallbackUsed } = args;
203
231
  if (!model || authFallbackUsed || candidates.length === 0) return undefined;
204
232
 
205
233
  const selectedIndex = candidates.findIndex(
@@ -208,8 +236,8 @@ function installSubagentRetryFallbackChain(args: {
208
236
  if (selectedIndex < 0) return undefined;
209
237
  const fallbackSelectors = candidates.slice(selectedIndex + 1).map(candidate => candidate.selector);
210
238
  const existingFallbackChains = settings.get("retry.fallbackChains");
211
- // A single explicit model may reuse a configured default chain, but never an implicit parent fallback.
212
- const fallbackChain = fallbackSelectors.length > 0 ? fallbackSelectors : defaultFallbackChain;
239
+ // A single configured model may reuse its role's (or the default) configured chain, but never an implicit parent fallback.
240
+ const fallbackChain = fallbackSelectors.length > 0 ? fallbackSelectors : inheritedFallbackChain;
213
241
  if (
214
242
  !Array.isArray(fallbackChain) ||
215
243
  fallbackChain.length === 0 ||
@@ -333,6 +361,8 @@ export interface ExecutorOptions {
333
361
  */
334
362
  detached?: boolean;
335
363
  modelOverride?: string | string[];
364
+ /** Explicit pre-expansion model role alias selected for this run. */
365
+ modelRole?: string;
336
366
  /**
337
367
  * Active model selector of the parent session, used as an auth-aware fallback
338
368
  * if the resolved subagent model has no working credentials. See #985.
@@ -455,6 +485,8 @@ export interface ExecutorOptions {
455
485
  * set this false so disposal unregisters them instead of leaving idle peers.
456
486
  */
457
487
  keepAlive?: boolean;
488
+ /** Internal ownership handoff for cleanup that outlives the visible Task result. */
489
+ onCleanupDeferred?: (completion: Promise<void>) => void;
458
490
  }
459
491
 
460
492
  function parseStringifiedJson(value: unknown): unknown {
@@ -877,6 +909,8 @@ interface RunMonitorArgs {
877
909
  /** Parent settings for tiny-model label generation. */
878
910
  settings?: Settings;
879
911
  modelOverride?: string | string[];
912
+ /** Explicit pre-expansion model role alias selected for this run. */
913
+ modelRole?: string;
880
914
  signal?: AbortSignal;
881
915
  onProgress?: (progress: AgentProgress) => void;
882
916
  eventBus?: EventBus;
@@ -992,6 +1026,7 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
992
1026
  cost: 0,
993
1027
  durationMs: 0,
994
1028
  modelOverride: args.modelOverride,
1029
+ modelRole: args.modelRole,
995
1030
  };
996
1031
 
997
1032
  const outputChunks: string[] = [];
@@ -1964,9 +1999,7 @@ async function driveSessionToYield(
1964
1999
  // yield: the next iteration's ladder demands a fresh one.
1965
2000
  }
1966
2001
 
1967
- if (monitor.yieldCalled()) {
1968
- await session.waitForIdle();
1969
- } else {
2002
+ if (!monitor.yieldCalled()) {
1970
2003
  await awaitAbortable(session.waitForIdle());
1971
2004
  }
1972
2005
 
@@ -2044,6 +2077,8 @@ interface FinalizeRunArgs {
2044
2077
  task: string;
2045
2078
  assignment?: string;
2046
2079
  modelOverride?: string | string[];
2080
+ /** Explicit pre-expansion model role alias selected for this run. */
2081
+ modelRole?: string;
2047
2082
  outputSchema?: unknown;
2048
2083
  outputSchemaMode?: StructuredSubagentSchemaMode;
2049
2084
  outputSchemaSource?: StructuredSubagentSchemaSource;
@@ -2063,7 +2098,7 @@ interface FinalizeRunArgs {
2063
2098
  * event.
2064
2099
  */
2065
2100
  async function finalizeRunResult(args: FinalizeRunArgs): Promise<SingleResult> {
2066
- const { monitor, done, index, id, agent, task, assignment, signal, modelOverride } = args;
2101
+ const { monitor, done, index, id, agent, task, assignment, signal, modelOverride, modelRole } = args;
2067
2102
  const progress = monitor.progress;
2068
2103
  let exitCode = done.exitCode;
2069
2104
  let stderr = done.error ?? "";
@@ -2140,14 +2175,17 @@ async function finalizeRunResult(args: FinalizeRunArgs): Promise<SingleResult> {
2140
2175
  exitCode = 1;
2141
2176
  }
2142
2177
  const wasAborted =
2143
- runtimeLimitExceeded || abortedViaYield || (!hasYield && (done.aborted || signal?.aborted || false));
2178
+ runtimeLimitExceeded || Boolean(done.aborted) || abortedViaYield || (!hasYield && Boolean(signal?.aborted));
2144
2179
  const finalAbortReason = wasAborted
2145
2180
  ? runtimeLimitExceeded
2146
2181
  ? monitor.resolveAbortReasonText()
2147
- : abortedViaYield
2148
- ? yieldAbortReason
2149
- : (done.abortReason ??
2150
- (signal?.aborted ? monitor.resolveSignalAbortReason() : monitor.resolveAbortReasonText()))
2182
+ : done.aborted
2183
+ ? (done.abortReason ?? monitor.resolveAbortReasonText())
2184
+ : abortedViaYield
2185
+ ? yieldAbortReason
2186
+ : signal?.aborted
2187
+ ? monitor.resolveSignalAbortReason()
2188
+ : monitor.resolveAbortReasonText()
2151
2189
  : undefined;
2152
2190
  progress.status = wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
2153
2191
  monitor.scheduleProgress(true);
@@ -2187,6 +2225,7 @@ async function finalizeRunResult(args: FinalizeRunArgs): Promise<SingleResult> {
2187
2225
  contextTokens: progress.contextTokens,
2188
2226
  contextWindow: progress.contextWindow,
2189
2227
  modelOverride,
2228
+ modelRole,
2190
2229
  resolvedModel: progress.resolvedModel,
2191
2230
  resolvedModelIsFallback: progress.resolvedModelIsFallback,
2192
2231
  error: exitCode !== 0 && stderr ? stderr : undefined,
@@ -2208,6 +2247,8 @@ export interface IrcWakeTurnMonitorOptions {
2208
2247
  agent: AgentDefinition;
2209
2248
  description?: string;
2210
2249
  modelOverride?: string | string[];
2250
+ /** Explicit pre-expansion model role alias selected for this run. */
2251
+ modelRole?: string;
2211
2252
  eventBus?: EventBus;
2212
2253
  parentToolCallId?: string;
2213
2254
  /** Fallback session file when the registry ref carries none. */
@@ -2252,6 +2293,7 @@ export function attachIrcWakeTurnMonitor(session: AgentSession, options: IrcWake
2252
2293
  task: ircTask,
2253
2294
  description: options.description,
2254
2295
  modelOverride: options.modelOverride,
2296
+ modelRole: options.modelRole,
2255
2297
  eventBus: options.eventBus,
2256
2298
  parentToolCallId: options.parentToolCallId,
2257
2299
  detached: true,
@@ -2309,6 +2351,7 @@ export function attachIrcWakeTurnMonitor(session: AgentSession, options: IrcWake
2309
2351
  agent,
2310
2352
  task: ircTask,
2311
2353
  modelOverride: options.modelOverride,
2354
+ modelRole: options.modelRole,
2312
2355
  outputSchema: options.outputSchema,
2313
2356
  outputSchemaMode: options.outputSchemaMode,
2314
2357
  outputSchemaSource: options.outputSchemaSource,
@@ -2346,15 +2389,27 @@ export async function finalizeSubagentLifecycle(args: {
2346
2389
  isolated: boolean;
2347
2390
  agentIdleTtlMs: number;
2348
2391
  reviveSession: AgentReviver | null;
2392
+ cleanupDeadlineAt?: number;
2393
+ onCleanupDeferred?: (completion: Promise<void>) => void;
2349
2394
  }): Promise<void> {
2350
2395
  const registry = AgentRegistry.global();
2351
2396
  const ref = registry.get(args.id);
2352
2397
  const ownsRef = Boolean(ref && ref.session === args.session);
2398
+ const cleanupDeadlineAt = args.cleanupDeadlineAt ?? Date.now() + 5000;
2353
2399
  const disposeSession = async (): Promise<void> => {
2400
+ const disposal = args.session.dispose();
2401
+ const remainingMs = Math.max(0, cleanupDeadlineAt - Date.now());
2354
2402
  try {
2355
- await untilAborted(AbortSignal.timeout(5000), () => args.session.dispose());
2356
- } catch {
2357
- // Ignore cleanup errors
2403
+ await untilAborted(AbortSignal.timeout(remainingMs), () => disposal);
2404
+ } catch (error) {
2405
+ if (Date.now() >= cleanupDeadlineAt) {
2406
+ args.onCleanupDeferred?.(disposal);
2407
+ return;
2408
+ }
2409
+ logger.warn("Subagent session cleanup failed", {
2410
+ id: args.id,
2411
+ error: error instanceof Error ? error.message : String(error),
2412
+ });
2358
2413
  }
2359
2414
  };
2360
2415
 
@@ -2365,14 +2420,20 @@ export async function finalizeSubagentLifecycle(args: {
2365
2420
  args.abortKind === "budget" && args.keepAlive && !args.isolated && args.reviveSession !== null;
2366
2421
  if (args.aborted && !resumableAbort) {
2367
2422
  if (ref && ownsRef) {
2368
- // Terminal hard kill: mark `aborted` and detach the session before
2369
- // disposing so the ref satisfies the AgentRef invariant (session null
2370
- // when aborted) ensureLive/hub focus must treat it as terminal, never
2371
- // route into the disposed session.
2372
- registry.setStatus(args.id, "aborted", ref);
2373
- registry.detachSession(args.id, ref);
2423
+ // Route hard kills through the lifecycle owner so the terminal
2424
+ // decision is durable and a restart cannot rediscover the transcript
2425
+ // as a revivable parked agent.
2426
+ try {
2427
+ await AgentLifecycleManager.global().release(args.id, ref, { tombstone: true });
2428
+ } catch (error) {
2429
+ logger.warn("runSubagent: failed to persist kill tombstone", { id: args.id, error: String(error) });
2430
+ registry.setStatus(args.id, "aborted", ref);
2431
+ registry.detachSession(args.id, ref);
2432
+ await disposeSession();
2433
+ }
2434
+ } else {
2435
+ await disposeSession();
2374
2436
  }
2375
- await disposeSession();
2376
2437
  return;
2377
2438
  }
2378
2439
 
@@ -2421,6 +2482,8 @@ export interface FollowUpTurnOptions {
2421
2482
  message: string;
2422
2483
  index?: number;
2423
2484
  description?: string;
2485
+ /** Explicit pre-expansion model role alias retained from the original run. */
2486
+ modelRole?: string;
2424
2487
  /** Structured-output state retained from the original invocation. */
2425
2488
  outputSchema?: unknown;
2426
2489
  outputSchemaMode?: StructuredSubagentSchemaMode;
@@ -2460,6 +2523,7 @@ export async function runSubagentFollowUpTurn(options: FollowUpTurnOptions): Pro
2460
2523
  agent,
2461
2524
  task: message,
2462
2525
  description: options.description,
2526
+ modelRole: options.modelRole,
2463
2527
  signal,
2464
2528
  onProgress: options.onProgress,
2465
2529
  eventBus: options.eventBus,
@@ -2509,6 +2573,7 @@ export async function runSubagentFollowUpTurn(options: FollowUpTurnOptions): Pro
2509
2573
  id,
2510
2574
  agent,
2511
2575
  task: message,
2576
+ modelRole: options.modelRole,
2512
2577
  outputSchema: options.outputSchema,
2513
2578
  outputSchemaMode: options.outputSchemaMode,
2514
2579
  outputSchemaSource: options.outputSchemaSource,
@@ -2535,6 +2600,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2535
2600
  id,
2536
2601
  worktree,
2537
2602
  modelOverride,
2603
+ modelRole,
2538
2604
  thinkingLevel,
2539
2605
  outputSchema,
2540
2606
  enableLsp,
@@ -2564,6 +2630,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2564
2630
  tokens: 0,
2565
2631
  requests: 0,
2566
2632
  modelOverride,
2633
+ modelRole,
2567
2634
  error: "Cancelled before start",
2568
2635
  aborted: true,
2569
2636
  abortReason: "Cancelled before start",
@@ -2654,6 +2721,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2654
2721
  modelRegistry: options.modelRegistry,
2655
2722
  settings,
2656
2723
  modelOverride,
2724
+ modelRole,
2657
2725
  signal,
2658
2726
  onProgress,
2659
2727
  eventBus: options.eventBus,
@@ -2687,6 +2755,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2687
2755
  agent,
2688
2756
  description: options.description,
2689
2757
  modelOverride,
2758
+ modelRole,
2690
2759
  eventBus: options.eventBus,
2691
2760
  parentToolCallId: options.parentToolCallId,
2692
2761
  sessionFile: subtaskSessionFile,
@@ -2764,9 +2833,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2764
2833
  checkAbort();
2765
2834
 
2766
2835
  const configuredModelPatterns = resolveConfiguredModelPatterns(modelPatterns, settings);
2767
- const defaultRetryFallbackChain =
2836
+ const inheritedRetryFallbackChain =
2768
2837
  configuredModelPatterns.length === 1
2769
- ? resolveSubagentDefaultRetryFallbackChain(subagentSettings)
2838
+ ? resolveSubagentInheritedRetryFallbackChain(subagentSettings, modelRegistry, modelPatterns)
2770
2839
  : undefined;
2771
2840
  const {
2772
2841
  model,
@@ -2800,8 +2869,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2800
2869
  const retryFallbackRole = installSubagentRetryFallbackChain({
2801
2870
  settings: subagentSettings,
2802
2871
  id,
2803
- candidates: resolveSubagentRetryFallbackCandidates(modelPatterns, modelRegistry, settings),
2804
- defaultFallbackChain: defaultRetryFallbackChain,
2872
+ candidates: resolveSubagentRetryFallbackCandidates(modelPatterns, modelRegistry, subagentSettings),
2873
+ inheritedFallbackChain: inheritedRetryFallbackChain,
2805
2874
  model,
2806
2875
  authFallbackUsed,
2807
2876
  });
@@ -2944,7 +3013,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2944
3013
  modelPatternFallbackRole:
2945
3014
  model || modelOverride === undefined ? undefined : `${SUBAGENT_RETRY_FALLBACK_ROLE_PREFIX}${id}`,
2946
3015
  modelPatternDefaultFallbackChain:
2947
- model || modelOverride === undefined ? undefined : defaultRetryFallbackChain,
3016
+ model || modelOverride === undefined ? undefined : inheritedRetryFallbackChain,
2948
3017
  thinkingLevel: effectiveThinkingLevel,
2949
3018
  thinkingLevelCeiling: spawnEffortCeiling,
2950
3019
  toolNames,
@@ -3072,6 +3141,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
3072
3141
  systemPrompt: session.agent.state.systemPrompt.join("\n\n"),
3073
3142
  task,
3074
3143
  tools: session.getActiveToolNames(),
3144
+ agent: agent.name,
3145
+ modelRole: modelRole ?? resolveExplicitModelRole(modelOverride ?? agent.model, subagentSettings),
3146
+ resolvedModel: progress.resolvedModel,
3147
+ readOnly: isReadOnlyAgent(agent),
3075
3148
  spawns: spawnsEnv,
3076
3149
  readSummarize: agent.readSummarize,
3077
3150
  outputSchema,
@@ -3121,7 +3194,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
3121
3194
  session.sessionManager.appendLabelChange(targetId, label);
3122
3195
  },
3123
3196
  getActiveTools: () => session.getEnabledToolNames(),
3124
- getAllTools: () => session.getAllToolNames(),
3197
+ getAllTools: () => session.getAllToolInfos(),
3125
3198
  setActiveTools: (toolNames: string[]) =>
3126
3199
  session.setActiveToolsByName(toolNames.filter(name => !isParentOwnedTool(name))),
3127
3200
  getCommands: () => getSessionSlashCommands(session),
@@ -3186,6 +3259,20 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
3186
3259
  error = err instanceof Error ? err.stack || err.message : String(err);
3187
3260
  }
3188
3261
  } finally {
3262
+ const cleanupDeadlineAt = Date.now() + TASK_ABORT_CLEANUP_GRACE_MS;
3263
+ const cleanupChangeStatus =
3264
+ worktree === undefined
3265
+ ? "This task was not isolated, so its changes may remain in the working directory."
3266
+ : "No isolated changes were applied.";
3267
+ const lateCleanups: Promise<void>[] = [];
3268
+ let deferredSessionShutdown: Promise<void> | undefined;
3269
+ const deferCleanup = (completion: Promise<void>): void => {
3270
+ lateCleanups.push(completion);
3271
+ exitCode = 1;
3272
+ aborted = true;
3273
+ abortReasonText = `cleanup exceeded ${TASK_ABORT_CLEANUP_GRACE_MS} ms`;
3274
+ error ??= `Task aborted. Cleanup did not finish within ${TASK_ABORT_CLEANUP_GRACE_MS} ms. ${cleanupChangeStatus}`;
3275
+ };
3189
3276
  if (abortSignal.aborted) {
3190
3277
  aborted = monitor.isAbortedRun();
3191
3278
  if (aborted) {
@@ -3194,10 +3281,21 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
3194
3281
  if (exitCode === 0) exitCode = 1;
3195
3282
  }
3196
3283
  sessionAbortController.abort();
3284
+ const activeSessionAbort = monitor.waitForActiveSessionAbort();
3197
3285
  try {
3198
- await untilAborted(AbortSignal.timeout(5000), () => monitor.waitForActiveSessionAbort());
3199
- } catch {
3200
- // Ignore abort cleanup timeouts/errors; terminal disposal below is still best-effort.
3286
+ await untilAborted(
3287
+ AbortSignal.timeout(Math.max(0, cleanupDeadlineAt - Date.now())),
3288
+ () => activeSessionAbort,
3289
+ );
3290
+ } catch (cleanupError) {
3291
+ if (Date.now() >= cleanupDeadlineAt) {
3292
+ deferCleanup(activeSessionAbort);
3293
+ } else {
3294
+ logger.warn("Subagent abort cleanup failed", {
3295
+ id,
3296
+ error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
3297
+ });
3298
+ }
3201
3299
  }
3202
3300
  if (unsubscribe) {
3203
3301
  try {
@@ -3207,6 +3305,17 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
3207
3305
  }
3208
3306
  unsubscribe = null;
3209
3307
  }
3308
+ const jobManager = AsyncJobManager.instance();
3309
+ if (jobManager) {
3310
+ const reap = await jobManager.cancelAndReapOwnerJobs(id, cleanupDeadlineAt);
3311
+ if (!reap.settled) {
3312
+ deferCleanup(reap.completion);
3313
+ logger.warn("Subagent async job cleanup exceeded its deadline", {
3314
+ id,
3315
+ pendingJobIds: reap.pendingJobIds,
3316
+ });
3317
+ }
3318
+ }
3210
3319
  const session = monitor.takeActiveSession();
3211
3320
  if (session) {
3212
3321
  monitor.captureSalvage(session);
@@ -3222,22 +3331,36 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
3222
3331
  isolated: worktree !== undefined,
3223
3332
  agentIdleTtlMs,
3224
3333
  reviveSession,
3334
+ cleanupDeadlineAt,
3335
+ onCleanupDeferred: completion => {
3336
+ deferredSessionShutdown = completion;
3337
+ deferCleanup(completion);
3338
+ },
3225
3339
  });
3226
3340
  }
3227
- // Structured-concurrency reap: cancel and await ALL surviving owner
3228
- // jobs (abort paths; suppressed/watched jobs the model left behind)
3229
- // so isolation capture/cleanup never races a live process writing
3230
- // into the worktree. This never proceeds while an owner process is
3231
- // live: cancellation SIGKILL-escalates, so settlement is expected
3232
- // within one interval — an unkillable process blocks here visibly
3233
- // (with periodic warnings) instead of silently racing teardown.
3234
- const jobManager = AsyncJobManager.instance();
3235
3341
  if (jobManager) {
3236
- jobManager.cancelAll({ ownerId: id });
3237
- while (!(await jobManager.waitForOwnerJobs(id, { timeoutMs: 10_000 }))) {
3238
- logger.warn("Subagent async jobs still settling; delaying teardown until process exit", { id });
3342
+ if (deferredSessionShutdown) {
3343
+ const finalReap = Promise.allSettled([deferredSessionShutdown]).then(async () => {
3344
+ const reap = await jobManager.cancelAndReapOwnerJobs(id, Date.now());
3345
+ await reap.completion;
3346
+ });
3347
+ lateCleanups.push(finalReap);
3348
+ } else {
3349
+ const reap = await jobManager.cancelAndReapOwnerJobs(id, cleanupDeadlineAt);
3350
+ if (!reap.settled) {
3351
+ deferCleanup(reap.completion);
3352
+ logger.warn("Subagent async job cleanup exceeded its deadline after session shutdown", {
3353
+ id,
3354
+ pendingJobIds: reap.pendingJobIds,
3355
+ });
3356
+ }
3239
3357
  }
3240
3358
  }
3359
+ if (lateCleanups.length > 0) {
3360
+ const completion = Promise.allSettled(lateCleanups).then(() => {});
3361
+ trackLateCleanup(completion, { id, resource: "subagent" });
3362
+ options.onCleanupDeferred?.(completion);
3363
+ }
3241
3364
  }
3242
3365
 
3243
3366
  // Launch-latency breakdown (subagent invocation → first chat dispatch).
@@ -3282,7 +3405,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
3282
3405
  const done = await runSubagent();
3283
3406
  monitor.finish();
3284
3407
 
3285
- return finalizeRunResult({
3408
+ const result = await finalizeRunResult({
3286
3409
  monitor,
3287
3410
  done,
3288
3411
  index,
@@ -3291,6 +3414,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
3291
3414
  task,
3292
3415
  assignment,
3293
3416
  modelOverride,
3417
+ modelRole,
3294
3418
  outputSchema,
3295
3419
  outputSchemaMode: options.outputSchemaMode,
3296
3420
  outputSchemaSource: options.outputSchemaSource,
@@ -3302,4 +3426,6 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
3302
3426
  sessionFile: subtaskSessionFile,
3303
3427
  startTime,
3304
3428
  });
3429
+ AgentRegistry.global().setHistory(id, { outputPath: result.outputPath });
3430
+ return result;
3305
3431
  }
package/src/task/index.ts CHANGED
@@ -27,6 +27,7 @@ import { TASK_EFFORTS, type TaskEffort } from "../thinking";
27
27
  import { truncateForPrompt } from "../tools/approval";
28
28
  import { isIrcEnabled } from "../tools/hub";
29
29
  import { formatBytes, formatDuration } from "../tools/render-utils";
30
+ import { isReadOnlyAgent } from "./read-only-policy";
30
31
  import { isScoutSpawnable, resolveSpawnPolicy } from "./spawn-policy";
31
32
  import {
32
33
  type AgentDefinition,
@@ -102,6 +103,7 @@ export { loadBundledAgents as BUNDLED_AGENTS } from "./agents";
102
103
  export { discoverCommands, expandCommand, getCommand } from "./commands";
103
104
  export { discoverAgents, getAgent } from "./discovery";
104
105
  export { AgentOutputManager } from "./output-manager";
106
+ export * from "./read-only-policy";
105
107
  export type {
106
108
  AgentDefinition,
107
109
  AgentProgress,
@@ -119,32 +121,6 @@ export {
119
121
  taskSchema,
120
122
  } from "./types";
121
123
 
122
- // Built-in tools whose approval tier is "read" (see tool classes' `approval`).
123
- // An agent is read-only iff its declared tools are a non-empty subset of this set.
124
- // Fail-safe: any unknown tool makes the agent not read-only.
125
- export const READ_ONLY_TOOL_NAMES: ReadonlySet<string> = new Set([
126
- "read",
127
- "grep",
128
- "glob",
129
- "web_search",
130
- "ast_grep",
131
- "yield",
132
- "hub",
133
- "ask",
134
- "todo",
135
- "recall",
136
- "reflect",
137
- "retain",
138
- "memory_edit",
139
- "inspect_image",
140
- "checkpoint",
141
- "rewind",
142
- ]);
143
-
144
- export function isReadOnlyAgent(agent: AgentDefinition): boolean {
145
- return !!agent.tools?.length && agent.tools.every(tool => READ_ONLY_TOOL_NAMES.has(tool));
146
- }
147
-
148
124
  /**
149
125
  * Preview text for a child result. Falls back to "(no output)" — annotated
150
126
  * with the request count when the child actually did work, so the parent can
@@ -860,6 +836,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
860
836
  id: agentId,
861
837
  agent: agentType,
862
838
  agentSource,
839
+ modelRole: policy.modelRole,
863
840
  status: "pending",
864
841
  task: renderSubagentUserPrompt(assignment),
865
842
  assignment,
@@ -1143,8 +1120,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1143
1120
  // polling row reflects the resolved model, reasoning level,
1144
1121
  // and running counters without reverting the "running"
1145
1122
  // status back to the subagent's initial "pending" snapshot.
1123
+ progress.modelRole = nextProgress.modelRole ?? progress.modelRole;
1146
1124
  progress.resolvedModel = nextProgress.resolvedModel;
1147
- progress.resolvedModelIsFallback = nextProgress.resolvedModelIsFallback;
1125
+ progress.resolvedModelIsFallback =
1126
+ nextProgress.resolvedModelIsFallback ?? progress.resolvedModelIsFallback;
1148
1127
  progress.tokens = nextProgress.tokens;
1149
1128
  progress.requests = nextProgress.requests;
1150
1129
  progress.contextTokens = nextProgress.contextTokens;
@@ -1187,9 +1166,11 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1187
1166
  progress.extractedToolData = singleResult?.extractedToolData;
1188
1167
  progress.retryFailure = singleResult?.retryFailure;
1189
1168
  progress.retryState = undefined;
1169
+ progress.modelRole = singleResult?.modelRole ?? progress.modelRole;
1190
1170
  if (singleResult?.resolvedModel) {
1191
1171
  progress.resolvedModel = singleResult.resolvedModel;
1192
- progress.resolvedModelIsFallback = singleResult.resolvedModelIsFallback;
1172
+ progress.resolvedModelIsFallback =
1173
+ singleResult.resolvedModelIsFallback ?? progress.resolvedModelIsFallback;
1193
1174
  } else {
1194
1175
  delete progress.resolvedModel;
1195
1176
  delete progress.resolvedModelIsFallback;