@oh-my-pi/pi-coding-agent 17.2.9 → 17.2.11

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 (281) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/dist/{CHANGELOG-8yy8h3bc.md → CHANGELOG-d9xenpn9.md} +83 -0
  3. package/dist/cli.js +10208 -12596
  4. package/dist/types/capability/mcp.d.ts +11 -0
  5. package/dist/types/capability/skill.d.ts +6 -0
  6. package/dist/types/cli/args.d.ts +1 -0
  7. package/dist/types/cli/command-help.d.ts +3 -0
  8. package/dist/types/commands/share.d.ts +25 -0
  9. package/dist/types/commit/agentic/index.d.ts +3 -1
  10. package/dist/types/commit/execute.d.ts +32 -0
  11. package/dist/types/commit/index.d.ts +2 -1
  12. package/dist/types/commit/pipeline.d.ts +7 -1
  13. package/dist/types/config/model-registry.d.ts +4 -0
  14. package/dist/types/config/model-resolver.d.ts +13 -0
  15. package/dist/types/config/settings-schema.d.ts +1 -34
  16. package/dist/types/discovery/agent-plugin-format.d.ts +143 -0
  17. package/dist/types/discovery/agent-plugins.d.ts +1 -0
  18. package/dist/types/discovery/contained-path.d.ts +34 -0
  19. package/dist/types/discovery/index.d.ts +1 -0
  20. package/dist/types/extensibility/custom-commands/types.d.ts +5 -5
  21. package/dist/types/extensibility/custom-tools/types.d.ts +3 -3
  22. package/dist/types/extensibility/extensions/loader.d.ts +9 -2
  23. package/dist/types/extensibility/extensions/types.d.ts +45 -8
  24. package/dist/types/extensibility/hooks/types.d.ts +5 -5
  25. package/dist/types/extensibility/shared-events.d.ts +3 -2
  26. package/dist/types/extensibility/skills.d.ts +5 -0
  27. package/dist/types/index.d.ts +2 -2
  28. package/dist/types/main.d.ts +1 -1
  29. package/dist/types/mcp/transports/header-policy.d.ts +45 -0
  30. package/dist/types/mcp/types.d.ts +15 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +1 -1
  32. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
  33. package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -1
  34. package/dist/types/modes/acp/acp-mode.d.ts +1 -1
  35. package/dist/types/modes/components/agent-hub-projection.d.ts +38 -0
  36. package/dist/types/modes/components/agent-hub-renderer.d.ts +41 -0
  37. package/dist/types/modes/components/agent-hub.d.ts +14 -5
  38. package/dist/types/modes/components/agent-transcript-viewer.d.ts +2 -0
  39. package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
  40. package/dist/types/modes/components/custom-editor.d.ts +1 -2
  41. package/dist/types/modes/components/model-browser.d.ts +9 -1
  42. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  43. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  44. package/dist/types/modes/interactive-mode.d.ts +8 -1
  45. package/dist/types/modes/utils/transcript-render-helpers.d.ts +3 -2
  46. package/dist/types/registry/agent-registry.d.ts +47 -0
  47. package/dist/types/registry/persisted-agents.d.ts +3 -1
  48. package/dist/types/secrets/index.d.ts +23 -1
  49. package/dist/types/session/agent-session-events.d.ts +2 -2
  50. package/dist/types/session/agent-session.d.ts +4 -2
  51. package/dist/types/session/messages.d.ts +1 -0
  52. package/dist/types/session/prewalk.d.ts +3 -1
  53. package/dist/types/session/session-entries.d.ts +10 -0
  54. package/dist/types/session/session-loader.d.ts +11 -1
  55. package/dist/types/session/session-manager.d.ts +9 -1
  56. package/dist/types/session/session-tools.d.ts +18 -1
  57. package/dist/types/session/turn-recovery.d.ts +7 -2
  58. package/dist/types/slash-commands/acp-builtins.d.ts +1 -1
  59. package/dist/types/slash-commands/available-commands.d.ts +1 -1
  60. package/dist/types/task/executor.d.ts +6 -0
  61. package/dist/types/task/index.d.ts +4 -3
  62. package/dist/types/task/read-only-policy.d.ts +3 -0
  63. package/dist/types/task/structured-subagent.d.ts +2 -0
  64. package/dist/types/task/types.d.ts +4 -0
  65. package/dist/types/tools/bash.d.ts +3 -3
  66. package/dist/types/tools/browser/launch.d.ts +2 -1
  67. package/dist/types/tools/browser/tab-worker.d.ts +2 -2
  68. package/dist/types/tools/path-utils.d.ts +1 -0
  69. package/dist/types/tools/read.d.ts +1 -5
  70. package/dist/types/tools/run-scope.d.ts +27 -1
  71. package/dist/types/tools/shell-tokenize.d.ts +16 -0
  72. package/dist/types/tools/terminal-output.d.ts +1 -1
  73. package/dist/types/utils/turndown.d.ts +1 -1
  74. package/dist/types/vibe/runtime.d.ts +2 -3
  75. package/dist/types/web/scrapers/readthedocs.d.ts +0 -3
  76. package/dist/types/web/search/providers/browser-headers.d.ts +2 -3
  77. package/examples/custom-tools/README.md +1 -1
  78. package/examples/extensions/README.md +1 -1
  79. package/examples/extensions/api-demo.ts +5 -6
  80. package/examples/extensions/chalk-logger.ts +1 -1
  81. package/examples/extensions/hello.ts +2 -2
  82. package/examples/extensions/reload-runtime.ts +1 -1
  83. package/examples/extensions/tools.ts +2 -2
  84. package/examples/extensions/with-deps/index.ts +1 -1
  85. package/examples/sdk/06-extensions.ts +1 -1
  86. package/package.json +15 -31
  87. package/scripts/bundle-dist.ts +1 -11
  88. package/scripts/legacy-pi-virtual-module.ts +4 -1
  89. package/src/advisor/runtime.ts +54 -0
  90. package/src/capability/mcp.ts +11 -0
  91. package/src/capability/skill.ts +6 -0
  92. package/src/cli/agents-cli.ts +1 -1
  93. package/src/cli/args.ts +24 -2
  94. package/src/cli/auth-broker-cli.ts +1 -1
  95. package/src/cli/auth-gateway-cli.ts +1 -1
  96. package/src/cli/bench-cli.ts +1 -1
  97. package/src/cli/claude-trace-cli.ts +1 -1
  98. package/src/cli/command-help.ts +4 -0
  99. package/src/cli/config-cli.ts +1 -1
  100. package/src/cli/dry-balance-cli.ts +1 -1
  101. package/src/cli/file-processor.ts +1 -1
  102. package/src/cli/flag-tables.ts +4 -0
  103. package/src/cli/gallery-cli.ts +1 -1
  104. package/src/cli/grep-cli.ts +1 -1
  105. package/src/cli/grievances-cli.ts +1 -1
  106. package/src/cli/help-extra.ts +1 -1
  107. package/src/cli/models-cli.ts +1 -1
  108. package/src/cli/plugin-cli.ts +1 -1
  109. package/src/cli/read-cli.ts +1 -1
  110. package/src/cli/setup-cli.ts +1 -1
  111. package/src/cli/shell-cli.ts +1 -1
  112. package/src/cli/ssh-cli.ts +1 -1
  113. package/src/cli/stats-cli.ts +1 -1
  114. package/src/cli/tiny-models-cli.ts +1 -1
  115. package/src/cli/ttsr-cli.ts +1 -1
  116. package/src/cli/update-cli.ts +1 -1
  117. package/src/cli/usage-cli.ts +1 -1
  118. package/src/cli/web-search-cli.ts +1 -1
  119. package/src/cli/worktree-cli.ts +1 -1
  120. package/src/cli-commands.ts +5 -0
  121. package/src/commands/commit.ts +16 -3
  122. package/src/commands/say.ts +1 -1
  123. package/src/commands/share.ts +71 -0
  124. package/src/commands/token.ts +1 -1
  125. package/src/commit/agentic/agent.ts +1 -1
  126. package/src/commit/agentic/index.ts +39 -21
  127. package/src/commit/cli.ts +1 -1
  128. package/src/commit/execute.ts +56 -0
  129. package/src/commit/index.ts +2 -1
  130. package/src/commit/pipeline.ts +20 -7
  131. package/src/config/model-discovery.ts +1 -1
  132. package/src/config/model-registry.ts +21 -1
  133. package/src/config/model-resolver.ts +54 -7
  134. package/src/config/settings-schema.ts +2 -33
  135. package/src/config/settings.ts +46 -0
  136. package/src/discovery/agent-plugin-format.ts +551 -0
  137. package/src/discovery/agent-plugins.ts +341 -0
  138. package/src/discovery/agents.ts +4 -2
  139. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +5 -4
  140. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -1
  141. package/src/discovery/claude-plugins.ts +21 -5
  142. package/src/discovery/contained-path.ts +78 -0
  143. package/src/discovery/helpers.ts +23 -9
  144. package/src/discovery/index.ts +1 -0
  145. package/src/discovery/omp-plugins.ts +20 -7
  146. package/src/eval/py/runner.py +38 -1
  147. package/src/exec/non-interactive-env.ts +1 -0
  148. package/src/extensibility/custom-commands/loader.ts +4 -4
  149. package/src/extensibility/custom-commands/types.ts +5 -5
  150. package/src/extensibility/custom-tools/loader.ts +4 -4
  151. package/src/extensibility/custom-tools/types.ts +3 -3
  152. package/src/extensibility/extensions/loader.ts +85 -20
  153. package/src/extensibility/extensions/runner.ts +6 -0
  154. package/src/extensibility/extensions/types.ts +51 -8
  155. package/src/extensibility/hooks/loader.ts +4 -5
  156. package/src/extensibility/hooks/types.ts +5 -5
  157. package/src/extensibility/plugins/legacy-pi-compat.ts +53 -0
  158. package/src/extensibility/plugins/marketplace/manager.ts +2 -1
  159. package/src/extensibility/shared-events.ts +3 -2
  160. package/src/extensibility/skills.ts +9 -0
  161. package/src/index.ts +2 -2
  162. package/src/internal-urls/memory-protocol.ts +5 -1
  163. package/src/internal-urls/skill-protocol.ts +14 -0
  164. package/src/internal-urls/vault-protocol.ts +2 -2
  165. package/src/launch/client.ts +11 -1
  166. package/src/launch/protocol.ts +7 -2
  167. package/src/launch/terminal-output.ts +1 -1
  168. package/src/main.ts +88 -23
  169. package/src/markit/converters/docx.ts +1 -1
  170. package/src/markit/converters/epub.ts +1 -1
  171. package/src/markit/converters/pptx.ts +1 -1
  172. package/src/markit/converters/xlsx.ts +1 -1
  173. package/src/mcp/config.ts +3 -0
  174. package/src/mcp/manager.ts +12 -9
  175. package/src/mcp/transports/header-policy.ts +95 -0
  176. package/src/mcp/transports/http.ts +35 -52
  177. package/src/mcp/transports/sse.ts +20 -27
  178. package/src/mcp/types.ts +15 -0
  179. package/src/modes/acp/acp-agent.ts +20 -9
  180. package/src/modes/acp/acp-client-bridge.ts +1 -1
  181. package/src/modes/acp/acp-event-mapper.ts +1 -1
  182. package/src/modes/acp/acp-mode.ts +1 -1
  183. package/src/modes/components/agent-dashboard.ts +2 -0
  184. package/src/modes/components/agent-hub-projection.ts +248 -0
  185. package/src/modes/components/agent-hub-renderer.ts +194 -0
  186. package/src/modes/components/agent-hub.ts +674 -207
  187. package/src/modes/components/agent-transcript-viewer.ts +3 -0
  188. package/src/modes/components/assistant-message.ts +1 -1
  189. package/src/modes/components/chat-transcript-builder.ts +3 -0
  190. package/src/modes/components/custom-editor.ts +0 -9
  191. package/src/modes/components/extensions/inspector-panel.ts +11 -6
  192. package/src/modes/components/footer.ts +1 -3
  193. package/src/modes/components/model-browser.ts +11 -3
  194. package/src/modes/components/model-hub.ts +4 -1
  195. package/src/modes/components/status-line/component.ts +1 -0
  196. package/src/modes/components/status-line/segments.ts +10 -7
  197. package/src/modes/components/status-line/types.ts +2 -0
  198. package/src/modes/components/tool-execution.ts +13 -16
  199. package/src/modes/components/tree-selector.ts +62 -3
  200. package/src/modes/controllers/command-controller.ts +4 -1
  201. package/src/modes/controllers/event-controller.ts +44 -20
  202. package/src/modes/controllers/extension-ui-controller.ts +2 -2
  203. package/src/modes/controllers/input-controller.ts +20 -2
  204. package/src/modes/controllers/mcp-command-controller.ts +81 -20
  205. package/src/modes/controllers/selector-controller.ts +41 -49
  206. package/src/modes/controllers/streaming-reveal.ts +1 -1
  207. package/src/modes/interactive-mode.ts +13 -2
  208. package/src/modes/rpc/rpc-mode.ts +2 -2
  209. package/src/modes/runtime-init.ts +1 -1
  210. package/src/modes/theme/theme.ts +2 -2
  211. package/src/modes/utils/transcript-render-helpers.ts +4 -2
  212. package/src/modes/utils/ui-helpers.ts +1 -0
  213. package/src/prompts/tools/computer.md +1 -1
  214. package/src/registry/agent-lifecycle.ts +14 -7
  215. package/src/registry/agent-registry.ts +65 -2
  216. package/src/registry/persisted-agents.ts +341 -16
  217. package/src/sdk.ts +6 -49
  218. package/src/secrets/index.ts +52 -1
  219. package/src/session/agent-session-events.ts +2 -2
  220. package/src/session/agent-session.ts +54 -16
  221. package/src/session/messages.ts +1 -0
  222. package/src/session/prewalk.ts +57 -17
  223. package/src/session/session-context.ts +3 -5
  224. package/src/session/session-entries.ts +10 -0
  225. package/src/session/session-handoff.ts +5 -1
  226. package/src/session/session-listing.ts +1 -1
  227. package/src/session/session-loader.ts +74 -8
  228. package/src/session/session-manager.ts +22 -2
  229. package/src/session/session-paths.ts +57 -9
  230. package/src/session/session-tools.ts +65 -4
  231. package/src/session/todo-tracker.ts +11 -1
  232. package/src/session/turn-recovery.ts +96 -74
  233. package/src/slash-commands/acp-builtins.ts +1 -1
  234. package/src/slash-commands/available-commands.ts +1 -1
  235. package/src/slash-commands/builtin-registry.ts +12 -8
  236. package/src/task/executor.ts +70 -20
  237. package/src/task/index.ts +30 -34
  238. package/src/task/isolation-runner.ts +24 -11
  239. package/src/task/persisted-revive.ts +12 -5
  240. package/src/task/read-only-policy.ts +27 -0
  241. package/src/task/structured-subagent.ts +14 -4
  242. package/src/task/types.ts +4 -0
  243. package/src/tools/auto-generated-guard.ts +1 -1
  244. package/src/tools/bash-interactive.ts +4 -4
  245. package/src/tools/bash-skill-urls.ts +15 -0
  246. package/src/tools/bash.ts +16 -17
  247. package/src/tools/browser/cmux/cmux-tab.ts +66 -18
  248. package/src/tools/browser/launch.ts +41 -12
  249. package/src/tools/browser/readable.ts +9 -9
  250. package/src/tools/browser/tab-supervisor.ts +12 -3
  251. package/src/tools/browser/tab-worker-entry.ts +1 -1
  252. package/src/tools/browser/tab-worker.ts +101 -12
  253. package/src/tools/debug.ts +1 -1
  254. package/src/tools/fetch.ts +1 -1
  255. package/src/tools/jtd-to-json-schema.ts +123 -21
  256. package/src/tools/path-utils.ts +9 -2
  257. package/src/tools/read.ts +21 -9
  258. package/src/tools/run-scope.ts +290 -4
  259. package/src/tools/shell-tokenize.ts +98 -0
  260. package/src/tools/terminal-output.ts +1 -1
  261. package/src/tools/todo.ts +1 -1
  262. package/src/utils/clipboard.ts +10 -2
  263. package/src/utils/external-editor.ts +4 -2
  264. package/src/utils/jj.ts +1 -1
  265. package/src/utils/turndown.ts +1 -2
  266. package/src/vibe/runtime.ts +5 -5
  267. package/src/web/scrapers/arxiv.ts +1 -1
  268. package/src/web/scrapers/go-pkg.ts +1 -1
  269. package/src/web/scrapers/iacr.ts +1 -1
  270. package/src/web/scrapers/readthedocs.ts +2 -1
  271. package/src/web/scrapers/twitter.ts +2 -2
  272. package/src/web/scrapers/types.ts +1 -1
  273. package/src/web/scrapers/wikipedia.ts +1 -1
  274. package/src/web/search/providers/browser-headers.ts +12 -39
  275. package/src/web/search/providers/codex.ts +3 -26
  276. package/src/web/search/providers/ecosia.ts +1 -1
  277. package/src/web/search/providers/exa.ts +1 -1
  278. package/src/web/search/providers/google.ts +1 -1
  279. package/src/web/search/providers/mojeek.ts +1 -1
  280. package/src/web/search/providers/startpage.ts +1 -1
  281. package/src/markit/converters/mammoth.d.ts +0 -24
@@ -82,6 +82,7 @@ import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
82
82
  import { MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
83
83
  import {
84
84
  $env,
85
+ APP_NAME,
85
86
  escapeXmlText,
86
87
  formatDuration,
87
88
  getAgentDbPath,
@@ -126,6 +127,7 @@ import type {
126
127
  ToolExecutionEndEvent,
127
128
  ToolExecutionStartEvent,
128
129
  ToolExecutionUpdateEvent,
130
+ ToolInfo,
129
131
  TreePreparation,
130
132
  TurnEndEvent,
131
133
  TurnStartEvent,
@@ -302,7 +304,7 @@ import {
302
304
  USER_INTERRUPT_LABEL,
303
305
  } from "./messages";
304
306
  import { ModelControls, type ModelControlsHost } from "./model-controls";
305
- import { PrewalkCoordinator, type PrewalkCoordinatorHost } from "./prewalk";
307
+ import { isPrewalkPlanNudge, PrewalkCoordinator, type PrewalkCoordinatorHost } from "./prewalk";
306
308
  import {
307
309
  isAdvisorCard,
308
310
  isDisplayableQueuedMessage,
@@ -445,6 +447,7 @@ export class AgentSession {
445
447
  // Event subscription state
446
448
  #unsubscribeAgent?: () => void;
447
449
  #cancelExitRecorder?: () => void;
450
+ #cancelFatalRecoveryHint?: () => void;
448
451
  #exitRecorded = false;
449
452
  #unsubscribeAppendOnly?: () => void;
450
453
  #unsubscribeModelRoles?: () => void;
@@ -878,8 +881,8 @@ export class AgentSession {
878
881
  /**
879
882
  * Arm prewalk outside the normal startup path so an explicit slash command starts immediately.
880
883
  */
881
- armPrewalk(target: Model, thinkingLevel?: ConfiguredThinkingLevel): void {
882
- this.#prewalk.arm(target, thinkingLevel);
884
+ armPrewalk(target: Model, thinkingLevel?: ConfiguredThinkingLevel): boolean {
885
+ return this.#prewalk.arm(target, thinkingLevel);
883
886
  }
884
887
 
885
888
  /** Validate the active plan artifact and shape an `xd://propose` result for review-mode hosts. */
@@ -1376,6 +1379,14 @@ export class AgentSession {
1376
1379
  this.#cancelExitRecorder = postmortem.register(`agent-session:${this.sessionManager.getSessionId()}`, reason => {
1377
1380
  this.#recordSessionExit(reason);
1378
1381
  });
1382
+ this.#cancelFatalRecoveryHint = postmortem.registerFatalRecoveryHint(() => {
1383
+ const sessionId = this.sessionManager.getSessionId();
1384
+ if (!sessionId || !this.sessionManager.getSessionFile()) return undefined;
1385
+ return {
1386
+ label: this.#agentId ?? (this.#agentKind === "main" ? "Main" : "Agent"),
1387
+ command: `${APP_NAME} --resume ${sessionId}`,
1388
+ };
1389
+ });
1379
1390
 
1380
1391
  const advisorsHost: SessionAdvisorsHost = {
1381
1392
  agent: this.agent,
@@ -2478,14 +2489,17 @@ export class AgentSession {
2478
2489
  const persistMessageEnd = () => {
2479
2490
  // Check if this is a hook/custom message
2480
2491
  if (event.message.role === "hookMessage" || event.message.role === "custom") {
2481
- // Persist as CustomMessageEntry
2482
- this.sessionManager.appendCustomMessageEntry(
2483
- event.message.customType,
2484
- event.message.content,
2485
- event.message.display,
2486
- event.message.details,
2487
- event.message.attribution ?? "agent",
2488
- );
2492
+ // Prewalk's plan nudge is a one-run steering instruction. Persisting it would
2493
+ // resurrect the consumed prompt on resume, fork, or any context rebuild.
2494
+ if (!isPrewalkPlanNudge(event.message)) {
2495
+ this.sessionManager.appendCustomMessageEntry(
2496
+ event.message.customType,
2497
+ event.message.content,
2498
+ event.message.display,
2499
+ event.message.details,
2500
+ event.message.attribution ?? "agent",
2501
+ );
2502
+ }
2489
2503
  if (event.message.role === "custom" && event.message.customType === "ttsr-injection") {
2490
2504
  this.#ttsr.markInjectedFromDetails(event.message.details);
2491
2505
  }
@@ -3006,11 +3020,23 @@ export class AgentSession {
3006
3020
  }
3007
3021
  this.#beginInFlight();
3008
3022
  try {
3009
- await this.#recovery.maybeRestoreRetryFallbackPrimary();
3023
+ const reverted = await this.#recovery.maybeRestoreRetryFallbackPrimary();
3010
3024
  if (signal.aborted || this.#isDisposed) {
3011
3025
  this.#skipAgentContinue("post-restore-unavailable", options);
3012
3026
  return;
3013
3027
  }
3028
+ // A cooldown-expiry revert can drop the active window below the
3029
+ // accumulated context. The user-prompt path re-checks context after
3030
+ // the revert via runPrePromptCompactionIfNeeded; the auto-continue
3031
+ // path must do the same so agent.continue() never sends a
3032
+ // predictably oversized request to the reverted (smaller) model.
3033
+ if (reverted) {
3034
+ await this.#maintenance.runPrePromptCompactionIfNeeded([]);
3035
+ if (signal.aborted || this.#isDisposed) {
3036
+ this.#skipAgentContinue("post-restore-unavailable", options);
3037
+ return;
3038
+ }
3039
+ }
3014
3040
  if (this.settings.get("retry.usageAwareFallback")) {
3015
3041
  if (!(await this.#runQueuedUsageAwarePreflight(signal))) {
3016
3042
  this.#skipAgentContinue("session-unavailable", options);
@@ -3429,7 +3455,7 @@ export class AgentSession {
3429
3455
  success: event.success,
3430
3456
  attempt: event.attempt,
3431
3457
  finalError: event.finalError,
3432
- recoveredErrors: event.recoveredErrors,
3458
+ retryErrors: event.retryErrors,
3433
3459
  });
3434
3460
  } else if (event.type === "ttsr_triggered") {
3435
3461
  await this.#extensionRunner.emit({ type: "ttsr_triggered", rules: event.rules });
@@ -3758,6 +3784,8 @@ export class AgentSession {
3758
3784
  this.#recordSessionExit(options.reason ?? "dispose");
3759
3785
  this.#cancelExitRecorder?.();
3760
3786
  this.#cancelExitRecorder = undefined;
3787
+ this.#cancelFatalRecoveryHint?.();
3788
+ this.#cancelFatalRecoveryHint = undefined;
3761
3789
  try {
3762
3790
  await emitSessionShutdownEvent(this.#extensionRunner);
3763
3791
  } catch (error) {
@@ -4169,6 +4197,11 @@ export class AgentSession {
4169
4197
  return this.#tools.getAllToolNames();
4170
4198
  }
4171
4199
 
4200
+ /** Full metadata for every registered tool, including source provenance (backs `getAllTools()`). */
4201
+ getAllToolInfos(): ToolInfo[] {
4202
+ return this.#tools.getAllToolInfos();
4203
+ }
4204
+
4172
4205
  /** Installs and activates the ephemeral vibe tool set. */
4173
4206
  activateVibeTools(baseToolNames: string[]): Promise<void> {
4174
4207
  return this.#tools.activateVibeTools(baseToolNames);
@@ -5255,11 +5288,13 @@ export class AgentSession {
5255
5288
 
5256
5289
  if (result?.systemPrompt !== undefined) {
5257
5290
  baseXdevCatalogDelivered = false;
5258
- this.agent.setSystemPrompt(result.systemPrompt);
5291
+ this.#tools.setTurnSystemPromptOverride(result.systemPrompt);
5259
5292
  } else {
5293
+ this.#tools.clearTurnSystemPromptOverride();
5260
5294
  this.agent.setSystemPrompt(beforeAgentStartSystemPrompt);
5261
5295
  }
5262
5296
  } else {
5297
+ this.#tools.clearTurnSystemPromptOverride();
5263
5298
  this.agent.setSystemPrompt(beforeAgentStartSystemPrompt);
5264
5299
  }
5265
5300
 
@@ -5324,6 +5359,8 @@ export class AgentSession {
5324
5359
  await this.#waitForPostPromptRecovery(generation);
5325
5360
  }
5326
5361
  } finally {
5362
+ // The per-turn before_agent_start override lives only for this turn.
5363
+ this.#tools.clearTurnSystemPromptOverride();
5327
5364
  this.#usagePreflightReadyForNextModelCall = false;
5328
5365
  this.#endInFlight();
5329
5366
  }
@@ -6669,9 +6706,10 @@ export class AgentSession {
6669
6706
  }
6670
6707
 
6671
6708
  #extractRewindReport(messages: AgentMessage[]): string | undefined {
6672
- if (!this.#checkpointState) return undefined;
6709
+ const checkpointState = this.#checkpointState;
6710
+ if (!checkpointState) return undefined;
6673
6711
  if (this.#pendingRewindReport) return this.#pendingRewindReport;
6674
- for (let i = messages.length - 1; i >= 0; i--) {
6712
+ for (let i = messages.length - 1; i >= checkpointState.checkpointMessageCount; i--) {
6675
6713
  const message = messages[i];
6676
6714
  if (message?.role !== "toolResult" || message.isError) continue;
6677
6715
  const semanticResult = semanticToolResult(message.toolName, message);
@@ -42,6 +42,7 @@ import { formatOutputNotice } from "../tools/output-meta";
42
42
  export const SKILL_PROMPT_MESSAGE_TYPE = "skill-prompt";
43
43
  export const LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE = "lsp-late-diagnostic";
44
44
  export const BACKGROUND_TAN_DISPATCH_MESSAGE_TYPE = "background-tan-dispatch";
45
+ export const PREWALK_PLAN_MESSAGE_TYPE = "prewalk-plan";
45
46
 
46
47
  /**
47
48
  * Logs provider-error turns so their actual cause is available outside the
@@ -14,11 +14,16 @@ import { type ConfiguredThinkingLevel, prewalkWouldBeNoop } from "../thinking";
14
14
  import type { PlanProposalHandler } from "../tools/resolve";
15
15
  import { ToolError } from "../tools/tool-errors";
16
16
  import type { PlanYolo, Prewalk } from "./agent-session-types";
17
+ import { PREWALK_PLAN_MESSAGE_TYPE } from "./messages";
17
18
  import type { SessionManager } from "./session-manager";
18
19
 
19
- const PREWALK_PLAN_MESSAGE_TYPE = "prewalk-plan";
20
20
  const PREWALK_CONTINUE_MESSAGE_TYPE = "prewalk-continue";
21
21
  const PREWALK_CHECKLIST_MESSAGE_TYPE = "prewalk-checklist";
22
+
23
+ /** Hidden plan steering is consumed within the live run and must not reappear after a context rebuild. */
24
+ export function isPrewalkPlanNudge(message: AgentMessage): boolean {
25
+ return message.role === "custom" && message.customType === PREWALK_PLAN_MESSAGE_TYPE;
26
+ }
22
27
  const PREWALK_ACTION_TOOLS: Record<string, true> = {
23
28
  edit: true,
24
29
  write: true,
@@ -99,10 +104,40 @@ export class PrewalkCoordinator {
99
104
  return this.#prewalk;
100
105
  }
101
106
 
107
+ #isNoop(prewalk: Prewalk): boolean {
108
+ return prewalkWouldBeNoop(
109
+ this.#host.model(),
110
+ this.#host.configuredThinkingLevel(),
111
+ prewalk.target,
112
+ prewalk.thinkingLevel,
113
+ );
114
+ }
115
+
116
+ #clearPrewalkState(): void {
117
+ this.#prewalk = undefined;
118
+ this.#planInjected = false;
119
+ this.#continuePending = false;
120
+ this.#todoSeen = false;
121
+ }
122
+
123
+ #disarmNoop(prewalk: Prewalk): void {
124
+ this.#clearPrewalkState();
125
+ this.#host.emitNotice(
126
+ "info",
127
+ `Prewalk: target ${prewalk.target.provider}/${prewalk.target.id} already matches the active model and thinking level; nothing to switch.`,
128
+ "prewalk",
129
+ );
130
+ }
131
+
102
132
  /** Advances the one-way prewalk switch at a completed assistant-turn boundary. */
103
133
  async advanceAtTurnEnd(liveMessages: AgentMessage[], context: AgentTurnEndContext | undefined): Promise<void> {
104
134
  const prewalk = this.#prewalk;
105
135
  if (!prewalk || context?.message.role !== "assistant") return;
136
+ if (this.#isNoop(prewalk)) {
137
+ this.#scrubPlanNudge(liveMessages);
138
+ this.#disarmNoop(prewalk);
139
+ return;
140
+ }
106
141
  if (context.toolResults.some(result => result.toolName === "todo" && !result.isError)) this.#todoSeen = true;
107
142
 
108
143
  const hasToolResults = context.toolResults.length > 0;
@@ -147,18 +182,12 @@ export class PrewalkCoordinator {
147
182
  }
148
183
  this.#scrubPlanNudge(liveMessages);
149
184
  const target = prewalk.target;
150
- const currentModel = this.#host.model();
151
- if (prewalkWouldBeNoop(currentModel, this.#host.configuredThinkingLevel(), target, prewalk.thinkingLevel)) {
152
- this.#prewalk = undefined;
153
- this.#host.emitNotice(
154
- "info",
155
- `Prewalk: target ${target.provider}/${target.id} already matches the active model and thinking level; nothing to switch.`,
156
- "prewalk",
157
- );
185
+ if (this.#isNoop(prewalk)) {
186
+ this.#disarmNoop(prewalk);
158
187
  return;
159
188
  }
160
189
  await this.#host.setModelTemporary(target, prewalk.thinkingLevel, { ephemeral: true });
161
- this.#prewalk = undefined;
190
+ this.#clearPrewalkState();
162
191
  this.#host.emitNotice(
163
192
  "info",
164
193
  `Prewalk: switched to ${target.provider}/${target.id} after first ${action.toolName} call.`,
@@ -175,18 +204,29 @@ export class PrewalkCoordinator {
175
204
  }
176
205
 
177
206
  /** Arms a prewalk immediately for an explicit slash-command request. */
178
- arm(target: Model, thinkingLevel?: ConfiguredThinkingLevel): void {
179
- if (this.#prewalk) {
207
+ arm(target: Model, thinkingLevel?: ConfiguredThinkingLevel): boolean {
208
+ const active = this.#prewalk;
209
+ if (active) {
180
210
  this.#host.emitNotice(
181
211
  "info",
182
- `Prewalk: already armed for ${this.#prewalk.target.provider}/${this.#prewalk.target.id}, waiting for the first edit/write.`,
212
+ `Prewalk: already armed for ${active.target.provider}/${active.target.id}, waiting for the first edit/write.`,
183
213
  "prewalk",
184
214
  );
185
- return;
215
+ return (
216
+ active.target.provider === target.provider &&
217
+ active.target.id === target.id &&
218
+ active.thinkingLevel === thinkingLevel
219
+ );
220
+ }
221
+ const candidate = { target, thinkingLevel };
222
+ if (this.#isNoop(candidate)) {
223
+ this.#disarmNoop(candidate);
224
+ return false;
186
225
  }
187
- this.#prewalk = { target, thinkingLevel };
226
+ this.#prewalk = candidate;
188
227
  this.#planInjected = true;
189
228
  this.#continuePending = true;
229
+ this.#todoSeen = false;
190
230
  this.#host.agent.steer({
191
231
  role: "custom",
192
232
  customType: PREWALK_PLAN_MESSAGE_TYPE,
@@ -200,6 +240,7 @@ export class PrewalkCoordinator {
200
240
  `Prewalk: armed for ${target.provider}/${target.id} — will switch at the first edit/write once the todo list exists.`,
201
241
  "prewalk",
202
242
  );
243
+ return true;
203
244
  }
204
245
 
205
246
  /** Lazily enables plan-yolo's plan phase before the first prompt is built. */
@@ -220,8 +261,7 @@ export class PrewalkCoordinator {
220
261
 
221
262
  #scrubPlanNudge(liveMessages: AgentMessage[]): void {
222
263
  if (!this.#planInjected) return;
223
- const isPlanNudge = (message: AgentMessage): boolean =>
224
- message.role === "custom" && message.customType === PREWALK_PLAN_MESSAGE_TYPE;
264
+ const isPlanNudge = isPrewalkPlanNudge;
225
265
  for (let index = liveMessages.length - 1; index >= 0; index--) {
226
266
  if (!isPlanNudge(liveMessages[index])) continue;
227
267
  invalidateMessageCache(liveMessages[index]);
@@ -8,6 +8,7 @@ import {
8
8
  INTERRUPTED_THINKING_MESSAGE_TYPE,
9
9
  isCustomMessageContent,
10
10
  normalizeCustomMessagePayload,
11
+ PREWALK_PLAN_MESSAGE_TYPE,
11
12
  } from "./messages";
12
13
  import { type CompactionEntry, EPHEMERAL_MODEL_CHANGE_ROLE, type SessionEntry } from "./session-entries";
13
14
 
@@ -332,15 +333,12 @@ export function buildSessionContext(
332
333
  const appendMessage = (entry: SessionEntry) => {
333
334
  handleEntryResetTracking(entry);
334
335
  if (entry.type === "message") {
335
- if (
336
- !options?.transcript &&
337
- entry.message.role === "assistant" &&
338
- entry.message.retryRecovery?.status === "recovered"
339
- ) {
336
+ if (!options?.transcript && entry.message.role === "assistant" && entry.message.retryRecovery) {
340
337
  return;
341
338
  }
342
339
  pushMessage(entry.message);
343
340
  } else if (entry.type === "custom_message") {
341
+ if (!options?.transcript && entry.customType === PREWALK_PLAN_MESSAGE_TYPE) return;
344
342
  if (!isCustomMessageContent(entry.content)) return;
345
343
  const normalized = normalizeCustomMessagePayload(entry);
346
344
  const attribution = entry.attribution === undefined ? undefined : normalized.attribution;
@@ -84,6 +84,8 @@ export interface ModelChangeEntry extends SessionEntryBase {
84
84
  model: string;
85
85
  /** Role: "default", "smol", "slow", etc. Undefined treated as "default" */
86
86
  role?: string;
87
+ /** True when this transition selected a retry-fallback model rather than the configured model. */
88
+ resolvedModelIsFallback?: boolean;
87
89
  }
88
90
 
89
91
  export interface ServiceTierChangeEntry extends SessionEntryBase {
@@ -207,6 +209,14 @@ export interface SessionInitEntry extends SessionEntryBase {
207
209
  task: string;
208
210
  /** Tools available to the agent */
209
211
  tools: string[];
212
+ /** Agent definition name (for example `scout` or `reviewer`). */
213
+ agent?: string;
214
+ /** Semantic model role declared by the agent, retained even after concrete model resolution. */
215
+ modelRole?: string;
216
+ /** Initially resolved provider/model selector for historical display. */
217
+ resolvedModel?: string;
218
+ /** Whether the agent definition is read-only, allowing an exact zero-LoC attribution. */
219
+ readOnly?: boolean;
210
220
  /** Output schema if structured output was requested. */
211
221
  outputSchema?: unknown;
212
222
  /** Enforcement policy recorded with the output schema for faithful revival. */
@@ -309,7 +309,11 @@ export class SessionHandoff {
309
309
 
310
310
  return { document: handoffText, savedPath };
311
311
  } catch (error) {
312
- if (handoffSignal.aborted || (error instanceof Error && error.name === "AbortError")) {
312
+ // Only a genuine abort (user Esc or the source turn cancelling) is a
313
+ // cancellation. A provider that throws a name==="AbortError" error without the
314
+ // handoff signal being aborted (stall/idle timeout, nested resolution failure)
315
+ // is a real failure and must surface verbatim, not be masked as "cancelled".
316
+ if (handoffSignal.aborted) {
313
317
  throw new Error("Handoff cancelled");
314
318
  }
315
319
  throw error;
@@ -2,7 +2,7 @@ import * as os from "node:os";
2
2
  import * as path from "node:path";
3
3
  import type { Message } from "@oh-my-pi/pi-ai";
4
4
  import { getAgentDir as getDefaultAgentDir, logger, parseJsonlLenient, toError } from "@oh-my-pi/pi-utils";
5
- import { LRUCache } from "lru-cache/raw";
5
+ import { LRUCache } from "@oh-my-pi/pi-utils/lru";
6
6
  import { computeDefaultSessionDir } from "./session-paths";
7
7
  import { FileSessionStorage, type SessionStorage, type SessionStorageStat } from "./session-storage";
8
8
 
@@ -21,6 +21,19 @@ import {
21
21
  } from "./session-title-slot";
22
22
 
23
23
  const STREAM_LOAD_THRESHOLD_BYTES = 8 * 1024 * 1024;
24
+ const STREAM_YIELD_BYTES = 1 * 1024 * 1024;
25
+ const STREAM_YIELD_ENTRIES = 8_192;
26
+
27
+ export interface VisitEntriesFromFileStreamOptions {
28
+ /** Stop after the visitor returns `false`. */
29
+ shouldContinue?: () => boolean;
30
+ /** Stop after this many valid or malformed JSONL records have been consumed. */
31
+ maxRecords?: number;
32
+ /** Yield to the macrotask queue after this many bytes have been consumed. */
33
+ yieldEveryBytes?: number;
34
+ /** Yield to the macrotask queue after this many entries have been visited. */
35
+ yieldEveryEntries?: number;
36
+ }
24
37
 
25
38
  function splitTitleSlot(content: string): { body: string; slot: SessionTitleUpdate | undefined } {
26
39
  const slot = titleUpdateFromSlot(parseTitleSlotFromContent(content));
@@ -59,11 +72,19 @@ export function parseSessionContent(content: string): {
59
72
  /** Parse session JSONL and visit each entry without retaining prior entries. */
60
73
  export async function visitEntriesFromFileStream(
61
74
  filePath: string,
62
- visit: (entry: FileEntry) => void,
75
+ visit: (entry: FileEntry) => void | boolean,
76
+ options: VisitEntriesFromFileStreamOptions = {},
63
77
  ): Promise<SessionTitleUpdate | undefined> {
64
78
  let titleSlot: SessionTitleUpdate | undefined;
65
79
  let sawFirstLine = false;
80
+ let bytesSinceYield = 0;
81
+ let entriesSinceYield = 0;
82
+ let recordsSeen = 0;
83
+ const maxRecords = Math.max(0, options.maxRecords ?? Number.POSITIVE_INFINITY);
84
+ let stopped = false;
66
85
  let visitorThrew = false;
86
+ const yieldEveryBytes = Math.max(0, options.yieldEveryBytes ?? STREAM_YIELD_BYTES);
87
+ const yieldEveryEntries = Math.max(0, options.yieldEveryEntries ?? STREAM_YIELD_ENTRIES);
67
88
  // Byte buffer (NOT a decoded string): multibyte UTF-8 sequences that straddle
68
89
  // a stream-chunk boundary stay intact, and Bun.JSONL.parseChunk accepts typed
69
90
  // arrays directly. Only the unconsumed remainder is held (≤ one record + a
@@ -72,22 +93,62 @@ export async function visitEntriesFromFileStream(
72
93
  let buffer: Uint8Array = new Uint8Array();
73
94
  const decoder = new TextDecoder();
74
95
 
75
- const drain = () => {
76
- while (buffer.length > 0) {
96
+ const yieldToMacrotask = async (): Promise<void> => {
97
+ if (yieldEveryBytes === 0 && yieldEveryEntries === 0) return;
98
+ const bytesReady = yieldEveryBytes === 0 || bytesSinceYield < yieldEveryBytes;
99
+ const entriesReady = yieldEveryEntries === 0 || entriesSinceYield < yieldEveryEntries;
100
+ if (bytesReady && entriesReady) {
101
+ return;
102
+ }
103
+ bytesSinceYield = 0;
104
+ entriesSinceYield = 0;
105
+ await Bun.sleep(0);
106
+ };
107
+
108
+ const drain = async (): Promise<void> => {
109
+ while (buffer.length > 0 && !stopped) {
110
+ if (recordsSeen >= maxRecords) {
111
+ stopped = true;
112
+ break;
113
+ }
77
114
  const { values, error, read, done } = Bun.JSONL.parseChunk(buffer);
78
115
  for (const value of values) {
116
+ if (recordsSeen >= maxRecords) {
117
+ stopped = true;
118
+ break;
119
+ }
120
+ if (options.shouldContinue && !options.shouldContinue()) {
121
+ stopped = true;
122
+ break;
123
+ }
79
124
  try {
80
- visit(value as FileEntry);
125
+ if (visit(value as FileEntry) === false) {
126
+ stopped = true;
127
+ break;
128
+ }
129
+ recordsSeen++;
130
+ entriesSinceYield++;
131
+ if (recordsSeen >= maxRecords) {
132
+ stopped = true;
133
+ break;
134
+ }
81
135
  } catch (err) {
82
136
  visitorThrew = true;
83
137
  throw err;
84
138
  }
139
+ await yieldToMacrotask();
85
140
  }
141
+ if (stopped) break;
86
142
  if (error) {
87
143
  // Malformed record: skip past the next newline and continue.
88
144
  const nextNewline = buffer.indexOf(0x0a, read);
89
145
  if (nextNewline === -1) break; // rest of the bad line not yet received
146
+ recordsSeen++;
90
147
  buffer = buffer.subarray(nextNewline + 1);
148
+ if (recordsSeen >= maxRecords) {
149
+ stopped = true;
150
+ break;
151
+ }
91
152
  continue;
92
153
  }
93
154
  if (read === 0) break; // incomplete record awaiting more data
@@ -101,6 +162,8 @@ export async function visitEntriesFromFileStream(
101
162
 
102
163
  try {
103
164
  for await (const chunk of Bun.file(filePath).stream()) {
165
+ if (stopped) break;
166
+ bytesSinceYield += chunk.byteLength;
104
167
  buffer = buffer.length === 0 ? chunk : Buffer.concat([buffer, chunk]);
105
168
  // The optional fixed-width title slot is a physical first line that is
106
169
  // NOT JSON; peel it before the parser would (correctly) reject it. The
@@ -121,14 +184,15 @@ export async function visitEntriesFromFileStream(
121
184
  }
122
185
  }
123
186
  }
124
- drain();
187
+ await drain();
188
+ await yieldToMacrotask();
125
189
  }
126
190
  // A trailing record without a final newline: terminate it so the parser
127
191
  // can complete it (readline yielded it; parseChunk needs the delimiter).
128
- if (buffer.length > 0 && buffer[buffer.length - 1] !== 0x0a) {
192
+ if (!stopped && buffer.length > 0 && buffer[buffer.length - 1] !== 0x0a) {
129
193
  buffer = Buffer.concat([buffer, new Uint8Array([0x0a])]);
194
+ await drain();
130
195
  }
131
- drain();
132
196
  } catch (err) {
133
197
  if (visitorThrew) throw err;
134
198
  if (isEnoent(err)) return undefined;
@@ -144,7 +208,9 @@ export async function loadEntriesFromFileStream(filePath: string): Promise<{
144
208
  titleSlot: SessionTitleUpdate | undefined;
145
209
  }> {
146
210
  const entries: FileEntry[] = [];
147
- const titleSlot = await visitEntriesFromFileStream(filePath, entry => entries.push(entry));
211
+ const titleSlot = await visitEntriesFromFileStream(filePath, entry => {
212
+ entries.push(entry);
213
+ });
148
214
  return { entries: foldTitleSlot(entries, titleSlot), titleSlot };
149
215
  }
150
216
 
@@ -2047,9 +2047,16 @@ export class SessionManager {
2047
2047
  * Append a model change as a child of the current leaf, then advance the leaf.
2048
2048
  * @param model Model in "provider/modelId" format
2049
2049
  * @param role Optional role (default: "default")
2050
+ * @param resolvedModelIsFallback Whether this transition selected a retry-fallback model
2050
2051
  */
2051
- appendModelChange(model: string, role?: string): string {
2052
- const entry: ModelChangeEntry = { type: "model_change", ...this.#freshEntryFields(), model, role };
2052
+ appendModelChange(model: string, role?: string, resolvedModelIsFallback = false): string {
2053
+ const entry: ModelChangeEntry = {
2054
+ type: "model_change",
2055
+ ...this.#freshEntryFields(),
2056
+ model,
2057
+ role,
2058
+ resolvedModelIsFallback,
2059
+ };
2053
2060
  this.#recordEntry(entry);
2054
2061
  return entry.id;
2055
2062
  }
@@ -2058,6 +2065,10 @@ export class SessionManager {
2058
2065
  systemPrompt: string;
2059
2066
  task: string;
2060
2067
  tools: string[];
2068
+ agent?: string;
2069
+ modelRole?: string;
2070
+ resolvedModel?: string;
2071
+ readOnly?: boolean;
2061
2072
  outputSchema?: unknown;
2062
2073
  outputSchemaMode?: StructuredSubagentSchemaMode;
2063
2074
  restrictToolNames?: boolean;
@@ -2547,6 +2558,9 @@ export class SessionManager {
2547
2558
  systemPrompt: string;
2548
2559
  task: string;
2549
2560
  tools: string[];
2561
+ agent?: string;
2562
+ modelRole?: string;
2563
+ resolvedModel?: string;
2550
2564
  outputSchema?: unknown;
2551
2565
  outputSchemaMode?: StructuredSubagentSchemaMode;
2552
2566
  restrictToolNames?: boolean;
@@ -2567,6 +2581,9 @@ export class SessionManager {
2567
2581
  systemPrompt: string;
2568
2582
  task: string;
2569
2583
  tools: string[];
2584
+ agent?: string;
2585
+ modelRole?: string;
2586
+ resolvedModel?: string;
2570
2587
  outputSchema?: unknown;
2571
2588
  outputSchemaMode?: StructuredSubagentSchemaMode;
2572
2589
  restrictToolNames?: boolean;
@@ -2580,6 +2597,9 @@ export class SessionManager {
2580
2597
  systemPrompt: entry.systemPrompt,
2581
2598
  task: entry.task,
2582
2599
  tools: entry.tools,
2600
+ agent: entry.agent,
2601
+ modelRole: entry.modelRole,
2602
+ resolvedModel: entry.resolvedModel,
2583
2603
  outputSchema: entry.outputSchema,
2584
2604
  outputSchemaMode: entry.outputSchemaMode,
2585
2605
  restrictToolNames: entry.restrictToolNames,