@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
@@ -20,9 +20,11 @@
20
20
  */
21
21
  import * as path from "node:path";
22
22
  import type * as natives from "@oh-my-pi/pi-natives";
23
+ import { AgentRegistry } from "../registry/agent-registry";
23
24
  import type { ToolSession } from "../tools";
24
25
  import { generateCommitMessage } from "../utils/commit-message-generator";
25
26
  import * as git from "../utils/git";
27
+ import { trackLateCleanup } from "../utils/late-cleanup";
26
28
  import type { ExecutorOptions } from "./executor";
27
29
  import { runSubprocess } from "./executor";
28
30
  import type { SingleResult } from "./types";
@@ -43,6 +45,15 @@ import {
43
45
 
44
46
  type IsoBackendKind = natives.IsoBackendKind;
45
47
 
48
+ function rememberAgentArtifacts(result: SingleResult): SingleResult {
49
+ AgentRegistry.global().setHistory(result.id, {
50
+ outputPath: result.outputPath,
51
+ patchPath: result.patchPath,
52
+ branchName: result.branchName,
53
+ });
54
+ return result;
55
+ }
56
+
46
57
  /** Resolved repo + baseline used by every isolated spawn in a single call. */
47
58
  export interface IsolationContext {
48
59
  repoRoot: string;
@@ -146,6 +157,7 @@ async function writeIsolationPatch(
146
157
  */
147
158
  export async function runIsolatedSubprocess(opts: IsolatedRunOptions): Promise<SingleResult> {
148
159
  let handle: IsolationHandle | undefined;
160
+ let deferredCleanup: Promise<void> | undefined;
149
161
  try {
150
162
  const taskBaseline = structuredClone(opts.context.baseline);
151
163
  handle = await ensureIsolation(opts.context.repoRoot, opts.agentId, opts.preferredBackend);
@@ -155,7 +167,12 @@ export async function runIsolatedSubprocess(opts: IsolatedRunOptions): Promise<S
155
167
  worktree: isolationDir,
156
168
  preloadedExtensionPaths: undefined,
157
169
  preloadedCustomToolPaths: undefined,
170
+ onCleanupDeferred: completion => {
171
+ deferredCleanup = completion;
172
+ opts.baseOptions.onCleanupDeferred?.(completion);
173
+ },
158
174
  });
175
+ if (deferredCleanup) return result;
159
176
  if (opts.mergeMode === "branch" && result.exitCode === 0) {
160
177
  try {
161
178
  const commitResult = await commitToBranch(
@@ -165,14 +182,14 @@ export async function runIsolatedSubprocess(opts: IsolatedRunOptions): Promise<S
165
182
  opts.description,
166
183
  opts.buildCommitMessage?.(),
167
184
  );
168
- return {
185
+ return rememberAgentArtifacts({
169
186
  ...result,
170
187
  branchName: commitResult?.branchName,
171
188
  branchBaseSha: commitResult?.baseSha,
172
189
  nestedPatches: commitResult?.nestedPatches,
173
- };
190
+ });
174
191
  } catch (mergeErr) {
175
- // Agent succeeded but branch commit failed — clean up stale branch
192
+ // Agent succeeded but branch commit failed — clean up stale branch.
176
193
  const branchName = `omp/task/${opts.agentId}`;
177
194
  await git.branch.tryDelete(opts.context.repoRoot, branchName);
178
195
  const msg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
@@ -183,37 +200,51 @@ export async function runIsolatedSubprocess(opts: IsolatedRunOptions): Promise<S
183
200
  opts.artifactsDir,
184
201
  opts.agentId,
185
202
  );
186
- return {
203
+ return rememberAgentArtifacts({
187
204
  ...result,
188
205
  patchPath: patchResult.patchPath,
189
206
  nestedPatches: patchResult.nestedPatches,
190
207
  error: `Merge failed: ${msg}`,
191
- };
208
+ });
192
209
  } catch (patchErr) {
193
210
  const patchMsg = patchErr instanceof Error ? patchErr.message : String(patchErr);
194
- return { ...result, error: `Merge failed: ${msg}; patch capture failed: ${patchMsg}` };
211
+ return rememberAgentArtifacts({
212
+ ...result,
213
+ error: `Merge failed: ${msg}; patch capture failed: ${patchMsg}`,
214
+ });
195
215
  }
196
216
  }
197
217
  }
198
218
  if (result.exitCode === 0) {
199
219
  try {
200
220
  const patchResult = await writeIsolationPatch(isolationDir, taskBaseline, opts.artifactsDir, opts.agentId);
201
- return {
221
+ return rememberAgentArtifacts({
202
222
  ...result,
203
223
  patchPath: patchResult.patchPath,
204
224
  nestedPatches: patchResult.nestedPatches,
205
- };
225
+ });
206
226
  } catch (patchErr) {
207
227
  const msg = patchErr instanceof Error ? patchErr.message : String(patchErr);
208
- return { ...result, error: `Patch capture failed: ${msg}` };
228
+ return rememberAgentArtifacts({ ...result, error: `Patch capture failed: ${msg}` });
209
229
  }
210
230
  }
211
- return result;
231
+ return rememberAgentArtifacts(result);
212
232
  } catch (err) {
213
- return opts.buildFailureResult(err);
233
+ return rememberAgentArtifacts(opts.buildFailureResult(err));
214
234
  } finally {
215
235
  if (handle) {
216
- await cleanupIsolation(handle);
236
+ const isolationHandle = handle;
237
+ if (deferredCleanup) {
238
+ trackLateCleanup(
239
+ deferredCleanup.then(() => cleanupIsolation(isolationHandle)),
240
+ {
241
+ agentId: opts.agentId,
242
+ resource: "isolation",
243
+ },
244
+ );
245
+ } else {
246
+ await cleanupIsolation(isolationHandle);
247
+ }
217
248
  }
218
249
  }
219
250
  }
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs/promises";
2
-
3
2
  import type { ModelRegistry } from "../config/model-registry";
3
+ import { formatModelRoleAlias } from "../config/model-roles";
4
4
  import type { Settings } from "../config/settings";
5
5
  import { MCPManager } from "../mcp/manager";
6
6
  import type { PersistedSubagentReviverFactory } from "../registry/agent-lifecycle";
@@ -79,6 +79,14 @@ export function createPersistedSubagentReviverFactory(
79
79
  taskDepth++;
80
80
  parentId = registry.get(parentId)?.parentId;
81
81
  }
82
+ const subagentSettings = createSubagentSettings(
83
+ ctx.settings,
84
+ init.readSummarize === false ? { "read.summarize.enabled": false } : undefined,
85
+ );
86
+ const persistedModelPattern =
87
+ init.modelRole && init.modelRole !== "default"
88
+ ? [formatModelRoleAlias(init.modelRole), ...(init.resolvedModel ? [init.resolvedModel] : [])]
89
+ : init.resolvedModel;
82
90
  return async expectedRef => {
83
91
  // Re-open fresh on every revive: park closes the writer, so this takes
84
92
  // the single-writer lock cleanly and restores the full message history.
@@ -96,10 +104,9 @@ export function createPersistedSubagentReviverFactory(
96
104
  cwd: ctx.session.sessionManager.getCwd(),
97
105
  authStorage: ctx.authStorage,
98
106
  modelRegistry: ctx.modelRegistry,
99
- settings: createSubagentSettings(
100
- ctx.settings,
101
- init.readSummarize === false ? { "read.summarize.enabled": false } : undefined,
102
- ),
107
+ ...(persistedModelPattern ? { modelPattern: persistedModelPattern } : {}),
108
+ modelPatternAuthFallback: init.resolvedModel,
109
+ settings: subagentSettings,
103
110
  sessionManager: reopened,
104
111
  agentId: ref.id,
105
112
  agentDisplayName: ref.displayName,
@@ -0,0 +1,27 @@
1
+ import type { AgentDefinition } from "./types";
2
+
3
+ // Built-in tools whose approval tier is "read" (see tool classes' `approval`).
4
+ // An agent is read-only iff its declared tools are a non-empty subset of this set.
5
+ // Fail-safe: any unknown tool makes the agent not read-only.
6
+ export const READ_ONLY_TOOL_NAMES: ReadonlySet<string> = new Set([
7
+ "read",
8
+ "grep",
9
+ "glob",
10
+ "web_search",
11
+ "ast_grep",
12
+ "yield",
13
+ "hub",
14
+ "ask",
15
+ "todo",
16
+ "recall",
17
+ "reflect",
18
+ "retain",
19
+ "memory_edit",
20
+ "inspect_image",
21
+ "checkpoint",
22
+ "rewind",
23
+ ]);
24
+
25
+ export function isReadOnlyAgent(agent: AgentDefinition): boolean {
26
+ return !!agent.tools?.length && agent.tools.every(tool => READ_ONLY_TOOL_NAMES.has(tool));
27
+ }
@@ -8,7 +8,7 @@ import * as fs from "node:fs/promises";
8
8
  import * as os from "node:os";
9
9
  import path from "node:path";
10
10
  import { $env, prompt, Snowflake } from "@oh-my-pi/pi-utils";
11
- import { resolveAgentModelPatterns } from "../config/model-resolver";
11
+ import { resolveAgentModelPatterns, resolveAgentModelSource, resolveExplicitModelRole } from "../config/model-resolver";
12
12
  import type { LocalProtocolOptions } from "../internal-urls";
13
13
  import { registerArtifactsDir } from "../internal-urls/registry-helpers";
14
14
  import { MCPManager } from "../mcp/manager";
@@ -20,6 +20,7 @@ import type { TaskEffort } from "../thinking";
20
20
  import type { ToolSession } from "../tools";
21
21
  import { isIrcEnabled } from "../tools/hub";
22
22
  import { buildOutputValidator } from "../tools/output-schema-validator";
23
+ import { trackLateCleanup } from "../utils/late-cleanup";
23
24
  import { type DiscoveryResult, discoverAgents, getAgent } from "./discovery";
24
25
  import { type ExecutorOptions, runSubprocess } from "./executor";
25
26
  import {
@@ -122,6 +123,8 @@ export interface EffectiveSubagentPolicy {
122
123
  agent: AgentDefinition;
123
124
  effectiveAgent: AgentDefinition;
124
125
  modelOverride?: string | string[];
126
+ /** Explicit pre-expansion model role alias selected for this run. */
127
+ modelRole?: string;
125
128
  parentActiveModelPattern?: string;
126
129
  schema: StructuredSubagentSchemaResolution;
127
130
  planMode: boolean;
@@ -277,13 +280,18 @@ export async function resolveEffectiveSubagentPolicy(
277
280
  }
278
281
  const agentModelOverrides = request.session.settings.get("task.agentModelOverrides");
279
282
  const parentActiveModelPattern = request.session.getActiveModelString?.();
280
- const modelOverride = resolveAgentModelPatterns({
281
- settingsOverride: request.model ?? agentModelOverrides[agentName],
283
+ const modelResolution = {
284
+ requestModel: request.model,
285
+ settingsOverride: agentModelOverrides[agentName],
282
286
  agentModel: effectiveAgent.model,
283
287
  settings: request.session.settings,
284
288
  activeModelPattern: parentActiveModelPattern,
285
289
  fallbackModelPattern: request.session.getModelString?.(),
286
- });
290
+ };
291
+ // Keep role identity from the same effective non-empty source that supplies
292
+ // model selection: caller request, settings override, then agent definition.
293
+ const modelRole = resolveExplicitModelRole(resolveAgentModelSource(modelResolution), request.session.settings);
294
+ const modelOverride = resolveAgentModelPatterns(modelResolution);
287
295
  const isolationMode = request.session.settings.get("task.isolation.mode");
288
296
  const isIsolated = request.isolation?.requested === true;
289
297
  if (isIsolated && isolationMode === "none") {
@@ -298,6 +306,7 @@ export async function resolveEffectiveSubagentPolicy(
298
306
  agent,
299
307
  effectiveAgent,
300
308
  modelOverride,
309
+ modelRole,
301
310
  parentActiveModelPattern,
302
311
  schema,
303
312
  planMode,
@@ -393,6 +402,7 @@ function buildExecutorOptions(
393
402
  invokedAt: request.invokedAt,
394
403
  acquiredAt: request.acquiredAt,
395
404
  modelOverride: policy.modelOverride,
405
+ modelRole: policy.modelRole,
396
406
  parentActiveModelPattern: policy.parentActiveModelPattern,
397
407
  thinkingLevel: policy.effectiveAgent.thinkingLevel,
398
408
  effort: request.effort,
@@ -475,6 +485,7 @@ function buildFailureResult(
475
485
  tokens: 0,
476
486
  requests: 0,
477
487
  modelOverride: policy.modelOverride,
488
+ modelRole: policy.modelRole,
478
489
  error: message,
479
490
  };
480
491
  };
@@ -540,12 +551,16 @@ export async function runStructuredSubagent(request: StructuredSubagentRequest):
540
551
  let mergeSummary = "";
541
552
  let requiresRecoveryArtifacts = false;
542
553
  let completedSuccessfully = false;
554
+ let deferredCleanup: Promise<void> | undefined;
543
555
  try {
544
556
  const id = await reserveStructuredSubagentId(request.session, {
545
557
  ...request.identity,
546
558
  label: request.identity?.label ?? (request.invocationKind === "eval" ? "EvalAgent" : undefined),
547
559
  });
548
560
  const baseOptions = buildExecutorOptions(request, policy, lease, id);
561
+ baseOptions.onCleanupDeferred = completion => {
562
+ deferredCleanup = completion;
563
+ };
549
564
  baseOptions.planReference = await loadPlanReference(request, policy);
550
565
  let isolationContext: IsolationContext | null = null;
551
566
  if (policy.isIsolated) {
@@ -639,8 +654,18 @@ export async function runStructuredSubagent(request: StructuredSubagentRequest):
639
654
  (policy.isIsolated && (!policy.applyChanges || changesApplied === false || requiresRecoveryArtifacts));
640
655
  const shouldCleanup = lease.temporary && !shouldRetainArtifacts;
641
656
  if (shouldCleanup) {
642
- await fs.rm(lease.artifactsDir, { recursive: true, force: true });
643
- lease.unregister?.();
657
+ const cleanupArtifacts = async (): Promise<void> => {
658
+ await fs.rm(lease.artifactsDir, { recursive: true, force: true });
659
+ lease.unregister?.();
660
+ };
661
+ if (deferredCleanup) {
662
+ trackLateCleanup(deferredCleanup.then(cleanupArtifacts), {
663
+ resource: "artifacts",
664
+ artifactsDir: lease.artifactsDir,
665
+ });
666
+ } else {
667
+ await cleanupArtifacts();
668
+ }
644
669
  }
645
670
  }
646
671
  }
package/src/task/types.ts CHANGED
@@ -426,6 +426,8 @@ export interface AgentProgress {
426
426
  cost: number;
427
427
  durationMs: number;
428
428
  modelOverride?: string | string[];
429
+ /** Explicit pre-expansion model role alias selected for this run. */
430
+ modelRole?: string;
429
431
  /** Resolved model display string in the form `<provider>/<id>`, optionally suffixed with `:<thinkingLevel>` when the level was set explicitly. Undefined when the model could not be resolved. */
430
432
  resolvedModel?: string;
431
433
  /** True when {@link resolvedModel} is the target of an active retry fallback (not the originally configured model). Lets observer-only UIs (collab guests, Agent Hub rows with no live session) flag the fallback and keep the provider. */
@@ -495,6 +497,8 @@ export interface SingleResult {
495
497
  /** Model's context window in tokens, when known. */
496
498
  contextWindow?: number;
497
499
  modelOverride?: string | string[];
500
+ /** Explicit pre-expansion model role alias selected for this run. */
501
+ modelRole?: string;
498
502
  /** Resolved model display string in the form `<provider>/<id>`, optionally suffixed with `:<thinkingLevel>` when the level was set explicitly. Omitted from tool-result JSON when undefined to keep wire payloads small. */
499
503
  resolvedModel?: string;
500
504
  /** True when {@link resolvedModel} is the target of an active retry fallback. Mirrors {@link AgentProgress.resolvedModelIsFallback} onto the settled result. */
@@ -94,7 +94,7 @@ function createStopOnTextCriteria(
94
94
  this.#text = text;
95
95
  }
96
96
 
97
- _call(inputIds: number[][]): boolean[] {
97
+ override _call(inputIds: number[][]): boolean[] {
98
98
  return inputIds.map(ids => {
99
99
  const tail = ids.slice(-STOP_DECODE_WINDOW_TOKENS);
100
100
  const decoded = this.#tokenizer.decode(tail, {
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import * as path from "node:path";
8
8
  import { isEnoent, peekFile } from "@oh-my-pi/pi-utils";
9
- import { LRUCache } from "lru-cache/raw";
9
+ import { LRUCache } from "@oh-my-pi/pi-utils/lru";
10
10
  import { settings as globalSettings, isSettingsInitialized, type Settings } from "../config/settings";
11
11
  import { getDefault } from "../config/settings-schema";
12
12
  import { ToolError } from "./tool-errors";
@@ -11,8 +11,8 @@ import {
11
11
  visibleWidth,
12
12
  } from "@oh-my-pi/pi-tui";
13
13
  import { sanitizeText } from "@oh-my-pi/pi-utils";
14
- import type * as XtermModule from "@xterm/headless";
15
- import type { Terminal as XtermTerminalType } from "@xterm/headless";
14
+ import type * as XtermModule from "@oh-my-pi/pi-utils/vterm";
15
+ import type { Terminal as XtermTerminalType } from "@oh-my-pi/pi-utils/vterm";
16
16
  import { Settings } from "../config/settings";
17
17
  import type { Theme } from "../modes/theme/theme";
18
18
  import { OutputSink, type OutputSummary } from "../session/streaming-output";
@@ -36,13 +36,13 @@ function normalizeCaptureChunk(chunk: string): string {
36
36
  // source of truth for the final captured output.
37
37
  const MAX_LIVE_WRITE_QUEUE_CHUNKS = 512;
38
38
 
39
- // @xterm/headless is only needed once an interactive PTY session actually starts,
39
+ // The virtual terminal is only needed once an interactive PTY session actually starts,
40
40
  // so it is loaded lazily (and memoized) instead of weighing down CLI startup.
41
41
  let xtermTerminalCtor: typeof XtermModule.Terminal | undefined;
42
42
 
43
43
  async function loadXtermTerminal(): Promise<typeof XtermModule.Terminal> {
44
44
  if (!xtermTerminalCtor) {
45
- const mod = (await import("@xterm/headless")) as typeof XtermModule & { default?: typeof XtermModule };
45
+ const mod = (await import("@oh-my-pi/pi-utils/vterm")) as typeof XtermModule & { default?: typeof XtermModule };
46
46
  xtermTerminalCtor = (mod.default ?? mod).Terminal;
47
47
  }
48
48
  return xtermTerminalCtor;
@@ -96,10 +96,14 @@ function withoutLeadingEnvironmentAssignments(command: string): string | null {
96
96
 
97
97
  function interceptionCandidates(command: string): string[] {
98
98
  const candidates = [command.trim()];
99
- const segments = extractFlatShellCommandSegments(command);
100
- candidates.push(...segments.map(segment => segment.trim()));
101
- for (const segment of segments) {
102
- const withoutAssignments = withoutLeadingEnvironmentAssignments(segment);
99
+ for (const segment of extractFlatShellCommandSegments(command)) {
100
+ // A segment that consumes the previous stage's stdout via `|` reads piped
101
+ // stdin, which no path-based dedicated tool (read/grep/glob) — nor any
102
+ // other dedicated tool — can replace, so it is not an interception
103
+ // candidate. Standalone and first-stage commands still match.
104
+ if (segment.pipedStdin) continue;
105
+ candidates.push(segment.text);
106
+ const withoutAssignments = withoutLeadingEnvironmentAssignments(segment.text);
103
107
  if (withoutAssignments) candidates.push(withoutAssignments);
104
108
  }
105
109
  return candidates;
package/src/tools/bash.ts CHANGED
@@ -384,8 +384,8 @@ function normalizeBashEnv(env: Record<string, string> | undefined): Record<strin
384
384
  return normalized;
385
385
  }
386
386
 
387
- function escapeBashEnvValueForDisplay(value: string): string {
388
- return value
387
+ function escapeBashEnvValueForDisplay(value: unknown): string {
388
+ return String(value)
389
389
  .replaceAll("\\", "\\\\")
390
390
  .replaceAll("\n", "\\n")
391
391
  .replaceAll("\r", "\\r")
@@ -395,7 +395,7 @@ function escapeBashEnvValueForDisplay(value: string): string {
395
395
  .replaceAll("`", "\\`");
396
396
  }
397
397
 
398
- function formatBashEnvAssignments(env: Record<string, string> | undefined): string {
398
+ function formatBashEnvAssignments(env: Record<string, unknown> | undefined): string {
399
399
  if (!env || Object.keys(env).length === 0) return "";
400
400
  return Object.entries(env)
401
401
  .sort(([a], [b]) => a.localeCompare(b))
@@ -1498,7 +1498,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
1498
1498
  // =============================================================================
1499
1499
  export interface BashRenderArgs {
1500
1500
  command?: string;
1501
- env?: Record<string, string>;
1501
+ env?: Record<string, unknown>;
1502
1502
  timeout?: number;
1503
1503
  cwd?: string;
1504
1504
  __partialJson?: string;
@@ -1522,7 +1522,7 @@ export interface ShellRendererConfig<TArgs> {
1522
1522
  resolveTitle: (args: TArgs | undefined, options: RenderResultOptions) => string;
1523
1523
  resolveCommand?: (args: TArgs | undefined) => string | undefined;
1524
1524
  resolveCwd?: (args: TArgs | undefined) => string | undefined;
1525
- resolveEnv?: (args: TArgs | undefined) => Record<string, string> | undefined;
1525
+ resolveEnv?: (args: TArgs | undefined) => Record<string, unknown> | undefined;
1526
1526
  showHeader?: boolean;
1527
1527
  }
1528
1528
 
@@ -1532,7 +1532,7 @@ function getPartialJson<TArgs>(args: TArgs | undefined): string | undefined {
1532
1532
  return typeof value === "string" ? value : undefined;
1533
1533
  }
1534
1534
 
1535
- export function getBashEnvForDisplay(args: BashRenderArgs): Record<string, string> | undefined {
1535
+ export function getBashEnvForDisplay(args: BashRenderArgs): Record<string, unknown> | undefined {
1536
1536
  // The parsed args don't always mirror the exact current stream prefix, so recover
1537
1537
  // env from the raw JSON buffer to surface `NAME="..." cmd` in the preview as it
1538
1538
  // streams rather than only once the args object finishes.
@@ -8,7 +8,16 @@ import { resizeImage } from "../../../utils/image-resize";
8
8
  import type { ToolSession } from "../../index";
9
9
  import { resolveToCwd } from "../../path-utils";
10
10
  import { formatScreenshot } from "../../render-utils";
11
- import { bindRunFacade, resolvePredicateTimeout, type WaitPredicateOptions, waitForRun } from "../../run-scope";
11
+ import {
12
+ bindRunFacade,
13
+ isBrowserRunOwnedRejection,
14
+ markBrowserRunRejection,
15
+ observeBrowserRunPromise,
16
+ resolvePredicateTimeout,
17
+ type WaitPredicateOptions,
18
+ waitForRun,
19
+ withBrowserPromiseCombinatorTracking,
20
+ } from "../../run-scope";
12
21
  import { ToolAbortError, ToolError, throwIfAborted } from "../../tool-errors";
13
22
  import { type AriaSnapshotOptions, assertSelectorString, buildAriaSnapshotScript } from "../aria/aria-snapshot";
14
23
  import { DEFAULT_VIEWPORT } from "../launch";
@@ -1368,6 +1377,7 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi
1368
1377
  const signal = AbortSignal.any(
1369
1378
  opts.signal ? [timeoutSignal, opts.signal, runAc.signal] : [timeoutSignal, runAc.signal],
1370
1379
  );
1380
+ const runEndedError = postmortem.markExpectedCleanupError(new ToolAbortError("Browser run ended"));
1371
1381
  const output = new RunOutput();
1372
1382
  const screenshots: ScreenshotResult[] = [];
1373
1383
  const runId = crypto.randomUUID();
@@ -1382,6 +1392,28 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi
1382
1392
  // handler to this promise; keep its armed rejection from surfacing as an
1383
1393
  // unhandled rejection — the postmortem-fatal path this run guards against.
1384
1394
  cancelRejection.catch(() => {});
1395
+ const rejectionOwner = {};
1396
+ const { promise: floatingFailure, reject: rejectFloatingFailure } = Promise.withResolvers<never>();
1397
+ floatingFailure.catch(() => {});
1398
+ let runActive = true;
1399
+ let hasFloatingFailure = false;
1400
+ const recordFloatingFailure = (reason: unknown): void => {
1401
+ if (hasFloatingFailure || postmortem.isExpectedCleanupError(reason)) return;
1402
+ const message = reason instanceof Error ? reason.message : String(reason);
1403
+ if (!runActive) {
1404
+ logger.warn("Unhandled rejection after browser run ended", { runId, error: message });
1405
+ return;
1406
+ }
1407
+ hasFloatingFailure = true;
1408
+ const error = new Error(`Unhandled rejection (missing await?): ${message}`, { cause: reason });
1409
+ if (reason instanceof Error) error.name = reason.name;
1410
+ rejectFloatingFailure(error);
1411
+ };
1412
+ const uninstallRejectionInterceptor = postmortem.interceptUnhandledRejections(reason => {
1413
+ if (!isBrowserRunOwnedRejection(reason, rejectionOwner, `cmux-run-${runId}.js`)) return false;
1414
+ recordFloatingFailure(reason);
1415
+ return true;
1416
+ });
1385
1417
  const onAbort = (): void => {
1386
1418
  if (timeoutSignal.aborted) {
1387
1419
  reject(new ToolError(`Browser code execution timed out after ${opts.timeoutMs}ms`));
@@ -1402,24 +1434,30 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi
1402
1434
  // Keep both inside try so a concurrent in-process eval/browser run surfaces as
1403
1435
  // a rejected promise the supervisor can report, never an unhandled rejection.
1404
1436
  runtime.setCwd(opts.snapshot.cwd);
1405
- const runTab = bindRunFacade(tab, signal);
1437
+ const runTab = bindRunFacade(tab, signal, rejectionOwner, recordFloatingFailure);
1406
1438
  runtime.setRunScope({
1407
- page: bindRunFacade(tab.page, signal),
1408
- browser: bindRunFacade(tab.browser, signal),
1439
+ page: bindRunFacade(tab.page, signal, rejectionOwner, recordFloatingFailure),
1440
+ browser: bindRunFacade(tab.browser, signal, rejectionOwner, recordFloatingFailure),
1409
1441
  tab: runTab,
1410
1442
  assert: (cond: unknown, text?: string): void => {
1411
1443
  if (!cond) throw new ToolError(text ?? "Assertion failed");
1412
1444
  },
1413
1445
  wait: (msOrPredicate: number | (() => unknown), waitOpts?: WaitPredicateOptions): Promise<unknown> =>
1414
- waitForRun(
1415
- msOrPredicate,
1416
- signal,
1417
- typeof msOrPredicate === "number"
1418
- ? waitOpts
1419
- : {
1420
- timeout: resolvePredicateTimeout(opts.timeoutMs, waitOpts?.timeout),
1421
- interval: waitOpts?.interval,
1422
- },
1446
+ observeBrowserRunPromise(
1447
+ waitForRun(
1448
+ msOrPredicate,
1449
+ signal,
1450
+ typeof msOrPredicate === "number"
1451
+ ? waitOpts
1452
+ : {
1453
+ timeout: resolvePredicateTimeout(opts.timeoutMs, waitOpts?.timeout),
1454
+ interval: waitOpts?.interval,
1455
+ },
1456
+ ).catch(error => {
1457
+ throw markBrowserRunRejection(error, rejectionOwner);
1458
+ }),
1459
+ rejectionOwner,
1460
+ recordFloatingFailure,
1423
1461
  ),
1424
1462
  });
1425
1463
 
@@ -1444,16 +1482,24 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi
1444
1482
  let runError: unknown;
1445
1483
  let runFailed = false;
1446
1484
  try {
1447
- returnValue = await Promise.race([
1448
- runtime.run(opts.code, filename, hooks, { runId, cwd: opts.snapshot.cwd }),
1449
- cancelRejection,
1450
- ]);
1485
+ returnValue = await withBrowserPromiseCombinatorTracking(
1486
+ rejectionOwner,
1487
+ recordFloatingFailure,
1488
+ async () =>
1489
+ await Promise.race([
1490
+ runtime.run(opts.code, filename, hooks, { runId, cwd: opts.snapshot.cwd }),
1491
+ cancelRejection,
1492
+ floatingFailure,
1493
+ ]),
1494
+ );
1451
1495
  } catch (error) {
1452
1496
  runFailed = true;
1453
1497
  runError = error;
1454
1498
  }
1499
+ runAc.abort(runEndedError);
1455
1500
  // Let rejection callbacks run while this run can still own guest-created promises.
1456
1501
  await Bun.sleep(0);
1502
+ if (hasFloatingFailure && !runFailed) await floatingFailure;
1457
1503
  if (runFailed) {
1458
1504
  for (const reason of activeRun.floatingRejections) {
1459
1505
  logger.warn("Unhandled rejection accompanied a failed cmux browser run", { filename, error: reason });
@@ -1470,8 +1516,10 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi
1470
1516
  }
1471
1517
  return { displays: output.finish(), returnValue: cloneSafe(returnValue), screenshots };
1472
1518
  } finally {
1519
+ runActive = false;
1520
+ uninstallRejectionInterceptor();
1473
1521
  signal.removeEventListener("abort", onAbort);
1474
- runAc.abort(postmortem.markExpectedCleanupError(new ToolAbortError("Browser run ended")));
1522
+ runAc.abort(runEndedError);
1475
1523
  activeCmuxRuns.delete(filename);
1476
1524
  rememberCmuxRunFile(filename);
1477
1525
  tab.clearRunContext();