@oh-my-pi/pi-coding-agent 15.9.67 → 15.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/dist/types/cli/args.d.ts +1 -1
  3. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  4. package/dist/types/cli/gallery-cli.d.ts +43 -0
  5. package/dist/types/cli/gallery-fixtures/agentic.d.ts +2 -0
  6. package/dist/types/cli/gallery-fixtures/codeintel.d.ts +3 -0
  7. package/dist/types/cli/gallery-fixtures/edit.d.ts +3 -0
  8. package/dist/types/cli/gallery-fixtures/fs.d.ts +2 -0
  9. package/dist/types/cli/gallery-fixtures/index.d.ts +4 -0
  10. package/dist/types/cli/gallery-fixtures/interaction.d.ts +3 -0
  11. package/dist/types/cli/gallery-fixtures/memory.d.ts +2 -0
  12. package/dist/types/cli/gallery-fixtures/misc.d.ts +3 -0
  13. package/dist/types/cli/gallery-fixtures/search.d.ts +3 -0
  14. package/dist/types/cli/gallery-fixtures/shell.d.ts +3 -0
  15. package/dist/types/cli/gallery-fixtures/types.d.ts +44 -0
  16. package/dist/types/cli/gallery-fixtures/web.d.ts +2 -0
  17. package/dist/types/cli/gallery-screenshot.d.ts +35 -0
  18. package/dist/types/commands/gallery.d.ts +47 -0
  19. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  20. package/dist/types/commit/analysis/summary.d.ts +2 -2
  21. package/dist/types/commit/changelog/generate.d.ts +2 -2
  22. package/dist/types/commit/changelog/index.d.ts +2 -2
  23. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  24. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  25. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  26. package/dist/types/commit/model-selection.d.ts +10 -4
  27. package/dist/types/config/api-key-resolver.d.ts +34 -0
  28. package/dist/types/config/keybindings.d.ts +6 -1
  29. package/dist/types/config/model-id-affixes.d.ts +2 -0
  30. package/dist/types/config/model-registry.d.ts +25 -2
  31. package/dist/types/config/settings-schema.d.ts +41 -6
  32. package/dist/types/dap/config.d.ts +14 -1
  33. package/dist/types/dap/types.d.ts +10 -0
  34. package/dist/types/extensibility/plugins/marketplace-auto-update.d.ts +8 -0
  35. package/dist/types/lsp/types.d.ts +10 -0
  36. package/dist/types/lsp/utils.d.ts +3 -2
  37. package/dist/types/main.d.ts +3 -2
  38. package/dist/types/memory-backend/index.d.ts +2 -1
  39. package/dist/types/memory-backend/resolve.d.ts +1 -1
  40. package/dist/types/memory-backend/types.d.ts +1 -1
  41. package/dist/types/modes/components/chat-block.d.ts +64 -0
  42. package/dist/types/modes/components/custom-editor.d.ts +5 -1
  43. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  44. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  45. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  46. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  47. package/dist/types/modes/components/tool-execution.d.ts +18 -0
  48. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  49. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  50. package/dist/types/modes/controllers/event-controller.d.ts +0 -1
  51. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  52. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  53. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  54. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  55. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  56. package/dist/types/modes/index.d.ts +5 -4
  57. package/dist/types/modes/interactive-mode.d.ts +16 -6
  58. package/dist/types/modes/setup-version.d.ts +11 -0
  59. package/dist/types/modes/setup-wizard/index.d.ts +2 -1
  60. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +2 -1
  61. package/dist/types/modes/theme/theme.d.ts +1 -1
  62. package/dist/types/modes/types.d.ts +19 -6
  63. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  64. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  65. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  66. package/dist/types/sdk.d.ts +3 -1
  67. package/dist/types/session/agent-session.d.ts +21 -0
  68. package/dist/types/session/messages.d.ts +12 -0
  69. package/dist/types/session/session-manager.d.ts +3 -1
  70. package/dist/types/slash-commands/types.d.ts +4 -6
  71. package/dist/types/task/executor.d.ts +14 -0
  72. package/dist/types/task/index.d.ts +1 -0
  73. package/dist/types/task/render.d.ts +3 -2
  74. package/dist/types/telemetry-export.d.ts +1 -1
  75. package/dist/types/tools/archive-reader.d.ts +5 -0
  76. package/dist/types/tools/ast-edit.d.ts +3 -0
  77. package/dist/types/tools/ast-grep.d.ts +3 -0
  78. package/dist/types/tools/bash.d.ts +1 -0
  79. package/dist/types/tools/eval-render.d.ts +1 -8
  80. package/dist/types/tools/fetch.d.ts +15 -7
  81. package/dist/types/tools/find.d.ts +8 -4
  82. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  83. package/dist/types/tools/memory-render.d.ts +4 -1
  84. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  85. package/dist/types/tools/render-utils.d.ts +13 -9
  86. package/dist/types/tools/renderers.d.ts +16 -2
  87. package/dist/types/tools/search.d.ts +5 -1
  88. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  89. package/dist/types/tools/todo.d.ts +3 -2
  90. package/dist/types/tools/write.d.ts +5 -0
  91. package/dist/types/tui/output-block.d.ts +16 -4
  92. package/dist/types/tui/status-line.d.ts +3 -0
  93. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  94. package/dist/types/web/scrapers/github.d.ts +22 -0
  95. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  96. package/dist/types/web/search/providers/perplexity.d.ts +8 -1
  97. package/dist/types/web/search/types.d.ts +1 -1
  98. package/package.json +9 -9
  99. package/scripts/dev-launch +42 -0
  100. package/scripts/dev-launch-preload.ts +19 -0
  101. package/src/auto-thinking/classifier.ts +5 -1
  102. package/src/cli/args.ts +2 -2
  103. package/src/cli/dry-balance-cli.ts +52 -17
  104. package/src/cli/gallery-cli.ts +226 -0
  105. package/src/cli/gallery-fixtures/agentic.ts +292 -0
  106. package/src/cli/gallery-fixtures/codeintel.ts +188 -0
  107. package/src/cli/gallery-fixtures/edit.ts +194 -0
  108. package/src/cli/gallery-fixtures/fs.ts +153 -0
  109. package/src/cli/gallery-fixtures/index.ts +40 -0
  110. package/src/cli/gallery-fixtures/interaction.ts +49 -0
  111. package/src/cli/gallery-fixtures/memory.ts +81 -0
  112. package/src/cli/gallery-fixtures/misc.ts +250 -0
  113. package/src/cli/gallery-fixtures/search.ts +213 -0
  114. package/src/cli/gallery-fixtures/shell.ts +167 -0
  115. package/src/cli/gallery-fixtures/types.ts +41 -0
  116. package/src/cli/gallery-fixtures/web.ts +158 -0
  117. package/src/cli/gallery-screenshot.ts +279 -0
  118. package/src/cli-commands.ts +1 -0
  119. package/src/commands/gallery.ts +52 -0
  120. package/src/commands/launch.ts +1 -1
  121. package/src/commit/analysis/conventional.ts +2 -2
  122. package/src/commit/analysis/summary.ts +2 -2
  123. package/src/commit/changelog/generate.ts +2 -2
  124. package/src/commit/changelog/index.ts +2 -2
  125. package/src/commit/map-reduce/index.ts +3 -3
  126. package/src/commit/map-reduce/map-phase.ts +2 -2
  127. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  128. package/src/commit/model-selection.ts +33 -9
  129. package/src/commit/pipeline.ts +4 -4
  130. package/src/config/api-key-resolver.ts +58 -0
  131. package/src/config/keybindings.ts +15 -6
  132. package/src/config/model-equivalence.ts +35 -12
  133. package/src/config/model-id-affixes.ts +39 -22
  134. package/src/config/model-registry.ts +41 -18
  135. package/src/config/settings-schema.ts +28 -5
  136. package/src/config/settings.ts +31 -2
  137. package/src/dap/client.ts +14 -16
  138. package/src/dap/config.ts +41 -2
  139. package/src/dap/defaults.json +1 -0
  140. package/src/dap/session.ts +1 -0
  141. package/src/dap/types.ts +10 -0
  142. package/src/debug/index.ts +40 -54
  143. package/src/edit/renderer.ts +111 -119
  144. package/src/eval/__tests__/agent-bridge.test.ts +75 -32
  145. package/src/eval/__tests__/llm-bridge.test.ts +90 -31
  146. package/src/eval/agent-bridge.ts +34 -7
  147. package/src/eval/llm-bridge.ts +8 -3
  148. package/src/extensibility/extensions/runner.ts +1 -0
  149. package/src/extensibility/plugins/doctor.ts +0 -1
  150. package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
  151. package/src/goals/tools/goal-tool.ts +37 -27
  152. package/src/internal-urls/docs-index.generated.ts +10 -10
  153. package/src/lsp/client.ts +104 -55
  154. package/src/lsp/types.ts +10 -0
  155. package/src/lsp/utils.ts +3 -2
  156. package/src/main.ts +53 -56
  157. package/src/memories/index.ts +12 -5
  158. package/src/memory-backend/index.ts +13 -1
  159. package/src/memory-backend/resolve.ts +3 -5
  160. package/src/memory-backend/types.ts +1 -1
  161. package/src/mnemopi/backend.ts +5 -1
  162. package/src/modes/acp/acp-agent.ts +33 -26
  163. package/src/modes/components/assistant-message.ts +2 -9
  164. package/src/modes/components/chat-block.ts +111 -0
  165. package/src/modes/components/copy-selector.ts +1 -44
  166. package/src/modes/components/custom-editor.ts +33 -1
  167. package/src/modes/components/custom-message.ts +1 -3
  168. package/src/modes/components/execution-shared.ts +1 -2
  169. package/src/modes/components/hook-message.ts +1 -3
  170. package/src/modes/components/overlay-box.ts +108 -0
  171. package/src/modes/components/plan-review-overlay.ts +799 -0
  172. package/src/modes/components/plan-toc.ts +138 -0
  173. package/src/modes/components/read-tool-group.ts +20 -4
  174. package/src/modes/components/skill-message.ts +0 -1
  175. package/src/modes/components/status-line.ts +3 -5
  176. package/src/modes/components/tips.txt +1 -0
  177. package/src/modes/components/todo-reminder.ts +0 -2
  178. package/src/modes/components/tool-execution.ts +115 -90
  179. package/src/modes/components/transcript-container.ts +84 -24
  180. package/src/modes/components/user-message.ts +1 -2
  181. package/src/modes/controllers/command-controller-shared.ts +7 -6
  182. package/src/modes/controllers/command-controller.ts +70 -57
  183. package/src/modes/controllers/event-controller.ts +41 -40
  184. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  185. package/src/modes/controllers/input-controller.ts +135 -122
  186. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  187. package/src/modes/controllers/selector-controller.ts +25 -27
  188. package/src/modes/controllers/streaming-reveal.ts +212 -0
  189. package/src/modes/controllers/tan-command-controller.ts +173 -0
  190. package/src/modes/index.ts +5 -4
  191. package/src/modes/interactive-mode.ts +171 -82
  192. package/src/modes/setup-version.ts +11 -0
  193. package/src/modes/setup-wizard/index.ts +3 -2
  194. package/src/modes/setup-wizard/scenes/web-search.ts +3 -2
  195. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  196. package/src/modes/theme/theme-schema.json +1 -1
  197. package/src/modes/theme/theme.ts +8 -4
  198. package/src/modes/types.ts +19 -8
  199. package/src/modes/utils/context-usage.ts +10 -6
  200. package/src/modes/utils/copy-targets.ts +133 -27
  201. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  202. package/src/modes/utils/ui-helpers.ts +44 -46
  203. package/src/plan-mode/approved-plan.ts +66 -43
  204. package/src/plan-mode/plan-protection.ts +4 -4
  205. package/src/prompts/system/background-tan-dispatch.md +8 -0
  206. package/src/prompts/system/plan-mode-active.md +67 -58
  207. package/src/prompts/system/plan-mode-approved.md +1 -1
  208. package/src/sdk.ts +32 -60
  209. package/src/session/agent-session.ts +89 -13
  210. package/src/session/messages.ts +26 -0
  211. package/src/session/session-manager.ts +13 -5
  212. package/src/slash-commands/builtin-registry.ts +37 -10
  213. package/src/slash-commands/helpers/usage-report.ts +2 -0
  214. package/src/slash-commands/types.ts +4 -6
  215. package/src/task/executor.ts +25 -4
  216. package/src/task/index.ts +4 -0
  217. package/src/task/render.ts +212 -148
  218. package/src/telemetry-export.ts +25 -7
  219. package/src/tools/archive-reader.ts +64 -0
  220. package/src/tools/ask.ts +119 -164
  221. package/src/tools/ast-edit.ts +98 -71
  222. package/src/tools/ast-grep.ts +37 -43
  223. package/src/tools/bash.ts +50 -6
  224. package/src/tools/debug.ts +20 -8
  225. package/src/tools/eval-backends.ts +6 -17
  226. package/src/tools/eval-render.ts +21 -18
  227. package/src/tools/eval.ts +5 -4
  228. package/src/tools/fetch.ts +391 -91
  229. package/src/tools/find.ts +44 -30
  230. package/src/tools/gh-renderer.ts +81 -42
  231. package/src/tools/grouped-file-output.ts +272 -48
  232. package/src/tools/image-gen.ts +150 -103
  233. package/src/tools/inspect-image-renderer.ts +63 -41
  234. package/src/tools/inspect-image.ts +8 -1
  235. package/src/tools/job.ts +3 -4
  236. package/src/tools/memory-render.ts +4 -1
  237. package/src/tools/plan-mode-guard.ts +21 -39
  238. package/src/tools/read.ts +23 -16
  239. package/src/tools/render-utils.ts +38 -40
  240. package/src/tools/renderers.ts +16 -1
  241. package/src/tools/report-tool-issue.ts +1 -1
  242. package/src/tools/resolve.ts +14 -0
  243. package/src/tools/search-tool-bm25.ts +36 -23
  244. package/src/tools/search.ts +189 -95
  245. package/src/tools/sqlite-reader.ts +9 -12
  246. package/src/tools/todo.ts +138 -59
  247. package/src/tools/write.ts +100 -60
  248. package/src/tui/output-block.ts +60 -13
  249. package/src/tui/status-line.ts +5 -1
  250. package/src/utils/commit-message-generator.ts +9 -1
  251. package/src/utils/enhanced-paste.ts +202 -0
  252. package/src/utils/title-generator.ts +2 -1
  253. package/src/web/scrapers/github.ts +255 -3
  254. package/src/web/scrapers/youtube.ts +3 -2
  255. package/src/web/search/providers/anthropic.ts +25 -19
  256. package/src/web/search/providers/exa.ts +11 -3
  257. package/src/web/search/providers/kimi.ts +28 -17
  258. package/src/web/search/providers/parallel.ts +35 -24
  259. package/src/web/search/providers/perplexity.ts +199 -51
  260. package/src/web/search/providers/synthetic.ts +8 -6
  261. package/src/web/search/providers/tavily.ts +9 -8
  262. package/src/web/search/providers/zai.ts +8 -6
  263. package/src/web/search/render.ts +39 -54
  264. package/src/web/search/types.ts +5 -1
  265. package/dist/types/eval/__tests__/shared-executors.test.d.ts +0 -1
  266. package/src/eval/__tests__/shared-executors.test.ts +0 -609
@@ -231,6 +231,57 @@ describe("runEvalAgent", () => {
231
231
  });
232
232
  await expect(runEvalAgent({ prompt: "fail" }, { session: makeSession() })).rejects.toThrow("boom");
233
233
  });
234
+
235
+ // Regression: a runtime-limit abort returns exitCode=1, stderr="", error=undefined,
236
+ // aborted=true, abortReason="Subagent runtime limit exceeded (...)". The previous
237
+ // failure-message coalesce stopped at the empty `stderr` (since `??` only skips
238
+ // nullish values) and shipped an empty error through the bridge — Python then
239
+ // surfaced the generic `bridge call '__agent__' failed`. See #2006.
240
+ it("surfaces abortReason for aborts that leave stderr empty", async () => {
241
+ mockAgents();
242
+ const runSpy = vi.spyOn(taskExecutor, "runSubprocess");
243
+ runSpy.mockImplementationOnce(async options =>
244
+ singleResult(options, {
245
+ exitCode: 1,
246
+ output: "",
247
+ stderr: "",
248
+ error: undefined,
249
+ aborted: true,
250
+ abortReason: "Subagent runtime limit exceeded (task.maxRuntimeMs=900000)",
251
+ }),
252
+ );
253
+ runSpy.mockImplementationOnce(async options =>
254
+ singleResult(options, {
255
+ exitCode: 1,
256
+ output: "",
257
+ stderr: " ",
258
+ error: " ",
259
+ aborted: true,
260
+ abortReason: "Cancelled by caller",
261
+ }),
262
+ );
263
+ runSpy.mockImplementationOnce(async options =>
264
+ singleResult(options, {
265
+ exitCode: 1,
266
+ output: "",
267
+ stderr: "",
268
+ error: undefined,
269
+ }),
270
+ );
271
+
272
+ await expect(runEvalAgent({ prompt: "slow" }, { session: makeSession() })).rejects.toThrow(
273
+ "Subagent runtime limit exceeded (task.maxRuntimeMs=900000)",
274
+ );
275
+ // Whitespace-only stderr/error must not mask abortReason either.
276
+ await expect(runEvalAgent({ prompt: "cancelled" }, { session: makeSession() })).rejects.toThrow(
277
+ "Cancelled by caller",
278
+ );
279
+ // Last resort: still produce a non-empty message even when nothing useful is set,
280
+ // so Python never falls back to `bridge call '__agent__' failed`.
281
+ await expect(runEvalAgent({ prompt: "blank" }, { session: makeSession() })).rejects.toThrow(
282
+ "agent() subagent 'task' failed.",
283
+ );
284
+ });
234
285
  });
235
286
 
236
287
  describe("agent() through eval runtimes", () => {
@@ -326,18 +377,6 @@ describe("agent() through eval runtimes", () => {
326
377
  singleResult(options, { output: "hello from python" }),
327
378
  );
328
379
 
329
- const probe = await executePython('print("probe")', {
330
- cwd: tempDir.path(),
331
- sessionId: `${sessionId}:probe`,
332
- sessionFile,
333
- kernelMode: "per-call",
334
- });
335
- if (probe.exitCode === undefined && probe.cancelled) {
336
- expect(probe.output).toBe("");
337
- return;
338
- }
339
- expect(probe.exitCode).toBe(0);
340
-
341
380
  const result = await executePython('print(agent("hi"))', {
342
381
  cwd: tempDir.path(),
343
382
  sessionId,
@@ -345,6 +384,10 @@ describe("agent() through eval runtimes", () => {
345
384
  kernelMode: "per-call",
346
385
  toolSession: session,
347
386
  });
387
+ if (result.exitCode === undefined && result.cancelled) {
388
+ expect(result.output).toBe("");
389
+ return; // kernel unavailable in this environment
390
+ }
348
391
 
349
392
  expect(result.exitCode).toBe(0);
350
393
  expect(result.output.trim()).toBe("hello from python");
@@ -373,22 +416,14 @@ describe("agent() through eval runtimes", () => {
373
416
  }
374
417
  });
375
418
 
376
- const probe = await executePython('print("probe")', {
377
- cwd: tempDir.path(),
378
- sessionId: `${sessionId}:probe`,
379
- sessionFile,
380
- kernelMode: "per-call",
381
- });
382
- if (probe.exitCode === undefined && probe.cancelled) {
383
- expect(probe.output).toBe("");
384
- return;
385
- }
386
- expect(probe.exitCode).toBe(0);
387
-
388
419
  const result = await executePython(
389
420
  'import json\nprint(json.dumps(parallel([lambda n=n: agent(n) for n in ["a", "b", "c", "d"]])))',
390
421
  { cwd: tempDir.path(), sessionId, sessionFile, kernelMode: "per-call", toolSession: session },
391
422
  );
423
+ if (result.exitCode === undefined && result.cancelled) {
424
+ expect(result.output).toBe("");
425
+ return; // kernel unavailable in this environment
426
+ }
392
427
 
393
428
  expect(result.exitCode).toBe(0);
394
429
  expect(JSON.parse(result.output.trim())).toEqual(["a", "b", "c", "d"]);
@@ -412,7 +447,14 @@ describe("agent() through eval runtimes", () => {
412
447
  // The host must respond the instant the cell aborts so the kernel can
413
448
  // unwind via KeyboardInterrupt instead of being hard-killed (which used to
414
449
  // surface "[kernel] Python kernel shutdown" and lose all session state).
450
+ let inFlight = 0;
451
+ let markSaturated: (() => void) | undefined;
452
+ const saturated = new Promise<void>(resolve => {
453
+ markSaturated = resolve;
454
+ });
415
455
  vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => {
456
+ // task.maxConcurrency=6 → six bridge calls block at once; signal then.
457
+ if (++inFlight >= 6) markSaturated?.();
416
458
  await Bun.sleep(9000); // deliberately ignores options.signal
417
459
  return singleResult(options, { output: options.assignment ?? "" });
418
460
  });
@@ -432,8 +474,9 @@ describe("agent() through eval runtimes", () => {
432
474
  expect(seed.exitCode).toBe(0);
433
475
 
434
476
  const ac = new AbortController();
435
- // Abort ~1s in, after the worker threads are blocked in their bridge calls.
436
- setTimeout(() => ac.abort(new Error("external interrupt")), 1000);
477
+ // Abort the instant all six worker threads are confirmed blocked in their
478
+ // bridge calls (condition-driven) instead of waiting a fixed wall second.
479
+ void saturated.then(() => ac.abort(new Error("external interrupt")));
437
480
 
438
481
  const start = Date.now();
439
482
  const result = await executePython(
@@ -568,12 +611,12 @@ describe("agent() through eval runtimes", () => {
568
611
  // of its own. The bridge pause must make that delegated time invisible to
569
612
  // the watchdog.
570
613
  vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => {
571
- await Bun.sleep(200);
614
+ await Bun.sleep(40);
572
615
  return singleResult(options, { output: "done" });
573
616
  });
574
617
 
575
618
  const ops: string[] = [];
576
- using idle = new IdleTimeout(60);
619
+ using idle = new IdleTimeout(20);
577
620
  const result = await runEvalAgent(
578
621
  { prompt: "investigate" },
579
622
  {
@@ -591,7 +634,7 @@ describe("agent() through eval runtimes", () => {
591
634
  expect(ops).toEqual([EVAL_TIMEOUT_PAUSE_OP, EVAL_TIMEOUT_RESUME_OP]);
592
635
  expect(idle.signal.aborted).toBe(false);
593
636
 
594
- await Bun.sleep(90);
637
+ await Bun.sleep(60);
595
638
  expect(idle.signal.aborted).toBe(true);
596
639
  });
597
640
 
@@ -604,7 +647,7 @@ describe("agent() through eval runtimes", () => {
604
647
  // They render as status, but timeout accounting is controlled only by the
605
648
  // bridge pause/resume events.
606
649
  vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => {
607
- for (let i = 0; i < 40; i++) {
650
+ for (let i = 0; i < 20; i++) {
608
651
  options.onProgress?.({
609
652
  index: options.index,
610
653
  id: options.id,
@@ -621,13 +664,13 @@ describe("agent() through eval runtimes", () => {
621
664
  cost: 0,
622
665
  durationMs: i * 10,
623
666
  });
624
- await Bun.sleep(10);
667
+ await Bun.sleep(5);
625
668
  }
626
669
  return singleResult(options, { output: "done" });
627
670
  });
628
671
 
629
672
  const ops: string[] = [];
630
- using idle = new IdleTimeout(80);
673
+ using idle = new IdleTimeout(40);
631
674
  const result = await runEvalAgent(
632
675
  { prompt: "investigate" },
633
676
  {
@@ -4,6 +4,7 @@ import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
4
4
  import * as ai from "@oh-my-pi/pi-ai";
5
5
  import { Effort } from "@oh-my-pi/pi-ai";
6
6
  import { TempDir } from "@oh-my-pi/pi-utils";
7
+ import { $ } from "bun";
7
8
  import type { ModelRegistry } from "../../config/model-registry";
8
9
  import { Settings } from "../../config/settings";
9
10
  import type { ToolSession } from "../../tools";
@@ -13,7 +14,7 @@ import { IdleTimeout } from "../idle-timeout";
13
14
  import { disposeAllVmContexts } from "../js/context-manager";
14
15
  import { executeJs } from "../js/executor";
15
16
  import { runEvalLlm } from "../llm-bridge";
16
- import { disposeAllKernelSessions, executePython } from "../py/executor";
17
+ import { disposeAllKernelSessions, type PythonResult } from "../py/executor";
17
18
 
18
19
  function makeModel(provider: string, id: string, extra: Partial<Model<Api>> = {}): Model<Api> {
19
20
  return {
@@ -57,6 +58,7 @@ function makeSession(opts: SessionOptions = {}): ToolSession {
57
58
  const modelRegistry = {
58
59
  getAvailable: () => opts.available ?? [SMOL, DEFAULT, SLOW],
59
60
  getApiKey: async () => (opts.apiKey === undefined ? "test-key" : opts.apiKey),
61
+ resolver: () => async () => (opts.apiKey === undefined ? "test-key" : opts.apiKey),
60
62
  } as unknown as ModelRegistry;
61
63
  return {
62
64
  settings,
@@ -96,6 +98,77 @@ function assistant(opts: {
96
98
  };
97
99
  }
98
100
 
101
+ async function runPythonLlmInSubprocess(options: { structured: boolean; tempDir: TempDir }): Promise<PythonResult> {
102
+ const repoRoot = path.resolve(import.meta.dir, "../../../..");
103
+ const scriptPath = path.join(options.tempDir.path(), "run-python-llm.ts");
104
+ const resultPath = path.join(options.tempDir.path(), "python-llm-result.json");
105
+ const aiPath = path.resolve(import.meta.dir, "../../../../ai/src/index.ts");
106
+ const executorPath = path.resolve(import.meta.dir, "../py/executor.ts");
107
+ const settingsPath = path.resolve(import.meta.dir, "../../config/settings.ts");
108
+ const code = options.structured
109
+ ? 'import json\nprint(json.dumps(llm("hi", schema={"type": "object"})))'
110
+ : 'print(llm("hi", model="smol"))';
111
+ const responseContent = options.structured
112
+ ? '[{ type: "toolCall", id: "tc-1", name: "respond", arguments: { ok: true } }]'
113
+ : '[{ type: "text", text: "hello from python" }]';
114
+ await Bun.write(
115
+ scriptPath,
116
+ `
117
+ import { vi } from "bun:test";
118
+ import * as ai from ${JSON.stringify(aiPath)};
119
+ import { executePython } from ${JSON.stringify(executorPath)};
120
+ import { Settings } from ${JSON.stringify(settingsPath)};
121
+
122
+ const SMOL = {
123
+ id: "smol",
124
+ name: "smol",
125
+ api: "openai-responses",
126
+ provider: "p",
127
+ baseUrl: "https://example.test/v1",
128
+ reasoning: false,
129
+ input: ["text"],
130
+ cost: { input: 1, output: 1, cacheRead: 0, cacheWrite: 1 },
131
+ contextWindow: 128000,
132
+ maxTokens: 4096,
133
+ };
134
+ const settings = Settings.isolated({ "async.enabled": false, "task.isolation.mode": "none" });
135
+ settings.setModelRole("smol", "p/smol");
136
+ settings.setModelRole("slow", "p/slow");
137
+ const session = {
138
+ settings,
139
+ modelRegistry: {
140
+ getAvailable: () => [SMOL],
141
+ getApiKey: async () => "test-key",
142
+ resolver: () => async () => "test-key",
143
+ },
144
+ getActiveModelString: () => "p/smol",
145
+ };
146
+ vi.spyOn(ai, "completeSimple").mockResolvedValue({
147
+ role: "assistant",
148
+ api: "openai-responses",
149
+ provider: "p",
150
+ model: "smol",
151
+ stopReason: "stop",
152
+ content: ${responseContent},
153
+ });
154
+ const result = await executePython(${JSON.stringify(code)}, {
155
+ cwd: ${JSON.stringify(options.tempDir.path())},
156
+ sessionId: ${JSON.stringify(`py-llm:${options.structured ? "struct" : "plain"}`)},
157
+ sessionFile: ${JSON.stringify(path.join(options.tempDir.path(), "session.jsonl"))},
158
+ toolSession: session,
159
+ kernelMode: "per-call",
160
+ });
161
+ await Bun.write(${JSON.stringify(resultPath)}, JSON.stringify(result));
162
+ process.exit(0);
163
+ `,
164
+ );
165
+ const child = await $`bun ${scriptPath}`.cwd(repoRoot).quiet().nothrow();
166
+ const stdout = child.stdout.toString();
167
+ const stderr = child.stderr.toString();
168
+ if (child.exitCode !== 0) throw new Error(stderr || stdout || `Python llm subprocess exited with ${child.exitCode}`);
169
+ return (await Bun.file(resultPath).json()) as PythonResult;
170
+ }
171
+
99
172
  describe("runEvalLlm", () => {
100
173
  afterEach(() => {
101
174
  vi.restoreAllMocks();
@@ -290,38 +363,24 @@ describe("llm() through eval runtimes", () => {
290
363
  });
291
364
 
292
365
  it("exposes llm() in the Python runtime", async () => {
293
- using tempDir = TempDir.createSync("@omp-eval-llm-py-");
294
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
295
- const sessionId = `py-llm:${crypto.randomUUID()}`;
296
- vi.spyOn(ai, "completeSimple").mockResolvedValue(assistant({ text: "hello from python" }));
297
-
298
- const result = await executePython('print(llm("hi", model="smol"))', {
299
- cwd: tempDir.path(),
300
- sessionId,
301
- sessionFile,
302
- toolSession: makeSession(),
303
- });
304
-
305
- expect(result.exitCode).toBe(0);
306
- expect(result.output.trim()).toBe("hello from python");
366
+ const tempDir = TempDir.createSync("@omp-eval-llm-py-");
367
+ try {
368
+ const result = await runPythonLlmInSubprocess({ structured: false, tempDir });
369
+ expect(result.exitCode).toBe(0);
370
+ expect(result.output.trim()).toBe("hello from python");
371
+ } finally {
372
+ tempDir.removeSync();
373
+ }
307
374
  });
308
375
 
309
376
  it("parses structured llm() output in the Python runtime", async () => {
310
- using tempDir = TempDir.createSync("@omp-eval-llm-py-struct-");
311
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
312
- const sessionId = `py-llm-struct:${crypto.randomUUID()}`;
313
- vi.spyOn(ai, "completeSimple").mockResolvedValue(
314
- assistant({ toolCall: { name: "respond", arguments: { ok: true } } }),
315
- );
316
-
317
- const result = await executePython('import json\nprint(json.dumps(llm("hi", schema={"type": "object"})))', {
318
- cwd: tempDir.path(),
319
- sessionId,
320
- sessionFile,
321
- toolSession: makeSession(),
322
- });
323
-
324
- expect(result.exitCode).toBe(0);
325
- expect(JSON.parse(result.output.trim())).toEqual({ ok: true });
377
+ const tempDir = TempDir.createSync("@omp-eval-llm-py-struct-");
378
+ try {
379
+ const result = await runPythonLlmInSubprocess({ structured: true, tempDir });
380
+ expect(result.exitCode).toBe(0);
381
+ expect(JSON.parse(result.output.trim())).toEqual({ ok: true });
382
+ } finally {
383
+ tempDir.removeSync();
384
+ }
326
385
  });
327
386
  });
@@ -13,7 +13,7 @@ import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.m
13
13
  import * as taskDiscovery from "../task/discovery";
14
14
  import * as taskExecutor from "../task/executor";
15
15
  import { AgentOutputManager } from "../task/output-manager";
16
- import type { AgentDefinition, AgentProgress } from "../task/types";
16
+ import type { AgentDefinition, AgentProgress, SingleResult } from "../task/types";
17
17
  import type { ToolSession } from "../tools";
18
18
  import { ToolError } from "../tools/tool-errors";
19
19
  import { withBridgeTimeoutPause } from "./bridge-timeout";
@@ -173,6 +173,26 @@ function emitProgressStatus(emitStatus: ((event: JsStatusEvent) => void) | undef
173
173
  });
174
174
  }
175
175
 
176
+ /**
177
+ * Coalesce a subagent failure into a non-empty, human-meaningful error message.
178
+ *
179
+ * When the executor aborts a subagent (runtime limit, parent cancellation, …)
180
+ * the actionable explanation lives on `abortReason`, while `error`/`stderr`
181
+ * are routinely empty strings. Plain `??` coalescing stops at the empty string
182
+ * and ships an empty error through the bridge — Python then surfaces only the
183
+ * generic `bridge call '__agent__' failed`. See #2006.
184
+ */
185
+ function buildSubagentFailureMessage(agentName: string, result: SingleResult): string {
186
+ const abortReason = trimToUndefined(result.abortReason);
187
+ if (result.aborted && abortReason) return abortReason;
188
+ return (
189
+ trimToUndefined(result.error) ??
190
+ trimToUndefined(result.stderr) ??
191
+ abortReason ??
192
+ `agent() subagent '${agentName}' failed.`
193
+ );
194
+ }
195
+
176
196
  /**
177
197
  * Run a single subagent on behalf of an eval cell's `agent()` call.
178
198
  */
@@ -225,7 +245,6 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
225
245
  getSessionId: options.session.getSessionId ?? (() => null),
226
246
  };
227
247
  const parentArtifactManager = options.session.getArtifactManager?.() ?? undefined;
228
- const parentEvalSessionId = options.session.getEvalSessionId?.() ?? undefined;
229
248
  const mcpManager = options.session.mcpManager ?? MCPManager.instance();
230
249
  const { sessionFile, artifactsDir, contextFile } = await getArtifacts(options.session);
231
250
  const outputManager = getOutputManager(options.session);
@@ -260,6 +279,12 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
260
279
  authStorage: options.session.authStorage,
261
280
  modelRegistry: options.session.modelRegistry,
262
281
  settings: options.session.settings,
282
+ // Eval `agent()` subagents are never wall-clock capped: the parent
283
+ // cell's idle watchdog is suspended for the whole bridge call
284
+ // (withBridgeTimeoutPause), so a long-running phase/recovery workflow
285
+ // must not be killed by `task.maxRuntimeMs`. Force the limit off
286
+ // regardless of the inherited session setting.
287
+ maxRuntimeMs: 0,
263
288
  mcpManager,
264
289
  contextFiles,
265
290
  skills: availableSkills,
@@ -271,14 +296,16 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
271
296
  parentHindsightSessionState: options.session.getHindsightSessionState?.(),
272
297
  parentMnemopiSessionState: options.session.getMnemopiSessionState?.(),
273
298
  parentTelemetry: options.session.getTelemetry?.(),
274
- parentEvalSessionId,
299
+ // Deliberately omit parentEvalSessionId: the parent's Python kernel is
300
+ // blocked on this bridge call, so sharing the eval session would deadlock
301
+ // (subagent queues behind the parent's in-flight execution, parent waits
302
+ // for subagent → circular). Each bridge-spawned subagent gets its own
303
+ // eval session with an independent kernel.
275
304
  }),
276
305
  );
277
306
 
278
- if (result.exitCode !== 0 || result.error) {
279
- const failureMessage =
280
- result.error ?? result.stderr ?? result.abortReason ?? `agent() subagent '${agentName}' failed.`;
281
- throw new ToolError(failureMessage);
307
+ if (result.exitCode !== 0 || result.error || result.aborted) {
308
+ throw new ToolError(buildSubagentFailureMessage(agentName, result));
282
309
  }
283
310
 
284
311
  options.session.recordEvalSubagentUsage?.(result.usage?.output ?? 0);
@@ -15,6 +15,7 @@ import { instrumentedCompleteSimple, resolveTelemetry } from "@oh-my-pi/pi-agent
15
15
  import { type Api, Effort, getSupportedEfforts, type Model, type Tool } from "@oh-my-pi/pi-ai";
16
16
  import * as z from "zod/v4";
17
17
  import { extractTextContent, extractToolCall, parseJsonPayload } from "../commit/utils";
18
+
18
19
  import { expandRoleAlias, formatModelString, resolveModelFromString } from "../config/model-resolver";
19
20
  import type { ToolSession } from "../tools";
20
21
  import { ToolError } from "../tools/tool-errors";
@@ -112,8 +113,9 @@ export async function runEvalLlm(args: unknown, options: EvalLlmBridgeOptions):
112
113
  );
113
114
  }
114
115
 
115
- const apiKey = await options.session.modelRegistry?.getApiKey(model);
116
- if (!apiKey) {
116
+ const registry = options.session.modelRegistry;
117
+ const apiKey = await registry?.getApiKey(model);
118
+ if (!registry || !apiKey) {
117
119
  throw new ToolError(
118
120
  `llm() has no API key for ${formatModelString(model)}. Configure credentials for this provider or choose another tier.`,
119
121
  );
@@ -143,7 +145,10 @@ export async function runEvalLlm(args: unknown, options: EvalLlmBridgeOptions):
143
145
  tools,
144
146
  },
145
147
  {
146
- apiKey,
148
+ apiKey: registry.resolver(model.provider, {
149
+ sessionId: options.session.getSessionId?.() ?? undefined,
150
+ baseUrl: model.baseUrl,
151
+ }),
147
152
  signal: options.signal,
148
153
  reasoning: reasoningForTier(tier, model),
149
154
  toolChoice: schema ? { type: "tool", name: STRUCTURED_TOOL_NAME } : undefined,
@@ -354,6 +354,7 @@ export class ExtensionRunner {
354
354
  "ctrl+o": true,
355
355
  "ctrl+t": true,
356
356
  "ctrl+g": true,
357
+ "alt+m": true,
357
358
  // Default chord for `app.message.followUp` (Windows Terminal can't deliver Ctrl+Enter; #1903).
358
359
  "ctrl+q": true,
359
360
  "shift+tab": true,
@@ -25,7 +25,6 @@ export async function runDoctorChecks(): Promise<DoctorCheck[]> {
25
25
  const apiKeys = [
26
26
  { name: "ANTHROPIC_API_KEY", description: "Anthropic API" },
27
27
  { name: "OPENAI_API_KEY", description: "OpenAI API" },
28
- { name: "PERPLEXITY_API_KEY", description: "Perplexity search" },
29
28
  { name: "EXA_API_KEY", description: "Exa search" },
30
29
  ];
31
30
 
@@ -0,0 +1,49 @@
1
+ import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
2
+
3
+ type MarketplaceAutoUpdateMode = "off" | "notify" | "auto";
4
+
5
+ interface MarketplaceAutoUpdateOptions {
6
+ autoUpdate: MarketplaceAutoUpdateMode;
7
+ resolveActiveProjectRegistryPath: (cwd: string) => Promise<string | null>;
8
+ clearPluginRootsCache: () => void;
9
+ }
10
+
11
+ export function scheduleMarketplaceAutoUpdate(options: MarketplaceAutoUpdateOptions): void {
12
+ if (options.autoUpdate === "off") {
13
+ return;
14
+ }
15
+
16
+ void runMarketplaceAutoUpdate(options);
17
+ }
18
+
19
+ async function runMarketplaceAutoUpdate(options: MarketplaceAutoUpdateOptions): Promise<void> {
20
+ try {
21
+ // Startup perf: marketplace manager pulls scraper/fetch/cache code; keep it out of the initial TUI graph.
22
+ const {
23
+ MarketplaceManager,
24
+ getInstalledPluginsRegistryPath,
25
+ getMarketplacesCacheDir,
26
+ getMarketplacesRegistryPath,
27
+ getPluginsCacheDir,
28
+ } = await import("./marketplace");
29
+ const mgr = new MarketplaceManager({
30
+ marketplacesRegistryPath: getMarketplacesRegistryPath(),
31
+ installedRegistryPath: getInstalledPluginsRegistryPath(),
32
+ projectInstalledRegistryPath: (await options.resolveActiveProjectRegistryPath(getProjectDir())) ?? undefined,
33
+ marketplacesCacheDir: getMarketplacesCacheDir(),
34
+ pluginsCacheDir: getPluginsCacheDir(),
35
+ clearPluginRootsCache: options.clearPluginRootsCache,
36
+ });
37
+ await mgr.refreshStaleMarketplaces();
38
+ const updates = await mgr.checkForUpdates();
39
+ if (updates.length === 0) return;
40
+ if (options.autoUpdate === "auto") {
41
+ await mgr.upgradeAllPlugins();
42
+ logger.debug(`Auto-upgraded ${updates.length} marketplace plugin(s)`);
43
+ } else {
44
+ logger.debug(`${updates.length} marketplace plugin update(s) available — /marketplace upgrade`);
45
+ }
46
+ } catch {
47
+ // Silently ignore — network failure, corrupt data, offline.
48
+ }
49
+ }
@@ -8,9 +8,9 @@ import type { Theme, ThemeColor } from "../../modes/theme/theme";
8
8
  import goalDescription from "../../prompts/tools/goal.md" with { type: "text" };
9
9
  import { formatDuration } from "../../slash-commands/helpers/format";
10
10
  import type { ToolSession } from "../../tools";
11
- import { formatErrorMessage, TRUNCATE_LENGTHS } from "../../tools/render-utils";
11
+ import { formatErrorDetail, TRUNCATE_LENGTHS } from "../../tools/render-utils";
12
12
  import { ToolError } from "../../tools/tool-errors";
13
- import { renderStatusLine, truncateToWidth } from "../../tui";
13
+ import { framedBlock, renderStatusLine, truncateToWidth } from "../../tui";
14
14
  import { completionBudgetReport, remainingTokens } from "../runtime";
15
15
  import type { Goal, GoalStatus, GoalToolDetails } from "../state";
16
16
 
@@ -173,8 +173,7 @@ export const goalToolRenderer = {
173
173
  if (args.op === "create" && args.token_budget !== undefined) {
174
174
  meta.push(`budget ${formatNumber(args.token_budget)}`);
175
175
  }
176
- const text = renderStatusLine({ icon: "pending", title: "Goal", description, meta }, uiTheme);
177
- return new Text(text, 0, 0);
176
+ return new Text(renderStatusLine({ icon: "pending", title: "Goal", description, meta }, uiTheme), 0, 0);
178
177
  },
179
178
 
180
179
  renderResult(
@@ -190,51 +189,62 @@ export const goalToolRenderer = {
190
189
 
191
190
  if (result.isError) {
192
191
  const header = renderStatusLine({ icon: "error", title: "Goal", description }, uiTheme);
193
- const body = formatErrorMessage(fallbackText || "Goal tool failed", uiTheme);
194
- return new Text([header, body].join("\n"), 0, 0);
192
+ return framedBlock(uiTheme, width => ({
193
+ header,
194
+ sections: [{ lines: formatErrorDetail(fallbackText || "Goal tool failed", uiTheme).split("\n") }],
195
+ state: "error",
196
+ borderColor: "error",
197
+ width,
198
+ }));
195
199
  }
196
200
 
197
201
  const goal = details?.goal ?? null;
198
202
  if (!goal) {
199
- const header = renderStatusLine({ icon: "warning", title: "Goal", description }, uiTheme);
200
- const body = uiTheme.fg("muted", "No active goal.");
201
- return new Text([header, body].join("\n"), 0, 0);
203
+ return new Text(
204
+ renderStatusLine({ icon: "warning", title: "Goal", description, meta: ["no active goal"] }, uiTheme),
205
+ 0,
206
+ 0,
207
+ );
202
208
  }
203
209
 
204
- const lines: string[] = [];
205
- lines.push(
206
- renderStatusLine(
207
- {
208
- icon: "success",
209
- title: "Goal",
210
- description,
211
- badge: { label: goal.status, color: goalBadgeColor(goal.status) },
212
- },
213
- uiTheme,
214
- ),
210
+ const header = renderStatusLine(
211
+ {
212
+ icon: "success",
213
+ title: "Goal",
214
+ description,
215
+ badge: { label: goal.status, color: goalBadgeColor(goal.status) },
216
+ },
217
+ uiTheme,
215
218
  );
216
219
 
220
+ const lines: string[] = [];
217
221
  const objectiveText = truncateToWidth(goal.objective.trim(), TRUNCATE_LENGTHS.LONG);
218
- lines.push(` ${uiTheme.italic(uiTheme.fg("muted", `"${objectiveText}"`))}`);
222
+ lines.push(uiTheme.italic(uiTheme.fg("muted", `"${objectiveText}"`)));
219
223
 
220
224
  const used = formatNumber(goal.tokensUsed);
221
225
  const tokensLine =
222
226
  goal.tokenBudget !== undefined
223
227
  ? `${used} / ${formatNumber(goal.tokenBudget)} tokens (${formatNumber(Math.max(0, goal.tokenBudget - goal.tokensUsed))} left)`
224
228
  : `${used} tokens`;
225
- lines.push(` ${uiTheme.fg("dim", tokensLine)}`);
226
-
229
+ const metaParts = [tokensLine];
227
230
  if (goal.timeUsedSeconds > 0) {
228
- lines.push(` ${uiTheme.fg("dim", `${formatDuration(goal.timeUsedSeconds * 1000)} elapsed`)}`);
231
+ metaParts.push(`${formatDuration(goal.timeUsedSeconds * 1000)} elapsed`);
229
232
  }
233
+ lines.push(uiTheme.fg("dim", metaParts.join(" · ")));
230
234
 
231
235
  const report = details?.completionBudgetReport;
236
+ const sections: Array<{ label?: string; lines: string[] }> = [{ lines }];
232
237
  if (report) {
233
- lines.push("");
234
- lines.push(uiTheme.italic(uiTheme.fg("muted", report)));
238
+ sections.push({ label: "Report", lines: report.split("\n").map(line => uiTheme.fg("muted", line)) });
235
239
  }
236
240
 
237
- return new Text(lines.join("\n"), 0, 0);
241
+ return framedBlock(uiTheme, width => ({
242
+ header,
243
+ sections,
244
+ state: "success",
245
+ borderColor: "borderMuted",
246
+ width,
247
+ }));
238
248
  },
239
249
 
240
250
  mergeCallAndResult: true,