@oh-my-pi/pi-coding-agent 17.2.4 → 17.2.6

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 (348) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/dist/{CHANGELOG-bpmhv26t.md → CHANGELOG-gs76k6wc.md} +69 -0
  3. package/dist/cli.js +4438 -3962
  4. package/dist/{template-8vdv6xb4.js → template-c2hyaytt.js} +8 -26
  5. package/dist/types/cli/args.d.ts +4 -1
  6. package/dist/types/cli/browser-relay-cli.d.ts +15 -0
  7. package/dist/types/cli/command-help.d.ts +99 -0
  8. package/dist/types/cli/gc-cli.d.ts +1 -0
  9. package/dist/types/cli/help-extra.d.ts +2 -0
  10. package/dist/types/cli/thinking-levels.d.ts +5 -0
  11. package/dist/types/cli/update-cli.d.ts +13 -0
  12. package/dist/types/commands/browser-relay.d.ts +38 -0
  13. package/dist/types/commands/commit.d.ts +3 -0
  14. package/dist/types/commands/complete.d.ts +1 -1
  15. package/dist/types/commands/completions.d.ts +6 -0
  16. package/dist/types/commands/grep.d.ts +3 -0
  17. package/dist/types/commands/join.d.ts +4 -0
  18. package/dist/types/commands/launch-help.d.ts +171 -0
  19. package/dist/types/commands/launch.d.ts +5 -1
  20. package/dist/types/commands/models.d.ts +3 -0
  21. package/dist/types/commands/say.d.ts +10 -0
  22. package/dist/types/commands/worktree.d.ts +3 -0
  23. package/dist/types/config/keybindings.d.ts +5 -0
  24. package/dist/types/config/model-discovery.d.ts +2 -2
  25. package/dist/types/config/models-config-schema-bundle.d.ts +3 -0
  26. package/dist/types/config/models-config-schema.d.ts +2 -0
  27. package/dist/types/config/models-config.d.ts +1 -0
  28. package/dist/types/config/service-tier.d.ts +2 -0
  29. package/dist/types/config/settings-schema.d.ts +69 -21
  30. package/dist/types/cursor-bridge-tools.d.ts +1 -1
  31. package/dist/types/cursor.d.ts +7 -4
  32. package/dist/types/edit/index.d.ts +3 -3
  33. package/dist/types/edit/modes/replace.d.ts +22 -15
  34. package/dist/types/edit/renderer.d.ts +1 -1
  35. package/dist/types/eval/py/kernel.d.ts +3 -1
  36. package/dist/types/eval/py/spawn-options.d.ts +12 -16
  37. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +8 -1
  38. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
  39. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
  40. package/dist/types/launch/broker.d.ts +1 -1
  41. package/dist/types/launch/client.d.ts +15 -4
  42. package/dist/types/launch/ensure.d.ts +8 -0
  43. package/dist/types/launch/paths.d.ts +1 -1
  44. package/dist/types/launch/protocol.d.ts +19 -2
  45. package/dist/types/lsp/client.d.ts +2 -0
  46. package/dist/types/lsp/mux/daemon.d.ts +15 -0
  47. package/dist/types/lsp/mux/protocol.d.ts +57 -0
  48. package/dist/types/lsp/mux/server.d.ts +18 -0
  49. package/dist/types/lsp/types.d.ts +22 -2
  50. package/dist/types/main.d.ts +2 -0
  51. package/dist/types/mnemopi/embed-client.d.ts +1 -1
  52. package/dist/types/mnemopi/state.d.ts +0 -23
  53. package/dist/types/modes/components/assistant-message.d.ts +2 -0
  54. package/dist/types/modes/components/btw-panel.d.ts +3 -0
  55. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  56. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  57. package/dist/types/modes/components/stripped-tool-calls-placeholder.d.ts +12 -0
  58. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  59. package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
  60. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  61. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  62. package/dist/types/modes/interactive-mode.d.ts +5 -1
  63. package/dist/types/modes/types.d.ts +4 -1
  64. package/dist/types/modes/workflow.d.ts +2 -1
  65. package/dist/types/sdk.d.ts +3 -1
  66. package/dist/types/secrets/index.d.ts +1 -1
  67. package/dist/types/security/contracts/schemas.d.ts +405 -403
  68. package/dist/types/session/agent-session-types.d.ts +8 -1
  69. package/dist/types/session/agent-session.d.ts +26 -2
  70. package/dist/types/session/indexed-session-storage.d.ts +1 -0
  71. package/dist/types/session/launch-completion.d.ts +10 -0
  72. package/dist/types/session/session-advisors.d.ts +1 -1
  73. package/dist/types/session/session-context.d.ts +2 -1
  74. package/dist/types/session/session-entries.d.ts +15 -1
  75. package/dist/types/session/session-maintenance.d.ts +1 -0
  76. package/dist/types/session/session-manager.d.ts +23 -11
  77. package/dist/types/session/session-stats.d.ts +10 -0
  78. package/dist/types/session/session-storage.d.ts +15 -3
  79. package/dist/types/session/yield-queue.d.ts +6 -1
  80. package/dist/types/system-prompt.d.ts +4 -1
  81. package/dist/types/task/index.d.ts +2 -1
  82. package/dist/types/task/spawn-policy.d.ts +5 -0
  83. package/dist/types/thinking.d.ts +1 -7
  84. package/dist/types/tools/ast-grep.d.ts +1 -1
  85. package/dist/types/tools/browser/registry.d.ts +2 -1
  86. package/dist/types/tools/browser/relay/bridge.d.ts +35 -0
  87. package/dist/types/tools/browser/relay/daemon.d.ts +16 -0
  88. package/dist/types/tools/browser/relay/kind.d.ts +19 -0
  89. package/dist/types/tools/browser/relay/protocol.d.ts +97 -0
  90. package/dist/types/tools/browser/relay/server.d.ts +35 -0
  91. package/dist/types/tools/browser/render.d.ts +1 -0
  92. package/dist/types/tools/browser.d.ts +3 -0
  93. package/dist/types/tools/computer/exposure.d.ts +9 -7
  94. package/dist/types/tools/computer/protocol.d.ts +70 -17
  95. package/dist/types/tools/computer/supervisor.d.ts +23 -10
  96. package/dist/types/tools/computer/worker-entry.d.ts +1 -0
  97. package/dist/types/tools/computer/worker.d.ts +42 -7
  98. package/dist/types/tools/computer-renderer.d.ts +7 -5
  99. package/dist/types/tools/computer.d.ts +25 -44
  100. package/dist/types/tools/context.d.ts +3 -0
  101. package/dist/types/tools/glob.d.ts +1 -1
  102. package/dist/types/tools/grep.d.ts +1 -1
  103. package/dist/types/tools/index.d.ts +9 -0
  104. package/dist/types/tools/render-utils.d.ts +2 -0
  105. package/dist/types/tools/{browser/run-cancellation.d.ts → run-scope.d.ts} +8 -8
  106. package/dist/types/tools/tool-timeouts.d.ts +5 -0
  107. package/dist/types/tools/xdev.d.ts +8 -0
  108. package/dist/types/utils/clipboard.d.ts +2 -2
  109. package/dist/types/web/kagi.d.ts +1 -0
  110. package/dist/types/web/search/index.d.ts +1 -1
  111. package/dist/types/web/search/providers/anthropic.d.ts +1 -0
  112. package/dist/types/web/search/providers/base.d.ts +2 -0
  113. package/dist/types/web/search/providers/brave.d.ts +1 -0
  114. package/dist/types/web/search/providers/browser-page.d.ts +1 -0
  115. package/dist/types/web/search/providers/codex.d.ts +1 -0
  116. package/dist/types/web/search/providers/exa.d.ts +1 -0
  117. package/dist/types/web/search/providers/firecrawl.d.ts +1 -0
  118. package/dist/types/web/search/providers/gemini.d.ts +1 -0
  119. package/dist/types/web/search/providers/jina.d.ts +1 -0
  120. package/dist/types/web/search/providers/kagi.d.ts +1 -0
  121. package/dist/types/web/search/providers/kimi.d.ts +1 -0
  122. package/dist/types/web/search/providers/parallel.d.ts +1 -0
  123. package/dist/types/web/search/providers/perplexity.d.ts +1 -0
  124. package/dist/types/web/search/providers/searxng.d.ts +1 -0
  125. package/dist/types/web/search/providers/tavily.d.ts +1 -0
  126. package/dist/types/web/search/providers/tinyfish.d.ts +1 -0
  127. package/dist/types/web/search/providers/utils.d.ts +6 -9
  128. package/dist/types/web/search/providers/zai.d.ts +1 -0
  129. package/dist/types/web/search/types.d.ts +4 -0
  130. package/package.json +12 -12
  131. package/src/cli/args.ts +7 -87
  132. package/src/cli/browser-relay-cli.ts +119 -0
  133. package/src/cli/command-help.ts +103 -0
  134. package/src/cli/completion-gen.ts +5 -5
  135. package/src/cli/flag-tables.ts +9 -0
  136. package/src/cli/gc-cli.ts +641 -21
  137. package/src/cli/help-extra.ts +89 -0
  138. package/src/cli/setup-cli.ts +13 -21
  139. package/src/cli/thinking-levels.ts +7 -0
  140. package/src/cli/update-cli.ts +26 -7
  141. package/src/cli-commands.ts +170 -33
  142. package/src/cli.ts +17 -5
  143. package/src/commands/acp.ts +3 -1
  144. package/src/commands/agents.ts +3 -2
  145. package/src/commands/auth-broker.ts +3 -2
  146. package/src/commands/auth-gateway.ts +3 -2
  147. package/src/commands/bench.ts +2 -3
  148. package/src/commands/browser-relay.ts +53 -0
  149. package/src/commands/cleanse.ts +2 -2
  150. package/src/commands/commit.ts +3 -2
  151. package/src/commands/complete.ts +2 -1
  152. package/src/commands/completions.ts +3 -2
  153. package/src/commands/config.ts +3 -2
  154. package/src/commands/dry-balance.ts +2 -2
  155. package/src/commands/gallery.ts +3 -2
  156. package/src/commands/gc.ts +3 -2
  157. package/src/commands/grep.ts +3 -2
  158. package/src/commands/grievances.ts +3 -2
  159. package/src/commands/install.ts +2 -2
  160. package/src/commands/join.ts +3 -2
  161. package/src/commands/launch-help.ts +116 -0
  162. package/src/commands/launch.ts +7 -191
  163. package/src/commands/models.ts +3 -2
  164. package/src/commands/plugin.ts +3 -2
  165. package/src/commands/read.ts +3 -2
  166. package/src/commands/say.ts +3 -2
  167. package/src/commands/setup.ts +3 -2
  168. package/src/commands/shell.ts +3 -2
  169. package/src/commands/ssh.ts +3 -2
  170. package/src/commands/stats.ts +3 -2
  171. package/src/commands/tiny-models.ts +2 -2
  172. package/src/commands/token.ts +2 -2
  173. package/src/commands/ttsr.ts +2 -2
  174. package/src/commands/update.ts +3 -2
  175. package/src/commands/usage.ts +3 -2
  176. package/src/commands/web-search.ts +3 -2
  177. package/src/commands/worktree.ts +3 -2
  178. package/src/config/keybindings.ts +5 -0
  179. package/src/config/model-discovery.ts +44 -27
  180. package/src/config/model-registry.ts +19 -13
  181. package/src/config/models-config-schema-bundle.ts +9 -0
  182. package/src/config/resolve-config-value.ts +2 -1
  183. package/src/config/service-tier.ts +5 -0
  184. package/src/config/settings-schema.ts +72 -20
  185. package/src/config/settings.ts +1 -1
  186. package/src/cursor-bridge-tools.ts +1 -1
  187. package/src/cursor.ts +10 -14
  188. package/src/edit/diff.ts +17 -8
  189. package/src/edit/index.ts +20 -7
  190. package/src/edit/modes/replace.ts +93 -40
  191. package/src/edit/renderer.ts +1 -1
  192. package/src/edit/streaming.ts +14 -17
  193. package/src/eval/py/kernel.ts +2 -1
  194. package/src/eval/py/spawn-options.ts +24 -29
  195. package/src/export/html/index.ts +10 -3
  196. package/src/export/html/template.js +8 -26
  197. package/src/export/share.ts +4 -0
  198. package/src/extensibility/extensions/wrapper.ts +1 -0
  199. package/src/extensibility/legacy-pi-ai-shim.ts +22 -1
  200. package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -6
  201. package/src/extensibility/plugins/legacy-pi-compat.ts +124 -57
  202. package/src/extensibility/skills.ts +11 -0
  203. package/src/extensibility/utils.ts +10 -2
  204. package/src/launch/broker.ts +223 -7
  205. package/src/launch/client.ts +192 -21
  206. package/src/launch/ensure.ts +73 -0
  207. package/src/launch/paths.ts +1 -1
  208. package/src/launch/protocol.ts +54 -2
  209. package/src/lsp/client.ts +23 -2
  210. package/src/lsp/index.ts +8 -0
  211. package/src/lsp/mux/daemon.ts +348 -0
  212. package/src/lsp/mux/protocol.ts +96 -0
  213. package/src/lsp/mux/server.ts +797 -0
  214. package/src/lsp/types.ts +28 -2
  215. package/src/main.ts +20 -8
  216. package/src/mcp/config-writer.ts +1 -1
  217. package/src/mcp/oauth-flow.ts +11 -1
  218. package/src/mnemopi/embed-client.ts +49 -2
  219. package/src/mnemopi/state.ts +57 -23
  220. package/src/modes/components/assistant-message.ts +11 -0
  221. package/src/modes/components/btw-panel.ts +41 -4
  222. package/src/modes/components/chat-transcript-builder.ts +4 -0
  223. package/src/modes/components/custom-editor.ts +9 -0
  224. package/src/modes/components/read-tool-group.ts +10 -0
  225. package/src/modes/components/status-line/component.ts +1 -1
  226. package/src/modes/components/stripped-tool-calls-placeholder.ts +35 -0
  227. package/src/modes/components/tool-execution.ts +8 -0
  228. package/src/modes/components/tree-selector.ts +46 -48
  229. package/src/modes/controllers/btw-controller.ts +55 -7
  230. package/src/modes/controllers/command-controller.ts +31 -0
  231. package/src/modes/controllers/event-controller.ts +17 -0
  232. package/src/modes/controllers/input-controller.ts +63 -8
  233. package/src/modes/controllers/mcp-command-controller.ts +11 -5
  234. package/src/modes/controllers/selector-controller.ts +20 -0
  235. package/src/modes/interactive-mode.ts +18 -2
  236. package/src/modes/types.ts +9 -1
  237. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  238. package/src/modes/utils/interactive-context-helpers.ts +1 -0
  239. package/src/modes/utils/ui-helpers.ts +6 -10
  240. package/src/modes/workflow.ts +8 -2
  241. package/src/prompts/agents/init.md +1 -1
  242. package/src/prompts/session/launch-completion.md +1 -0
  243. package/src/prompts/system/plan-mode-active.md +2 -2
  244. package/src/prompts/system/system-prompt.md +26 -52
  245. package/src/prompts/system/workflow-notice.md +3 -3
  246. package/src/prompts/tools/ast-grep.md +1 -1
  247. package/src/prompts/tools/bash.md +1 -1
  248. package/src/prompts/tools/browser.md +1 -0
  249. package/src/prompts/tools/computer.md +21 -21
  250. package/src/prompts/tools/glob.md +1 -1
  251. package/src/prompts/tools/grep.md +1 -1
  252. package/src/prompts/tools/replace.md +5 -4
  253. package/src/prompts/tools/task.md +4 -4
  254. package/src/sdk.ts +49 -6
  255. package/src/secrets/index.ts +1 -1
  256. package/src/security/contracts/schemas.ts +205 -183
  257. package/src/security/contracts/validation.ts +5 -1
  258. package/src/security/store.ts +2 -2
  259. package/src/session/agent-session-types.ts +9 -1
  260. package/src/session/agent-session.ts +204 -15
  261. package/src/session/indexed-session-storage.ts +9 -0
  262. package/src/session/launch-completion.ts +37 -0
  263. package/src/session/prewalk.ts +24 -2
  264. package/src/session/session-advisors.ts +2 -2
  265. package/src/session/session-context.ts +42 -12
  266. package/src/session/session-dump-format.ts +1 -1
  267. package/src/session/session-entries.ts +17 -1
  268. package/src/session/session-maintenance.ts +38 -4
  269. package/src/session/session-manager.ts +123 -67
  270. package/src/session/session-paths.ts +118 -55
  271. package/src/session/session-stats.ts +48 -3
  272. package/src/session/session-storage.ts +30 -22
  273. package/src/session/stream-guards.ts +1 -1
  274. package/src/session/yield-queue.ts +121 -16
  275. package/src/slash-commands/builtin-registry.ts +37 -24
  276. package/src/system-prompt.ts +8 -3
  277. package/src/task/index.ts +23 -8
  278. package/src/task/spawn-policy.ts +14 -0
  279. package/src/thinking.ts +2 -8
  280. package/src/tools/ast-grep.ts +10 -4
  281. package/src/tools/browser/cmux/cmux-tab.ts +97 -14
  282. package/src/tools/browser/cmux/rpc.ts +2 -8
  283. package/src/tools/browser/registry.ts +53 -2
  284. package/src/tools/browser/relay/bridge.ts +945 -0
  285. package/src/tools/browser/relay/daemon.ts +117 -0
  286. package/src/tools/browser/relay/extension-assets/background.js.txt +242 -0
  287. package/src/tools/browser/relay/extension-assets/manifest.json.txt +14 -0
  288. package/src/tools/browser/relay/extension-assets/options.html.txt +53 -0
  289. package/src/tools/browser/relay/extension-assets/options.js.txt +23 -0
  290. package/src/tools/browser/relay/kind.ts +41 -0
  291. package/src/tools/browser/relay/protocol.ts +54 -0
  292. package/src/tools/browser/relay/server.ts +141 -0
  293. package/src/tools/browser/render.ts +4 -1
  294. package/src/tools/browser/shared-daemon.ts +7 -58
  295. package/src/tools/browser/tab-supervisor.ts +5 -4
  296. package/src/tools/browser/tab-worker.ts +33 -12
  297. package/src/tools/browser.ts +26 -1
  298. package/src/tools/computer/exposure.ts +11 -35
  299. package/src/tools/computer/protocol.ts +51 -10
  300. package/src/tools/computer/supervisor.ts +227 -124
  301. package/src/tools/computer/worker-entry.ts +11 -6
  302. package/src/tools/computer/worker.ts +695 -81
  303. package/src/tools/computer-renderer.ts +104 -65
  304. package/src/tools/computer.ts +134 -428
  305. package/src/tools/context.ts +3 -0
  306. package/src/tools/glob.ts +9 -2
  307. package/src/tools/grep.ts +12 -6
  308. package/src/tools/hub/launch.ts +122 -6
  309. package/src/tools/index.ts +9 -0
  310. package/src/tools/inspect-image.ts +32 -6
  311. package/src/tools/render-utils.ts +2 -0
  312. package/src/tools/{browser/run-cancellation.ts → run-scope.ts} +11 -11
  313. package/src/tools/tool-timeouts.ts +1 -0
  314. package/src/tools/xdev.ts +29 -2
  315. package/src/utils/clipboard.ts +40 -14
  316. package/src/utils/command-args.ts +26 -28
  317. package/src/utils/tool-choice.ts +0 -13
  318. package/src/web/kagi.ts +2 -1
  319. package/src/web/search/index.ts +18 -2
  320. package/src/web/search/providers/anthropic.ts +4 -1
  321. package/src/web/search/providers/base.ts +2 -0
  322. package/src/web/search/providers/brave.ts +3 -1
  323. package/src/web/search/providers/browser-page.ts +6 -4
  324. package/src/web/search/providers/codex.ts +4 -1
  325. package/src/web/search/providers/duckduckgo.ts +2 -1
  326. package/src/web/search/providers/ecosia.ts +2 -1
  327. package/src/web/search/providers/exa.ts +4 -2
  328. package/src/web/search/providers/firecrawl.ts +3 -1
  329. package/src/web/search/providers/gemini.ts +8 -2
  330. package/src/web/search/providers/google.ts +2 -1
  331. package/src/web/search/providers/jina.ts +12 -2
  332. package/src/web/search/providers/kagi.ts +3 -0
  333. package/src/web/search/providers/kimi.ts +5 -1
  334. package/src/web/search/providers/mojeek.ts +2 -1
  335. package/src/web/search/providers/parallel.ts +5 -1
  336. package/src/web/search/providers/perplexity.ts +6 -3
  337. package/src/web/search/providers/public.ts +1 -1
  338. package/src/web/search/providers/searxng.ts +11 -5
  339. package/src/web/search/providers/startpage.ts +10 -4
  340. package/src/web/search/providers/synthetic.ts +11 -5
  341. package/src/web/search/providers/tavily.ts +3 -1
  342. package/src/web/search/providers/tinyfish.ts +3 -1
  343. package/src/web/search/providers/utils.ts +12 -10
  344. package/src/web/search/providers/xai.ts +2 -2
  345. package/src/web/search/providers/zai.ts +18 -3
  346. package/src/web/search/types.ts +6 -0
  347. package/dist/types/config/file-lock.d.ts +0 -29
  348. package/src/config/file-lock.ts +0 -164
@@ -109,6 +109,8 @@ export interface AgentSessionConfig {
109
109
  agent: Agent;
110
110
  sessionManager: SessionManager;
111
111
  settings: Settings;
112
+ /** Whether the session spawn policy permits the read-only `scout` subagent. Defaults to true. */
113
+ scoutAllowedBySpawnPolicy?: boolean;
112
114
  /** Whether the caller explicitly requested yolo/auto-approve behavior for this session. */
113
115
  autoApprove?: boolean;
114
116
  /** Models to cycle through with Ctrl+P (from --models flag). */
@@ -232,7 +234,7 @@ export interface AgentSessionConfig {
232
234
  /**
233
235
  * Build the `replace`-mode `edit` a Cursor `pi_edit` frame needs, against the
234
236
  * advisor-scoped tool session. The advisor's ordinary instance follows the
235
- * configured `edit.mode` and rejects the frame's `old_text`/`new_text` pairs.
237
+ * configured `edit.mode` and rejects the frame's `old_string`/`new_string` args.
236
238
  */
237
239
  advisorCreateEditTool?(): AgentTool | undefined;
238
240
  /**
@@ -387,5 +389,11 @@ export interface FreshSessionResult {
387
389
  closedProviderSessions: number;
388
390
  }
389
391
 
392
+ /** Outcome of an in-place `/reset` conversation-context reset. */
393
+ export interface ResetSessionContextResult {
394
+ /** Number of live messages dropped from the model's context. */
395
+ droppedCount: number;
396
+ }
397
+
390
398
  /** Queued user content restored to the editor. */
391
399
  export type RestoredQueuedMessage = { text: string; images?: ImageContent[] };
@@ -144,6 +144,7 @@ import type { GoalModeState } from "../goals/state";
144
144
  import type { HindsightSessionState } from "../hindsight/state";
145
145
  import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
146
146
  import type { IrcMessage } from "../irc/bus";
147
+ import type { DaemonCompletionNotification } from "../launch/protocol";
147
148
  import { shutdownMnemopiEmbedClient } from "../mnemopi/embed-client";
148
149
  import { getMnemopiSessionState, type MnemopiSessionState, setMnemopiSessionState } from "../mnemopi/state";
149
150
  import { containsOrchestrate, ORCHESTRATE_NOTICE } from "../modes/orchestrate";
@@ -221,6 +222,7 @@ import type {
221
222
  ModelCycleResult,
222
223
  Prewalk,
223
224
  PromptOptions,
225
+ ResetSessionContextResult,
224
226
  ResolvedRoleModel,
225
227
  RestoredQueuedMessage,
226
228
  RoleModelCycle,
@@ -271,6 +273,12 @@ import {
271
273
  type ToolExecutionStartData,
272
274
  } from "./exit-diagnostics";
273
275
  import { IrcBridge, type IrcBridgeHost } from "./irc-bridge";
276
+ import {
277
+ buildLaunchCompletionBatchMessage,
278
+ isLaunchCompletionOwner,
279
+ LAUNCH_COMPLETION_MESSAGE_TYPE,
280
+ type LaunchCompletionEntry,
281
+ } from "./launch-completion";
274
282
  import {
275
283
  type BashExecutionMessage,
276
284
  buildReplanTitleContext,
@@ -340,6 +348,7 @@ import { TodoTracker, type TodoTrackerHost } from "./todo-tracker";
340
348
  import { TtsrCoordinator, type TtsrCoordinatorHost } from "./ttsr-coordinator";
341
349
 
342
350
  const PLAN_MODE_REMINDER_MAX = 3;
351
+ const POST_PROMPT_DRAIN_TIMEOUT_MS = 5_000;
343
352
 
344
353
  /** Internal marker for hook messages queued through the agent loop */
345
354
  // ============================================================================
@@ -445,6 +454,8 @@ export class AgentSession {
445
454
  #lastAppendOnlyResolution?: { enable: boolean; providerId: string | undefined };
446
455
  #eventListeners: AgentSessionEventListener[] = [];
447
456
  #commandMetadataChangedListeners: CommandMetadataChangedListener[] = [];
457
+ #sessionChangeCallbacks = new Set<() => void>();
458
+ #observedSessionId: string | undefined;
448
459
 
449
460
  /** Messages queued to be included with the next user prompt as context ("asides"). */
450
461
  #pendingNextTurnMessages: CustomMessage[] = [];
@@ -518,6 +529,7 @@ export class AgentSession {
518
529
  // Agent identity (registry id) used for IRC routing and job ownership.
519
530
  #agentId: string | undefined;
520
531
  #agentKind: "main" | "sub" = "main";
532
+ #scoutAllowedBySpawnPolicy = true;
521
533
  #providerSessionId: string | undefined;
522
534
  #freshProviderSessionId: string | undefined;
523
535
  #inheritedProviderPromptCacheKey: string | undefined;
@@ -657,6 +669,7 @@ export class AgentSession {
657
669
  if (onSettled) this.#inFlightSettledCallbacks.push(onSettled);
658
670
  this.#promptInFlightCount = Math.max(0, this.#promptInFlightCount - 1);
659
671
  if (this.#promptInFlightCount !== 0) return;
672
+ this.yieldQueue.requestIdleFlush();
660
673
  this.#releasePowerAssertion();
661
674
  this.#flushPendingAgentEnd();
662
675
  if (this.#inFlightSettledCallbacks.length === 0) {
@@ -838,6 +851,7 @@ export class AgentSession {
838
851
 
839
852
  #resetInFlight(): void {
840
853
  this.#promptInFlightCount = 0;
854
+ this.yieldQueue.requestIdleFlush();
841
855
  this.#releasePowerAssertion();
842
856
  this.#flushPendingAgentEnd();
843
857
  if (this.#inFlightSettledCallbacks.length === 0) {
@@ -1121,17 +1135,30 @@ export class AgentSession {
1121
1135
  injectIdle: async messages => {
1122
1136
  const first = messages[0];
1123
1137
  if (!first) return;
1124
- await this.agent.prompt(messages.length === 1 ? first : messages);
1138
+ this.#beginInFlight();
1139
+ try {
1140
+ await this.agent.prompt(messages.length === 1 ? first : messages);
1141
+ } finally {
1142
+ this.#endInFlight();
1143
+ }
1125
1144
  },
1126
1145
  scheduleIdleFlush: run => {
1127
1146
  this.#schedulePostPromptTask(
1128
1147
  async () => {
1129
1148
  await run();
1130
1149
  },
1131
- { delayMs: 1 },
1150
+ {
1151
+ delayMs: 1,
1152
+ onSkip: () => this.yieldQueue.cancelIdleFlushScheduling(),
1153
+ },
1132
1154
  );
1133
1155
  },
1134
1156
  });
1157
+ this.yieldQueue.register<LaunchCompletionEntry>(LAUNCH_COMPLETION_MESSAGE_TYPE, {
1158
+ isStale: entry =>
1159
+ this.#isDisposed || !isLaunchCompletionOwner(entry.owner, this.sessionManager.getSessionId()),
1160
+ build: buildLaunchCompletionBatchMessage,
1161
+ });
1135
1162
  // Background-job completions / late diagnostics are pulled into the run at
1136
1163
  // each step boundary as non-interrupting asides. Peer IRCs share the aside
1137
1164
  // injection boundary, but also expose a non-consuming interrupt peek so
@@ -1237,6 +1264,7 @@ export class AgentSession {
1237
1264
  this.#loopGuards = new LoopGuards(streamGuardsHost);
1238
1265
  this.#agentId = config.agentId;
1239
1266
  this.#agentKind = config.agentKind ?? "main";
1267
+ this.#scoutAllowedBySpawnPolicy = config.scoutAllowedBySpawnPolicy ?? true;
1240
1268
  this.#providerSessionId = config.providerSessionId;
1241
1269
  this.#inheritedProviderPromptCacheKey =
1242
1270
  config.providerPromptCacheKeySource === "fork" ? this.agent.promptCacheKey : undefined;
@@ -1418,6 +1446,7 @@ export class AgentSession {
1418
1446
  syncTodoPhasesFromBranch: () => this.#todo.syncFromBranch(),
1419
1447
  resetAdvisorRuntimes: () => this.#advisors.resetAllRuntimes(),
1420
1448
  rebaseAfterCompaction: () => this.#stats.rebaseAfterCompaction(),
1449
+ recordAnchoredHistoryRewrite: tokensRemoved => this.#stats.recordAnchoredHistoryRewrite(tokensRemoved),
1421
1450
  getContextBreakdown: options => this.getContextBreakdown(options),
1422
1451
  getContextUsage: options => this.getContextUsage(options),
1423
1452
  shake: (mode, options) => this.shake(mode, options),
@@ -2897,6 +2926,7 @@ export class AgentSession {
2897
2926
  try {
2898
2927
  await scheduler.wait(delayMs, { signal });
2899
2928
  } catch {
2929
+ if (signal.aborted) options?.onSkip?.("aborted");
2900
2930
  return;
2901
2931
  }
2902
2932
  }
@@ -3397,6 +3427,12 @@ export class AgentSession {
3397
3427
  };
3398
3428
  }
3399
3429
 
3430
+ /** Register cleanup that runs when this AgentSession adopts a different session ID. */
3431
+ registerSessionChangeCallback(callback: () => void): () => void {
3432
+ this.#sessionChangeCallbacks.add(callback);
3433
+ return () => this.#sessionChangeCallbacks.delete(callback);
3434
+ }
3435
+
3400
3436
  subscribeCommandMetadataChanged(listener: CommandMetadataChangedListener): () => void {
3401
3437
  this.#commandMetadataChangedListeners.push(listener);
3402
3438
  return () => {
@@ -3471,7 +3507,14 @@ export class AgentSession {
3471
3507
  * (login/logout, token refresh that surfaces a new account UUID) without
3472
3508
  * needing to re-call `#syncAgentSessionId()` on every such event.
3473
3509
  */
3474
- #syncAgentSessionId(sessionId?: string): void {
3510
+ #syncAgentSessionId(sessionId?: string, notifyChange = true): void {
3511
+ const currentSessionId = this.sessionManager.getSessionId();
3512
+ if (this.#observedSessionId === undefined) {
3513
+ this.#observedSessionId = currentSessionId;
3514
+ } else if (this.#observedSessionId !== currentSessionId) {
3515
+ this.#observedSessionId = currentSessionId;
3516
+ if (notifyChange) this.#notifySessionChangeCallbacks();
3517
+ }
3475
3518
  const sid = this.#activeProviderSessionId(sessionId);
3476
3519
  this.agent.sessionId = sid;
3477
3520
  this.agent.setMetadataResolver((provider: string) =>
@@ -3493,6 +3536,16 @@ export class AgentSession {
3493
3536
  if (this.#advisors) this.#advisors.refreshProviderIdentity();
3494
3537
  }
3495
3538
 
3539
+ #notifySessionChangeCallbacks(): void {
3540
+ for (const callback of [...this.#sessionChangeCallbacks]) {
3541
+ try {
3542
+ callback();
3543
+ } catch (error) {
3544
+ logger.warn("Session change callback failed", { error: String(error) });
3545
+ }
3546
+ }
3547
+ }
3548
+
3496
3549
  /** Run one abortable auto-learn capture outside the primary agent loop. */
3497
3550
  async runAutolearnCapture(capture: (signal: AbortSignal) => Promise<void>): Promise<void> {
3498
3551
  if (this.#autolearnCaptureTask || this.#isDisposed) return;
@@ -3673,7 +3726,11 @@ export class AgentSession {
3673
3726
  const postPromptDrain = this.#cancelPostPromptTasks();
3674
3727
  this.agent.abort();
3675
3728
  try {
3676
- await withTimeout(postPromptDrain, 5_000, "Timed out draining post-prompt tasks during dispose");
3729
+ await withTimeout(
3730
+ postPromptDrain,
3731
+ POST_PROMPT_DRAIN_TIMEOUT_MS,
3732
+ "Timed out draining post-prompt tasks during dispose",
3733
+ );
3677
3734
  } catch (error) {
3678
3735
  logger.warn("Post-prompt tasks still draining at dispose deadline", { error: String(error) });
3679
3736
  }
@@ -3720,6 +3777,7 @@ export class AgentSession {
3720
3777
  this.#unsubscribeModelRoles = undefined;
3721
3778
  }
3722
3779
  this.#eventListeners = [];
3780
+ this.#sessionChangeCallbacks.clear();
3723
3781
  }
3724
3782
 
3725
3783
  #closeAllProviderSessions(reason: string): void {
@@ -3754,6 +3812,101 @@ export class AgentSession {
3754
3812
  };
3755
3813
  }
3756
3814
 
3815
+ /**
3816
+ * Reset the current conversation in place: drop every message, queued turn,
3817
+ * and pending tool call from the model's context while keeping the session
3818
+ * itself — its id, title, cwd, model, settings, and on-disk transcript all
3819
+ * survive. The next turn is sent with only the base system prompt plus the
3820
+ * project rules/AGENTS.md.
3821
+ *
3822
+ * This is the in-place sibling of {@link newSession}: it reuses the same
3823
+ * conversation-boundary teardown (drop the conversation, rotate provider-side
3824
+ * session state so providers that keep history server-side resume nothing,
3825
+ * re-prime the advisors, and undo any memory promotion) but skips minting a
3826
+ * new session id and opening a fresh transcript file. Unlike
3827
+ * {@link freshSession} (which only rotates provider stream state) it also
3828
+ * clears the conversation.
3829
+ *
3830
+ * Returns `undefined` without mutating anything while a response is
3831
+ * streaming or a foreground bash/python execution is in flight.
3832
+ */
3833
+ async resetSessionContext(): Promise<ResetSessionContextResult | undefined> {
3834
+ // Refuse while a response streams OR a foreground user bash/python
3835
+ // execution is in flight: those complete via recordBashResult()/
3836
+ // recordPythonResult(), which append directly to agent.state when not
3837
+ // streaming, so a command finishing after the reset would land its output
3838
+ // after the boundary and re-enter the supposedly empty context. The
3839
+ // sibling boundary op (branchFromBtw) guards on the same predicates.
3840
+ if (this.isStreaming || this.isBashRunning || this.isEvalRunning) return undefined;
3841
+ const droppedCount = this.agent.state.messages.length;
3842
+
3843
+ // Tear down the same per-turn runtime state that newSession() resets across
3844
+ // a conversation boundary, so work scheduled from the pre-reset turn cannot
3845
+ // re-enter the cleared context:
3846
+ // - bump #promptGeneration + drain post-prompt tasks so an already-queued
3847
+ // post-prompt continuation (recovery can be scheduled after agent_end
3848
+ // while isStreaming is false) sees a stale generation and skips
3849
+ // (mirrors abort()).
3850
+ // - cancel this agent's async bash/task jobs so their completions can't
3851
+ // re-deliver stale tool output into the cleared conversation
3852
+ // (mirrors newSession()).
3853
+ this.#promptGeneration++;
3854
+ await this.#cancelPostPromptTasks();
3855
+ this.#cancelOwnAsyncJobs();
3856
+
3857
+ // Drop the conversation: messages, queued steers/follow-ups, pending tool
3858
+ // calls, and error state. agent.reset() keeps the model and system prompt.
3859
+ this.agent.reset();
3860
+ this.#pendingNextTurnMessages = [];
3861
+ this.#scheduledHiddenNextTurnGeneration = undefined;
3862
+ // Reset the session_stop continuation chain: the queued continuation
3863
+ // message is gone with the conversation, but the counters would otherwise
3864
+ // carry over, so the next post-reset turn is reported to hooks as part of
3865
+ // the old chain and can hit SESSION_STOP_CONTINUATION_CAP early (mirrors
3866
+ // abort()/newSession()).
3867
+ this.#resetSessionStopContinuationState();
3868
+
3869
+ // Drop checkpoint/rewind runtime state and deferred tool directives
3870
+ // alongside the messages that carried them: the checkpoint tool result is
3871
+ // gone from agent.state, so an intact #checkpointState would otherwise
3872
+ // force a rewind onto the pre-reset transcript on the next turn (mirrors
3873
+ // newSession()).
3874
+ this.#clearCheckpointRuntimeState();
3875
+ this.#clearSessionScopedToolState();
3876
+
3877
+ // Rotate provider-side session state so a provider that keeps conversation
3878
+ // history server-side starts a brand-new exchange rather than resuming the
3879
+ // context we just dropped (mirrors freshSession()).
3880
+ this.#closeAllProviderSessions("reset context");
3881
+ this.#freshProviderSessionId = Bun.randomUUIDv7();
3882
+ this.#syncAgentSessionId();
3883
+ this.#memory.rekeyForCurrentSessionId();
3884
+ this.agent.appendOnlyContext?.invalidateForModelChange();
3885
+
3886
+ // Re-arm the approved-plan reference: the reset dropped the plan-approved
3887
+ // prompt/reference from agent.state, so mark it unsent (preserving the
3888
+ // path — the plan file on disk is still the active plan) to let
3889
+ // #buildPlanReferenceMessage re-read and re-inject it on the next turn.
3890
+ // Mirrors the sent-flag reset newSession() and compaction perform after a
3891
+ // history rewrite (issue #1246).
3892
+ this.#planReferenceSent = false;
3893
+
3894
+ // Re-prime the advisors across the conversation boundary and undo any
3895
+ // memory promotion so the next turn rebuilds from the base system prompt.
3896
+ this.#advisors.resetSessionState();
3897
+ await this.#memory.resetContextForNewTranscript();
3898
+
3899
+ // Record a durable boundary on the persisted branch. The collapsed live
3900
+ // transcript and the model-context rebuild start emission after the latest
3901
+ // boundary, so a rebuild across a `/reset` (theme change, focus attach,
3902
+ // /shake, resume) does not resurrect the pre-reset conversation. The
3903
+ // on-disk record and the plain `transcript:true` export path keep the full
3904
+ // pre-reset history.
3905
+ this.sessionManager.appendResetBoundary();
3906
+
3907
+ return { droppedCount };
3908
+ }
3909
+
3757
3910
  // =========================================================================
3758
3911
  // Read-only State Access
3759
3912
  // =========================================================================
@@ -4635,6 +4788,11 @@ export class AgentSession {
4635
4788
  };
4636
4789
  }
4637
4790
 
4791
+ #isScoutAvailable(): boolean {
4792
+ const disabledAgents = this.settings.get("task.disabledAgents") as string[] | undefined;
4793
+ return this.#scoutAllowedBySpawnPolicy && !disabledAgents?.includes("scout");
4794
+ }
4795
+
4638
4796
  async #buildPlanModeMessage(): Promise<CustomMessage | null> {
4639
4797
  const state = this.#planModeState;
4640
4798
  if (!state?.enabled) return null;
@@ -4658,6 +4816,7 @@ export class AgentSession {
4658
4816
  isHashlineEditMode: this.#resolveActiveEditMode() === "hashline",
4659
4817
  reentry: state.reentry ?? false,
4660
4818
  iterative: state.workflow === "iterative",
4819
+ scoutAvailable: this.#isScoutAvailable(),
4661
4820
  });
4662
4821
 
4663
4822
  return {
@@ -4789,7 +4948,10 @@ export class AgentSession {
4789
4948
  keywordNotices.push({
4790
4949
  role: "custom",
4791
4950
  customType: "workflow-notice",
4792
- content: renderWorkflowNotice({ taskBatch: this.settings.get("task.batch") }),
4951
+ content: renderWorkflowNotice({
4952
+ taskBatch: this.settings.get("task.batch"),
4953
+ scoutAvailable: this.#isScoutAvailable(),
4954
+ }),
4793
4955
  display: false,
4794
4956
  attribution: "user",
4795
4957
  timestamp,
@@ -5522,6 +5684,16 @@ export class AgentSession {
5522
5684
  this.#queueHiddenNextTurnMessage(message, true);
5523
5685
  }
5524
5686
 
5687
+ queueLaunchCompletion(notification: DaemonCompletionNotification): Promise<void> {
5688
+ if (this.#isDisposed) return Promise.reject(new Error("Session disposed before launch completion delivery"));
5689
+ const delivered = this.yieldQueue.enqueueWithReceipt<LaunchCompletionEntry>(
5690
+ LAUNCH_COMPLETION_MESSAGE_TYPE,
5691
+ notification,
5692
+ );
5693
+ this.yieldQueue.requestIdleFlush();
5694
+ return delivered;
5695
+ }
5696
+
5525
5697
  #queueHiddenNextTurnMessage(message: CustomMessage, triggerTurn: boolean): void {
5526
5698
  this.#pendingNextTurnMessages.push(message);
5527
5699
  if (!triggerTurn) return;
@@ -7239,7 +7411,7 @@ export class AgentSession {
7239
7411
  this.#clearInheritedProviderPromptCacheKey();
7240
7412
  this.#adoptInheritedProviderPromptCacheKey();
7241
7413
  }
7242
- this.#syncAgentSessionId();
7414
+ this.#syncAgentSessionId(undefined, false);
7243
7415
  this.#memory.rekeyForCurrentSessionId();
7244
7416
 
7245
7417
  let sessionContext = this.buildDisplaySessionContext();
@@ -7376,11 +7548,14 @@ export class AgentSession {
7376
7548
  this.#advisors.restoreCost(await loadAdvisorTranscriptCosts(this.sessionFile));
7377
7549
  }
7378
7550
  this.#bash.finishSessionTransition(bashTransition, true);
7551
+ if (previousSessionState.sessionId !== this.sessionManager.getSessionId()) {
7552
+ this.#notifySessionChangeCallbacks();
7553
+ }
7379
7554
  return true;
7380
7555
  } catch (error) {
7381
7556
  this.sessionManager.restoreState(previousSessionState);
7382
7557
  this.#freshProviderSessionId = previousFreshProviderSessionId;
7383
- this.#syncAgentSessionId(previousSessionState.sessionId);
7558
+ this.#syncAgentSessionId(previousSessionState.sessionId, false);
7384
7559
  this.#memory.rekeyForCurrentSessionId();
7385
7560
  this.agent.setTools(previousTools);
7386
7561
  this.#tools.setBaseSystemPrompt(previousBaseSystemPrompt);
@@ -7494,7 +7669,10 @@ export class AgentSession {
7494
7669
  await this.#advisors.drainAndDetachRecorders();
7495
7670
  try {
7496
7671
  if (!selectedEntry.parentId) {
7672
+ const title = this.sessionManager.getSessionName();
7673
+ const titleSource = this.sessionManager.titleSource;
7497
7674
  await this.sessionManager.newSession({ parentSession: previousSessionFile });
7675
+ if (title) await this.sessionManager.setSessionName(title, titleSource);
7498
7676
  } else {
7499
7677
  this.sessionManager.createBranchedSession(selectedEntry.parentId);
7500
7678
  }
@@ -7541,21 +7719,24 @@ export class AgentSession {
7541
7719
  }
7542
7720
  }
7543
7721
 
7722
+ /** Promotes a completed /btw answer from the explicitly authorized session and leaf. */
7544
7723
  async branchFromBtw(
7545
7724
  question: string,
7546
7725
  assistantMessage: AssistantMessage,
7726
+ leafId: string,
7727
+ sessionId: string,
7547
7728
  ): Promise<{ cancelled: boolean; sessionFile: string | undefined }> {
7548
7729
  const previousSessionFile = this.sessionFile;
7549
7730
  if (!this.sessionManager.getSessionFile()) {
7550
7731
  throw new Error("Cannot branch /btw: session is not persisted");
7551
7732
  }
7552
7733
 
7553
- const leafId = this.sessionManager.getLeafId();
7554
- if (!leafId) {
7555
- throw new Error("Cannot branch /btw: current session has no leaf");
7734
+ if (!leafId || this.sessionManager.getSessionId() !== sessionId || this.sessionManager.getLeafId() !== leafId) {
7735
+ throw new Error("Cannot branch /btw: session changed since /btw started");
7556
7736
  }
7557
7737
 
7558
7738
  if (
7739
+ this.isStreaming ||
7559
7740
  this.isBashRunning ||
7560
7741
  this.isEvalRunning ||
7561
7742
  this.isCompacting ||
@@ -7576,8 +7757,17 @@ export class AgentSession {
7576
7757
  }
7577
7758
  }
7578
7759
 
7579
- await this.#cancelPostPromptTasks();
7760
+ if (this.sessionManager.getSessionId() !== sessionId || this.sessionManager.getLeafId() !== leafId) {
7761
+ throw new Error("Cannot branch /btw: session changed since /btw started");
7762
+ }
7763
+
7764
+ await withTimeout(
7765
+ this.#cancelPostPromptTasks(),
7766
+ POST_PROMPT_DRAIN_TIMEOUT_MS,
7767
+ "Timed out draining post-prompt tasks before /btw branch",
7768
+ );
7580
7769
  if (
7770
+ this.isStreaming ||
7581
7771
  this.isBashRunning ||
7582
7772
  this.isEvalRunning ||
7583
7773
  this.isCompacting ||
@@ -7590,10 +7780,6 @@ export class AgentSession {
7590
7780
  this.#pendingNextTurnMessages = [];
7591
7781
  this.#scheduledHiddenNextTurnGeneration = undefined;
7592
7782
  this.agent.replaceQueues([], []);
7593
- if (this.isStreaming) {
7594
- await this.abort({ goalReason: "internal", reason: "branching /btw" });
7595
- this.agent.replaceQueues([], []);
7596
- }
7597
7783
  await this.#bash.flushPending();
7598
7784
  await this.sessionManager.flush();
7599
7785
  const bashTransition = this.#bash.beginSessionTransition();
@@ -7607,6 +7793,9 @@ export class AgentSession {
7607
7793
  advisorRecordersDetached = true;
7608
7794
  await this.#advisors.drainAndDetachRecorders();
7609
7795
  try {
7796
+ if (this.sessionManager.getSessionId() !== sessionId || this.sessionManager.getLeafId() !== leafId) {
7797
+ throw new Error("Cannot branch /btw: session changed since /btw started");
7798
+ }
7610
7799
  this.sessionManager.createBranchedSession(leafId);
7611
7800
  this.#bash.markSessionTransition(bashTransition);
7612
7801
  this.#advisors.clearCost();
@@ -511,6 +511,15 @@ class IndexedSessionStorageWriter implements SessionStorageWriter {
511
511
  return next;
512
512
  }
513
513
 
514
+ appendSync(line: string): void {
515
+ if (this.#closed) throw new Error("Writer closed");
516
+ if (this.#error) throw this.#error;
517
+ // Local index is updated immediately; remote publish stays ordered on the
518
+ // path queue. Callers that need remote durability still await append()/flush().
519
+ const mtimeMs = this.#storage._appendForWriter(this.#path, line);
520
+ void this.#trackPromise(this.#storage._queueAppend(this.#path, line, mtimeMs, () => this.#error));
521
+ }
522
+
514
523
  async append(line: string): Promise<void> {
515
524
  if (this.#closed) throw new Error("Writer closed");
516
525
  if (this.#error) throw this.#error;
@@ -0,0 +1,37 @@
1
+ import { prompt } from "@oh-my-pi/pi-utils";
2
+ import type { DaemonCompletionNotification } from "../launch/protocol";
3
+ import launchCompletionTemplate from "../prompts/session/launch-completion.md" with { type: "text" };
4
+ import type { CustomMessage } from "./messages";
5
+
6
+ /** Yield-queue kind for broker-owned supervised process completions. */
7
+ export const LAUNCH_COMPLETION_MESSAGE_TYPE = "launch-completion";
8
+
9
+ /** One broker completion awaiting injection into its owning session. */
10
+ export type LaunchCompletionEntry = DaemonCompletionNotification;
11
+
12
+ /** Whether a broker completion belongs to the primary session or its advisor. */
13
+ export function isLaunchCompletionOwner(owner: string, sessionId: string): boolean {
14
+ return owner === sessionId || owner === `${sessionId}-advisor`;
15
+ }
16
+
17
+ /** Build one model-visible notification per terminal supervised process exit. */
18
+ export function buildLaunchCompletionBatchMessage(entries: LaunchCompletionEntry[]): CustomMessage {
19
+ return {
20
+ role: "custom",
21
+ customType: LAUNCH_COMPLETION_MESSAGE_TYPE,
22
+ content: entries
23
+ .map(({ daemon }) =>
24
+ prompt.render(launchCompletionTemplate, {
25
+ name: daemon.name,
26
+ state: daemon.state,
27
+ exitCode: daemon.exitCode,
28
+ hasExitCode: daemon.exitCode !== undefined,
29
+ }),
30
+ )
31
+ .join("\n"),
32
+ display: true,
33
+ attribution: "agent",
34
+ details: { daemons: entries.map(entry => entry.daemon) },
35
+ timestamp: Date.now(),
36
+ };
37
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Agent, AgentMessage, AgentToolResult, AgentTurnEndContext } from "@oh-my-pi/pi-agent-core";
2
2
  import { invalidateMessageCache } from "@oh-my-pi/pi-agent-core/compaction";
3
- import type { Model } from "@oh-my-pi/pi-ai";
3
+ import type { Model, ToolResultMessage } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
5
  import type { LocalProtocolOptions } from "../internal-urls";
6
6
  import { resolveApprovedPlan } from "../plan-mode/approved-plan";
@@ -25,6 +25,28 @@ const PREWALK_ACTION_TOOLS: Record<string, true> = {
25
25
  };
26
26
  const PLAN_YOLO_HANDOFF_MESSAGE_TYPE = "plan-yolo-handoff";
27
27
 
28
+ /**
29
+ * Whether a completed tool result is the first workspace-mutating action that
30
+ * arms the prewalk hand-off. A direct `edit`/`write` call always counts; a
31
+ * `write` that dispatched an `xd://` device (e.g. `lsp`, `ast_edit`, `debug`)
32
+ * counts only when the wrapped tool resolved to a `write`/`exec` approval tier.
33
+ * Read-only device calls — LSP navigation, `debug` inspection, `ast_edit` on
34
+ * internal URLs, help lookups — leave the tier `read` (or absent) and must not
35
+ * switch the model mid-investigation (issue #7312).
36
+ */
37
+ function isPrewalkImplementationAction(result: ToolResultMessage): boolean {
38
+ if (!PREWALK_ACTION_TOOLS[result.toolName]) return false;
39
+ const details = result.details;
40
+ // A direct filesystem edit/write carries no `xd://` dispatch metadata.
41
+ if (!details || typeof details !== "object" || !("xdev" in details) || !details.xdev) return true;
42
+ const xdev = details.xdev;
43
+ // Device dispatch: switch only on a genuine mutation tier. An absent tier
44
+ // (help lookup, unresolved approval) declines the switch, matching the
45
+ // reporter's "stay on the large model a couple turns longer" preference.
46
+ if (typeof xdev !== "object" || !("tier" in xdev)) return false;
47
+ return xdev.tier === "write" || xdev.tier === "exec";
48
+ }
49
+
28
50
  /** Capabilities the prewalk coordinator borrows from its owning session. */
29
51
  export interface PrewalkCoordinatorHost {
30
52
  agent: Agent;
@@ -100,7 +122,7 @@ export class PrewalkCoordinator {
100
122
 
101
123
  const todoGateOpen = this.#todoSeen || !this.#host.getActiveToolNames().includes("todo");
102
124
  const action = todoGateOpen
103
- ? context.toolResults.find(result => PREWALK_ACTION_TOOLS[result.toolName])
125
+ ? context.toolResults.find(result => isPrewalkImplementationAction(result))
104
126
  : undefined;
105
127
  if (!action) {
106
128
  if (!this.#planInjected) {
@@ -191,7 +191,7 @@ export interface SessionAdvisorsOptions {
191
191
  /**
192
192
  * Build the `replace`-mode `edit` a Cursor `pi_edit` frame needs. The
193
193
  * advisor's own instance follows the configured `edit.mode` (`hashline` by
194
- * default), whose schema the frame's `old_text`/`new_text` pairs do not
194
+ * default), whose schema the frame's `old_string`/`new_string` args do not
195
195
  * match, so without this every native advisor edit fails validation.
196
196
  */
197
197
  createEditTool?(): AgentTool | undefined;
@@ -760,7 +760,7 @@ export class SessionAdvisors {
760
760
  // to delete workspace files it was never granted (issue #5680 review).
761
761
  const advisorCanMutateFiles = advisorToolMap.has("write") || advisorToolMap.has("edit");
762
762
  if (advisorCanMutateFiles) availableAdvisorToolNames.add("delete");
763
- // `pi_edit` speaks `replace`'s `old_text`/`new_text` schema, which the
763
+ // `pi_edit` speaks `replace`'s `old_string`/`new_string` schema, which the
764
764
  // advisor's ordinary `EditTool` (built at the session's configured
765
765
  // `edit.mode`, `hashline` by default) does not accept. The bridge map
766
766
  // swaps in a `replace` instance for the exec channel only — the
@@ -154,6 +154,21 @@ function snapcompactHistoryBlocksForContext(
154
154
  return snapcompact.historyBlocks(archive, snapcompactHistoryBlockOptions(archive, options));
155
155
  }
156
156
 
157
+ export function getOpenAiRemoteCompactionPayload(
158
+ compaction: CompactionEntry | null | undefined,
159
+ ): ProviderPayload | undefined {
160
+ const candidate = compaction?.preserveData?.openaiRemoteCompaction;
161
+ if (!candidate || typeof candidate !== "object") return undefined;
162
+ const remote = candidate as { provider?: unknown; replacementHistory?: unknown };
163
+ if (typeof remote.provider !== "string" || remote.provider.length === 0) return undefined;
164
+ if (!Array.isArray(remote.replacementHistory)) return undefined;
165
+ return {
166
+ type: "openaiResponsesHistory",
167
+ provider: remote.provider,
168
+ items: remote.replacementHistory as Array<Record<string, unknown>>,
169
+ };
170
+ }
171
+
157
172
  export function buildSessionContext(
158
173
  entries: SessionEntry[],
159
174
  leafId?: string | null,
@@ -269,6 +284,12 @@ export function buildSessionContext(
269
284
 
270
285
  const injectedTtsrRules = Array.from(injectedTtsrRulesSet);
271
286
 
287
+ // Index on the path of the latest `/reset` boundary, or -1 when none. The
288
+ // collapsed live transcript and the model-context rebuild start emission
289
+ // after it (see the emission branch below); the full-history export path
290
+ // ignores it.
291
+ const resetBoundaryIdx = path.reduce((latest, entry, i) => (entry.type === "reset_boundary" ? i : latest), -1);
292
+
272
293
  // Build messages and collect corresponding entries
273
294
  // When there's a compaction, we need to:
274
295
  // 1. Emit summary first (entry = compaction)
@@ -364,19 +385,28 @@ export function buildSessionContext(
364
385
  appendMessage(entry);
365
386
  }
366
387
  }
388
+ } else if (
389
+ resetBoundaryIdx >= 0 &&
390
+ resetBoundaryIdx > (compaction ? path.findIndex(e => e.type === "compaction" && e.id === compaction.id) : -1)
391
+ ) {
392
+ // A `/reset` boundary durably starts emission after it — for BOTH the
393
+ // collapsed live transcript AND the model context (non-transcript) rebuild
394
+ // that feeds agent.replaceMessages (resume, /shake, reload, image drop).
395
+ // Without honoring it here, those model-context rebuilds walk the full
396
+ // persisted branch and put the pre-reset turns back into the LLM context
397
+ // even though `/reset` reported it empty. The full-history export path
398
+ // (`transcript && !collapseCompactedHistory`) is handled by the first
399
+ // branch above and left untouched, so on-disk history stays recoverable.
400
+ // When a compaction and a reset boundary interact, the later one on the
401
+ // path wins: a boundary after the latest compaction elides that compaction
402
+ // (and its kept tail) too, so only genuinely post-reset entries emit; a
403
+ // boundary before the latest compaction is superseded by it (the
404
+ // `else if (compaction)` branch below handles that case via this guard).
405
+ for (let i = resetBoundaryIdx + 1; i < path.length; i++) {
406
+ appendMessage(path[i]);
407
+ }
367
408
  } else if (compaction) {
368
- const providerPayload: ProviderPayload | undefined = (() => {
369
- const candidate = compaction.preserveData?.openaiRemoteCompaction;
370
- if (!candidate || typeof candidate !== "object") return undefined;
371
- const remote = candidate as { provider?: unknown; replacementHistory?: unknown };
372
- if (typeof remote.provider !== "string" || remote.provider.length === 0) return undefined;
373
- if (!Array.isArray(remote.replacementHistory)) return undefined;
374
- return {
375
- type: "openaiResponsesHistory",
376
- provider: remote.provider,
377
- items: remote.replacementHistory as Array<Record<string, unknown>>,
378
- };
379
- })();
409
+ const providerPayload = getOpenAiRemoteCompactionPayload(compaction);
380
410
  const remoteReplacementHistory = providerPayload?.items;
381
411
 
382
412
  // Re-attach any archived snapcompact frames so the model can keep
@@ -82,7 +82,7 @@ function renderDumpHeader(options: FormatSessionDumpTextOptions, inventoryTools:
82
82
  const hasSystemPromptToolInventory = options.inlineToolDescriptors === true;
83
83
  if (inventoryTools.length > 0 && !hasSystemPromptToolInventory) {
84
84
  lines.push("## Available Tools\n");
85
- lines.push(renderToolInventory(inventoryTools, model?.id ?? ""));
85
+ lines.push(renderToolInventory(inventoryTools));
86
86
  lines.push("\n");
87
87
  }
88
88