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

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 (319) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/{CHANGELOG-bpmhv26t.md → CHANGELOG-q2w43aw3.md} +51 -0
  3. package/dist/cli.js +4148 -3717
  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/help-extra.d.ts +2 -0
  9. package/dist/types/cli/thinking-levels.d.ts +5 -0
  10. package/dist/types/cli/update-cli.d.ts +13 -0
  11. package/dist/types/commands/browser-relay.d.ts +38 -0
  12. package/dist/types/commands/commit.d.ts +3 -0
  13. package/dist/types/commands/complete.d.ts +1 -1
  14. package/dist/types/commands/completions.d.ts +6 -0
  15. package/dist/types/commands/grep.d.ts +3 -0
  16. package/dist/types/commands/join.d.ts +4 -0
  17. package/dist/types/commands/launch-help.d.ts +171 -0
  18. package/dist/types/commands/launch.d.ts +5 -1
  19. package/dist/types/commands/models.d.ts +3 -0
  20. package/dist/types/commands/say.d.ts +10 -0
  21. package/dist/types/commands/worktree.d.ts +3 -0
  22. package/dist/types/config/keybindings.d.ts +5 -0
  23. package/dist/types/config/model-discovery.d.ts +2 -2
  24. package/dist/types/config/models-config-schema-bundle.d.ts +3 -0
  25. package/dist/types/config/models-config-schema.d.ts +2 -0
  26. package/dist/types/config/models-config.d.ts +1 -0
  27. package/dist/types/config/service-tier.d.ts +2 -0
  28. package/dist/types/config/settings-schema.d.ts +69 -21
  29. package/dist/types/cursor-bridge-tools.d.ts +1 -1
  30. package/dist/types/cursor.d.ts +7 -4
  31. package/dist/types/edit/index.d.ts +3 -3
  32. package/dist/types/edit/modes/replace.d.ts +22 -15
  33. package/dist/types/edit/renderer.d.ts +1 -1
  34. package/dist/types/eval/py/kernel.d.ts +3 -1
  35. package/dist/types/eval/py/spawn-options.d.ts +12 -16
  36. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +8 -1
  37. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
  38. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
  39. package/dist/types/launch/broker.d.ts +1 -1
  40. package/dist/types/launch/client.d.ts +6 -3
  41. package/dist/types/launch/ensure.d.ts +8 -0
  42. package/dist/types/launch/paths.d.ts +1 -1
  43. package/dist/types/launch/protocol.d.ts +2 -2
  44. package/dist/types/lsp/client.d.ts +2 -0
  45. package/dist/types/lsp/mux/daemon.d.ts +15 -0
  46. package/dist/types/lsp/mux/protocol.d.ts +57 -0
  47. package/dist/types/lsp/mux/server.d.ts +18 -0
  48. package/dist/types/lsp/types.d.ts +22 -2
  49. package/dist/types/main.d.ts +2 -0
  50. package/dist/types/mnemopi/embed-client.d.ts +1 -1
  51. package/dist/types/mnemopi/state.d.ts +0 -23
  52. package/dist/types/modes/components/assistant-message.d.ts +2 -0
  53. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  54. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  55. package/dist/types/modes/components/stripped-tool-calls-placeholder.d.ts +12 -0
  56. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  57. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  58. package/dist/types/modes/interactive-mode.d.ts +1 -0
  59. package/dist/types/modes/types.d.ts +1 -0
  60. package/dist/types/modes/workflow.d.ts +2 -1
  61. package/dist/types/sdk.d.ts +3 -1
  62. package/dist/types/secrets/index.d.ts +1 -1
  63. package/dist/types/session/agent-session-types.d.ts +3 -1
  64. package/dist/types/session/indexed-session-storage.d.ts +1 -0
  65. package/dist/types/session/session-advisors.d.ts +1 -1
  66. package/dist/types/session/session-context.d.ts +2 -1
  67. package/dist/types/session/session-maintenance.d.ts +1 -0
  68. package/dist/types/session/session-manager.d.ts +16 -11
  69. package/dist/types/session/session-stats.d.ts +10 -0
  70. package/dist/types/session/session-storage.d.ts +15 -3
  71. package/dist/types/system-prompt.d.ts +4 -1
  72. package/dist/types/task/index.d.ts +2 -1
  73. package/dist/types/task/spawn-policy.d.ts +5 -0
  74. package/dist/types/thinking.d.ts +1 -7
  75. package/dist/types/tools/ast-grep.d.ts +1 -1
  76. package/dist/types/tools/browser/registry.d.ts +2 -1
  77. package/dist/types/tools/browser/relay/bridge.d.ts +35 -0
  78. package/dist/types/tools/browser/relay/daemon.d.ts +16 -0
  79. package/dist/types/tools/browser/relay/kind.d.ts +19 -0
  80. package/dist/types/tools/browser/relay/protocol.d.ts +97 -0
  81. package/dist/types/tools/browser/relay/server.d.ts +35 -0
  82. package/dist/types/tools/browser/render.d.ts +1 -0
  83. package/dist/types/tools/browser.d.ts +3 -0
  84. package/dist/types/tools/computer/exposure.d.ts +9 -7
  85. package/dist/types/tools/computer/protocol.d.ts +70 -17
  86. package/dist/types/tools/computer/supervisor.d.ts +23 -10
  87. package/dist/types/tools/computer/worker-entry.d.ts +1 -0
  88. package/dist/types/tools/computer/worker.d.ts +42 -7
  89. package/dist/types/tools/computer-renderer.d.ts +7 -5
  90. package/dist/types/tools/computer.d.ts +25 -44
  91. package/dist/types/tools/context.d.ts +3 -0
  92. package/dist/types/tools/glob.d.ts +1 -1
  93. package/dist/types/tools/grep.d.ts +1 -1
  94. package/dist/types/tools/render-utils.d.ts +2 -0
  95. package/dist/types/tools/{browser/run-cancellation.d.ts → run-scope.d.ts} +8 -8
  96. package/dist/types/tools/tool-timeouts.d.ts +5 -0
  97. package/dist/types/tools/xdev.d.ts +8 -0
  98. package/dist/types/utils/clipboard.d.ts +2 -2
  99. package/dist/types/web/kagi.d.ts +1 -0
  100. package/dist/types/web/search/index.d.ts +1 -1
  101. package/dist/types/web/search/providers/anthropic.d.ts +1 -0
  102. package/dist/types/web/search/providers/base.d.ts +2 -0
  103. package/dist/types/web/search/providers/brave.d.ts +1 -0
  104. package/dist/types/web/search/providers/browser-page.d.ts +1 -0
  105. package/dist/types/web/search/providers/codex.d.ts +1 -0
  106. package/dist/types/web/search/providers/exa.d.ts +1 -0
  107. package/dist/types/web/search/providers/firecrawl.d.ts +1 -0
  108. package/dist/types/web/search/providers/gemini.d.ts +1 -0
  109. package/dist/types/web/search/providers/jina.d.ts +1 -0
  110. package/dist/types/web/search/providers/kagi.d.ts +1 -0
  111. package/dist/types/web/search/providers/kimi.d.ts +1 -0
  112. package/dist/types/web/search/providers/parallel.d.ts +1 -0
  113. package/dist/types/web/search/providers/perplexity.d.ts +1 -0
  114. package/dist/types/web/search/providers/searxng.d.ts +1 -0
  115. package/dist/types/web/search/providers/tavily.d.ts +1 -0
  116. package/dist/types/web/search/providers/tinyfish.d.ts +1 -0
  117. package/dist/types/web/search/providers/utils.d.ts +6 -9
  118. package/dist/types/web/search/providers/zai.d.ts +1 -0
  119. package/dist/types/web/search/types.d.ts +4 -0
  120. package/package.json +12 -12
  121. package/src/cli/args.ts +7 -87
  122. package/src/cli/browser-relay-cli.ts +119 -0
  123. package/src/cli/command-help.ts +103 -0
  124. package/src/cli/completion-gen.ts +5 -5
  125. package/src/cli/flag-tables.ts +9 -0
  126. package/src/cli/help-extra.ts +89 -0
  127. package/src/cli/setup-cli.ts +13 -21
  128. package/src/cli/thinking-levels.ts +7 -0
  129. package/src/cli/update-cli.ts +26 -7
  130. package/src/cli-commands.ts +170 -33
  131. package/src/cli.ts +17 -5
  132. package/src/commands/acp.ts +3 -1
  133. package/src/commands/agents.ts +3 -2
  134. package/src/commands/auth-broker.ts +3 -2
  135. package/src/commands/auth-gateway.ts +3 -2
  136. package/src/commands/bench.ts +2 -3
  137. package/src/commands/browser-relay.ts +53 -0
  138. package/src/commands/cleanse.ts +2 -2
  139. package/src/commands/commit.ts +3 -2
  140. package/src/commands/complete.ts +2 -1
  141. package/src/commands/completions.ts +3 -2
  142. package/src/commands/config.ts +3 -2
  143. package/src/commands/dry-balance.ts +2 -2
  144. package/src/commands/gallery.ts +3 -2
  145. package/src/commands/gc.ts +3 -2
  146. package/src/commands/grep.ts +3 -2
  147. package/src/commands/grievances.ts +3 -2
  148. package/src/commands/install.ts +2 -2
  149. package/src/commands/join.ts +3 -2
  150. package/src/commands/launch-help.ts +116 -0
  151. package/src/commands/launch.ts +7 -191
  152. package/src/commands/models.ts +3 -2
  153. package/src/commands/plugin.ts +3 -2
  154. package/src/commands/read.ts +3 -2
  155. package/src/commands/say.ts +3 -2
  156. package/src/commands/setup.ts +3 -2
  157. package/src/commands/shell.ts +3 -2
  158. package/src/commands/ssh.ts +3 -2
  159. package/src/commands/stats.ts +3 -2
  160. package/src/commands/tiny-models.ts +2 -2
  161. package/src/commands/token.ts +2 -2
  162. package/src/commands/ttsr.ts +2 -2
  163. package/src/commands/update.ts +3 -2
  164. package/src/commands/usage.ts +3 -2
  165. package/src/commands/web-search.ts +3 -2
  166. package/src/commands/worktree.ts +3 -2
  167. package/src/config/keybindings.ts +5 -0
  168. package/src/config/model-discovery.ts +30 -13
  169. package/src/config/model-registry.ts +9 -5
  170. package/src/config/models-config-schema-bundle.ts +9 -0
  171. package/src/config/resolve-config-value.ts +2 -1
  172. package/src/config/service-tier.ts +5 -0
  173. package/src/config/settings-schema.ts +72 -20
  174. package/src/cursor-bridge-tools.ts +1 -1
  175. package/src/cursor.ts +10 -14
  176. package/src/edit/diff.ts +17 -8
  177. package/src/edit/index.ts +20 -7
  178. package/src/edit/modes/replace.ts +93 -40
  179. package/src/edit/renderer.ts +1 -1
  180. package/src/edit/streaming.ts +14 -17
  181. package/src/eval/py/kernel.ts +2 -1
  182. package/src/eval/py/spawn-options.ts +24 -29
  183. package/src/export/html/template.js +8 -26
  184. package/src/extensibility/extensions/wrapper.ts +1 -0
  185. package/src/extensibility/legacy-pi-ai-shim.ts +22 -1
  186. package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -6
  187. package/src/extensibility/plugins/legacy-pi-compat.ts +124 -57
  188. package/src/extensibility/skills.ts +11 -0
  189. package/src/extensibility/utils.ts +10 -2
  190. package/src/launch/broker.ts +1 -1
  191. package/src/launch/client.ts +32 -13
  192. package/src/launch/ensure.ts +73 -0
  193. package/src/launch/paths.ts +1 -1
  194. package/src/launch/protocol.ts +2 -2
  195. package/src/lsp/client.ts +23 -2
  196. package/src/lsp/index.ts +8 -0
  197. package/src/lsp/mux/daemon.ts +348 -0
  198. package/src/lsp/mux/protocol.ts +96 -0
  199. package/src/lsp/mux/server.ts +797 -0
  200. package/src/lsp/types.ts +28 -2
  201. package/src/main.ts +7 -2
  202. package/src/mcp/oauth-flow.ts +11 -1
  203. package/src/mnemopi/embed-client.ts +49 -2
  204. package/src/mnemopi/state.ts +57 -23
  205. package/src/modes/components/assistant-message.ts +11 -0
  206. package/src/modes/components/chat-transcript-builder.ts +4 -0
  207. package/src/modes/components/custom-editor.ts +9 -0
  208. package/src/modes/components/read-tool-group.ts +10 -0
  209. package/src/modes/components/status-line/component.ts +1 -1
  210. package/src/modes/components/stripped-tool-calls-placeholder.ts +35 -0
  211. package/src/modes/components/tool-execution.ts +8 -0
  212. package/src/modes/components/tree-selector.ts +46 -48
  213. package/src/modes/controllers/event-controller.ts +17 -0
  214. package/src/modes/controllers/input-controller.ts +39 -1
  215. package/src/modes/controllers/mcp-command-controller.ts +11 -5
  216. package/src/modes/controllers/selector-controller.ts +20 -0
  217. package/src/modes/interactive-mode.ts +2 -0
  218. package/src/modes/types.ts +1 -0
  219. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  220. package/src/modes/utils/interactive-context-helpers.ts +1 -0
  221. package/src/modes/utils/ui-helpers.ts +6 -10
  222. package/src/modes/workflow.ts +8 -2
  223. package/src/prompts/agents/init.md +1 -1
  224. package/src/prompts/system/plan-mode-active.md +2 -2
  225. package/src/prompts/system/system-prompt.md +26 -52
  226. package/src/prompts/system/workflow-notice.md +3 -3
  227. package/src/prompts/tools/ast-grep.md +1 -1
  228. package/src/prompts/tools/bash.md +1 -1
  229. package/src/prompts/tools/browser.md +1 -0
  230. package/src/prompts/tools/computer.md +21 -21
  231. package/src/prompts/tools/glob.md +1 -1
  232. package/src/prompts/tools/grep.md +1 -1
  233. package/src/prompts/tools/replace.md +5 -4
  234. package/src/prompts/tools/task.md +4 -4
  235. package/src/sdk.ts +34 -6
  236. package/src/secrets/index.ts +1 -1
  237. package/src/session/agent-session-types.ts +3 -1
  238. package/src/session/agent-session.ts +13 -1
  239. package/src/session/indexed-session-storage.ts +9 -0
  240. package/src/session/prewalk.ts +24 -2
  241. package/src/session/session-advisors.ts +2 -2
  242. package/src/session/session-context.ts +16 -12
  243. package/src/session/session-dump-format.ts +1 -1
  244. package/src/session/session-maintenance.ts +38 -4
  245. package/src/session/session-manager.ts +102 -67
  246. package/src/session/session-paths.ts +118 -55
  247. package/src/session/session-stats.ts +48 -3
  248. package/src/session/session-storage.ts +30 -22
  249. package/src/session/stream-guards.ts +1 -1
  250. package/src/slash-commands/builtin-registry.ts +27 -24
  251. package/src/system-prompt.ts +8 -3
  252. package/src/task/index.ts +23 -8
  253. package/src/task/spawn-policy.ts +14 -0
  254. package/src/thinking.ts +2 -8
  255. package/src/tools/ast-grep.ts +10 -4
  256. package/src/tools/browser/cmux/cmux-tab.ts +5 -10
  257. package/src/tools/browser/cmux/rpc.ts +2 -8
  258. package/src/tools/browser/registry.ts +53 -2
  259. package/src/tools/browser/relay/bridge.ts +945 -0
  260. package/src/tools/browser/relay/daemon.ts +117 -0
  261. package/src/tools/browser/relay/extension-assets/background.js.txt +242 -0
  262. package/src/tools/browser/relay/extension-assets/manifest.json.txt +14 -0
  263. package/src/tools/browser/relay/extension-assets/options.html.txt +53 -0
  264. package/src/tools/browser/relay/extension-assets/options.js.txt +23 -0
  265. package/src/tools/browser/relay/kind.ts +41 -0
  266. package/src/tools/browser/relay/protocol.ts +54 -0
  267. package/src/tools/browser/relay/server.ts +141 -0
  268. package/src/tools/browser/render.ts +4 -1
  269. package/src/tools/browser/shared-daemon.ts +7 -58
  270. package/src/tools/browser/tab-supervisor.ts +5 -4
  271. package/src/tools/browser/tab-worker.ts +33 -12
  272. package/src/tools/browser.ts +26 -1
  273. package/src/tools/computer/exposure.ts +11 -35
  274. package/src/tools/computer/protocol.ts +51 -10
  275. package/src/tools/computer/supervisor.ts +227 -124
  276. package/src/tools/computer/worker-entry.ts +11 -6
  277. package/src/tools/computer/worker.ts +695 -81
  278. package/src/tools/computer-renderer.ts +104 -65
  279. package/src/tools/computer.ts +134 -428
  280. package/src/tools/context.ts +3 -0
  281. package/src/tools/glob.ts +9 -2
  282. package/src/tools/grep.ts +12 -6
  283. package/src/tools/inspect-image.ts +32 -6
  284. package/src/tools/render-utils.ts +2 -0
  285. package/src/tools/{browser/run-cancellation.ts → run-scope.ts} +11 -11
  286. package/src/tools/tool-timeouts.ts +1 -0
  287. package/src/tools/xdev.ts +29 -2
  288. package/src/utils/clipboard.ts +40 -14
  289. package/src/utils/command-args.ts +26 -28
  290. package/src/utils/tool-choice.ts +0 -13
  291. package/src/web/kagi.ts +2 -1
  292. package/src/web/search/index.ts +18 -2
  293. package/src/web/search/providers/anthropic.ts +4 -1
  294. package/src/web/search/providers/base.ts +2 -0
  295. package/src/web/search/providers/brave.ts +3 -1
  296. package/src/web/search/providers/browser-page.ts +6 -4
  297. package/src/web/search/providers/codex.ts +4 -1
  298. package/src/web/search/providers/duckduckgo.ts +2 -1
  299. package/src/web/search/providers/ecosia.ts +2 -1
  300. package/src/web/search/providers/exa.ts +4 -2
  301. package/src/web/search/providers/firecrawl.ts +3 -1
  302. package/src/web/search/providers/gemini.ts +8 -2
  303. package/src/web/search/providers/google.ts +2 -1
  304. package/src/web/search/providers/jina.ts +12 -2
  305. package/src/web/search/providers/kagi.ts +3 -0
  306. package/src/web/search/providers/kimi.ts +5 -1
  307. package/src/web/search/providers/mojeek.ts +2 -1
  308. package/src/web/search/providers/parallel.ts +5 -1
  309. package/src/web/search/providers/perplexity.ts +6 -3
  310. package/src/web/search/providers/public.ts +1 -1
  311. package/src/web/search/providers/searxng.ts +11 -5
  312. package/src/web/search/providers/startpage.ts +10 -4
  313. package/src/web/search/providers/synthetic.ts +11 -5
  314. package/src/web/search/providers/tavily.ts +3 -1
  315. package/src/web/search/providers/tinyfish.ts +3 -1
  316. package/src/web/search/providers/utils.ts +12 -10
  317. package/src/web/search/providers/xai.ts +2 -2
  318. package/src/web/search/providers/zai.ts +18 -3
  319. package/src/web/search/types.ts +6 -0
@@ -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,
@@ -365,18 +380,7 @@ export function buildSessionContext(
365
380
  }
366
381
  }
367
382
  } 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
- })();
383
+ const providerPayload = getOpenAiRemoteCompactionPayload(compaction);
380
384
  const remoteReplacementHistory = providerPayload?.items;
381
385
 
382
386
  // 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
 
@@ -26,8 +26,10 @@ import {
26
26
  DEFAULT_SHAKE_CONFIG,
27
27
  effectiveReserveTokens,
28
28
  estimateTokens,
29
+ hasContextTokenUsage,
29
30
  NativeCompactionError,
30
31
  prepareCompaction,
32
+ RESCUE_SHAKE_CONFIG,
31
33
  resolveBudgetReserveTokens,
32
34
  resolveThresholdTokens,
33
35
  type ShakeConfig,
@@ -74,7 +76,7 @@ import {
74
76
  resolveRoleModelFull,
75
77
  } from "./role-models";
76
78
  import type { SessionContext } from "./session-context";
77
- import { getLatestCompactionEntry } from "./session-context";
79
+ import { getLatestCompactionEntry, getOpenAiRemoteCompactionPayload } from "./session-context";
78
80
  import type { CompactionEntry, SessionEntry } from "./session-entries";
79
81
  import type { SessionManager } from "./session-manager";
80
82
  import type { ShakeMode, ShakeResult } from "./shake-types";
@@ -235,6 +237,7 @@ export interface SessionMaintenanceHost {
235
237
  syncTodoPhasesFromBranch(): void;
236
238
  resetAdvisorRuntimes(): void;
237
239
  rebaseAfterCompaction(): void;
240
+ recordAnchoredHistoryRewrite(tokensRemoved: number): void;
238
241
  getContextBreakdown(options?: {
239
242
  contextWindow?: number;
240
243
  pendingMessages?: AgentMessage[];
@@ -461,11 +464,12 @@ export class SessionMaintenance {
461
464
  }
462
465
 
463
466
  const branchEntries = this.#host.sessionManager.getBranch();
467
+ const latestCompaction = getLatestCompactionEntry(branchEntries);
464
468
  const config = this.#withPlanProtection({
465
469
  ...(opts.config ?? AGGRESSIVE_SHAKE_CONFIG),
466
470
  // Skip entries summarized away by the latest compaction — shaking them
467
471
  // only churns persisted history with no prompt/cache effect.
468
- keepBoundaryId: getLatestCompactionEntry(branchEntries)?.firstKeptEntryId,
472
+ keepBoundaryId: latestCompaction?.firstKeptEntryId,
469
473
  });
470
474
  const regions = collectShakeRegions(branchEntries, config);
471
475
  if (regions.length === 0) {
@@ -475,20 +479,50 @@ export class SessionMaintenance {
475
479
  const artifactId = await this.#saveShakeArtifact(regions);
476
480
  const replacements = regions.map((region, index) => this.#shakeElidePlaceholder(region, index, artifactId));
477
481
 
482
+ const hasRemoteReplacementHistory = getOpenAiRemoteCompactionPayload(latestCompaction) !== undefined;
483
+ const compactionIndex = latestCompaction ? branchEntries.lastIndexOf(latestCompaction) : -1;
484
+ let anchorIndex = -1;
485
+ for (let index = branchEntries.length - 1; index > compactionIndex; index--) {
486
+ const entry = branchEntries[index];
487
+ if (entry.type !== "message" || entry.message.role !== "assistant") continue;
488
+ const assistant = entry.message;
489
+ if (
490
+ assistant.stopReason !== "aborted" &&
491
+ assistant.stopReason !== "error" &&
492
+ assistant.usage &&
493
+ hasContextTokenUsage(assistant.usage)
494
+ ) {
495
+ anchorIndex = index;
496
+ break;
497
+ }
498
+ }
499
+ const entryIndexes = new Map(branchEntries.map((entry, index) => [entry, index]));
500
+
478
501
  let toolResultsDropped = 0;
479
502
  let blocksDropped = 0;
480
503
  let originalTokens = 0;
481
504
  let replacementTokens = 0;
505
+ let anchoredTokensRemoved = 0;
482
506
  const items = regions.map((region, index) => {
483
507
  if (region.kind === "toolResult") toolResultsDropped++;
484
508
  else blocksDropped++;
485
509
  originalTokens += region.tokens;
486
510
  const replacement = replacements[index];
487
- if (replacement.length > 0) replacementTokens += countTokens(replacement);
511
+ const replacementTokenCount = replacement.length > 0 ? countTokens(replacement) : 0;
512
+ replacementTokens += replacementTokenCount;
513
+ const entryIndex = entryIndexes.get(region.entry) ?? -1;
514
+ if (
515
+ entryIndex >= 0 &&
516
+ entryIndex < anchorIndex &&
517
+ (!hasRemoteReplacementHistory || entryIndex > compactionIndex)
518
+ ) {
519
+ anchoredTokensRemoved += Math.max(0, region.tokens - replacementTokenCount);
520
+ }
488
521
  return { region, replacement };
489
522
  });
490
523
 
491
524
  applyShakeRegions(items);
525
+ this.#host.recordAnchoredHistoryRewrite(anchoredTokensRemoved);
492
526
 
493
527
  await this.#host.sessionManager.rewriteEntries();
494
528
  const sessionContext = this.#host.buildDisplaySessionContext();
@@ -1859,7 +1893,7 @@ export class SessionMaintenance {
1859
1893
  let elideSink = "placeholders";
1860
1894
  if (!options.skipElide) {
1861
1895
  try {
1862
- const result = await this.#host.shake("elide", { signal });
1896
+ const result = await this.#host.shake("elide", { config: RESCUE_SHAKE_CONFIG, signal });
1863
1897
  elided = result.toolResultsDropped + result.blocksDropped;
1864
1898
  elidedTokens = result.tokensFreed;
1865
1899
  if (result.artifactId) elideSink = "an artifact";
@@ -405,17 +405,22 @@ export class SessionPersistenceIndeterminateError extends AggregateError {
405
405
  * tree by `(id, parentId)`, and the mutable leaf pointer selects which path is
406
406
  * active for future appends and for LLM context construction.
407
407
  *
408
- * Durability is software-crash safe but not power-loss safe: appends are normally
409
- * handed to the OS synchronously in-body but never `fsync`'d. While an in-place
410
- * atomic rewrite publishes, a synchronous `flushSync` still persists fenced
411
- * appends by superseding the pending publish against the same (stable) path.
412
- * A {@link moveTo} relocation is the one exception: the session file is being
413
- * renamed to a new path, so appends landing in the rename window are held in
414
- * memory and persisted only by the move's trailing rewrite. A `flushSync` in
415
- * that narrow window cannot durably place them writing the vacated source
416
- * would recreate the orphan the move eliminates, and a queued backend publish
417
- * (fire-and-forget on `IndexedSessionStorage`) could land after the rename all
418
- * the same. This gap is inherent to closing the writer for a Windows-safe rename.
408
+ * Durability is software-crash safe but not power-loss safe: completed entries
409
+ * (user/assistant/toolResult messages, tool_execution_start markers, custom
410
+ * entries) are handed to the OS synchronously in-body on append and never
411
+ * `fsync`'d. In-flight streaming text is intentionally not durable until
412
+ * `message_end` persists the finished message.
413
+ *
414
+ * While an in-place atomic rewrite is publishing, a concurrent completed append
415
+ * supersedes that publish with a synchronous full-body rewrite so the entry is
416
+ * software-crash durable before the append returns; the abandoned atomic's
417
+ * `commitGuard` then refuses to clobber the fresher body.
418
+ *
419
+ * During {@link moveTo}, appends write a full body to the live relocation path
420
+ * (source until rename, destination once the rename has landed) so a crash mid-
421
+ * move still preserves completed entries without recreating a vacated source.
422
+ * A trailing atomic rewrite still rewrites the header cwd after the path is
423
+ * repointed.
419
424
  */
420
425
  export class SessionManager {
421
426
  #cwd: string;
@@ -484,15 +489,12 @@ export class SessionManager {
484
489
  /** Set by synchronous appends that land while an atomic replacement is active. */
485
490
  #atomicRewriteDirty = false;
486
491
  /**
487
- * True while {@link moveTo} is renaming the session file but has not yet
488
- * repointed `#sessionFile`. Both the synchronous append hot path
489
- * ({@link #appendToSessionFile}) and synchronous rewrites
490
- * ({@link #rewriteSynchronously}, reached via `flushSync` on Ctrl+C) would
491
- * otherwise touch the pre-rename path — opening a fresh writer or writing the
492
- * full body — recreating the very orphan the move eliminates. Both defer
493
- * while this is set; the move's trailing atomic rewrite persists them.
492
+ * Active {@link moveTo} relocation. Concurrent completed appends write a
493
+ * full body to the live path: source while it still exists, destination
494
+ * once rename has landed (source gone). Never recreates a vacated source.
495
+ * `null` outside an active relocation.
494
496
  */
495
- #sessionFileRelocating = false;
497
+ #sessionFileRelocating: { source: string; dest: string } | null = null;
496
498
  /** Atomic entry batch currently staged for a full-file commit. */
497
499
  #atomicEntryBatch: AtomicEntryBatch | undefined;
498
500
 
@@ -763,30 +765,56 @@ export class SessionManager {
763
765
  return this.#forceFileCreation || this.#fileIsCurrent || this.#historyContainsAssistantMessage();
764
766
  }
765
767
 
768
+ /**
769
+ * Live path for concurrent completed appends during {@link moveTo}.
770
+ * Prefers destination once rename has landed (source gone); otherwise
771
+ * source. Never invents a path that does not already exist.
772
+ */
773
+ #liveRelocationWritePath(): string | null {
774
+ const relocating = this.#sessionFileRelocating;
775
+ if (!relocating) return null;
776
+ if (this.#storage.existsSync(relocating.dest)) return relocating.dest;
777
+ if (this.#storage.existsSync(relocating.source)) return relocating.source;
778
+ // Rename in flight with neither path visible (rare cross-device edge):
779
+ // fall back to destination so we do not recreate a vacated source.
780
+ return relocating.dest;
781
+ }
782
+
766
783
  /**
767
784
  * Synchronously rewrite the whole file (header + entries) and keep no open
768
785
  * writer; the next append re-opens one. `writeTextSync` returns with the
769
786
  * bytes in the kernel page cache, so the file is software-crash durable.
787
+ *
788
+ * During {@link moveTo}, writes to the live relocation path (source pre-
789
+ * rename, destination post-rename) rather than always `#sessionFile`, so
790
+ * concurrent completed entries are durable without recreating a vacated source.
770
791
  */
771
792
  #rewriteSynchronously(): void {
772
- if (!this.#persist || !this.#sessionFile || !this.#shouldHaveSessionFile()) return;
773
- // A move is renaming the file out from under `#sessionFile`. Writing the
774
- // full body now would target either the vacated source (recreating the
775
- // orphan the move eliminates) or a path a queued backend publish may clobber
776
- // post-rename, so defer to moveTo's trailing atomic rewrite. This narrows
777
- // flushSync durability for entries in the rename window (see class doc).
778
- if (this.#sessionFileRelocating) return;
793
+ if (!this.#persist || !this.#shouldHaveSessionFile()) return;
794
+ const targetPath = this.#liveRelocationWritePath() ?? this.#sessionFile;
795
+ if (!targetPath) return;
779
796
 
780
797
  try {
781
798
  const body = this.#fileBody();
782
799
  this.#diskEpoch++;
783
800
  this.#diskTail = Promise.resolve();
784
801
  this.#closeWriterEventually();
785
- this.#storage.writeTextSync(this.#sessionFile, body);
786
- this.#fileIsCurrent = true;
787
- this.#materializeBreadcrumb();
788
- this.#rewriteRequired = false;
789
- this.#hasTitleSlot = true;
802
+ this.#storage.writeTextSync(targetPath, body);
803
+ // Only mark the manager current when writing the active session path.
804
+ // Mid-move writes update the live relocation path; `#sessionFile` is
805
+ // still the pre-repoint source until moveTo repoints it.
806
+ if (!this.#sessionFileRelocating || targetPath === this.#sessionFile) {
807
+ this.#fileIsCurrent = true;
808
+ this.#materializeBreadcrumb();
809
+ this.#rewriteRequired = false;
810
+ this.#hasTitleSlot = true;
811
+ } else {
812
+ // Destination body is current on disk; in-memory still needs a
813
+ // header-cwd rewrite after repoint, but entries are durable.
814
+ this.#fileIsCurrent = false;
815
+ this.#rewriteRequired = true;
816
+ this.#hasTitleSlot = true;
817
+ }
790
818
  } catch (err) {
791
819
  this.#noteDiskFailure(err);
792
820
  }
@@ -872,22 +900,21 @@ export class SessionManager {
872
900
  return;
873
901
  }
874
902
 
875
- // Atomic replacement window: the old path may be moved aside underneath
876
- // any newly-opened append handle (Windows EPERM fallback), or a `moveTo`
877
- // may be renaming the session file to a new directory. Do not open a
878
- // writer here; the active rewrite loops and serializes a fresh full body,
879
- // and the move's trailing atomic rewrite folds in these deferred entries.
880
- // A superseding synchronous rewrite bumps `#diskEpoch`, at which point
881
- // the pending atomic publish is guaranteed to abandon via its
882
- // `commitGuard`, so appends can (and must) take the hot path so they
883
- // don't strand in memory while `close()` returns without a rewrite.
884
- if (
885
- this.#sessionFileRelocating ||
886
- (this.#atomicRewriteFenceEpoch !== null && this.#atomicRewriteFenceEpoch === this.#diskEpoch)
887
- ) {
888
- this.#fileIsCurrent = false;
889
- this.#rewriteRequired = true;
903
+ // Atomic replacement / move window: do not open a fresh append writer that
904
+ // a Windows EPERM replace could detach from the current JSONL path.
905
+ // - moveTo: write a full body to the live relocation path (source pre-
906
+ // rename, destination post-rename) so completed entries are durable
907
+ // without recreating a vacated source.
908
+ // - in-place atomic fence: supersede the pending publish with a
909
+ // synchronous full-body rewrite; bumping `#diskEpoch` abandons the
910
+ // in-flight atomic via its `commitGuard`.
911
+ if (this.#sessionFileRelocating) {
912
+ this.#rewriteSynchronously();
913
+ return;
914
+ }
915
+ if (this.#atomicRewriteFenceEpoch !== null && this.#atomicRewriteFenceEpoch === this.#diskEpoch) {
890
916
  this.#atomicRewriteDirty = true;
917
+ this.#rewriteSynchronously();
891
918
  return;
892
919
  }
893
920
  // Cold/divergent: not on disk yet, or in-memory entries diverged from the
@@ -897,15 +924,23 @@ export class SessionManager {
897
924
  return;
898
925
  }
899
926
 
900
- // Hot path: queue the entry directly on the writer, outside the async disk
901
- // chain. File writers batch same-turn appends until their microtask
902
- // boundary; flush(), flushSync(), and close() drain that batch explicitly.
927
+ // Hot path: write the entry directly on the writer, outside the async disk
928
+ // chain. Prefer appendSync so write failures latch `#diskFailure` before
929
+ // this call returns (not via a discarded rejected Promise after a later
930
+ // microtask). Callers stay non-throwing here — the core turn loop invokes
931
+ // appendMessage/appendCustomEntry without try/catch; flushSync/close and
932
+ // subsequent appends still throw the latched error. File writers apply
933
+ // each line to the OS page cache before return.
903
934
  // A mid-close writer leaves `#writer` undefined, so `#appendWriter` simply
904
935
  // opens a fresh append handle and the entry still lands.
905
936
  try {
906
- void this.#appendWriter()
907
- .append(this.#lineFor(entry))
908
- .catch(err => this.#noteDiskFailure(err));
937
+ const writer = this.#appendWriter();
938
+ const line = this.#lineFor(entry);
939
+ if (writer.appendSync) {
940
+ writer.appendSync(line);
941
+ } else {
942
+ void writer.append(line).catch(err => this.#noteDiskFailure(err));
943
+ }
909
944
  } catch (err) {
910
945
  this.#noteDiskFailure(err);
911
946
  }
@@ -921,12 +956,11 @@ export class SessionManager {
921
956
  }
922
957
 
923
958
  // Title changes use their own asynchronous append path rather than
924
- // #appendToSessionFile. Defer them under the same move fence so they cannot
925
- // recreate the vacated source path before #sessionFile is repointed.
959
+ // #appendToSessionFile. During move, write the full body (including the
960
+ // title entry) to the live relocation path so a crash mid-move still
961
+ // keeps the title change; the trailing rewrite still updates header cwd.
926
962
  if (this.#sessionFileRelocating) {
927
- this.#fileIsCurrent = false;
928
- this.#rewriteRequired = true;
929
- this.#atomicRewriteDirty = true;
963
+ this.#rewriteSynchronously();
930
964
  return;
931
965
  }
932
966
 
@@ -1362,14 +1396,15 @@ export class SessionManager {
1362
1396
  : computeDefaultSessionDir(resolvedCwd, this.#storage));
1363
1397
 
1364
1398
  let sessionFileExisted = false;
1365
- // Fence synchronous appends and rewrites away from the session file for the
1366
- // duration of the relocation (see `#sessionFileRelocating`). This flag
1367
- // not a `#diskEpoch` bump is used deliberately: bumping the epoch here
1368
- // would cancel any disk task already queued at the current epoch (e.g. a
1369
- // header-only `ensureOnDisk()` materializing rewrite) before the drain
1370
- // below runs it, silently dropping an explicitly materialized session.
1399
+ // Track source+dest for concurrent completed appends during relocation
1400
+ // (see `#sessionFileRelocating`). Existence of either path decides the
1401
+ // live write target — not a `#diskEpoch` bump, which would cancel any
1402
+ // disk task already queued at the current epoch (e.g. a header-only
1403
+ // `ensureOnDisk()` materializing rewrite) before the drain below runs it.
1371
1404
  if (this.#persist && this.#sessionFile) {
1372
- this.#sessionFileRelocating = true;
1405
+ const source = this.#sessionFile;
1406
+ const dest = path.join(nextSessionDir, path.basename(source));
1407
+ this.#sessionFileRelocating = { source, dest };
1373
1408
  }
1374
1409
 
1375
1410
  try {
@@ -1433,8 +1468,8 @@ export class SessionManager {
1433
1468
  this.#sessionFile = newSessionFile;
1434
1469
  this.#artifactManager = null;
1435
1470
  this.#artifactManagerSessionFile = null;
1436
- // Path is repointed; a sync rewrite may now safely target the new file.
1437
- this.#sessionFileRelocating = false;
1471
+ // Path is repointed; hot-path appends may use `#sessionFile` again.
1472
+ this.#sessionFileRelocating = null;
1438
1473
  }
1439
1474
 
1440
1475
  this.#cwd = resolvedCwd;
@@ -1459,7 +1494,7 @@ export class SessionManager {
1459
1494
 
1460
1495
  if (this.#sessionFile) this.#rememberBreadcrumb(resolvedCwd, this.#sessionFile);
1461
1496
  } finally {
1462
- this.#sessionFileRelocating = false;
1497
+ this.#sessionFileRelocating = null;
1463
1498
  }
1464
1499
  }
1465
1500