@oh-my-pi/pi-coding-agent 17.2.3 → 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 (375) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/dist/{CHANGELOG-c5hpqt9r.md → CHANGELOG-q2w43aw3.md} +92 -0
  3. package/dist/cli.js +3979 -3548
  4. package/dist/{template-8vdv6xb4.js → template-c2hyaytt.js} +8 -26
  5. package/dist/types/advisor/advise-tool.d.ts +6 -0
  6. package/dist/types/advisor/runtime.d.ts +4 -4
  7. package/dist/types/capability/mcp.d.ts +9 -0
  8. package/dist/types/cli/args.d.ts +4 -1
  9. package/dist/types/cli/browser-relay-cli.d.ts +15 -0
  10. package/dist/types/cli/command-help.d.ts +99 -0
  11. package/dist/types/cli/help-extra.d.ts +2 -0
  12. package/dist/types/cli/models-cli.d.ts +1 -1
  13. package/dist/types/cli/thinking-levels.d.ts +5 -0
  14. package/dist/types/cli/update-cli.d.ts +13 -0
  15. package/dist/types/commands/browser-relay.d.ts +38 -0
  16. package/dist/types/commands/commit.d.ts +3 -0
  17. package/dist/types/commands/complete.d.ts +1 -1
  18. package/dist/types/commands/completions.d.ts +6 -0
  19. package/dist/types/commands/grep.d.ts +3 -0
  20. package/dist/types/commands/join.d.ts +4 -0
  21. package/dist/types/commands/launch-help.d.ts +171 -0
  22. package/dist/types/commands/launch.d.ts +5 -1
  23. package/dist/types/commands/models.d.ts +3 -0
  24. package/dist/types/commands/say.d.ts +10 -0
  25. package/dist/types/commands/worktree.d.ts +3 -0
  26. package/dist/types/config/keybindings.d.ts +6 -1
  27. package/dist/types/config/model-discovery.d.ts +2 -2
  28. package/dist/types/config/models-config-schema-bundle.d.ts +3 -0
  29. package/dist/types/config/models-config-schema.d.ts +2 -0
  30. package/dist/types/config/models-config.d.ts +1 -0
  31. package/dist/types/config/service-tier.d.ts +2 -0
  32. package/dist/types/config/settings-schema.d.ts +70 -22
  33. package/dist/types/cursor-bridge-tools.d.ts +1 -1
  34. package/dist/types/cursor.d.ts +7 -4
  35. package/dist/types/discovery/helpers.d.ts +6 -0
  36. package/dist/types/discovery/omp-extension-roots.d.ts +21 -2
  37. package/dist/types/edit/index.d.ts +3 -3
  38. package/dist/types/edit/modes/replace.d.ts +22 -15
  39. package/dist/types/edit/renderer.d.ts +1 -1
  40. package/dist/types/eval/py/kernel.d.ts +3 -1
  41. package/dist/types/eval/py/spawn-options.d.ts +12 -16
  42. package/dist/types/extensibility/extensions/loader.d.ts +6 -2
  43. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +8 -1
  44. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
  45. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
  46. package/dist/types/extensibility/plugins/marketplace/registry.d.ts +2 -2
  47. package/dist/types/launch/broker.d.ts +1 -1
  48. package/dist/types/launch/client.d.ts +6 -3
  49. package/dist/types/launch/ensure.d.ts +8 -0
  50. package/dist/types/launch/paths.d.ts +3 -2
  51. package/dist/types/launch/protocol.d.ts +2 -2
  52. package/dist/types/lsp/client.d.ts +2 -0
  53. package/dist/types/lsp/mux/daemon.d.ts +15 -0
  54. package/dist/types/lsp/mux/protocol.d.ts +57 -0
  55. package/dist/types/lsp/mux/server.d.ts +18 -0
  56. package/dist/types/lsp/types.d.ts +22 -2
  57. package/dist/types/main.d.ts +2 -0
  58. package/dist/types/mcp/request-id.d.ts +19 -0
  59. package/dist/types/mcp/types.d.ts +14 -0
  60. package/dist/types/memory-backend/index.d.ts +1 -0
  61. package/dist/types/memory-backend/messages.d.ts +15 -0
  62. package/dist/types/mnemopi/embed-client.d.ts +1 -1
  63. package/dist/types/mnemopi/state.d.ts +0 -23
  64. package/dist/types/modes/components/assistant-message.d.ts +2 -0
  65. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  66. package/dist/types/modes/components/model-browser.d.ts +7 -5
  67. package/dist/types/modes/components/model-picker.d.ts +9 -3
  68. package/dist/types/modes/components/read-tool-group.d.ts +11 -0
  69. package/dist/types/modes/components/stripped-tool-calls-placeholder.d.ts +12 -0
  70. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  71. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  72. package/dist/types/modes/interactive-mode.d.ts +1 -0
  73. package/dist/types/modes/types.d.ts +2 -1
  74. package/dist/types/modes/workflow.d.ts +2 -1
  75. package/dist/types/registry/agent-lifecycle.d.ts +14 -5
  76. package/dist/types/registry/agent-registry.d.ts +4 -3
  77. package/dist/types/sdk.d.ts +4 -2
  78. package/dist/types/secrets/index.d.ts +7 -9
  79. package/dist/types/session/agent-session-types.d.ts +3 -1
  80. package/dist/types/session/agent-session.d.ts +6 -1
  81. package/dist/types/session/indexed-session-storage.d.ts +1 -0
  82. package/dist/types/session/model-controls.d.ts +0 -1
  83. package/dist/types/session/session-advisors.d.ts +8 -1
  84. package/dist/types/session/session-context.d.ts +2 -1
  85. package/dist/types/session/session-maintenance.d.ts +2 -0
  86. package/dist/types/session/session-manager.d.ts +16 -5
  87. package/dist/types/session/session-stats.d.ts +10 -0
  88. package/dist/types/session/session-storage.d.ts +15 -3
  89. package/dist/types/system-prompt.d.ts +6 -1
  90. package/dist/types/task/index.d.ts +2 -1
  91. package/dist/types/task/spawn-policy.d.ts +5 -0
  92. package/dist/types/thinking.d.ts +1 -7
  93. package/dist/types/tools/ast-grep.d.ts +1 -1
  94. package/dist/types/tools/browser/registry.d.ts +2 -1
  95. package/dist/types/tools/browser/relay/bridge.d.ts +35 -0
  96. package/dist/types/tools/browser/relay/daemon.d.ts +16 -0
  97. package/dist/types/tools/browser/relay/kind.d.ts +19 -0
  98. package/dist/types/tools/browser/relay/protocol.d.ts +97 -0
  99. package/dist/types/tools/browser/relay/server.d.ts +35 -0
  100. package/dist/types/tools/browser/render.d.ts +1 -0
  101. package/dist/types/tools/browser.d.ts +3 -0
  102. package/dist/types/tools/computer/exposure.d.ts +9 -7
  103. package/dist/types/tools/computer/protocol.d.ts +70 -17
  104. package/dist/types/tools/computer/supervisor.d.ts +23 -10
  105. package/dist/types/tools/computer/worker-entry.d.ts +1 -0
  106. package/dist/types/tools/computer/worker.d.ts +42 -7
  107. package/dist/types/tools/computer-renderer.d.ts +7 -5
  108. package/dist/types/tools/computer.d.ts +25 -44
  109. package/dist/types/tools/context.d.ts +3 -0
  110. package/dist/types/tools/default-renderer.d.ts +3 -0
  111. package/dist/types/tools/glob.d.ts +1 -1
  112. package/dist/types/tools/grep.d.ts +1 -1
  113. package/dist/types/tools/render-utils.d.ts +2 -0
  114. package/dist/types/tools/{browser/run-cancellation.d.ts → run-scope.d.ts} +8 -8
  115. package/dist/types/tools/tool-timeouts.d.ts +5 -0
  116. package/dist/types/tools/xdev.d.ts +8 -0
  117. package/dist/types/utils/clipboard.d.ts +2 -2
  118. package/dist/types/web/kagi.d.ts +1 -0
  119. package/dist/types/web/search/index.d.ts +1 -1
  120. package/dist/types/web/search/providers/anthropic.d.ts +1 -0
  121. package/dist/types/web/search/providers/base.d.ts +2 -0
  122. package/dist/types/web/search/providers/brave.d.ts +1 -0
  123. package/dist/types/web/search/providers/browser-page.d.ts +1 -0
  124. package/dist/types/web/search/providers/codex.d.ts +1 -0
  125. package/dist/types/web/search/providers/exa.d.ts +1 -0
  126. package/dist/types/web/search/providers/firecrawl.d.ts +1 -0
  127. package/dist/types/web/search/providers/gemini.d.ts +1 -0
  128. package/dist/types/web/search/providers/jina.d.ts +1 -0
  129. package/dist/types/web/search/providers/kagi.d.ts +1 -0
  130. package/dist/types/web/search/providers/kimi.d.ts +1 -0
  131. package/dist/types/web/search/providers/parallel.d.ts +1 -0
  132. package/dist/types/web/search/providers/perplexity.d.ts +1 -0
  133. package/dist/types/web/search/providers/searxng.d.ts +1 -0
  134. package/dist/types/web/search/providers/tavily.d.ts +1 -0
  135. package/dist/types/web/search/providers/tinyfish.d.ts +1 -0
  136. package/dist/types/web/search/providers/utils.d.ts +6 -9
  137. package/dist/types/web/search/providers/zai.d.ts +1 -0
  138. package/dist/types/web/search/types.d.ts +4 -0
  139. package/package.json +12 -12
  140. package/scripts/legacy-pi-virtual-module.ts +12 -2
  141. package/src/advisor/advise-tool.ts +18 -0
  142. package/src/advisor/runtime.ts +6 -6
  143. package/src/capability/mcp.ts +7 -0
  144. package/src/cli/args.ts +7 -87
  145. package/src/cli/browser-relay-cli.ts +119 -0
  146. package/src/cli/command-help.ts +103 -0
  147. package/src/cli/completion-gen.ts +5 -5
  148. package/src/cli/flag-tables.ts +9 -0
  149. package/src/cli/help-extra.ts +89 -0
  150. package/src/cli/models-cli.ts +13 -16
  151. package/src/cli/setup-cli.ts +13 -21
  152. package/src/cli/thinking-levels.ts +7 -0
  153. package/src/cli/update-cli.ts +26 -7
  154. package/src/cli-commands.ts +170 -33
  155. package/src/cli.ts +17 -5
  156. package/src/collab/host.ts +1 -1
  157. package/src/commands/acp.ts +3 -1
  158. package/src/commands/agents.ts +3 -2
  159. package/src/commands/auth-broker.ts +3 -2
  160. package/src/commands/auth-gateway.ts +3 -2
  161. package/src/commands/bench.ts +2 -3
  162. package/src/commands/browser-relay.ts +53 -0
  163. package/src/commands/cleanse.ts +2 -2
  164. package/src/commands/commit.ts +3 -2
  165. package/src/commands/complete.ts +2 -1
  166. package/src/commands/completions.ts +3 -2
  167. package/src/commands/config.ts +3 -2
  168. package/src/commands/dry-balance.ts +2 -2
  169. package/src/commands/gallery.ts +3 -2
  170. package/src/commands/gc.ts +3 -2
  171. package/src/commands/grep.ts +3 -2
  172. package/src/commands/grievances.ts +3 -2
  173. package/src/commands/install.ts +2 -2
  174. package/src/commands/join.ts +3 -2
  175. package/src/commands/launch-help.ts +116 -0
  176. package/src/commands/launch.ts +7 -191
  177. package/src/commands/models.ts +3 -2
  178. package/src/commands/plugin.ts +3 -2
  179. package/src/commands/read.ts +3 -2
  180. package/src/commands/say.ts +3 -2
  181. package/src/commands/setup.ts +3 -2
  182. package/src/commands/shell.ts +3 -2
  183. package/src/commands/ssh.ts +3 -2
  184. package/src/commands/stats.ts +3 -2
  185. package/src/commands/tiny-models.ts +2 -2
  186. package/src/commands/token.ts +2 -2
  187. package/src/commands/ttsr.ts +2 -2
  188. package/src/commands/update.ts +3 -2
  189. package/src/commands/usage.ts +3 -2
  190. package/src/commands/web-search.ts +3 -2
  191. package/src/commands/worktree.ts +3 -2
  192. package/src/config/keybindings.ts +19 -9
  193. package/src/config/mcp-schema.json +5 -0
  194. package/src/config/model-discovery.ts +30 -13
  195. package/src/config/model-registry.ts +16 -6
  196. package/src/config/models-config-schema-bundle.ts +9 -0
  197. package/src/config/resolve-config-value.ts +2 -1
  198. package/src/config/service-tier.ts +5 -0
  199. package/src/config/settings-schema.ts +73 -21
  200. package/src/cursor-bridge-tools.ts +1 -1
  201. package/src/cursor.ts +10 -14
  202. package/src/discovery/builtin.ts +10 -0
  203. package/src/discovery/helpers.ts +10 -0
  204. package/src/discovery/mcp-json.ts +11 -1
  205. package/src/discovery/omp-extension-roots.ts +79 -20
  206. package/src/discovery/omp-plugins.ts +10 -1
  207. package/src/edit/diff.ts +17 -8
  208. package/src/edit/index.ts +20 -7
  209. package/src/edit/modes/replace.ts +93 -40
  210. package/src/edit/renderer.ts +1 -1
  211. package/src/edit/streaming.ts +14 -17
  212. package/src/eval/py/kernel.ts +2 -1
  213. package/src/eval/py/spawn-options.ts +24 -29
  214. package/src/export/html/template.js +8 -26
  215. package/src/extensibility/extensions/loader.ts +66 -28
  216. package/src/extensibility/extensions/wrapper.ts +1 -0
  217. package/src/extensibility/legacy-pi-ai-shim.ts +22 -1
  218. package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -6
  219. package/src/extensibility/plugins/legacy-pi-compat.ts +124 -57
  220. package/src/extensibility/plugins/marketplace/registry.ts +4 -8
  221. package/src/extensibility/skills.ts +11 -0
  222. package/src/extensibility/utils.ts +10 -2
  223. package/src/launch/broker.ts +1 -1
  224. package/src/launch/client.ts +32 -13
  225. package/src/launch/ensure.ts +73 -0
  226. package/src/launch/paths.ts +3 -6
  227. package/src/launch/protocol.ts +2 -2
  228. package/src/lsp/client.ts +23 -2
  229. package/src/lsp/index.ts +8 -0
  230. package/src/lsp/mux/daemon.ts +348 -0
  231. package/src/lsp/mux/protocol.ts +96 -0
  232. package/src/lsp/mux/server.ts +797 -0
  233. package/src/lsp/types.ts +28 -2
  234. package/src/main.ts +31 -24
  235. package/src/mcp/config.ts +1 -0
  236. package/src/mcp/oauth-flow.ts +11 -1
  237. package/src/mcp/request-id.ts +24 -0
  238. package/src/mcp/transports/http.ts +4 -2
  239. package/src/mcp/transports/sse.ts +4 -2
  240. package/src/mcp/transports/stdio.ts +4 -2
  241. package/src/mcp/types.ts +15 -0
  242. package/src/memory-backend/index.ts +1 -0
  243. package/src/memory-backend/messages.ts +19 -0
  244. package/src/mnemopi/embed-client.ts +49 -2
  245. package/src/mnemopi/state.ts +57 -23
  246. package/src/modes/components/agent-hub.ts +1 -1
  247. package/src/modes/components/assistant-message.ts +11 -0
  248. package/src/modes/components/chat-transcript-builder.ts +4 -0
  249. package/src/modes/components/custom-editor.ts +10 -1
  250. package/src/modes/components/model-browser.ts +25 -17
  251. package/src/modes/components/model-picker.ts +10 -6
  252. package/src/modes/components/read-tool-group.ts +36 -0
  253. package/src/modes/components/status-line/component.ts +1 -1
  254. package/src/modes/components/stripped-tool-calls-placeholder.ts +35 -0
  255. package/src/modes/components/tool-execution.ts +85 -20
  256. package/src/modes/components/tree-selector.ts +46 -48
  257. package/src/modes/controllers/command-controller.ts +2 -2
  258. package/src/modes/controllers/event-controller.ts +199 -10
  259. package/src/modes/controllers/input-controller.ts +39 -1
  260. package/src/modes/controllers/mcp-command-controller.ts +11 -5
  261. package/src/modes/controllers/selector-controller.ts +47 -6
  262. package/src/modes/interactive-mode.ts +2 -0
  263. package/src/modes/types.ts +6 -1
  264. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  265. package/src/modes/utils/interactive-context-helpers.ts +1 -0
  266. package/src/modes/utils/ui-helpers.ts +6 -10
  267. package/src/modes/workflow.ts +8 -2
  268. package/src/prompts/agents/init.md +1 -1
  269. package/src/prompts/system/plan-mode-active.md +2 -2
  270. package/src/prompts/system/system-prompt.md +26 -52
  271. package/src/prompts/system/workflow-notice.md +3 -3
  272. package/src/prompts/tools/ast-grep.md +1 -1
  273. package/src/prompts/tools/bash.md +1 -1
  274. package/src/prompts/tools/browser.md +1 -0
  275. package/src/prompts/tools/computer.md +21 -21
  276. package/src/prompts/tools/glob.md +1 -1
  277. package/src/prompts/tools/grep.md +1 -1
  278. package/src/prompts/tools/replace.md +5 -4
  279. package/src/prompts/tools/task.md +4 -4
  280. package/src/registry/agent-lifecycle.ts +36 -11
  281. package/src/registry/agent-registry.ts +12 -5
  282. package/src/sdk.ts +102 -35
  283. package/src/secrets/index.ts +16 -18
  284. package/src/session/agent-session-types.ts +3 -1
  285. package/src/session/agent-session.ts +23 -2
  286. package/src/session/agent-storage.ts +6 -4
  287. package/src/session/history-storage.ts +4 -2
  288. package/src/session/indexed-session-storage.ts +9 -0
  289. package/src/session/messages.ts +31 -7
  290. package/src/session/model-controls.ts +0 -1
  291. package/src/session/prewalk.ts +24 -2
  292. package/src/session/session-advisors.ts +21 -3
  293. package/src/session/session-context.ts +16 -12
  294. package/src/session/session-dump-format.ts +1 -1
  295. package/src/session/session-maintenance.ts +41 -4
  296. package/src/session/session-manager.ts +181 -94
  297. package/src/session/session-paths.ts +118 -55
  298. package/src/session/session-stats.ts +48 -3
  299. package/src/session/session-storage.ts +30 -22
  300. package/src/session/session-tools.ts +25 -23
  301. package/src/session/stream-guards.ts +1 -1
  302. package/src/slash-commands/builtin-registry.ts +29 -26
  303. package/src/system-prompt.ts +14 -4
  304. package/src/task/executor.ts +8 -1
  305. package/src/task/index.ts +23 -8
  306. package/src/task/spawn-policy.ts +14 -0
  307. package/src/thinking.ts +2 -8
  308. package/src/tiny/text.ts +12 -0
  309. package/src/tools/ast-grep.ts +10 -4
  310. package/src/tools/browser/cmux/cmux-tab.ts +5 -10
  311. package/src/tools/browser/cmux/rpc.ts +2 -8
  312. package/src/tools/browser/registry.ts +53 -2
  313. package/src/tools/browser/relay/bridge.ts +945 -0
  314. package/src/tools/browser/relay/daemon.ts +117 -0
  315. package/src/tools/browser/relay/extension-assets/background.js.txt +242 -0
  316. package/src/tools/browser/relay/extension-assets/manifest.json.txt +14 -0
  317. package/src/tools/browser/relay/extension-assets/options.html.txt +53 -0
  318. package/src/tools/browser/relay/extension-assets/options.js.txt +23 -0
  319. package/src/tools/browser/relay/kind.ts +41 -0
  320. package/src/tools/browser/relay/protocol.ts +54 -0
  321. package/src/tools/browser/relay/server.ts +141 -0
  322. package/src/tools/browser/render.ts +4 -1
  323. package/src/tools/browser/shared-daemon.ts +7 -58
  324. package/src/tools/browser/tab-supervisor.ts +5 -4
  325. package/src/tools/browser/tab-worker.ts +33 -12
  326. package/src/tools/browser.ts +26 -1
  327. package/src/tools/computer/exposure.ts +11 -35
  328. package/src/tools/computer/protocol.ts +51 -10
  329. package/src/tools/computer/supervisor.ts +227 -124
  330. package/src/tools/computer/worker-entry.ts +11 -6
  331. package/src/tools/computer/worker.ts +695 -81
  332. package/src/tools/computer-renderer.ts +104 -65
  333. package/src/tools/computer.ts +134 -428
  334. package/src/tools/context.ts +3 -0
  335. package/src/tools/default-renderer.ts +19 -4
  336. package/src/tools/glob.ts +9 -2
  337. package/src/tools/grep.ts +12 -6
  338. package/src/tools/index.ts +10 -6
  339. package/src/tools/inspect-image.ts +32 -6
  340. package/src/tools/render-utils.ts +2 -0
  341. package/src/tools/{browser/run-cancellation.ts → run-scope.ts} +11 -11
  342. package/src/tools/tool-timeouts.ts +1 -0
  343. package/src/tools/xdev.ts +29 -2
  344. package/src/utils/clipboard.ts +40 -14
  345. package/src/utils/command-args.ts +26 -28
  346. package/src/utils/tool-choice.ts +0 -13
  347. package/src/web/kagi.ts +2 -1
  348. package/src/web/search/index.ts +18 -2
  349. package/src/web/search/providers/anthropic.ts +7 -2
  350. package/src/web/search/providers/base.ts +2 -0
  351. package/src/web/search/providers/brave.ts +3 -1
  352. package/src/web/search/providers/browser-page.ts +6 -4
  353. package/src/web/search/providers/codex.ts +4 -1
  354. package/src/web/search/providers/duckduckgo.ts +2 -1
  355. package/src/web/search/providers/ecosia.ts +2 -1
  356. package/src/web/search/providers/exa.ts +4 -2
  357. package/src/web/search/providers/firecrawl.ts +3 -1
  358. package/src/web/search/providers/gemini.ts +8 -2
  359. package/src/web/search/providers/google.ts +2 -1
  360. package/src/web/search/providers/jina.ts +12 -2
  361. package/src/web/search/providers/kagi.ts +3 -0
  362. package/src/web/search/providers/kimi.ts +5 -1
  363. package/src/web/search/providers/mojeek.ts +2 -1
  364. package/src/web/search/providers/parallel.ts +5 -1
  365. package/src/web/search/providers/perplexity.ts +6 -3
  366. package/src/web/search/providers/public.ts +1 -1
  367. package/src/web/search/providers/searxng.ts +11 -5
  368. package/src/web/search/providers/startpage.ts +10 -4
  369. package/src/web/search/providers/synthetic.ts +11 -5
  370. package/src/web/search/providers/tavily.ts +3 -1
  371. package/src/web/search/providers/tinyfish.ts +3 -1
  372. package/src/web/search/providers/utils.ts +12 -10
  373. package/src/web/search/providers/xai.ts +2 -2
  374. package/src/web/search/providers/zai.ts +18 -3
  375. package/src/web/search/types.ts +6 -0
@@ -405,11 +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 handed
409
- * to the OS synchronously in-body (so an entry survives an OOM/SIGKILL the
410
- * instant `appendMessage` returns) but never `fsync`'d. Full-file rewrites go
411
- * through the storage layer's atomic temp-write+rename so a crash mid-rewrite
412
- * cannot truncate the prior good file.
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.
413
424
  */
414
425
  export class SessionManager {
415
426
  #cwd: string;
@@ -477,6 +488,13 @@ export class SessionManager {
477
488
  #atomicRewriteFenceEpoch: number | null = null;
478
489
  /** Set by synchronous appends that land while an atomic replacement is active. */
479
490
  #atomicRewriteDirty = false;
491
+ /**
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.
496
+ */
497
+ #sessionFileRelocating: { source: string; dest: string } | null = null;
480
498
  /** Atomic entry batch currently staged for a full-file commit. */
481
499
  #atomicEntryBatch: AtomicEntryBatch | undefined;
482
500
 
@@ -747,24 +765,56 @@ export class SessionManager {
747
765
  return this.#forceFileCreation || this.#fileIsCurrent || this.#historyContainsAssistantMessage();
748
766
  }
749
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
+
750
783
  /**
751
784
  * Synchronously rewrite the whole file (header + entries) and keep no open
752
785
  * writer; the next append re-opens one. `writeTextSync` returns with the
753
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.
754
791
  */
755
792
  #rewriteSynchronously(): void {
756
- if (!this.#persist || !this.#sessionFile || !this.#shouldHaveSessionFile()) return;
793
+ if (!this.#persist || !this.#shouldHaveSessionFile()) return;
794
+ const targetPath = this.#liveRelocationWritePath() ?? this.#sessionFile;
795
+ if (!targetPath) return;
757
796
 
758
797
  try {
759
798
  const body = this.#fileBody();
760
799
  this.#diskEpoch++;
761
800
  this.#diskTail = Promise.resolve();
762
801
  this.#closeWriterEventually();
763
- this.#storage.writeTextSync(this.#sessionFile, body);
764
- this.#fileIsCurrent = true;
765
- this.#materializeBreadcrumb();
766
- this.#rewriteRequired = false;
767
- 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
+ }
768
818
  } catch (err) {
769
819
  this.#noteDiskFailure(err);
770
820
  }
@@ -850,17 +900,21 @@ export class SessionManager {
850
900
  return;
851
901
  }
852
902
 
853
- // Atomic replacement window: the old path may be moved aside underneath
854
- // any newly-opened append handle (Windows EPERM fallback). Do not open a
855
- // writer here; the active rewrite loops and serializes a fresh full body.
856
- // A superseding synchronous rewrite bumps `#diskEpoch`, at which point
857
- // the pending atomic publish is guaranteed to abandon via its
858
- // `commitGuard`, so appends can (and must) take the hot path so they
859
- // don't strand in memory while `close()` returns without a rewrite.
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
+ }
860
915
  if (this.#atomicRewriteFenceEpoch !== null && this.#atomicRewriteFenceEpoch === this.#diskEpoch) {
861
- this.#fileIsCurrent = false;
862
- this.#rewriteRequired = true;
863
916
  this.#atomicRewriteDirty = true;
917
+ this.#rewriteSynchronously();
864
918
  return;
865
919
  }
866
920
  // Cold/divergent: not on disk yet, or in-memory entries diverged from the
@@ -870,15 +924,23 @@ export class SessionManager {
870
924
  return;
871
925
  }
872
926
 
873
- // Hot path: queue the entry directly on the writer, outside the async disk
874
- // chain. File writers batch same-turn appends until their microtask
875
- // 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.
876
934
  // A mid-close writer leaves `#writer` undefined, so `#appendWriter` simply
877
935
  // opens a fresh append handle and the entry still lands.
878
936
  try {
879
- void this.#appendWriter()
880
- .append(this.#lineFor(entry))
881
- .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
+ }
882
944
  } catch (err) {
883
945
  this.#noteDiskFailure(err);
884
946
  }
@@ -893,6 +955,15 @@ export class SessionManager {
893
955
  return;
894
956
  }
895
957
 
958
+ // Title changes use their own asynchronous append path rather than
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.
962
+ if (this.#sessionFileRelocating) {
963
+ this.#rewriteSynchronously();
964
+ return;
965
+ }
966
+
896
967
  if (
897
968
  !this.#fileIsCurrent ||
898
969
  this.#rewriteRequired ||
@@ -1325,90 +1396,106 @@ export class SessionManager {
1325
1396
  : computeDefaultSessionDir(resolvedCwd, this.#storage));
1326
1397
 
1327
1398
  let sessionFileExisted = false;
1328
-
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.
1329
1404
  if (this.#persist && this.#sessionFile) {
1330
- this.#storage.ensureDirSync(nextSessionDir);
1331
- await this.#drainAndCloseWriter();
1332
- this.#clearDiskError();
1333
-
1334
- const oldSessionFile = this.#sessionFile;
1335
- const newSessionFile = path.join(nextSessionDir, path.basename(oldSessionFile));
1336
- const oldArtifactsDir = artifactsDirectoryFor(oldSessionFile)!;
1337
- const newArtifactsDir = artifactsDirectoryFor(newSessionFile)!;
1338
- const sessionPathChanged = path.resolve(oldSessionFile) !== path.resolve(newSessionFile);
1339
- const artifactPathChanged = path.resolve(oldArtifactsDir) !== path.resolve(newArtifactsDir);
1340
- sessionFileExisted = this.#storage.existsSync(oldSessionFile);
1405
+ const source = this.#sessionFile;
1406
+ const dest = path.join(nextSessionDir, path.basename(source));
1407
+ this.#sessionFileRelocating = { source, dest };
1408
+ }
1341
1409
 
1342
- let sessionMoved = false;
1343
- let artifactsMoved = false;
1410
+ try {
1411
+ if (this.#persist && this.#sessionFile) {
1412
+ this.#storage.ensureDirSync(nextSessionDir);
1413
+ await this.#drainAndCloseWriter();
1414
+ this.#clearDiskError();
1415
+
1416
+ const oldSessionFile = this.#sessionFile;
1417
+ const newSessionFile = path.join(nextSessionDir, path.basename(oldSessionFile));
1418
+ const oldArtifactsDir = artifactsDirectoryFor(oldSessionFile)!;
1419
+ const newArtifactsDir = artifactsDirectoryFor(newSessionFile)!;
1420
+ const sessionPathChanged = path.resolve(oldSessionFile) !== path.resolve(newSessionFile);
1421
+ const artifactPathChanged = path.resolve(oldArtifactsDir) !== path.resolve(newArtifactsDir);
1422
+ sessionFileExisted = this.#storage.existsSync(oldSessionFile);
1423
+
1424
+ let sessionMoved = false;
1425
+ let artifactsMoved = false;
1344
1426
 
1345
- try {
1346
- if (sessionFileExisted && sessionPathChanged) {
1347
- await fs.promises.rename(oldSessionFile, newSessionFile);
1348
- sessionMoved = true;
1349
- }
1427
+ try {
1428
+ if (sessionFileExisted && sessionPathChanged) {
1429
+ await fs.promises.rename(oldSessionFile, newSessionFile);
1430
+ sessionMoved = true;
1431
+ }
1350
1432
 
1351
- if (artifactPathChanged) {
1352
- try {
1353
- const artifactStat = await fs.promises.stat(oldArtifactsDir);
1354
- if (artifactStat.isDirectory()) {
1355
- await fs.promises.rename(oldArtifactsDir, newArtifactsDir);
1356
- artifactsMoved = true;
1433
+ if (artifactPathChanged) {
1434
+ try {
1435
+ const artifactStat = await fs.promises.stat(oldArtifactsDir);
1436
+ if (artifactStat.isDirectory()) {
1437
+ await fs.promises.rename(oldArtifactsDir, newArtifactsDir);
1438
+ artifactsMoved = true;
1439
+ }
1440
+ } catch (err) {
1441
+ if (!isEnoent(err)) throw err;
1357
1442
  }
1358
- } catch (err) {
1359
- if (!isEnoent(err)) throw err;
1360
1443
  }
1361
- }
1362
- } catch (err) {
1363
- if (artifactsMoved) {
1364
- try {
1365
- await fs.promises.rename(newArtifactsDir, oldArtifactsDir);
1366
- } catch (rollbackErr) {
1367
- throw new Error(
1368
- `Failed to move artifacts and rollback: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
1369
- );
1444
+ } catch (err) {
1445
+ if (artifactsMoved) {
1446
+ try {
1447
+ await fs.promises.rename(newArtifactsDir, oldArtifactsDir);
1448
+ } catch (rollbackErr) {
1449
+ throw new Error(
1450
+ `Failed to move artifacts and rollback: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
1451
+ );
1452
+ }
1370
1453
  }
1371
- }
1372
1454
 
1373
- if (sessionMoved) {
1374
- try {
1375
- await fs.promises.rename(newSessionFile, oldSessionFile);
1376
- } catch (rollbackErr) {
1377
- throw new Error(
1378
- `Failed to move session file and rollback: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
1379
- );
1455
+ if (sessionMoved) {
1456
+ try {
1457
+ await fs.promises.rename(newSessionFile, oldSessionFile);
1458
+ } catch (rollbackErr) {
1459
+ throw new Error(
1460
+ `Failed to move session file and rollback: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
1461
+ );
1462
+ }
1380
1463
  }
1464
+
1465
+ throw err;
1381
1466
  }
1382
1467
 
1383
- throw err;
1468
+ this.#sessionFile = newSessionFile;
1469
+ this.#artifactManager = null;
1470
+ this.#artifactManagerSessionFile = null;
1471
+ // Path is repointed; hot-path appends may use `#sessionFile` again.
1472
+ this.#sessionFileRelocating = null;
1384
1473
  }
1385
1474
 
1386
- this.#sessionFile = newSessionFile;
1387
- this.#artifactManager = null;
1388
- this.#artifactManagerSessionFile = null;
1389
- }
1475
+ this.#cwd = resolvedCwd;
1476
+ this.#sessionDir = nextSessionDir;
1477
+ this.#header.cwd = resolvedCwd;
1478
+ // Re-filter additional roots: the new cwd may have been an additional root,
1479
+ // or it may now contain/subsume one. Re-normalize to keep the invariant
1480
+ // that cwd is never also listed as an additional directory.
1481
+ if (this.#additionalDirectories.length > 0) {
1482
+ this.#additionalDirectories = this.#additionalDirectories.filter(d => d !== resolvedCwd);
1483
+ this.#header.additionalDirectories =
1484
+ this.#additionalDirectories.length > 0 ? this.#additionalDirectories : undefined;
1485
+ }
1390
1486
 
1391
- this.#cwd = resolvedCwd;
1392
- this.#sessionDir = nextSessionDir;
1393
- this.#header.cwd = resolvedCwd;
1394
- // Re-filter additional roots: the new cwd may have been an additional root,
1395
- // or it may now contain/subsume one. Re-normalize to keep the invariant
1396
- // that cwd is never also listed as an additional directory.
1397
- if (this.#additionalDirectories.length > 0) {
1398
- this.#additionalDirectories = this.#additionalDirectories.filter(d => d !== resolvedCwd);
1399
- this.#header.additionalDirectories =
1400
- this.#additionalDirectories.length > 0 ? this.#additionalDirectories : undefined;
1401
- }
1487
+ // Rewrite at the new location when the file already existed (update cwd) or
1488
+ // there is in-memory output worth materializing; otherwise stay lazy.
1489
+ const hasAssistant = this.#historyContainsAssistantMessage();
1490
+ if (this.#persist && this.#sessionFile && (sessionFileExisted || hasAssistant)) {
1491
+ this.#forceFileCreation = true;
1492
+ await this.#rewriteAtomically();
1493
+ }
1402
1494
 
1403
- // Rewrite at the new location when the file already existed (update cwd) or
1404
- // there is in-memory output worth materializing; otherwise stay lazy.
1405
- const hasAssistant = this.#historyContainsAssistantMessage();
1406
- if (this.#persist && this.#sessionFile && (sessionFileExisted || hasAssistant)) {
1407
- this.#forceFileCreation = true;
1408
- await this.#rewriteAtomically();
1495
+ if (this.#sessionFile) this.#rememberBreadcrumb(resolvedCwd, this.#sessionFile);
1496
+ } finally {
1497
+ this.#sessionFileRelocating = null;
1409
1498
  }
1410
-
1411
- if (this.#sessionFile) this.#rememberBreadcrumb(resolvedCwd, this.#sessionFile);
1412
1499
  }
1413
1500
 
1414
1501
  /**
@@ -2,10 +2,17 @@ import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { getTerminalId } from "@oh-my-pi/pi-tui";
5
- import { getSessionsDir, getTerminalSessionsDir, isEnoent, logger, resolveEquivalentPath } from "@oh-my-pi/pi-utils";
5
+ import {
6
+ getSessionsDir,
7
+ getTerminalSessionsDir,
8
+ isEnoent,
9
+ isRecord,
10
+ logger,
11
+ resolveEquivalentPath,
12
+ } from "@oh-my-pi/pi-utils";
6
13
  import type { SessionStorage } from "./session-storage";
7
14
 
8
- const migratedSessionRoots = new Set<string>();
15
+ const SESSION_HEADER_PREFIX_BYTES = 4096;
9
16
 
10
17
  /**
11
18
  * Merge or rename a legacy session directory into its canonical target.
@@ -35,12 +42,16 @@ function encodeLegacyAbsoluteSessionDirName(cwd: string): string {
35
42
  return `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
36
43
  }
37
44
 
38
- function encodeRelativeSessionDirName(prefix: string, relative: string): string {
45
+ function encodeLegacyRelativeSessionDirName(prefix: string, relative: string): string {
39
46
  const encoded = relative.replace(/[/\\:]/g, "-");
40
47
  return encoded ? (prefix.endsWith("-") ? `${prefix}${encoded}` : `${prefix}-${encoded}`) : prefix;
41
48
  }
42
49
 
43
- function getDefaultSessionDirName(cwd: string): { encodedDirName: string; resolvedCwd: string } {
50
+ function getDefaultSessionDirName(cwd: string): {
51
+ encodedDirName: string;
52
+ legacyRelativeDirName: string | undefined;
53
+ resolvedCwd: string;
54
+ } {
44
55
  const resolvedCwd = path.resolve(cwd);
45
56
  const canonicalCwd = resolveEquivalentPath(resolvedCwd);
46
57
  const home = os.homedir();
@@ -49,72 +60,111 @@ function getDefaultSessionDirName(cwd: string): { encodedDirName: string; resolv
49
60
  const canonicalTempRoot = resolveEquivalentPath(tempRoot);
50
61
  const homeRelative = path.relative(canonicalHome, canonicalCwd);
51
62
  const tempRelative = path.relative(canonicalTempRoot, canonicalCwd);
52
- const encodedDirName =
53
- homeRelative === "" || (!homeRelative.startsWith("..") && !path.isAbsolute(homeRelative))
54
- ? encodeRelativeSessionDirName("-", homeRelative)
55
- : tempRelative === "" || (!tempRelative.startsWith("..") && !path.isAbsolute(tempRelative))
56
- ? encodeRelativeSessionDirName("-tmp", tempRelative)
57
- : encodeLegacyAbsoluteSessionDirName(canonicalCwd);
58
- return { encodedDirName, resolvedCwd };
59
- }
60
63
 
61
- /**
62
- * Migrate old `--<home-encoded>-*--` session dirs to the new `-*` format.
63
- * Runs once per sessions root on first access, best-effort.
64
- */
65
- function migrateHomeSessionDirs(sessionsRoot: string): void {
66
- if (migratedSessionRoots.has(sessionsRoot)) return;
67
- migratedSessionRoots.add(sessionsRoot);
64
+ let scope: "home" | "tmp" | "abs";
65
+ let legacyRelativeDirName: string | undefined;
66
+ if (homeRelative === "" || (!homeRelative.startsWith("..") && !path.isAbsolute(homeRelative))) {
67
+ scope = "home";
68
+ legacyRelativeDirName = encodeLegacyRelativeSessionDirName("-", homeRelative);
69
+ } else if (tempRelative === "" || (!tempRelative.startsWith("..") && !path.isAbsolute(tempRelative))) {
70
+ scope = "tmp";
71
+ legacyRelativeDirName = encodeLegacyRelativeSessionDirName("-tmp", tempRelative);
72
+ } else {
73
+ scope = "abs";
74
+ }
68
75
 
69
- const home = os.homedir();
70
- const homeEncoded = home.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-");
71
- const oldPrefix = `--${homeEncoded}-`;
72
- const oldExact = `--${homeEncoded}--`;
76
+ const normalized = canonicalCwd.replaceAll("\\", "/");
77
+ const readable = path
78
+ .basename(canonicalCwd)
79
+ .replace(/[^a-zA-Z0-9._-]+/g, "-")
80
+ .replace(/^-+|-+$/g, "")
81
+ .slice(-80);
82
+ const digest = Bun.SHA256.hash(normalized, "hex");
83
+ const encodedDirName = `${scope}-${readable || "project"}-${digest}`;
84
+ return { encodedDirName, legacyRelativeDirName, resolvedCwd };
85
+ }
73
86
 
74
- let entries: string[];
87
+ function readSessionCwd(sessionFile: string, buffer: Buffer): string | undefined {
88
+ let descriptor: number | undefined;
75
89
  try {
76
- entries = fs.readdirSync(sessionsRoot);
90
+ descriptor = fs.openSync(sessionFile, "r");
91
+ const bytesRead = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
92
+ const prefix = buffer.toString("utf8", 0, bytesRead);
93
+ for (const line of prefix.split(/\r?\n/, 3)) {
94
+ try {
95
+ const record: unknown = JSON.parse(line);
96
+ if (isRecord(record) && record.type === "session" && typeof record.cwd === "string") {
97
+ return record.cwd;
98
+ }
99
+ } catch {
100
+ // Ignore title slots or truncated/corrupt headers.
101
+ }
102
+ }
77
103
  } catch {
78
- return;
104
+ // Best-effort migration leaves unreadable sessions in the current bucket.
105
+ } finally {
106
+ if (descriptor !== undefined) fs.closeSync(descriptor);
79
107
  }
108
+ return undefined;
109
+ }
80
110
 
111
+ function moveSessionBundle(sourceDir: string, targetDir: string, sessionName: string, entries: string[]): void {
112
+ fs.mkdirSync(targetDir, { recursive: true });
113
+ const artifactsName = path.basename(sessionName, ".jsonl");
81
114
  for (const entry of entries) {
82
- let remainder: string;
83
- if (entry === oldExact) {
84
- remainder = "";
85
- } else if (entry.startsWith(oldPrefix) && entry.endsWith("--")) {
86
- remainder = entry.slice(oldPrefix.length, -2);
115
+ if (entry !== sessionName && entry !== artifactsName && !entry.startsWith(`${sessionName}.`)) continue;
116
+ const source = path.join(sourceDir, entry);
117
+ if (!fs.existsSync(source)) continue;
118
+ const target = path.join(targetDir, entry);
119
+ const existing = fs.statSync(target, { throwIfNoEntry: false });
120
+ if (!existing) {
121
+ fs.renameSync(source, target);
122
+ } else if (existing.isDirectory() && fs.statSync(source).isDirectory()) {
123
+ migrateSessionDirPath(source, target);
87
124
  } else {
88
- continue;
89
- }
90
-
91
- const newName = remainder ? `-${remainder}` : "-";
92
- const oldPath = path.join(sessionsRoot, entry);
93
- const newPath = path.join(sessionsRoot, newName);
94
-
95
- try {
96
- migrateSessionDirPath(oldPath, newPath);
97
- } catch {
98
- // Best effort
125
+ fs.rmSync(source, { recursive: true, force: true });
99
126
  }
100
127
  }
101
128
  }
102
129
 
103
- function migrateLegacyAbsoluteSessionDir(cwd: string, sessionDir: string, sessionsRoot: string): void {
104
- const legacyDir = path.join(sessionsRoot, encodeLegacyAbsoluteSessionDirName(cwd));
105
- if (legacyDir === sessionDir || !fs.existsSync(legacyDir)) return;
106
-
107
- try {
108
- migrateSessionDirPath(legacyDir, sessionDir);
109
- } catch {
110
- // Best effort
130
+ function rerouteCollidingSessions(
131
+ cwd: string,
132
+ legacyDir: string,
133
+ sessionsRoot: string,
134
+ kind: "relative" | "absolute",
135
+ ): void {
136
+ const entries = fs.readdirSync(legacyDir);
137
+ const currentCwd = resolveEquivalentPath(path.resolve(cwd));
138
+ const buffer = Buffer.allocUnsafe(SESSION_HEADER_PREFIX_BYTES);
139
+ for (const entry of entries) {
140
+ if (!entry.endsWith(".jsonl")) continue;
141
+ const recordedCwd = readSessionCwd(path.join(legacyDir, entry), buffer);
142
+ if (!recordedCwd) continue;
143
+ const recordedCanonical = resolveEquivalentPath(path.resolve(recordedCwd));
144
+ if (
145
+ recordedCanonical === currentCwd ||
146
+ !fs.statSync(recordedCanonical, { throwIfNoEntry: false })?.isDirectory()
147
+ ) {
148
+ continue;
149
+ }
150
+ const recordedNames = getDefaultSessionDirName(recordedCanonical);
151
+ const recordedLegacyName =
152
+ kind === "relative"
153
+ ? recordedNames.legacyRelativeDirName
154
+ : encodeLegacyAbsoluteSessionDirName(recordedCanonical);
155
+ if (recordedLegacyName !== path.basename(legacyDir)) continue;
156
+ moveSessionBundle(legacyDir, path.join(sessionsRoot, recordedNames.encodedDirName), entry, entries);
111
157
  }
112
158
  }
113
159
 
114
160
  export function resolveManagedSessionRoot(sessionDir: string, cwd: string): string | undefined {
115
161
  const currentDirName = path.basename(sessionDir);
116
- const { encodedDirName } = getDefaultSessionDirName(cwd);
117
- if (currentDirName !== encodedDirName && currentDirName !== encodeLegacyAbsoluteSessionDirName(cwd)) {
162
+ const { encodedDirName, legacyRelativeDirName } = getDefaultSessionDirName(cwd);
163
+ if (
164
+ currentDirName !== encodedDirName &&
165
+ currentDirName !== legacyRelativeDirName &&
166
+ currentDirName !== encodeLegacyAbsoluteSessionDirName(cwd)
167
+ ) {
118
168
  return undefined;
119
169
  }
120
170
  return path.dirname(sessionDir);
@@ -130,10 +180,23 @@ export function computeDefaultSessionDir(
130
180
  storage: SessionStorage,
131
181
  sessionsRoot: string = getSessionsDir(),
132
182
  ): string {
133
- const { encodedDirName, resolvedCwd } = getDefaultSessionDirName(cwd);
134
- migrateHomeSessionDirs(sessionsRoot);
183
+ const { encodedDirName, legacyRelativeDirName, resolvedCwd } = getDefaultSessionDirName(cwd);
135
184
  const sessionDir = path.join(sessionsRoot, encodedDirName);
136
- migrateLegacyAbsoluteSessionDir(resolvedCwd, sessionDir, sessionsRoot);
185
+ const legacyDirs: Array<{ kind: "relative" | "absolute"; name: string | undefined }> = [
186
+ { kind: "relative", name: legacyRelativeDirName },
187
+ { kind: "absolute", name: encodeLegacyAbsoluteSessionDirName(resolvedCwd) },
188
+ ];
189
+ for (const legacy of legacyDirs) {
190
+ if (!legacy.name) continue;
191
+ const legacyDir = path.join(sessionsRoot, legacy.name);
192
+ if (legacyDir === sessionDir || !fs.existsSync(legacyDir)) continue;
193
+ try {
194
+ rerouteCollidingSessions(resolvedCwd, legacyDir, sessionsRoot, legacy.kind);
195
+ migrateSessionDirPath(legacyDir, sessionDir);
196
+ } catch {
197
+ // Best effort
198
+ }
199
+ }
137
200
  storage.ensureDirSync(sessionDir);
138
201
  return sessionDir;
139
202
  }
@@ -34,6 +34,11 @@ export interface SessionStatsTrackerHost {
34
34
  sessionId(): string;
35
35
  }
36
36
 
37
+ function correctedPromptTokens(assistant: AssistantMessage): number {
38
+ const providerPromptTokens = assistant.contextSnapshot?.promptTokens ?? calculatePromptTokens(assistant.usage);
39
+ return Math.max(0, providerPromptTokens - (assistant.contextSnapshot?.historyRewriteTokensRemoved ?? 0));
40
+ }
41
+
37
42
  /** Computes session totals and tracks the in-flight context estimate. */
38
43
  export class SessionStatsTracker {
39
44
  readonly #host: SessionStatsTrackerHost;
@@ -160,8 +165,7 @@ export class SessionStatsTracker {
160
165
  const useAnchor =
161
166
  anchorAssistant !== undefined && anchorIndex !== -1 && (!pending || anchorIndex >= pending.cutoffCount);
162
167
  if (useAnchor && anchorAssistant) {
163
- const promptTokens =
164
- anchorAssistant.contextSnapshot?.promptTokens ?? calculatePromptTokens(anchorAssistant.usage);
168
+ const promptTokens = correctedPromptTokens(anchorAssistant);
165
169
  const nonMessageTokens =
166
170
  anchorAssistant.contextSnapshot?.nonMessageTokens ?? computeNonMessageTokens(this.#host.session);
167
171
  anchored = true;
@@ -199,7 +203,7 @@ export class SessionStatsTracker {
199
203
  ) {
200
204
  continue;
201
205
  }
202
- const promptTokens = message.contextSnapshot?.promptTokens ?? calculatePromptTokens(message.usage);
206
+ const promptTokens = correctedPromptTokens(message);
203
207
  const nonMessageTokens =
204
208
  message.contextSnapshot?.nonMessageTokens ?? computeNonMessageTokens(this.#host.session);
205
209
  let tailTokens = 0;
@@ -256,6 +260,47 @@ export class SessionStatsTracker {
256
260
  return this.#pendingContextSnapshot?.nonMessageTokens;
257
261
  }
258
262
 
263
+ /**
264
+ * Apply an estimated prompt-prefix reduction to the current provider anchor.
265
+ *
266
+ * History after the anchor is estimated live by {@link getContextBreakdown};
267
+ * callers must pass only savings from entries already included in the
268
+ * anchor's provider-reported prompt. Persisting the correction on the
269
+ * assistant snapshot keeps reloads accurate, and the next successful
270
+ * assistant response naturally replaces it with a fresh provider anchor.
271
+ */
272
+ recordAnchoredHistoryRewrite(tokensRemoved: number): void {
273
+ if (!Number.isFinite(tokensRemoved) || tokensRemoved <= 0) return;
274
+
275
+ const branchEntries = this.#host.sessionManager.getBranch();
276
+ const latestCompaction = getLatestCompactionEntry(branchEntries);
277
+ const compactionIndex = latestCompaction ? branchEntries.lastIndexOf(latestCompaction) : -1;
278
+ for (let index = branchEntries.length - 1; index > compactionIndex; index--) {
279
+ const entry = branchEntries[index];
280
+ if (entry.type !== "message" || entry.message.role !== "assistant") continue;
281
+ const assistant = entry.message;
282
+ if (
283
+ assistant.stopReason === "aborted" ||
284
+ assistant.stopReason === "error" ||
285
+ !assistant.usage ||
286
+ !hasContextTokenUsage(assistant.usage)
287
+ ) {
288
+ continue;
289
+ }
290
+
291
+ if (!assistant.contextSnapshot) {
292
+ assistant.contextSnapshot = {
293
+ promptTokens: calculatePromptTokens(assistant.usage),
294
+ nonMessageTokens: computeNonMessageTokens(this.#host.session),
295
+ };
296
+ }
297
+ const snapshot = assistant.contextSnapshot;
298
+ snapshot.historyRewriteTokensRemoved = (snapshot.historyRewriteTokensRemoved ?? 0) + Math.floor(tokensRemoved);
299
+ this.#contextUsageRevision++;
300
+ return;
301
+ }
302
+ }
303
+
259
304
  /** Sets or clears the in-flight context snapshot. */
260
305
  setPendingSnapshot(snapshot: PendingContextSnapshot | undefined): void {
261
306
  this.#pendingContextSnapshot = snapshot;