@oh-my-pi/pi-coding-agent 15.12.3 → 15.13.0

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 (457) hide show
  1. package/CHANGELOG.md +347 -7
  2. package/dist/cli.js +1615 -1231
  3. package/dist/types/async/job-manager.d.ts +15 -0
  4. package/dist/types/autolearn/controller.d.ts +25 -0
  5. package/dist/types/autolearn/managed-skills.d.ts +45 -0
  6. package/dist/types/autoresearch/state.d.ts +1 -1
  7. package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
  8. package/dist/types/autoresearch/tools/log-experiment.d.ts +1 -1
  9. package/dist/types/autoresearch/tools/run-experiment.d.ts +1 -1
  10. package/dist/types/autoresearch/tools/update-notes.d.ts +1 -1
  11. package/dist/types/autoresearch/types.d.ts +1 -1
  12. package/dist/types/cli/args.d.ts +19 -2
  13. package/dist/types/cli/models-cli.d.ts +49 -0
  14. package/dist/types/cli/session-picker.d.ts +1 -1
  15. package/dist/types/cli/setup-cli.d.ts +1 -1
  16. package/dist/types/cli/setup-model-picker.d.ts +14 -0
  17. package/dist/types/collab/protocol.d.ts +1 -1
  18. package/dist/types/commands/launch.d.ts +0 -3
  19. package/dist/types/commands/models.d.ts +33 -0
  20. package/dist/types/commands/say.d.ts +24 -0
  21. package/dist/types/commands/token.d.ts +25 -0
  22. package/dist/types/commit/agentic/tools/analyze-file.d.ts +1 -1
  23. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +1 -1
  24. package/dist/types/commit/agentic/tools/git-hunk.d.ts +1 -1
  25. package/dist/types/commit/agentic/tools/git-overview.d.ts +1 -1
  26. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +1 -1
  27. package/dist/types/commit/agentic/tools/propose-commit.d.ts +1 -1
  28. package/dist/types/commit/agentic/tools/recent-commits.d.ts +1 -1
  29. package/dist/types/commit/agentic/tools/schemas.d.ts +1 -1
  30. package/dist/types/commit/agentic/tools/split-commit.d.ts +1 -1
  31. package/dist/types/commit/changelog/generate.d.ts +1 -1
  32. package/dist/types/commit/shared-llm.d.ts +1 -1
  33. package/dist/types/config/keybindings.d.ts +3 -3
  34. package/dist/types/config/model-registry.d.ts +17 -0
  35. package/dist/types/config/models-config-schema.d.ts +13 -1
  36. package/dist/types/config/models-config.d.ts +8 -2
  37. package/dist/types/config/settings-schema.d.ts +281 -58
  38. package/dist/types/edit/hashline/params.d.ts +1 -1
  39. package/dist/types/edit/modes/apply-patch.d.ts +1 -1
  40. package/dist/types/edit/modes/patch.d.ts +1 -1
  41. package/dist/types/edit/modes/replace.d.ts +1 -1
  42. package/dist/types/export/html/index.d.ts +2 -1
  43. package/dist/types/extensibility/custom-commands/types.d.ts +2 -2
  44. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  45. package/dist/types/extensibility/extensions/model-api.d.ts +17 -0
  46. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  47. package/dist/types/extensibility/extensions/types.d.ts +49 -3
  48. package/dist/types/extensibility/hooks/index.d.ts +2 -1
  49. package/dist/types/extensibility/hooks/types.d.ts +2 -2
  50. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +9 -0
  51. package/dist/types/extensibility/plugins/loader.d.ts +11 -0
  52. package/dist/types/extensibility/shared-events.d.ts +1 -1
  53. package/dist/types/extensibility/skills.d.ts +10 -0
  54. package/dist/types/goals/guided-setup.d.ts +18 -0
  55. package/dist/types/goals/state.d.ts +1 -1
  56. package/dist/types/goals/tools/goal-tool.d.ts +1 -1
  57. package/dist/types/hindsight/transcript.d.ts +1 -1
  58. package/dist/types/index.d.ts +5 -0
  59. package/dist/types/internal-urls/local-protocol.d.ts +4 -2
  60. package/dist/types/lsp/types.d.ts +1 -1
  61. package/dist/types/main.d.ts +4 -3
  62. package/dist/types/mcp/manager.d.ts +8 -0
  63. package/dist/types/mcp/startup-events.d.ts +11 -0
  64. package/dist/types/memories/index.d.ts +7 -0
  65. package/dist/types/memory-backend/local-backend.d.ts +4 -3
  66. package/dist/types/mnemopi/config.d.ts +28 -0
  67. package/dist/types/modes/acp/acp-agent.d.ts +1 -2
  68. package/dist/types/modes/components/agent-hub.d.ts +6 -0
  69. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  70. package/dist/types/modes/components/compaction-summary-message.d.ts +15 -1
  71. package/dist/types/modes/components/custom-editor.d.ts +39 -1
  72. package/dist/types/modes/components/custom-editor.test.d.ts +1 -0
  73. package/dist/types/modes/components/index.d.ts +1 -0
  74. package/dist/types/modes/components/logout-account-selector.d.ts +8 -0
  75. package/dist/types/modes/components/session-selector.d.ts +1 -1
  76. package/dist/types/modes/components/status-line/component.d.ts +9 -5
  77. package/dist/types/modes/components/status-line/types.d.ts +2 -1
  78. package/dist/types/modes/components/tool-execution.d.ts +26 -16
  79. package/dist/types/modes/components/transcript-container.d.ts +23 -2
  80. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  81. package/dist/types/modes/components/usage-row.d.ts +3 -0
  82. package/dist/types/modes/controllers/command-controller.d.ts +2 -2
  83. package/dist/types/modes/controllers/event-controller.d.ts +0 -17
  84. package/dist/types/modes/controllers/input-controller.d.ts +14 -0
  85. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  86. package/dist/types/modes/gradient-highlight.d.ts +9 -4
  87. package/dist/types/modes/image-references.d.ts +6 -0
  88. package/dist/types/modes/interactive-mode.d.ts +27 -6
  89. package/dist/types/modes/magic-keywords.d.ts +13 -1
  90. package/dist/types/modes/rpc/rpc-mode.d.ts +35 -1
  91. package/dist/types/modes/rpc/rpc-types.d.ts +9 -1
  92. package/dist/types/modes/runtime-init.d.ts +4 -0
  93. package/dist/types/modes/theme/theme.d.ts +13 -2
  94. package/dist/types/modes/types.d.ts +8 -7
  95. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  96. package/dist/types/registry/agent-registry.d.ts +17 -0
  97. package/dist/types/secrets/obfuscator.d.ts +1 -1
  98. package/dist/types/session/agent-session.d.ts +28 -35
  99. package/dist/types/session/agent-storage.d.ts +2 -1
  100. package/dist/types/session/indexed-session-storage.d.ts +3 -3
  101. package/dist/types/session/messages.d.ts +8 -10
  102. package/dist/types/session/session-context.d.ts +39 -0
  103. package/dist/types/session/session-entries.d.ts +159 -0
  104. package/dist/types/session/session-listing.d.ts +69 -0
  105. package/dist/types/session/session-loader.d.ts +16 -0
  106. package/dist/types/session/session-manager.d.ts +85 -462
  107. package/dist/types/session/session-migrations.d.ts +12 -0
  108. package/dist/types/session/session-paths.d.ts +25 -0
  109. package/dist/types/session/session-persistence.d.ts +8 -0
  110. package/dist/types/session/session-storage.d.ts +11 -7
  111. package/dist/types/session/snapcompact-inline.d.ts +12 -1
  112. package/dist/types/session/snapcompact-savings-journal.d.ts +46 -0
  113. package/dist/types/session/tool-choice-queue.d.ts +6 -6
  114. package/dist/types/slash-commands/helpers/logout.d.ts +15 -0
  115. package/dist/types/stt/asr-client.d.ts +90 -0
  116. package/dist/types/stt/asr-protocol.d.ts +97 -0
  117. package/dist/types/stt/asr-worker.d.ts +2 -0
  118. package/dist/types/stt/downloader.d.ts +38 -0
  119. package/dist/types/stt/endpointer.d.ts +59 -0
  120. package/dist/types/stt/index.d.ts +5 -1
  121. package/dist/types/stt/models.d.ts +120 -0
  122. package/dist/types/stt/recorder.d.ts +17 -0
  123. package/dist/types/stt/stt-controller.d.ts +6 -0
  124. package/dist/types/stt/transcriber.d.ts +5 -7
  125. package/dist/types/stt/wav.d.ts +29 -0
  126. package/dist/types/system-prompt.d.ts +4 -0
  127. package/dist/types/task/executor.d.ts +2 -0
  128. package/dist/types/task/index.d.ts +9 -1
  129. package/dist/types/task/types.d.ts +37 -1
  130. package/dist/types/tools/ask.d.ts +1 -1
  131. package/dist/types/tools/ast-edit.d.ts +1 -1
  132. package/dist/types/tools/ast-grep.d.ts +1 -1
  133. package/dist/types/tools/bash.d.ts +3 -3
  134. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +202 -0
  135. package/dist/types/tools/browser/cmux/rpc.d.ts +70 -0
  136. package/dist/types/tools/browser/cmux/socket-client.d.ts +19 -0
  137. package/dist/types/tools/browser/registry.d.ts +16 -3
  138. package/dist/types/tools/browser/render.d.ts +2 -0
  139. package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
  140. package/dist/types/tools/browser/tab-supervisor.d.ts +16 -4
  141. package/dist/types/tools/browser.d.ts +3 -1
  142. package/dist/types/tools/checkpoint.d.ts +1 -1
  143. package/dist/types/tools/debug.d.ts +1 -1
  144. package/dist/types/tools/eval-render.d.ts +1 -1
  145. package/dist/types/tools/eval.d.ts +1 -1
  146. package/dist/types/tools/find.d.ts +1 -1
  147. package/dist/types/tools/gh.d.ts +1 -1
  148. package/dist/types/tools/image-gen.d.ts +1 -1
  149. package/dist/types/tools/index.d.ts +14 -2
  150. package/dist/types/tools/inspect-image.d.ts +1 -1
  151. package/dist/types/tools/irc.d.ts +2 -1
  152. package/dist/types/tools/job.d.ts +1 -1
  153. package/dist/types/tools/learn.d.ts +51 -0
  154. package/dist/types/tools/manage-skill.d.ts +40 -0
  155. package/dist/types/tools/memory-edit.d.ts +1 -1
  156. package/dist/types/tools/memory-recall.d.ts +1 -1
  157. package/dist/types/tools/memory-reflect.d.ts +1 -1
  158. package/dist/types/tools/memory-retain.d.ts +1 -1
  159. package/dist/types/tools/plan-mode-guard.d.ts +10 -0
  160. package/dist/types/tools/read.d.ts +1 -1
  161. package/dist/types/tools/render-mermaid.d.ts +1 -1
  162. package/dist/types/tools/renderers.d.ts +7 -11
  163. package/dist/types/tools/resolve.d.ts +1 -1
  164. package/dist/types/tools/review.d.ts +1 -1
  165. package/dist/types/tools/search-tool-bm25.d.ts +1 -1
  166. package/dist/types/tools/search.d.ts +1 -1
  167. package/dist/types/tools/ssh.d.ts +2 -2
  168. package/dist/types/tools/todo.d.ts +2 -2
  169. package/dist/types/tools/tts.d.ts +26 -1
  170. package/dist/types/tools/write.d.ts +2 -2
  171. package/dist/types/tts/downloader.d.ts +20 -0
  172. package/dist/types/tts/index.d.ts +8 -0
  173. package/dist/types/tts/models.d.ts +82 -0
  174. package/dist/types/tts/player.d.ts +32 -0
  175. package/dist/types/tts/runtime.d.ts +6 -0
  176. package/dist/types/tts/streaming-player.d.ts +41 -0
  177. package/dist/types/tts/tts-client.d.ts +93 -0
  178. package/dist/types/tts/tts-protocol.d.ts +95 -0
  179. package/dist/types/tts/tts-worker.d.ts +2 -0
  180. package/dist/types/tts/vocalizer.d.ts +41 -0
  181. package/dist/types/tts/wav.d.ts +8 -0
  182. package/dist/types/utils/clipboard.d.ts +4 -3
  183. package/dist/types/utils/image-loading.d.ts +18 -1
  184. package/dist/types/utils/thinking-display.d.ts +17 -0
  185. package/dist/types/utils/tool-choice.d.ts +8 -0
  186. package/dist/types/utils/tools-manager.d.ts +2 -1
  187. package/dist/types/utils/tools-manager.test.d.ts +1 -0
  188. package/dist/types/web/scrapers/github.d.ts +1 -1
  189. package/dist/types/web/search/index.d.ts +1 -1
  190. package/package.json +17 -16
  191. package/src/async/job-manager.ts +49 -0
  192. package/src/autolearn/controller.ts +139 -0
  193. package/src/autolearn/managed-skills.ts +257 -0
  194. package/src/autoresearch/state.ts +1 -1
  195. package/src/autoresearch/storage.ts +2 -1
  196. package/src/autoresearch/tools/init-experiment.ts +1 -1
  197. package/src/autoresearch/tools/log-experiment.ts +1 -1
  198. package/src/autoresearch/tools/run-experiment.ts +1 -1
  199. package/src/autoresearch/tools/update-notes.ts +1 -1
  200. package/src/autoresearch/types.ts +1 -1
  201. package/src/cli/args.ts +56 -10
  202. package/src/cli/auth-gateway-cli.ts +1 -1
  203. package/src/cli/bench-cli.ts +1 -1
  204. package/src/cli/dry-balance-cli.ts +1 -1
  205. package/src/cli/models-cli.ts +427 -0
  206. package/src/cli/session-picker.ts +2 -1
  207. package/src/cli/setup-cli.ts +148 -47
  208. package/src/cli/setup-model-picker.ts +43 -0
  209. package/src/cli-commands.ts +3 -0
  210. package/src/cli.ts +45 -13
  211. package/src/collab/host.ts +10 -13
  212. package/src/collab/protocol.ts +1 -1
  213. package/src/commands/launch.ts +0 -3
  214. package/src/commands/models.ts +61 -0
  215. package/src/commands/say.ts +102 -0
  216. package/src/commands/setup.ts +1 -1
  217. package/src/commands/token.ts +89 -0
  218. package/src/commit/agentic/tools/analyze-file.ts +4 -1
  219. package/src/commit/agentic/tools/git-file-diff.ts +1 -1
  220. package/src/commit/agentic/tools/git-hunk.ts +1 -1
  221. package/src/commit/agentic/tools/git-overview.ts +1 -1
  222. package/src/commit/agentic/tools/propose-changelog.ts +1 -1
  223. package/src/commit/agentic/tools/propose-commit.ts +1 -1
  224. package/src/commit/agentic/tools/recent-commits.ts +1 -1
  225. package/src/commit/agentic/tools/schemas.ts +1 -1
  226. package/src/commit/agentic/tools/split-commit.ts +1 -1
  227. package/src/commit/analysis/summary.ts +1 -1
  228. package/src/commit/changelog/generate.ts +1 -1
  229. package/src/commit/shared-llm.ts +1 -1
  230. package/src/config/keybindings.ts +2 -2
  231. package/src/config/model-discovery.ts +11 -5
  232. package/src/config/model-registry.ts +79 -21
  233. package/src/config/model-resolver.ts +2 -2
  234. package/src/config/models-config-schema.ts +5 -2
  235. package/src/config/models-config.ts +2 -1
  236. package/src/config/settings-schema.ts +266 -32
  237. package/src/config/settings.ts +10 -0
  238. package/src/discovery/builtin.ts +23 -1
  239. package/src/discovery/claude-plugins.ts +44 -5
  240. package/src/discovery/helpers.ts +41 -1
  241. package/src/edit/hashline/params.ts +1 -1
  242. package/src/edit/modes/apply-patch.ts +1 -1
  243. package/src/edit/modes/patch.ts +1 -1
  244. package/src/edit/modes/replace.ts +1 -1
  245. package/src/eval/__tests__/budget-bridge.test.ts +1 -1
  246. package/src/eval/agent-bridge.ts +1 -1
  247. package/src/eval/completion-bridge.ts +1 -1
  248. package/src/eval/js/shared/prelude.txt +69 -17
  249. package/src/export/html/index.ts +3 -6
  250. package/src/export/html/template.js +24 -2
  251. package/src/export/html/tool-views.generated.js +2 -2
  252. package/src/extensibility/custom-commands/loader.ts +1 -1
  253. package/src/extensibility/custom-commands/types.ts +2 -2
  254. package/src/extensibility/custom-tools/loader.ts +1 -1
  255. package/src/extensibility/custom-tools/types.ts +2 -2
  256. package/src/extensibility/extensions/loader.ts +2 -2
  257. package/src/extensibility/extensions/model-api.ts +41 -0
  258. package/src/extensibility/extensions/runner.ts +4 -0
  259. package/src/extensibility/extensions/types.ts +54 -3
  260. package/src/extensibility/extensions/wrapper.ts +41 -5
  261. package/src/extensibility/hooks/index.ts +2 -1
  262. package/src/extensibility/hooks/loader.ts +1 -1
  263. package/src/extensibility/hooks/types.ts +2 -2
  264. package/src/extensibility/plugins/legacy-pi-compat.ts +43 -13
  265. package/src/extensibility/plugins/loader.ts +30 -19
  266. package/src/extensibility/plugins/manager.ts +221 -90
  267. package/src/extensibility/shared-events.ts +1 -1
  268. package/src/extensibility/skills.ts +101 -5
  269. package/src/goals/guided-setup.ts +133 -0
  270. package/src/goals/state.ts +1 -1
  271. package/src/goals/tools/goal-tool.ts +1 -1
  272. package/src/hindsight/transcript.ts +1 -1
  273. package/src/index.ts +5 -0
  274. package/src/internal-urls/docs-index.generated.ts +13 -10
  275. package/src/internal-urls/history-protocol.ts +1 -1
  276. package/src/internal-urls/local-protocol.ts +29 -7
  277. package/src/lsp/types.ts +1 -1
  278. package/src/main.ts +27 -32
  279. package/src/mcp/config-writer.ts +7 -3
  280. package/src/mcp/manager.ts +11 -0
  281. package/src/mcp/startup-events.ts +21 -0
  282. package/src/mcp/transports/stdio.ts +2 -1
  283. package/src/memories/index.ts +149 -12
  284. package/src/memories/storage.ts +2 -1
  285. package/src/memory-backend/local-backend.ts +11 -5
  286. package/src/mnemopi/backend.ts +1 -0
  287. package/src/mnemopi/config.ts +112 -12
  288. package/src/modes/acp/acp-agent.ts +8 -53
  289. package/src/modes/acp/acp-event-mapper.ts +5 -1
  290. package/src/modes/components/agent-hub.ts +51 -5
  291. package/src/modes/components/assistant-message.ts +12 -44
  292. package/src/modes/components/compaction-summary-message.ts +125 -26
  293. package/src/modes/components/custom-editor.test.ts +96 -0
  294. package/src/modes/components/custom-editor.ts +164 -8
  295. package/src/modes/components/index.ts +1 -0
  296. package/src/modes/components/logout-account-selector.ts +130 -0
  297. package/src/modes/components/mcp-add-wizard.ts +1 -1
  298. package/src/modes/components/model-selector.ts +2 -2
  299. package/src/modes/components/session-selector.ts +1 -1
  300. package/src/modes/components/settings-defs.ts +7 -0
  301. package/src/modes/components/status-line/component.ts +54 -157
  302. package/src/modes/components/status-line/segments.ts +1 -1
  303. package/src/modes/components/status-line/types.ts +2 -1
  304. package/src/modes/components/tool-execution.ts +82 -43
  305. package/src/modes/components/transcript-container.ts +70 -1
  306. package/src/modes/components/tree-selector.ts +1 -1
  307. package/src/modes/components/usage-row.ts +18 -0
  308. package/src/modes/components/user-message.ts +4 -2
  309. package/src/modes/controllers/command-controller.ts +14 -16
  310. package/src/modes/controllers/event-controller.ts +101 -73
  311. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  312. package/src/modes/controllers/input-controller.ts +311 -57
  313. package/src/modes/controllers/mcp-command-controller.ts +44 -3
  314. package/src/modes/controllers/selector-controller.ts +68 -12
  315. package/src/modes/controllers/streaming-reveal.ts +4 -3
  316. package/src/modes/gradient-highlight.ts +21 -9
  317. package/src/modes/image-references.ts +20 -0
  318. package/src/modes/interactive-mode.ts +288 -48
  319. package/src/modes/magic-keywords.ts +27 -5
  320. package/src/modes/rpc/rpc-mode.ts +146 -14
  321. package/src/modes/rpc/rpc-subagents.ts +2 -2
  322. package/src/modes/rpc/rpc-types.ts +8 -2
  323. package/src/modes/runtime-init.ts +28 -3
  324. package/src/modes/theme/theme.ts +99 -51
  325. package/src/modes/types.ts +6 -7
  326. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  327. package/src/modes/utils/ui-helpers.ts +36 -7
  328. package/src/priority.json +5 -1
  329. package/src/prompts/agents/task.md +1 -0
  330. package/src/prompts/goals/guided-goal-interview.md +8 -0
  331. package/src/prompts/goals/guided-goal-system.md +12 -0
  332. package/src/prompts/memories/read-path.md +6 -0
  333. package/src/prompts/system/autolearn-guidance-learn.md +1 -0
  334. package/src/prompts/system/autolearn-guidance.md +7 -0
  335. package/src/prompts/system/autolearn-nudge.md +3 -0
  336. package/src/prompts/system/eager-task.md +7 -0
  337. package/src/prompts/system/eager-todo.md +11 -6
  338. package/src/prompts/system/empty-stop-retry.md +4 -6
  339. package/src/prompts/system/subagent-system-prompt.md +4 -0
  340. package/src/prompts/system/system-prompt.md +10 -5
  341. package/src/prompts/system/title-marker-instruction.md +1 -0
  342. package/src/prompts/system/title-system-marker.md +16 -0
  343. package/src/prompts/tools/job.md +1 -0
  344. package/src/prompts/tools/learn.md +7 -0
  345. package/src/prompts/tools/manage-skill.md +9 -0
  346. package/src/prompts/tools/task.md +3 -0
  347. package/src/registry/agent-registry.ts +30 -0
  348. package/src/sdk.ts +103 -43
  349. package/src/secrets/obfuscator.ts +1 -1
  350. package/src/session/agent-session.ts +331 -318
  351. package/src/session/agent-storage.ts +18 -9
  352. package/src/session/history-storage.ts +3 -2
  353. package/src/session/indexed-session-storage.ts +7 -10
  354. package/src/session/messages.ts +9 -11
  355. package/src/session/session-context.ts +352 -0
  356. package/src/session/session-dump-format.ts +4 -2
  357. package/src/session/session-entries.ts +194 -0
  358. package/src/session/session-listing.ts +588 -0
  359. package/src/session/session-loader.ts +106 -0
  360. package/src/session/session-manager.ts +968 -3064
  361. package/src/session/session-migrations.ts +78 -0
  362. package/src/session/session-paths.ts +193 -0
  363. package/src/session/session-persistence.ts +131 -0
  364. package/src/session/session-storage.ts +91 -30
  365. package/src/session/snapcompact-inline.ts +21 -1
  366. package/src/session/snapcompact-savings-journal.ts +113 -0
  367. package/src/session/tool-choice-queue.ts +23 -11
  368. package/src/slash-commands/builtin-registry.ts +40 -4
  369. package/src/slash-commands/helpers/logout.ts +88 -0
  370. package/src/stt/asr-client.ts +520 -0
  371. package/src/stt/asr-protocol.ts +65 -0
  372. package/src/stt/asr-worker.ts +790 -0
  373. package/src/stt/downloader.ts +107 -47
  374. package/src/stt/endpointer.ts +259 -0
  375. package/src/stt/index.ts +5 -1
  376. package/src/stt/models.ts +150 -0
  377. package/src/stt/recorder.ts +247 -60
  378. package/src/stt/stt-controller.ts +201 -22
  379. package/src/stt/transcriber.ts +37 -68
  380. package/src/stt/wav.ts +173 -0
  381. package/src/system-prompt.ts +8 -0
  382. package/src/task/agents.ts +1 -2
  383. package/src/task/executor.ts +49 -15
  384. package/src/task/index.ts +60 -6
  385. package/src/task/render.ts +83 -8
  386. package/src/task/types.ts +54 -1
  387. package/src/tools/ask.ts +9 -1
  388. package/src/tools/ast-edit.ts +1 -1
  389. package/src/tools/ast-grep.ts +1 -1
  390. package/src/tools/bash.ts +5 -4
  391. package/src/tools/browser/cmux/cmux-tab.ts +1264 -0
  392. package/src/tools/browser/cmux/rpc.ts +156 -0
  393. package/src/tools/browser/cmux/socket-client.ts +309 -0
  394. package/src/tools/browser/registry.ts +37 -3
  395. package/src/tools/browser/render.ts +6 -1
  396. package/src/tools/browser/tab-protocol.ts +2 -0
  397. package/src/tools/browser/tab-supervisor.ts +189 -18
  398. package/src/tools/browser/tab-worker.ts +1 -1
  399. package/src/tools/browser.ts +16 -1
  400. package/src/tools/checkpoint.ts +1 -1
  401. package/src/tools/debug.ts +1 -1
  402. package/src/tools/eval-render.ts +4 -3
  403. package/src/tools/eval.ts +11 -6
  404. package/src/tools/fetch.ts +13 -2
  405. package/src/tools/find.ts +1 -1
  406. package/src/tools/gh.ts +1 -1
  407. package/src/tools/github-cache.ts +2 -1
  408. package/src/tools/image-gen.ts +1 -1
  409. package/src/tools/index.ts +43 -5
  410. package/src/tools/inspect-image.ts +3 -1
  411. package/src/tools/irc.ts +11 -3
  412. package/src/tools/job.ts +15 -3
  413. package/src/tools/learn.ts +144 -0
  414. package/src/tools/manage-skill.ts +104 -0
  415. package/src/tools/memory-edit.ts +1 -1
  416. package/src/tools/memory-recall.ts +1 -1
  417. package/src/tools/memory-reflect.ts +1 -1
  418. package/src/tools/memory-retain.ts +1 -1
  419. package/src/tools/plan-mode-guard.ts +53 -19
  420. package/src/tools/read.ts +8 -2
  421. package/src/tools/render-mermaid.ts +1 -1
  422. package/src/tools/renderers.ts +7 -11
  423. package/src/tools/report-tool-issue.ts +3 -2
  424. package/src/tools/resolve.ts +1 -1
  425. package/src/tools/review.ts +1 -1
  426. package/src/tools/search-tool-bm25.ts +1 -1
  427. package/src/tools/search.ts +1 -1
  428. package/src/tools/ssh.ts +5 -4
  429. package/src/tools/todo.ts +2 -2
  430. package/src/tools/tts.ts +204 -93
  431. package/src/tools/write.ts +19 -3
  432. package/src/tts/downloader.ts +64 -0
  433. package/src/tts/index.ts +8 -0
  434. package/src/tts/models.ts +137 -0
  435. package/src/tts/player.ts +137 -0
  436. package/src/tts/runtime.ts +21 -0
  437. package/src/tts/streaming-player.ts +266 -0
  438. package/src/tts/tts-client.ts +647 -0
  439. package/src/tts/tts-protocol.ts +60 -0
  440. package/src/tts/tts-worker.ts +497 -0
  441. package/src/tts/vocalizer.ts +162 -0
  442. package/src/tts/wav.ts +58 -0
  443. package/src/utils/clipboard.ts +35 -18
  444. package/src/utils/image-loading.ts +35 -4
  445. package/src/utils/thinking-display.ts +37 -0
  446. package/src/utils/title-generator.ts +48 -5
  447. package/src/utils/tool-choice.ts +16 -0
  448. package/src/utils/tools-manager.test.ts +25 -0
  449. package/src/utils/tools-manager.ts +19 -1
  450. package/src/web/scrapers/github.ts +96 -0
  451. package/src/web/search/index.ts +14 -1
  452. package/src/web/search/providers/searxng.ts +13 -1
  453. package/dist/types/cli/list-models.d.ts +0 -30
  454. package/dist/types/stt/setup.d.ts +0 -18
  455. package/src/cli/list-models.ts +0 -194
  456. package/src/stt/setup.ts +0 -52
  457. package/src/stt/transcribe.py +0 -70
@@ -0,0 +1,78 @@
1
+ import { Snowflake } from "@oh-my-pi/pi-utils";
2
+ import { type CompactionEntry, CURRENT_SESSION_VERSION, type FileEntry, type SessionHeader } from "./session-entries";
3
+
4
+ /** Generate a unique short ID (8 hex chars, collision-checked) */
5
+ export function generateId(byId: { has(id: string): boolean }): string {
6
+ for (let i = 0; i < 100; i++) {
7
+ const id = crypto.randomUUID().slice(-8);
8
+ if (!byId.has(id)) return id;
9
+ }
10
+ return Snowflake.next(); // fallback to full snowflake id
11
+ }
12
+
13
+ /** Migrate v1 → v2: add id/parentId tree structure. Mutates in place. */
14
+ function migrateV1ToV2(entries: FileEntry[]): void {
15
+ const ids = new Set<string>();
16
+ let prevId: string | null = null;
17
+
18
+ for (const entry of entries) {
19
+ if (entry.type === "session") {
20
+ entry.version = 2;
21
+ continue;
22
+ }
23
+
24
+ entry.id = generateId(ids);
25
+ entry.parentId = prevId;
26
+ prevId = entry.id;
27
+
28
+ // Convert firstKeptEntryIndex to firstKeptEntryId for compaction
29
+ if (entry.type === "compaction") {
30
+ const comp = entry as CompactionEntry & { firstKeptEntryIndex?: number };
31
+ if (typeof comp.firstKeptEntryIndex === "number") {
32
+ const targetEntry = entries[comp.firstKeptEntryIndex];
33
+ if (targetEntry && targetEntry.type !== "session") {
34
+ comp.firstKeptEntryId = targetEntry.id;
35
+ }
36
+ delete comp.firstKeptEntryIndex;
37
+ }
38
+ }
39
+ }
40
+ }
41
+
42
+ /** Migrate v2 → v3: rename hookMessage role to custom. Mutates in place. */
43
+ function migrateV2ToV3(entries: FileEntry[]): void {
44
+ for (const entry of entries) {
45
+ if (entry.type === "session") {
46
+ entry.version = 3;
47
+ continue;
48
+ }
49
+
50
+ if (entry.type === "message") {
51
+ const msg = entry.message as { role?: string };
52
+ if (msg.role === "hookMessage") {
53
+ (entry.message as { role: string }).role = "custom";
54
+ }
55
+ }
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Run all necessary migrations to bring entries to current version.
61
+ * Mutates entries in place. Returns true if any migration was applied.
62
+ */
63
+ export function migrateToCurrentVersion(entries: FileEntry[]): boolean {
64
+ const header = entries.find(e => e.type === "session") as SessionHeader | undefined;
65
+ const version = header?.version ?? 1;
66
+
67
+ if (version >= CURRENT_SESSION_VERSION) return false;
68
+
69
+ if (version < 2) migrateV1ToV2(entries);
70
+ if (version < 3) migrateV2ToV3(entries);
71
+
72
+ return true;
73
+ }
74
+
75
+ /** Exported for testing */
76
+ export function migrateSessionEntries(entries: FileEntry[]): void {
77
+ migrateToCurrentVersion(entries);
78
+ }
@@ -0,0 +1,193 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { getTerminalId } from "@oh-my-pi/pi-tui";
5
+ import { getSessionsDir, getTerminalSessionsDir, isEnoent, logger, resolveEquivalentPath } from "@oh-my-pi/pi-utils";
6
+ import type { SessionStorage } from "./session-storage";
7
+
8
+ const migratedSessionRoots = new Set<string>();
9
+
10
+ /**
11
+ * Merge or rename a legacy session directory into its canonical target.
12
+ * Best effort: callers decide whether migration failures should surface.
13
+ */
14
+ function migrateSessionDirPath(oldPath: string, newPath: string): void {
15
+ const existing = fs.statSync(newPath, { throwIfNoEntry: false });
16
+ if (existing?.isDirectory()) {
17
+ for (const file of fs.readdirSync(oldPath)) {
18
+ const src = path.join(oldPath, file);
19
+ const dst = path.join(newPath, file);
20
+ if (!fs.existsSync(dst)) {
21
+ fs.renameSync(src, dst);
22
+ }
23
+ }
24
+ fs.rmSync(oldPath, { recursive: true, force: true });
25
+ return;
26
+ }
27
+ if (existing) {
28
+ fs.rmSync(newPath, { recursive: true, force: true });
29
+ }
30
+ fs.renameSync(oldPath, newPath);
31
+ }
32
+
33
+ function encodeLegacyAbsoluteSessionDirName(cwd: string): string {
34
+ const resolvedCwd = path.resolve(cwd);
35
+ return `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
36
+ }
37
+
38
+ function encodeRelativeSessionDirName(prefix: string, relative: string): string {
39
+ const encoded = relative.replace(/[/\\:]/g, "-");
40
+ return encoded ? (prefix.endsWith("-") ? `${prefix}${encoded}` : `${prefix}-${encoded}`) : prefix;
41
+ }
42
+
43
+ function getDefaultSessionDirName(cwd: string): { encodedDirName: string; resolvedCwd: string } {
44
+ const resolvedCwd = path.resolve(cwd);
45
+ const canonicalCwd = resolveEquivalentPath(resolvedCwd);
46
+ const home = os.homedir();
47
+ const canonicalHome = resolveEquivalentPath(home);
48
+ const tempRoot = os.tmpdir();
49
+ const canonicalTempRoot = resolveEquivalentPath(tempRoot);
50
+ const homeRelative = path.relative(canonicalHome, canonicalCwd);
51
+ 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
+
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);
68
+
69
+ const home = os.homedir();
70
+ const homeEncoded = home.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-");
71
+ const oldPrefix = `--${homeEncoded}-`;
72
+ const oldExact = `--${homeEncoded}--`;
73
+
74
+ let entries: string[];
75
+ try {
76
+ entries = fs.readdirSync(sessionsRoot);
77
+ } catch {
78
+ return;
79
+ }
80
+
81
+ 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);
87
+ } 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
99
+ }
100
+ }
101
+ }
102
+
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
111
+ }
112
+ }
113
+
114
+ export function resolveManagedSessionRoot(sessionDir: string, cwd: string): string | undefined {
115
+ const currentDirName = path.basename(sessionDir);
116
+ const { encodedDirName } = getDefaultSessionDirName(cwd);
117
+ if (currentDirName !== encodedDirName && currentDirName !== encodeLegacyAbsoluteSessionDirName(cwd)) {
118
+ return undefined;
119
+ }
120
+ return path.dirname(sessionDir);
121
+ }
122
+
123
+ /**
124
+ * Compute the default session directory for a cwd.
125
+ * Classifies cwd by canonical location so symlink/alias paths resolve to the
126
+ * same home-relative or temp-root directory names as their real targets.
127
+ */
128
+ export function computeDefaultSessionDir(
129
+ cwd: string,
130
+ storage: SessionStorage,
131
+ sessionsRoot: string = getSessionsDir(),
132
+ ): string {
133
+ const { encodedDirName, resolvedCwd } = getDefaultSessionDirName(cwd);
134
+ migrateHomeSessionDirs(sessionsRoot);
135
+ const sessionDir = path.join(sessionsRoot, encodedDirName);
136
+ migrateLegacyAbsoluteSessionDir(resolvedCwd, sessionDir, sessionsRoot);
137
+ storage.ensureDirSync(sessionDir);
138
+ return sessionDir;
139
+ }
140
+
141
+ // =============================================================================
142
+ // Terminal breadcrumbs: maps terminal (TTY) -> last session file for --continue
143
+ // =============================================================================
144
+
145
+ /**
146
+ * Write a breadcrumb linking the current terminal to a session file.
147
+ * The breadcrumb contains the cwd and session path so --continue can
148
+ * find "this terminal's last session" even when running concurrent instances.
149
+ */
150
+ export function writeTerminalBreadcrumb(cwd: string, sessionFile: string): void {
151
+ const terminalId = getTerminalId();
152
+ if (!terminalId) return;
153
+
154
+ const breadcrumbDir = getTerminalSessionsDir();
155
+ const breadcrumbFile = path.join(breadcrumbDir, terminalId);
156
+ const content = `${cwd}\n${sessionFile}\n`;
157
+ // Best-effort — don't break session creation if breadcrumb fails
158
+ Bun.write(breadcrumbFile, content).catch(() => {});
159
+ }
160
+
161
+ export interface TerminalBreadcrumb {
162
+ cwd: string;
163
+ sessionFile: string;
164
+ }
165
+
166
+ /**
167
+ * Read the raw terminal breadcrumb for the current terminal.
168
+ * Returns the recorded cwd + session file (verified to exist) regardless of
169
+ * whether the recorded cwd still matches the current one. Callers decide how
170
+ * to interpret a cwd mismatch (e.g. a moved/renamed worktree).
171
+ */
172
+ export async function readTerminalBreadcrumbEntry(): Promise<TerminalBreadcrumb | null> {
173
+ const terminalId = getTerminalId();
174
+ if (!terminalId) return null;
175
+
176
+ try {
177
+ const breadcrumbFile = path.join(getTerminalSessionsDir(), terminalId);
178
+ const content = await Bun.file(breadcrumbFile).text();
179
+ const lines = content.trim().split("\n");
180
+ if (lines.length < 2) return null;
181
+
182
+ const breadcrumbCwd = lines[0];
183
+ const sessionFile = lines[1];
184
+
185
+ // Verify the session file still exists
186
+ const stat = fs.statSync(sessionFile, { throwIfNoEntry: false });
187
+ if (stat?.isFile()) return { cwd: breadcrumbCwd, sessionFile };
188
+ } catch (err) {
189
+ if (!isEnoent(err)) logger.debug("Terminal breadcrumb read failed", { err });
190
+ // Breadcrumb doesn't exist or is corrupt — fall through
191
+ }
192
+ return null;
193
+ }
@@ -0,0 +1,131 @@
1
+ import {
2
+ type BlobStore,
3
+ externalizeImageDataSync,
4
+ externalizeImageDataUrlSync,
5
+ isBlobRef,
6
+ isImageDataUrl,
7
+ } from "./blob-store";
8
+ import type { FileEntry } from "./session-entries";
9
+
10
+ const MAX_PERSIST_CHARS = 500_000;
11
+ const TRUNCATION_NOTICE = "\n\n[Session persistence truncated large content]";
12
+ /** Minimum base64 length to externalize to blob store (skip tiny inline images) */
13
+ const BLOB_EXTERNALIZE_THRESHOLD = 1024;
14
+ const TEXT_CONTENT_KEY = "content";
15
+
16
+ function truncateString(value: string, maxLength: number): string {
17
+ if (value.length <= maxLength) return value;
18
+ let truncated = value.slice(0, maxLength);
19
+ if (truncated.length > 0) {
20
+ const last = truncated.charCodeAt(truncated.length - 1);
21
+ if (last >= 0xd800 && last <= 0xdbff) {
22
+ truncated = truncated.slice(0, -1);
23
+ }
24
+ }
25
+ return truncated;
26
+ }
27
+
28
+ export function isImageBlock(value: unknown): value is { type: "image"; data: string; mimeType?: string } {
29
+ return (
30
+ typeof value === "object" &&
31
+ value !== null &&
32
+ "type" in value &&
33
+ (value as { type?: string }).type === "image" &&
34
+ "data" in value &&
35
+ typeof (value as { data?: string }).data === "string"
36
+ );
37
+ }
38
+
39
+ /**
40
+ * Recursively truncate large strings in an object for session persistence.
41
+ * - Truncates any oversized string fields (key-agnostic)
42
+ * - Replaces oversized image blocks with text notices
43
+ * - Updates lineCount when content is truncated
44
+ * - Returns original object if no changes needed (structural sharing)
45
+ *
46
+ * Runs in one synchronous tick so an OOM/SIGKILL landing right after a persist
47
+ * call returns cannot lose the entry. Image externalization happens via the
48
+ * synchronous blob-store path (`fs.writeFileSync`), so blob bytes are in the
49
+ * kernel page cache before the JSONL line referencing them is written.
50
+ */
51
+ function truncateForPersistence(obj: unknown, blobStore: BlobStore, key?: string): unknown {
52
+ if (obj === null || obj === undefined) return obj;
53
+
54
+ if (typeof obj === "string") {
55
+ if (key === "image_url" && isImageDataUrl(obj)) {
56
+ return externalizeImageDataUrlSync(blobStore, obj);
57
+ }
58
+ if (obj.length > MAX_PERSIST_CHARS) {
59
+ // Cryptographic signatures must be preserved exactly or cleared entirely — never truncated.
60
+ // Truncation would produce an invalid signature that the API rejects.
61
+ if (key === "thinkingSignature" || key === "thoughtSignature" || key === "textSignature") {
62
+ return "";
63
+ }
64
+ const limit = Math.max(0, MAX_PERSIST_CHARS - TRUNCATION_NOTICE.length);
65
+ return `${truncateString(obj, limit)}${TRUNCATION_NOTICE}`;
66
+ }
67
+ return obj;
68
+ }
69
+
70
+ if (Array.isArray(obj)) {
71
+ let changed = false;
72
+ const result: unknown[] = new Array(obj.length);
73
+ for (let i = 0; i < obj.length; i++) {
74
+ const item = obj[i];
75
+ if (
76
+ key === TEXT_CONTENT_KEY &&
77
+ isImageBlock(item) &&
78
+ !isBlobRef(item.data) &&
79
+ item.data.length >= BLOB_EXTERNALIZE_THRESHOLD
80
+ ) {
81
+ changed = true;
82
+ result[i] = { ...item, data: externalizeImageDataSync(blobStore, item.data, item.mimeType) };
83
+ continue;
84
+ }
85
+ const newItem = truncateForPersistence(item, blobStore, key);
86
+ if (newItem !== item) changed = true;
87
+ result[i] = newItem;
88
+ }
89
+ return changed ? result : obj;
90
+ }
91
+
92
+ if (typeof obj === "object") {
93
+ let changed = false;
94
+ const entries: Array<readonly [string, unknown]> = [];
95
+ for (const [childKey, value] of Object.entries(obj)) {
96
+ // Strip transient/redundant properties that shouldn't be persisted.
97
+ // - partialJson: streaming accumulator for tool call JSON parsing
98
+ // - jsonlEvents: raw subprocess streaming events (already saved to artifact files)
99
+ if (childKey === "partialJson" || childKey === "jsonlEvents") {
100
+ changed = true;
101
+ continue;
102
+ }
103
+ const newValue = truncateForPersistence(value, blobStore, childKey);
104
+ if (newValue !== value) changed = true;
105
+ entries.push([childKey, newValue]);
106
+ }
107
+ if (!changed) return obj;
108
+
109
+ const contentEntry = entries.find(([childKey]) => childKey === "content");
110
+ const lineCountEntry = entries.find(([childKey]) => childKey === "lineCount");
111
+ if (
112
+ contentEntry &&
113
+ typeof contentEntry[1] === "string" &&
114
+ lineCountEntry &&
115
+ typeof lineCountEntry[1] === "number"
116
+ ) {
117
+ const content = contentEntry[1];
118
+ const updatedEntries = entries.map(([childKey, value]) =>
119
+ childKey === "lineCount" ? ([childKey, content.split("\n").length] as const) : ([childKey, value] as const),
120
+ );
121
+ return Object.fromEntries(updatedEntries);
122
+ }
123
+ return Object.fromEntries(entries);
124
+ }
125
+
126
+ return obj;
127
+ }
128
+
129
+ export function prepareEntryForPersistence(entry: FileEntry, blobStore: BlobStore): FileEntry {
130
+ return truncateForPersistence(entry, blobStore) as FileEntry;
131
+ }
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as fsp from "node:fs/promises";
3
3
  import * as path from "node:path";
4
- import { isEnoent, peekFileEnds, toError } from "@oh-my-pi/pi-utils";
4
+ import { hasFsCode, isEnoent, logger, peekFileEnds, Snowflake, toError } from "@oh-my-pi/pi-utils";
5
5
 
6
6
  const utf8Decoder = new TextDecoder("utf-8");
7
7
 
@@ -12,17 +12,17 @@ export interface SessionStorageStat {
12
12
  }
13
13
 
14
14
  export interface SessionStorageWriter {
15
- writeLine(line: string): Promise<void>;
16
15
  /**
17
- * Synchronously append a single line. Returns once the bytes are handed to the kernel
18
- * (page cache), so the data survives a non-graceful process death (OOM, SIGKILL, etc.)
19
- * even though it has not yet been fsynced to the underlying disk.
16
+ * Append one newline-terminated line. File and memory storage perform the
17
+ * write synchronously in-body; indexed backends queue in call order.
20
18
  *
21
- * `line` MUST already include the trailing newline. Throws synchronously on I/O error.
19
+ * `line` MUST include the trailing newline.
22
20
  */
23
- writeLineSync(line: string): void;
21
+ append(line: string): Promise<void>;
22
+ /** Resolve once all queued appends complete. No fsync. */
24
23
  flush(): Promise<void>;
25
- fsync(): Promise<void>;
24
+ /** False once close() has begun/finished. */
25
+ isOpen(): boolean;
26
26
  close(): Promise<void>;
27
27
  getError(): Error | undefined;
28
28
  }
@@ -39,6 +39,7 @@ export interface SessionStorage {
39
39
  /** Read the requested UTF-8 byte windows from the head and tail of the file. */
40
40
  readTextSlices(path: string, prefixBytes: number, suffixBytes: number): Promise<[string, string]>;
41
41
  writeText(path: string, content: string): Promise<void>;
42
+ writeTextAtomic(path: string, content: string): Promise<void>;
42
43
  rename(path: string, nextPath: string): Promise<void>;
43
44
  unlink(path: string): Promise<void>;
44
45
  deleteSessionWithArtifacts(sessionPath: string): Promise<void>;
@@ -81,7 +82,7 @@ class FileSessionStorageWriter implements SessionStorageWriter {
81
82
  return error;
82
83
  }
83
84
 
84
- writeLineSync(line: string): void {
85
+ async append(line: string): Promise<void> {
85
86
  if (this.#closed) throw new Error("Writer closed");
86
87
  if (this.#error) throw this.#error;
87
88
  try {
@@ -99,23 +100,12 @@ class FileSessionStorageWriter implements SessionStorageWriter {
99
100
  }
100
101
  }
101
102
 
102
- async writeLine(line: string): Promise<void> {
103
- this.writeLineSync(line);
104
- }
105
-
106
103
  async flush(): Promise<void> {
107
104
  if (this.#error) throw this.#error;
108
- // OS buffers are flushed on fsync, nothing to do here
109
105
  }
110
106
 
111
- async fsync(): Promise<void> {
112
- if (this.#closed) throw new Error("Writer closed");
113
- if (this.#error) throw this.#error;
114
- try {
115
- fs.fsyncSync(this.#fd);
116
- } catch (err) {
117
- throw this.#recordError(err);
118
- }
107
+ isOpen(): boolean {
108
+ return !this.#closed;
119
109
  }
120
110
 
121
111
  async close(): Promise<void> {
@@ -189,6 +179,77 @@ export class FileSessionStorage implements SessionStorage {
189
179
  await Bun.write(path, content, { createPath: true });
190
180
  }
191
181
 
182
+ async writeTextAtomic(fpath: string, content: string): Promise<void> {
183
+ const dir = path.resolve(fpath, "..");
184
+ const tempPath = path.join(dir, `.${path.basename(fpath)}.${Snowflake.next()}.tmp`);
185
+ await fs.promises.mkdir(dir, { recursive: true });
186
+ try {
187
+ await fs.promises.writeFile(tempPath, content);
188
+ try {
189
+ await this.rename(tempPath, fpath);
190
+ return;
191
+ } catch (err) {
192
+ if (!hasFsCode(err, "EPERM")) throw toError(err);
193
+ await this.#replaceSessionFileAfterEperm(tempPath, fpath, err);
194
+ return;
195
+ }
196
+ } catch (err) {
197
+ try {
198
+ await this.unlink(tempPath);
199
+ } catch (cleanupErr) {
200
+ if (!isEnoent(cleanupErr)) {
201
+ logger.warn("Failed to remove session rewrite temp file", {
202
+ sessionFile: fpath,
203
+ tempPath,
204
+ error: toError(cleanupErr).message,
205
+ });
206
+ }
207
+ }
208
+ throw toError(err);
209
+ }
210
+ }
211
+
212
+ async #replaceSessionFileAfterEperm(tempPath: string, targetPath: string, renameError: unknown): Promise<void> {
213
+ const dir = path.resolve(targetPath, "..");
214
+ const backupPath = path.join(dir, `${path.basename(targetPath)}.${Snowflake.next()}.bak`);
215
+ try {
216
+ await this.rename(targetPath, backupPath);
217
+ } catch (moveAsideError) {
218
+ if (isEnoent(moveAsideError)) {
219
+ await this.rename(tempPath, targetPath);
220
+ return;
221
+ }
222
+ throw toError(renameError);
223
+ }
224
+ try {
225
+ await this.rename(tempPath, targetPath);
226
+ } catch (replaceError) {
227
+ try {
228
+ await this.rename(backupPath, targetPath);
229
+ } catch (rollbackErr) {
230
+ const rollbackError = toError(rollbackErr);
231
+ throw new Error(
232
+ `Failed to replace session file after EPERM (original: ${toError(renameError).message}; retry: ${
233
+ toError(replaceError).message
234
+ }; rollback: ${rollbackError.message})`,
235
+ { cause: toError(renameError) },
236
+ );
237
+ }
238
+ throw toError(replaceError);
239
+ }
240
+ try {
241
+ await this.unlink(backupPath);
242
+ } catch (err) {
243
+ if (!isEnoent(err)) {
244
+ logger.warn("Failed to remove session rewrite backup", {
245
+ sessionFile: targetPath,
246
+ backupPath,
247
+ error: toError(err).message,
248
+ });
249
+ }
250
+ }
251
+ }
252
+
192
253
  async rename(path: string, nextPath: string): Promise<void> {
193
254
  try {
194
255
  await fs.promises.rename(path, nextPath);
@@ -267,7 +328,7 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
267
328
  return error;
268
329
  }
269
330
 
270
- writeLineSync(line: string): void {
331
+ async append(line: string): Promise<void> {
271
332
  if (this.#closed) throw new Error("Writer closed");
272
333
  if (this.#error) throw this.#error;
273
334
  try {
@@ -278,17 +339,12 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
278
339
  }
279
340
  }
280
341
 
281
- async writeLine(line: string): Promise<void> {
282
- this.writeLineSync(line);
283
- }
284
-
285
342
  async flush(): Promise<void> {
286
343
  if (this.#error) throw this.#error;
287
344
  }
288
345
 
289
- async fsync(): Promise<void> {
290
- // No-op for in-memory storage
291
- if (this.#error) throw this.#error;
346
+ isOpen(): boolean {
347
+ return !this.#closed;
292
348
  }
293
349
 
294
350
  async close(): Promise<void> {
@@ -507,6 +563,11 @@ export class MemorySessionStorage implements SessionStorage {
507
563
  return Promise.resolve();
508
564
  }
509
565
 
566
+ writeTextAtomic(path: string, content: string): Promise<void> {
567
+ this.writeTextSync(path, content);
568
+ return Promise.resolve();
569
+ }
570
+
510
571
  rename(path: string, nextPath: string): Promise<void> {
511
572
  const entry = this.#files.get(path);
512
573
  if (!entry) return Promise.reject(new Error(`File not found: ${path}`));
@@ -32,6 +32,17 @@ export interface SnapcompactInlineOptions {
32
32
  shape?: snapcompact.ShapeVariantName | "auto";
33
33
  }
34
34
 
35
+ /**
36
+ * Reports the per-tool-result tokens kept off the wire when a swap is applied.
37
+ * `savedTokens` is `textTokens - frames * shape.frameTokenEstimate` for each
38
+ * imaged tool result (always > 0; the savings gate guarantees it). Wired to the
39
+ * append-only savings journal; never throws into the request path.
40
+ */
41
+ export type SnapcompactSavingsSink = (
42
+ savings: ReadonlyArray<{ toolCallId: string; savedTokens: number }>,
43
+ model: Model,
44
+ ) => void;
45
+
35
46
  // Per-provider image-count budgets live in @oh-my-pi/snapcompact
36
47
  // (`providerImageBudget`): snapcompact frames are 1568px (<2000px) so
37
48
  // dimension/size limits never bind; only COUNT does. Once the budget is
@@ -398,7 +409,10 @@ export class SnapcompactInlineTransformer {
398
409
  #toolCache = new Map<string, FrameCacheEntry>();
399
410
  #systemCache?: FrameCacheEntry;
400
411
 
401
- constructor(private readonly options: SnapcompactInlineOptions) {}
412
+ constructor(
413
+ private readonly options: SnapcompactInlineOptions,
414
+ private readonly onToolResultSavings?: SnapcompactSavingsSink,
415
+ ) {}
402
416
 
403
417
  transform(context: Context, model: Model): Context {
404
418
  // Vision gate: providers silently DROP images on text-only models —
@@ -464,13 +478,19 @@ export class SnapcompactInlineTransformer {
464
478
  });
465
479
 
466
480
  let changed = false;
481
+ const savings: Array<{ toolCallId: string; savedTokens: number }> = [];
467
482
  for (const swap of plan.toolResults) {
468
483
  const target = targets.get(swap.id);
469
484
  if (!target) continue;
470
485
  const frames = this.#framesFor(this.#toolCache, swap.id, target.text, shape);
471
486
  messages[target.index] = { ...target.message, content: [{ type: "text", text: toolResultNote }, ...frames] };
472
487
  changed = true;
488
+ savings.push({
489
+ toolCallId: swap.id,
490
+ savedTokens: Math.max(0, swap.textTokens - swap.frames * shape.frameTokenEstimate),
491
+ });
473
492
  }
493
+ if (savings.length > 0) this.onToolResultSavings?.(savings, model);
474
494
  if (this.options.renderToolResults) {
475
495
  // Drop cache entries for tool calls no longer in the context
476
496
  // (compacted away) so the cache stays bounded by live history.