@kr78/pi-coding-agent 17.2.10

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 (1593) hide show
  1. package/CHANGELOG.md +14861 -0
  2. package/README.md +35 -0
  3. package/dist/CHANGELOG-chtdxh1z.md +14861 -0
  4. package/dist/cli.js +20483 -0
  5. package/dist/template-c2hyaytt.js +1653 -0
  6. package/dist/template-f8wx9vfn.css +1355 -0
  7. package/dist/template-qat058wr.html +55 -0
  8. package/dist/tool-views.generated-jdfmzwmn.js +35 -0
  9. package/examples/README.md +21 -0
  10. package/examples/custom-tools/README.md +104 -0
  11. package/examples/custom-tools/hello/index.ts +20 -0
  12. package/examples/extensions/README.md +142 -0
  13. package/examples/extensions/api-demo.ts +78 -0
  14. package/examples/extensions/chalk-logger.ts +25 -0
  15. package/examples/extensions/hello.ts +31 -0
  16. package/examples/extensions/pirate.ts +43 -0
  17. package/examples/extensions/plan-mode.ts +549 -0
  18. package/examples/extensions/reload-runtime.ts +38 -0
  19. package/examples/extensions/thinking-note.ts +13 -0
  20. package/examples/extensions/tools.ts +145 -0
  21. package/examples/extensions/with-deps/index.ts +36 -0
  22. package/examples/extensions/with-deps/package-lock.json +31 -0
  23. package/examples/extensions/with-deps/package.json +17 -0
  24. package/examples/hooks/README.md +56 -0
  25. package/examples/hooks/auto-commit-on-exit.ts +48 -0
  26. package/examples/hooks/confirm-destructive.ts +58 -0
  27. package/examples/hooks/custom-compaction.ts +115 -0
  28. package/examples/hooks/dirty-repo-guard.ts +51 -0
  29. package/examples/hooks/file-trigger.ts +40 -0
  30. package/examples/hooks/git-checkpoint.ts +52 -0
  31. package/examples/hooks/handoff.ts +149 -0
  32. package/examples/hooks/permission-gate.ts +33 -0
  33. package/examples/hooks/protected-paths.ts +29 -0
  34. package/examples/hooks/qna.ts +118 -0
  35. package/examples/hooks/status-line.ts +39 -0
  36. package/examples/sdk/01-minimal.ts +21 -0
  37. package/examples/sdk/02-custom-model.ts +49 -0
  38. package/examples/sdk/03-custom-prompt.ts +46 -0
  39. package/examples/sdk/04-skills.ts +43 -0
  40. package/examples/sdk/06-extensions.ts +82 -0
  41. package/examples/sdk/06-hooks.ts +61 -0
  42. package/examples/sdk/07-context-files.ts +35 -0
  43. package/examples/sdk/08-prompt-templates.ts +41 -0
  44. package/examples/sdk/08-slash-commands.ts +46 -0
  45. package/examples/sdk/09-api-keys-and-oauth.ts +54 -0
  46. package/examples/sdk/11-sessions.ts +47 -0
  47. package/examples/sdk/12-redis-sessions.ts +54 -0
  48. package/examples/sdk/13-sql-sessions.ts +61 -0
  49. package/examples/sdk/README.md +169 -0
  50. package/package.json +587 -0
  51. package/scripts/bench-guard.ts +71 -0
  52. package/scripts/bench-title-models.ts +332 -0
  53. package/scripts/build-binary.ts +117 -0
  54. package/scripts/bundle-dist.ts +123 -0
  55. package/scripts/compile-binary.ts +69 -0
  56. package/scripts/embed-mupdf-wasm.ts +67 -0
  57. package/scripts/format-prompts.ts +68 -0
  58. package/scripts/generate-aria-snapshot.ts +134 -0
  59. package/scripts/generate-docs-index.ts +58 -0
  60. package/scripts/generate-share-viewer.ts +34 -0
  61. package/scripts/legacy-pi-virtual-module.ts +205 -0
  62. package/scripts/measure-prompt-tokens.ts +63 -0
  63. package/scripts/omp +51 -0
  64. package/scripts/omp.ts +19 -0
  65. package/scripts/security-compare.ts +40 -0
  66. package/src/advisor/advise-tool.ts +234 -0
  67. package/src/advisor/config.ts +341 -0
  68. package/src/advisor/emission-guard.ts +172 -0
  69. package/src/advisor/index.ts +6 -0
  70. package/src/advisor/runtime.ts +1412 -0
  71. package/src/advisor/transcript-recorder.ts +215 -0
  72. package/src/advisor/watchdog.ts +135 -0
  73. package/src/async/index.ts +1 -0
  74. package/src/async/job-manager.ts +847 -0
  75. package/src/auto-thinking/classifier.ts +220 -0
  76. package/src/autolearn/controller.ts +152 -0
  77. package/src/autolearn/managed-skills.ts +255 -0
  78. package/src/autoresearch/command-resume.md +14 -0
  79. package/src/autoresearch/dashboard.ts +436 -0
  80. package/src/autoresearch/git.ts +331 -0
  81. package/src/autoresearch/helpers.ts +218 -0
  82. package/src/autoresearch/index.ts +541 -0
  83. package/src/autoresearch/prompt-setup.md +43 -0
  84. package/src/autoresearch/prompt.md +103 -0
  85. package/src/autoresearch/resume-message.md +10 -0
  86. package/src/autoresearch/state.ts +273 -0
  87. package/src/autoresearch/storage.ts +700 -0
  88. package/src/autoresearch/tools/init-experiment.ts +268 -0
  89. package/src/autoresearch/tools/log-experiment.ts +520 -0
  90. package/src/autoresearch/tools/run-experiment.ts +407 -0
  91. package/src/autoresearch/tools/update-notes.ts +109 -0
  92. package/src/autoresearch/types.ts +168 -0
  93. package/src/capability/context-file.ts +44 -0
  94. package/src/capability/extension-module.ts +34 -0
  95. package/src/capability/extension.ts +47 -0
  96. package/src/capability/fs.ts +117 -0
  97. package/src/capability/hook.ts +40 -0
  98. package/src/capability/index.ts +467 -0
  99. package/src/capability/instruction.ts +37 -0
  100. package/src/capability/mcp.ts +115 -0
  101. package/src/capability/prompt.ts +35 -0
  102. package/src/capability/rule-buckets.ts +66 -0
  103. package/src/capability/rule.ts +298 -0
  104. package/src/capability/settings.ts +34 -0
  105. package/src/capability/skill.ts +69 -0
  106. package/src/capability/slash-command.ts +40 -0
  107. package/src/capability/ssh.ts +41 -0
  108. package/src/capability/system-prompt.ts +34 -0
  109. package/src/capability/tool.ts +38 -0
  110. package/src/capability/types.ts +187 -0
  111. package/src/cleanse/agent.ts +226 -0
  112. package/src/cleanse/balance.ts +79 -0
  113. package/src/cleanse/checkers.ts +996 -0
  114. package/src/cleanse/index.ts +190 -0
  115. package/src/cleanse/loop.ts +51 -0
  116. package/src/cleanse/parsers.ts +726 -0
  117. package/src/cleanse/progress.ts +50 -0
  118. package/src/cleanse/prompts/assignment.md +47 -0
  119. package/src/cleanse/types.ts +72 -0
  120. package/src/cli/agents-cli.ts +138 -0
  121. package/src/cli/args.ts +368 -0
  122. package/src/cli/auth-broker-cli.ts +940 -0
  123. package/src/cli/auth-gateway-cli.ts +674 -0
  124. package/src/cli/bench-cli.ts +990 -0
  125. package/src/cli/browser-relay-cli.ts +119 -0
  126. package/src/cli/classify-install-target.ts +76 -0
  127. package/src/cli/claude-trace-cli.ts +795 -0
  128. package/src/cli/command-help.ts +107 -0
  129. package/src/cli/commands/init-xdg.ts +27 -0
  130. package/src/cli/completion-gen.ts +550 -0
  131. package/src/cli/config-cli.ts +459 -0
  132. package/src/cli/dry-balance-cli.ts +864 -0
  133. package/src/cli/extension-flags.ts +48 -0
  134. package/src/cli/file-processor.ts +132 -0
  135. package/src/cli/flag-tables.ts +367 -0
  136. package/src/cli/gallery-cli.ts +272 -0
  137. package/src/cli/gallery-fixtures/agentic.ts +420 -0
  138. package/src/cli/gallery-fixtures/codeintel.ts +187 -0
  139. package/src/cli/gallery-fixtures/edit.ts +254 -0
  140. package/src/cli/gallery-fixtures/fs.ts +245 -0
  141. package/src/cli/gallery-fixtures/index.ts +40 -0
  142. package/src/cli/gallery-fixtures/interaction.ts +46 -0
  143. package/src/cli/gallery-fixtures/memory.ts +81 -0
  144. package/src/cli/gallery-fixtures/misc.ts +177 -0
  145. package/src/cli/gallery-fixtures/search.ts +135 -0
  146. package/src/cli/gallery-fixtures/shell.ts +241 -0
  147. package/src/cli/gallery-fixtures/types.ts +57 -0
  148. package/src/cli/gallery-fixtures/web.ts +158 -0
  149. package/src/cli/gallery-screenshot.ts +279 -0
  150. package/src/cli/gc-cli.ts +1566 -0
  151. package/src/cli/grep-cli.ts +161 -0
  152. package/src/cli/grievances-cli.ts +256 -0
  153. package/src/cli/help-extra.ts +89 -0
  154. package/src/cli/initial-message.ts +58 -0
  155. package/src/cli/models-cli.ts +385 -0
  156. package/src/cli/plugin-cli.ts +996 -0
  157. package/src/cli/profile-alias.ts +369 -0
  158. package/src/cli/profile-bootstrap.ts +233 -0
  159. package/src/cli/read-cli.ts +99 -0
  160. package/src/cli/session-picker.ts +110 -0
  161. package/src/cli/setup-cli.ts +312 -0
  162. package/src/cli/setup-model-picker.ts +43 -0
  163. package/src/cli/shell-cli.ts +176 -0
  164. package/src/cli/ssh-cli.ts +179 -0
  165. package/src/cli/startup-cwd.ts +58 -0
  166. package/src/cli/stats-cli.ts +229 -0
  167. package/src/cli/thinking-levels.ts +7 -0
  168. package/src/cli/tiny-models-cli.ts +153 -0
  169. package/src/cli/ttsr-cli.ts +1013 -0
  170. package/src/cli/update-cli.ts +1184 -0
  171. package/src/cli/usage-cli.ts +1084 -0
  172. package/src/cli/usage-error.ts +7 -0
  173. package/src/cli/web-search-cli.ts +144 -0
  174. package/src/cli/worktree-cli.ts +311 -0
  175. package/src/cli-commands.ts +308 -0
  176. package/src/cli.ts +428 -0
  177. package/src/collab/crypto.ts +63 -0
  178. package/src/collab/display-name.ts +13 -0
  179. package/src/collab/guest.ts +764 -0
  180. package/src/collab/host.ts +692 -0
  181. package/src/collab/protocol.ts +296 -0
  182. package/src/collab/relay-client.ts +282 -0
  183. package/src/collab/replication-shrink.ts +111 -0
  184. package/src/commands/acp.ts +35 -0
  185. package/src/commands/agents.ts +58 -0
  186. package/src/commands/auth-broker.ts +100 -0
  187. package/src/commands/auth-gateway.ts +70 -0
  188. package/src/commands/bench.ts +64 -0
  189. package/src/commands/browser-relay.ts +53 -0
  190. package/src/commands/cleanse.ts +45 -0
  191. package/src/commands/commit.ts +47 -0
  192. package/src/commands/complete.ts +67 -0
  193. package/src/commands/completions.ts +61 -0
  194. package/src/commands/config.ts +52 -0
  195. package/src/commands/dry-balance.ts +43 -0
  196. package/src/commands/gallery.ts +61 -0
  197. package/src/commands/gc.ts +47 -0
  198. package/src/commands/grep.ts +49 -0
  199. package/src/commands/grievances.ts +52 -0
  200. package/src/commands/install.ts +107 -0
  201. package/src/commands/join.ts +40 -0
  202. package/src/commands/launch-help.ts +116 -0
  203. package/src/commands/launch.ts +34 -0
  204. package/src/commands/models.ts +61 -0
  205. package/src/commands/plugin.ts +79 -0
  206. package/src/commands/read.ts +39 -0
  207. package/src/commands/say.ts +146 -0
  208. package/src/commands/setup.ts +68 -0
  209. package/src/commands/share.ts +71 -0
  210. package/src/commands/shell.ts +30 -0
  211. package/src/commands/ssh.ts +61 -0
  212. package/src/commands/stats.ts +30 -0
  213. package/src/commands/tiny-models.ts +36 -0
  214. package/src/commands/token.ts +164 -0
  215. package/src/commands/ttsr.ts +125 -0
  216. package/src/commands/update.ts +34 -0
  217. package/src/commands/usage.ts +55 -0
  218. package/src/commands/web-search.ts +43 -0
  219. package/src/commands/worktree.ts +63 -0
  220. package/src/commit/agentic/agent.ts +322 -0
  221. package/src/commit/agentic/fallback.ts +96 -0
  222. package/src/commit/agentic/index.ts +383 -0
  223. package/src/commit/agentic/lock-files.ts +107 -0
  224. package/src/commit/agentic/prompts/analyze-file.md +22 -0
  225. package/src/commit/agentic/prompts/session-user.md +25 -0
  226. package/src/commit/agentic/prompts/split-confirm.md +1 -0
  227. package/src/commit/agentic/prompts/system.md +38 -0
  228. package/src/commit/agentic/state.ts +60 -0
  229. package/src/commit/agentic/tools/analyze-file.ts +148 -0
  230. package/src/commit/agentic/tools/git-file-diff.ts +191 -0
  231. package/src/commit/agentic/tools/git-hunk.ts +52 -0
  232. package/src/commit/agentic/tools/git-overview.ts +62 -0
  233. package/src/commit/agentic/tools/index.ts +54 -0
  234. package/src/commit/agentic/tools/propose-changelog.ts +147 -0
  235. package/src/commit/agentic/tools/propose-commit.ts +109 -0
  236. package/src/commit/agentic/tools/recent-commits.ts +81 -0
  237. package/src/commit/agentic/tools/schemas.ts +11 -0
  238. package/src/commit/agentic/tools/split-commit.ts +241 -0
  239. package/src/commit/agentic/topo-sort.ts +44 -0
  240. package/src/commit/agentic/trivial.ts +51 -0
  241. package/src/commit/agentic/validation.ts +183 -0
  242. package/src/commit/analysis/conventional.ts +64 -0
  243. package/src/commit/analysis/index.ts +4 -0
  244. package/src/commit/analysis/scope.ts +242 -0
  245. package/src/commit/analysis/summary.ts +107 -0
  246. package/src/commit/analysis/validation.ts +66 -0
  247. package/src/commit/changelog/detect.ts +40 -0
  248. package/src/commit/changelog/generate.ts +101 -0
  249. package/src/commit/changelog/index.ts +234 -0
  250. package/src/commit/changelog/parse.ts +44 -0
  251. package/src/commit/cli.ts +85 -0
  252. package/src/commit/git/diff.ts +148 -0
  253. package/src/commit/index.ts +5 -0
  254. package/src/commit/map-reduce/index.ts +69 -0
  255. package/src/commit/map-reduce/map-phase.ts +193 -0
  256. package/src/commit/map-reduce/reduce-phase.ts +49 -0
  257. package/src/commit/map-reduce/utils.ts +9 -0
  258. package/src/commit/message.ts +11 -0
  259. package/src/commit/model-selection.ts +95 -0
  260. package/src/commit/pipeline.ts +244 -0
  261. package/src/commit/prompts/analysis-system.md +148 -0
  262. package/src/commit/prompts/analysis-user.md +38 -0
  263. package/src/commit/prompts/changelog-system.md +50 -0
  264. package/src/commit/prompts/changelog-user.md +18 -0
  265. package/src/commit/prompts/file-observer-system.md +24 -0
  266. package/src/commit/prompts/file-observer-user.md +8 -0
  267. package/src/commit/prompts/reduce-system.md +50 -0
  268. package/src/commit/prompts/reduce-user.md +17 -0
  269. package/src/commit/prompts/summary-retry.md +3 -0
  270. package/src/commit/prompts/summary-system.md +38 -0
  271. package/src/commit/prompts/summary-user.md +13 -0
  272. package/src/commit/prompts/types-description.md +2 -0
  273. package/src/commit/shared-llm.ts +70 -0
  274. package/src/commit/types.ts +118 -0
  275. package/src/commit/utils/exclusions.ts +42 -0
  276. package/src/commit/utils.ts +58 -0
  277. package/src/config/api-key-resolver.ts +81 -0
  278. package/src/config/append-only-context-mode.ts +76 -0
  279. package/src/config/config-file.ts +347 -0
  280. package/src/config/inline-tool-descriptors-mode.ts +26 -0
  281. package/src/config/keybindings.ts +703 -0
  282. package/src/config/mcp-schema.json +252 -0
  283. package/src/config/model-discovery.ts +1077 -0
  284. package/src/config/model-registry.ts +2822 -0
  285. package/src/config/model-resolver.ts +2115 -0
  286. package/src/config/model-roles.ts +113 -0
  287. package/src/config/models-config-schema-bundle.ts +315 -0
  288. package/src/config/models-config-schema.ts +14 -0
  289. package/src/config/models-config.ts +130 -0
  290. package/src/config/prompt-templates.ts +205 -0
  291. package/src/config/provider-globals.ts +25 -0
  292. package/src/config/resolve-config-value.ts +95 -0
  293. package/src/config/service-tier.ts +146 -0
  294. package/src/config/settings-schema.ts +5887 -0
  295. package/src/config/settings.ts +2394 -0
  296. package/src/config.ts +242 -0
  297. package/src/cursor-bridge-tools.ts +81 -0
  298. package/src/cursor.ts +960 -0
  299. package/src/dap/client.ts +1043 -0
  300. package/src/dap/config.ts +480 -0
  301. package/src/dap/defaults.json +212 -0
  302. package/src/dap/index.ts +4 -0
  303. package/src/dap/session.ts +1841 -0
  304. package/src/dap/types.ts +611 -0
  305. package/src/debug/index.ts +584 -0
  306. package/src/debug/log-formatting.ts +58 -0
  307. package/src/debug/log-viewer.ts +966 -0
  308. package/src/debug/profiler.ts +168 -0
  309. package/src/debug/protocol-probe.ts +267 -0
  310. package/src/debug/raw-sse-buffer.ts +421 -0
  311. package/src/debug/raw-sse.ts +312 -0
  312. package/src/debug/remote-debugger.ts +151 -0
  313. package/src/debug/report-bundle.ts +411 -0
  314. package/src/debug/system-info.ts +111 -0
  315. package/src/debug/terminal-info.ts +124 -0
  316. package/src/discovery/agent-plugin-format.ts +551 -0
  317. package/src/discovery/agent-plugins.ts +341 -0
  318. package/src/discovery/agents-md.ts +67 -0
  319. package/src/discovery/agents.ts +300 -0
  320. package/src/discovery/at-imports.ts +273 -0
  321. package/src/discovery/builtin-defaults.ts +39 -0
  322. package/src/discovery/builtin-rules/go-add-cleanup.md +33 -0
  323. package/src/discovery/builtin-rules/go-bench-loop.md +36 -0
  324. package/src/discovery/builtin-rules/go-exp-promoted.md +40 -0
  325. package/src/discovery/builtin-rules/go-ioutil.md +37 -0
  326. package/src/discovery/builtin-rules/go-join-hostport.md +30 -0
  327. package/src/discovery/builtin-rules/go-new-expr.md +44 -0
  328. package/src/discovery/builtin-rules/go-rand-v2.md +41 -0
  329. package/src/discovery/builtin-rules/go-range-int.md +45 -0
  330. package/src/discovery/builtin-rules/index.ts +74 -0
  331. package/src/discovery/builtin-rules/rs-box-leak.md +49 -0
  332. package/src/discovery/builtin-rules/rs-future-prelude.md +24 -0
  333. package/src/discovery/builtin-rules/rs-lazylock.md +52 -0
  334. package/src/discovery/builtin-rules/rs-match-ergonomics.md +68 -0
  335. package/src/discovery/builtin-rules/rs-parking-lot.md +45 -0
  336. package/src/discovery/builtin-rules/rs-result-type.md +20 -0
  337. package/src/discovery/builtin-rules/ts-bare-catch.md +39 -0
  338. package/src/discovery/builtin-rules/ts-import-type.md +43 -0
  339. package/src/discovery/builtin-rules/ts-no-any.md +66 -0
  340. package/src/discovery/builtin-rules/ts-no-deprecated-leftovers.md +45 -0
  341. package/src/discovery/builtin-rules/ts-no-dynamic-import.md +40 -0
  342. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +56 -0
  343. package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
  344. package/src/discovery/builtin-rules/ts-no-return-type.md +45 -0
  345. package/src/discovery/builtin-rules/ts-no-test-timers.md +55 -0
  346. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +51 -0
  347. package/src/discovery/builtin-rules/ts-promise-with-resolvers.md +66 -0
  348. package/src/discovery/builtin-rules/ts-redundant-clear-guard.md +75 -0
  349. package/src/discovery/builtin-rules/ts-set-map.md +28 -0
  350. package/src/discovery/builtin.ts +945 -0
  351. package/src/discovery/claude-plugins.ts +640 -0
  352. package/src/discovery/claude.ts +591 -0
  353. package/src/discovery/cline.ts +83 -0
  354. package/src/discovery/codex.ts +553 -0
  355. package/src/discovery/contained-path.ts +78 -0
  356. package/src/discovery/cursor.ts +223 -0
  357. package/src/discovery/gemini.ts +386 -0
  358. package/src/discovery/github.ts +337 -0
  359. package/src/discovery/helpers.ts +1183 -0
  360. package/src/discovery/index.ts +82 -0
  361. package/src/discovery/mcp-json.ts +182 -0
  362. package/src/discovery/omp-extension-roots.ts +272 -0
  363. package/src/discovery/omp-plugins.ts +409 -0
  364. package/src/discovery/opencode.ts +441 -0
  365. package/src/discovery/plugin-dir-roots.ts +28 -0
  366. package/src/discovery/ssh.ts +153 -0
  367. package/src/discovery/substitute-plugin-root.ts +77 -0
  368. package/src/discovery/vscode.ts +106 -0
  369. package/src/discovery/windsurf.ts +149 -0
  370. package/src/edit/apply-patch/index.ts +87 -0
  371. package/src/edit/apply-patch/parser.ts +174 -0
  372. package/src/edit/diff.ts +1008 -0
  373. package/src/edit/edit-clipboard.ts +23 -0
  374. package/src/edit/file-snapshot-store.ts +148 -0
  375. package/src/edit/hashline/block-resolver.ts +33 -0
  376. package/src/edit/hashline/diff.ts +402 -0
  377. package/src/edit/hashline/execute.ts +313 -0
  378. package/src/edit/hashline/filesystem.ts +247 -0
  379. package/src/edit/hashline/index.ts +5 -0
  380. package/src/edit/hashline/noop-loop-guard.ts +99 -0
  381. package/src/edit/hashline/params.ts +12 -0
  382. package/src/edit/index.ts +699 -0
  383. package/src/edit/modes/apply-patch.lark +19 -0
  384. package/src/edit/modes/apply-patch.ts +53 -0
  385. package/src/edit/modes/patch.ts +1958 -0
  386. package/src/edit/modes/replace.ts +1191 -0
  387. package/src/edit/normalize.ts +345 -0
  388. package/src/edit/notebook.ts +242 -0
  389. package/src/edit/read-file.ts +25 -0
  390. package/src/edit/renderer.ts +998 -0
  391. package/src/edit/snapshot-details.ts +77 -0
  392. package/src/edit/streaming.ts +710 -0
  393. package/src/eval/agent-bridge.ts +224 -0
  394. package/src/eval/backend-helpers.ts +48 -0
  395. package/src/eval/backend.ts +71 -0
  396. package/src/eval/bridge-timeout.ts +65 -0
  397. package/src/eval/budget-bridge.ts +48 -0
  398. package/src/eval/completion-bridge.ts +212 -0
  399. package/src/eval/concurrency-bridge.ts +34 -0
  400. package/src/eval/executor-base.ts +562 -0
  401. package/src/eval/idle-timeout.ts +91 -0
  402. package/src/eval/index.ts +6 -0
  403. package/src/eval/jl/executor.ts +551 -0
  404. package/src/eval/jl/index.ts +54 -0
  405. package/src/eval/jl/kernel.ts +236 -0
  406. package/src/eval/jl/prelude.jl +736 -0
  407. package/src/eval/jl/prelude.ts +3 -0
  408. package/src/eval/jl/runner.jl +666 -0
  409. package/src/eval/jl/runtime.ts +118 -0
  410. package/src/eval/js/context-manager.ts +872 -0
  411. package/src/eval/js/executor.ts +178 -0
  412. package/src/eval/js/index.ts +41 -0
  413. package/src/eval/js/process-entry.ts +31 -0
  414. package/src/eval/js/shared/helpers.ts +170 -0
  415. package/src/eval/js/shared/indirect-eval.ts +30 -0
  416. package/src/eval/js/shared/local-module-loader.ts +364 -0
  417. package/src/eval/js/shared/prelude.ts +2 -0
  418. package/src/eval/js/shared/prelude.txt +294 -0
  419. package/src/eval/js/shared/rewrite-imports.ts +550 -0
  420. package/src/eval/js/shared/runtime.ts +590 -0
  421. package/src/eval/js/shared/types.ts +18 -0
  422. package/src/eval/js/tool-bridge.ts +163 -0
  423. package/src/eval/js/worker-core.ts +380 -0
  424. package/src/eval/js/worker-entry.ts +37 -0
  425. package/src/eval/js/worker-protocol.ts +47 -0
  426. package/src/eval/kernel-base.ts +569 -0
  427. package/src/eval/py/display.ts +71 -0
  428. package/src/eval/py/executor.ts +643 -0
  429. package/src/eval/py/index.ts +57 -0
  430. package/src/eval/py/kernel.ts +235 -0
  431. package/src/eval/py/prelude.py +673 -0
  432. package/src/eval/py/prelude.ts +3 -0
  433. package/src/eval/py/runner.py +1425 -0
  434. package/src/eval/py/runtime.ts +276 -0
  435. package/src/eval/py/spawn-options.ts +134 -0
  436. package/src/eval/py/tool-bridge.ts +201 -0
  437. package/src/eval/rb/executor.ts +511 -0
  438. package/src/eval/rb/index.ts +54 -0
  439. package/src/eval/rb/kernel.ts +231 -0
  440. package/src/eval/rb/prelude.rb +551 -0
  441. package/src/eval/rb/prelude.ts +3 -0
  442. package/src/eval/rb/runner.rb +581 -0
  443. package/src/eval/rb/runtime.ts +132 -0
  444. package/src/eval/runtime-env.ts +104 -0
  445. package/src/eval/session-id.ts +8 -0
  446. package/src/eval/types.ts +48 -0
  447. package/src/exa/index.ts +2 -0
  448. package/src/exa/mcp-client.ts +370 -0
  449. package/src/exa/types.ts +69 -0
  450. package/src/exec/bash-executor.ts +627 -0
  451. package/src/exec/direnv.ts +145 -0
  452. package/src/exec/exec.ts +53 -0
  453. package/src/exec/non-interactive-env.ts +118 -0
  454. package/src/export/custom-share.ts +65 -0
  455. package/src/export/html/args.ts +20 -0
  456. package/src/export/html/index.ts +320 -0
  457. package/src/export/html/share-loader.js +102 -0
  458. package/src/export/html/template.css +1355 -0
  459. package/src/export/html/template.html +55 -0
  460. package/src/export/html/template.js +1653 -0
  461. package/src/export/html/tool-views.generated.js +35 -0
  462. package/src/export/html/vendor/highlight.min.js +1213 -0
  463. package/src/export/html/vendor/marked.min.js +6 -0
  464. package/src/export/html/web-palette.ts +126 -0
  465. package/src/export/share.ts +686 -0
  466. package/src/export/ttsr.ts +590 -0
  467. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +54 -0
  468. package/src/extensibility/custom-commands/bundled/review/index.ts +698 -0
  469. package/src/extensibility/custom-commands/index.ts +2 -0
  470. package/src/extensibility/custom-commands/loader.ts +242 -0
  471. package/src/extensibility/custom-commands/types.ts +119 -0
  472. package/src/extensibility/custom-tools/index.ts +7 -0
  473. package/src/extensibility/custom-tools/loader.ts +301 -0
  474. package/src/extensibility/custom-tools/types.ts +287 -0
  475. package/src/extensibility/custom-tools/wrapper.ts +50 -0
  476. package/src/extensibility/extensions/compact-handler.ts +40 -0
  477. package/src/extensibility/extensions/get-commands-handler.ts +78 -0
  478. package/src/extensibility/extensions/index.ts +16 -0
  479. package/src/extensibility/extensions/load-errors.ts +13 -0
  480. package/src/extensibility/extensions/loader.ts +663 -0
  481. package/src/extensibility/extensions/managed-timers.ts +83 -0
  482. package/src/extensibility/extensions/model-api.ts +39 -0
  483. package/src/extensibility/extensions/runner.ts +1396 -0
  484. package/src/extensibility/extensions/types.ts +1603 -0
  485. package/src/extensibility/extensions/wrapper.ts +395 -0
  486. package/src/extensibility/hooks/index.ts +6 -0
  487. package/src/extensibility/hooks/loader.ts +243 -0
  488. package/src/extensibility/hooks/runner.ts +425 -0
  489. package/src/extensibility/hooks/tool-wrapper.ts +124 -0
  490. package/src/extensibility/hooks/types.ts +612 -0
  491. package/src/extensibility/legacy-pi-ai-shim.ts +161 -0
  492. package/src/extensibility/legacy-pi-coding-agent-shim.ts +1460 -0
  493. package/src/extensibility/legacy-pi-tui-shim.ts +43 -0
  494. package/src/extensibility/legacy-typebox.ts +101 -0
  495. package/src/extensibility/plugins/bun-git-cache.ts +91 -0
  496. package/src/extensibility/plugins/doctor.ts +65 -0
  497. package/src/extensibility/plugins/git-url.ts +367 -0
  498. package/src/extensibility/plugins/index.ts +9 -0
  499. package/src/extensibility/plugins/installer.ts +201 -0
  500. package/src/extensibility/plugins/legacy-pi-compat.ts +2643 -0
  501. package/src/extensibility/plugins/legacy-pi-virtual-modules.d.ts +4 -0
  502. package/src/extensibility/plugins/loader.ts +535 -0
  503. package/src/extensibility/plugins/manager.ts +1142 -0
  504. package/src/extensibility/plugins/marketplace/cache.ts +136 -0
  505. package/src/extensibility/plugins/marketplace/fetcher.ts +316 -0
  506. package/src/extensibility/plugins/marketplace/index.ts +6 -0
  507. package/src/extensibility/plugins/marketplace/manager.ts +927 -0
  508. package/src/extensibility/plugins/marketplace/registry.ts +192 -0
  509. package/src/extensibility/plugins/marketplace/source-resolver.ts +147 -0
  510. package/src/extensibility/plugins/marketplace/types.ts +192 -0
  511. package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
  512. package/src/extensibility/plugins/parser.ts +107 -0
  513. package/src/extensibility/plugins/runtime-config.ts +9 -0
  514. package/src/extensibility/plugins/types.ts +194 -0
  515. package/src/extensibility/session-handler-types.ts +21 -0
  516. package/src/extensibility/shared-events.ts +401 -0
  517. package/src/extensibility/skills.ts +531 -0
  518. package/src/extensibility/slash-commands.ts +131 -0
  519. package/src/extensibility/tool-event-input.ts +80 -0
  520. package/src/extensibility/tool-proxy.ts +28 -0
  521. package/src/extensibility/utils.ts +192 -0
  522. package/src/goals/index.ts +3 -0
  523. package/src/goals/runtime.ts +521 -0
  524. package/src/goals/state.ts +37 -0
  525. package/src/goals/tools/goal-tool.ts +251 -0
  526. package/src/hindsight/backend.ts +354 -0
  527. package/src/hindsight/bank.ts +156 -0
  528. package/src/hindsight/client.ts +680 -0
  529. package/src/hindsight/config.ts +193 -0
  530. package/src/hindsight/content.ts +266 -0
  531. package/src/hindsight/index.ts +8 -0
  532. package/src/hindsight/mental-models.ts +429 -0
  533. package/src/hindsight/seeds.json +32 -0
  534. package/src/hindsight/state.ts +530 -0
  535. package/src/hindsight/transcript.ts +71 -0
  536. package/src/index.ts +66 -0
  537. package/src/internal-urls/agent-protocol.ts +180 -0
  538. package/src/internal-urls/artifact-protocol.ts +151 -0
  539. package/src/internal-urls/docs-index.ts +102 -0
  540. package/src/internal-urls/filesystem-resource.ts +34 -0
  541. package/src/internal-urls/history-protocol.ts +198 -0
  542. package/src/internal-urls/index.ts +28 -0
  543. package/src/internal-urls/issue-pr-protocol.ts +594 -0
  544. package/src/internal-urls/json-query.ts +126 -0
  545. package/src/internal-urls/local-protocol.ts +471 -0
  546. package/src/internal-urls/mcp-protocol.ts +168 -0
  547. package/src/internal-urls/memory-protocol.ts +380 -0
  548. package/src/internal-urls/omp-protocol.ts +94 -0
  549. package/src/internal-urls/parse.ts +103 -0
  550. package/src/internal-urls/registry-helpers.ts +132 -0
  551. package/src/internal-urls/router.ts +153 -0
  552. package/src/internal-urls/rule-protocol.ts +45 -0
  553. package/src/internal-urls/security-protocol.ts +261 -0
  554. package/src/internal-urls/skill-protocol.ts +131 -0
  555. package/src/internal-urls/ssh-protocol.ts +368 -0
  556. package/src/internal-urls/types.ts +196 -0
  557. package/src/internal-urls/vault-protocol.ts +940 -0
  558. package/src/internal-urls/xd-protocol.ts +46 -0
  559. package/src/irc/bus.ts +380 -0
  560. package/src/jsonrpc/message-framing.ts +142 -0
  561. package/src/launch/broker.ts +1368 -0
  562. package/src/launch/client.ts +521 -0
  563. package/src/launch/ensure.ts +73 -0
  564. package/src/launch/paths.ts +14 -0
  565. package/src/launch/presence.ts +82 -0
  566. package/src/launch/protocol.ts +448 -0
  567. package/src/launch/spawn-options.ts +17 -0
  568. package/src/launch/terminal-output-worker-client.ts +53 -0
  569. package/src/launch/terminal-output-worker-protocol.ts +11 -0
  570. package/src/launch/terminal-output-worker.ts +23 -0
  571. package/src/launch/terminal-output.ts +46 -0
  572. package/src/lib/xai-http.ts +150 -0
  573. package/src/live/attestation.ts +91 -0
  574. package/src/live/controller.ts +517 -0
  575. package/src/live/prompts/agent-final-message.md +3 -0
  576. package/src/live/prompts/live-instructions.md +23 -0
  577. package/src/live/protocol.ts +233 -0
  578. package/src/live/transport.ts +422 -0
  579. package/src/live/visualizer.ts +221 -0
  580. package/src/live/voices.ts +18 -0
  581. package/src/lsp/client.ts +1465 -0
  582. package/src/lsp/clients/biome-client.ts +263 -0
  583. package/src/lsp/clients/index.ts +50 -0
  584. package/src/lsp/clients/lsp-linter-client.ts +85 -0
  585. package/src/lsp/clients/swiftlint-client.ts +120 -0
  586. package/src/lsp/config.ts +549 -0
  587. package/src/lsp/defaults.json +499 -0
  588. package/src/lsp/deferred-diagnostics.ts +66 -0
  589. package/src/lsp/diagnostics-ledger.ts +51 -0
  590. package/src/lsp/edits.ts +288 -0
  591. package/src/lsp/format-options.ts +119 -0
  592. package/src/lsp/index.ts +2821 -0
  593. package/src/lsp/lspmux.ts +233 -0
  594. package/src/lsp/mux/daemon.ts +348 -0
  595. package/src/lsp/mux/protocol.ts +96 -0
  596. package/src/lsp/mux/server.ts +797 -0
  597. package/src/lsp/render.ts +668 -0
  598. package/src/lsp/startup-events.ts +13 -0
  599. package/src/lsp/types.ts +479 -0
  600. package/src/lsp/utils.ts +747 -0
  601. package/src/main.ts +1799 -0
  602. package/src/markit/NOTICE +32 -0
  603. package/src/markit/converters/docx.ts +56 -0
  604. package/src/markit/converters/epub.ts +136 -0
  605. package/src/markit/converters/pdf/columns.ts +103 -0
  606. package/src/markit/converters/pdf/extract.ts +598 -0
  607. package/src/markit/converters/pdf/grid.ts +780 -0
  608. package/src/markit/converters/pdf/headers.ts +106 -0
  609. package/src/markit/converters/pdf/index.ts +146 -0
  610. package/src/markit/converters/pdf/render.ts +501 -0
  611. package/src/markit/converters/pdf/types.ts +84 -0
  612. package/src/markit/converters/pptx.ts +325 -0
  613. package/src/markit/converters/xlsx.ts +173 -0
  614. package/src/markit/index.ts +2 -0
  615. package/src/markit/registry.ts +59 -0
  616. package/src/markit/types.ts +35 -0
  617. package/src/mcp/client.ts +511 -0
  618. package/src/mcp/config-writer.ts +377 -0
  619. package/src/mcp/config.ts +385 -0
  620. package/src/mcp/index.ts +29 -0
  621. package/src/mcp/json-rpc.ts +122 -0
  622. package/src/mcp/loader.ts +125 -0
  623. package/src/mcp/manager.ts +1524 -0
  624. package/src/mcp/oauth-credentials.ts +104 -0
  625. package/src/mcp/oauth-discovery.ts +587 -0
  626. package/src/mcp/oauth-flow.ts +830 -0
  627. package/src/mcp/render.ts +214 -0
  628. package/src/mcp/request-id.ts +24 -0
  629. package/src/mcp/smithery-auth.ts +108 -0
  630. package/src/mcp/smithery-connect.ts +154 -0
  631. package/src/mcp/smithery-registry.ts +500 -0
  632. package/src/mcp/startup-events.ts +116 -0
  633. package/src/mcp/timeout.ts +59 -0
  634. package/src/mcp/tool-bridge.ts +691 -0
  635. package/src/mcp/tool-cache.ts +117 -0
  636. package/src/mcp/transports/header-policy.ts +95 -0
  637. package/src/mcp/transports/http.ts +508 -0
  638. package/src/mcp/transports/index.ts +7 -0
  639. package/src/mcp/transports/sse.ts +372 -0
  640. package/src/mcp/transports/stdio.ts +905 -0
  641. package/src/mcp/types.ts +467 -0
  642. package/src/memories/index.ts +1434 -0
  643. package/src/memories/storage.ts +578 -0
  644. package/src/memory-backend/index.ts +19 -0
  645. package/src/memory-backend/local-backend.ts +47 -0
  646. package/src/memory-backend/messages.ts +19 -0
  647. package/src/memory-backend/off-backend.ts +25 -0
  648. package/src/memory-backend/resolve.ts +25 -0
  649. package/src/memory-backend/runtime.ts +66 -0
  650. package/src/memory-backend/tool-names.ts +2 -0
  651. package/src/memory-backend/types.ts +166 -0
  652. package/src/mnemopi/backend.ts +629 -0
  653. package/src/mnemopi/config.ts +267 -0
  654. package/src/mnemopi/embed-client.ts +293 -0
  655. package/src/mnemopi/embed-protocol.ts +35 -0
  656. package/src/mnemopi/embed-worker.ts +114 -0
  657. package/src/mnemopi/index.ts +3 -0
  658. package/src/mnemopi/state.ts +940 -0
  659. package/src/modes/acp/acp-agent.ts +2595 -0
  660. package/src/modes/acp/acp-client-bridge.ts +154 -0
  661. package/src/modes/acp/acp-event-mapper.ts +1084 -0
  662. package/src/modes/acp/acp-mode.ts +48 -0
  663. package/src/modes/acp/index.ts +2 -0
  664. package/src/modes/acp/terminal-auth.ts +37 -0
  665. package/src/modes/components/advisor-config.ts +635 -0
  666. package/src/modes/components/advisor-message.ts +109 -0
  667. package/src/modes/components/agent-dashboard.ts +1252 -0
  668. package/src/modes/components/agent-hub-projection.ts +248 -0
  669. package/src/modes/components/agent-hub-renderer.ts +194 -0
  670. package/src/modes/components/agent-hub.ts +1126 -0
  671. package/src/modes/components/agent-transcript-viewer.ts +647 -0
  672. package/src/modes/components/ask-dialog.ts +1017 -0
  673. package/src/modes/components/assistant-message.ts +978 -0
  674. package/src/modes/components/background-tan-message.ts +36 -0
  675. package/src/modes/components/bash-execution.ts +233 -0
  676. package/src/modes/components/bordered-loader.ts +41 -0
  677. package/src/modes/components/btw-panel.ts +161 -0
  678. package/src/modes/components/cache-invalidation-marker.ts +110 -0
  679. package/src/modes/components/chat-block.ts +111 -0
  680. package/src/modes/components/chat-transcript-builder.ts +518 -0
  681. package/src/modes/components/codex-reset-fireworks.ts +369 -0
  682. package/src/modes/components/collab-prompt-message.ts +32 -0
  683. package/src/modes/components/compaction-summary-message.ts +221 -0
  684. package/src/modes/components/copy-selector.ts +218 -0
  685. package/src/modes/components/countdown-timer.ts +75 -0
  686. package/src/modes/components/custom-editor.ts +1036 -0
  687. package/src/modes/components/custom-message.ts +70 -0
  688. package/src/modes/components/diff.ts +254 -0
  689. package/src/modes/components/dynamic-border.ts +37 -0
  690. package/src/modes/components/error-banner.ts +33 -0
  691. package/src/modes/components/eval-execution.ts +169 -0
  692. package/src/modes/components/execution-shared.ts +101 -0
  693. package/src/modes/components/extensions/extension-dashboard.ts +492 -0
  694. package/src/modes/components/extensions/extension-list.ts +507 -0
  695. package/src/modes/components/extensions/index.ts +9 -0
  696. package/src/modes/components/extensions/inspector-panel.ts +326 -0
  697. package/src/modes/components/extensions/state-manager.ts +648 -0
  698. package/src/modes/components/extensions/types.ts +186 -0
  699. package/src/modes/components/footer.ts +274 -0
  700. package/src/modes/components/history-search.ts +268 -0
  701. package/src/modes/components/hook-editor.ts +213 -0
  702. package/src/modes/components/hook-input.ts +87 -0
  703. package/src/modes/components/hook-message.ts +67 -0
  704. package/src/modes/components/hook-selector.ts +691 -0
  705. package/src/modes/components/index.ts +42 -0
  706. package/src/modes/components/keybinding-hints.ts +56 -0
  707. package/src/modes/components/late-diagnostics-message.ts +60 -0
  708. package/src/modes/components/login-dialog.ts +197 -0
  709. package/src/modes/components/logout-account-selector.ts +130 -0
  710. package/src/modes/components/mcp-add-wizard.ts +1413 -0
  711. package/src/modes/components/message-frame.ts +98 -0
  712. package/src/modes/components/model-browser.ts +888 -0
  713. package/src/modes/components/model-hub.ts +2014 -0
  714. package/src/modes/components/model-picker.ts +237 -0
  715. package/src/modes/components/move-overlay.ts +293 -0
  716. package/src/modes/components/oauth-selector.ts +474 -0
  717. package/src/modes/components/omfg-panel.ts +141 -0
  718. package/src/modes/components/overlay-box.ts +109 -0
  719. package/src/modes/components/pause-screen.ts +208 -0
  720. package/src/modes/components/plan-review-overlay.ts +1226 -0
  721. package/src/modes/components/plan-toc.ts +138 -0
  722. package/src/modes/components/plugin-selector.ts +100 -0
  723. package/src/modes/components/plugin-settings.ts +745 -0
  724. package/src/modes/components/queue-mode-selector.ts +61 -0
  725. package/src/modes/components/read-tool-group.ts +856 -0
  726. package/src/modes/components/reset-usage-selector.ts +161 -0
  727. package/src/modes/components/segment-track.ts +89 -0
  728. package/src/modes/components/select-list-mouse-routing.ts +35 -0
  729. package/src/modes/components/selector-helpers.ts +129 -0
  730. package/src/modes/components/session-account-selector.ts +62 -0
  731. package/src/modes/components/session-selector.ts +1035 -0
  732. package/src/modes/components/settings-defs.ts +267 -0
  733. package/src/modes/components/settings-selector.ts +1445 -0
  734. package/src/modes/components/show-images-selector.ts +50 -0
  735. package/src/modes/components/skill-message.ts +110 -0
  736. package/src/modes/components/snapcompact-shape-preview-doc.md +14 -0
  737. package/src/modes/components/snapcompact-shape-preview.ts +192 -0
  738. package/src/modes/components/status-line/component.ts +1843 -0
  739. package/src/modes/components/status-line/context-thresholds.ts +86 -0
  740. package/src/modes/components/status-line/git-utils.ts +42 -0
  741. package/src/modes/components/status-line/index.ts +5 -0
  742. package/src/modes/components/status-line/presets.ts +106 -0
  743. package/src/modes/components/status-line/segments.ts +710 -0
  744. package/src/modes/components/status-line/separators.ts +55 -0
  745. package/src/modes/components/status-line/types.ts +159 -0
  746. package/src/modes/components/stripped-tool-calls-placeholder.ts +35 -0
  747. package/src/modes/components/theme-selector.ts +68 -0
  748. package/src/modes/components/thinking-selector.ts +57 -0
  749. package/src/modes/components/tiny-title-download-progress.ts +90 -0
  750. package/src/modes/components/tips.txt +26 -0
  751. package/src/modes/components/todo-reminder.ts +43 -0
  752. package/src/modes/components/tool-execution.ts +1467 -0
  753. package/src/modes/components/transcript-container.ts +527 -0
  754. package/src/modes/components/tree-selector.ts +1006 -0
  755. package/src/modes/components/ttsr-notification.ts +123 -0
  756. package/src/modes/components/usage-row.ts +52 -0
  757. package/src/modes/components/user-message-selector.ts +227 -0
  758. package/src/modes/components/user-message.ts +167 -0
  759. package/src/modes/components/visual-truncate.ts +63 -0
  760. package/src/modes/components/welcome.ts +578 -0
  761. package/src/modes/controllers/btw-controller.ts +246 -0
  762. package/src/modes/controllers/command-controller-shared.ts +109 -0
  763. package/src/modes/controllers/command-controller.ts +2024 -0
  764. package/src/modes/controllers/event-controller.ts +2173 -0
  765. package/src/modes/controllers/extension-ui-controller.ts +1243 -0
  766. package/src/modes/controllers/input-controller.ts +2037 -0
  767. package/src/modes/controllers/live-command-controller.ts +261 -0
  768. package/src/modes/controllers/mcp-command-controller.ts +2552 -0
  769. package/src/modes/controllers/omfg-controller.ts +287 -0
  770. package/src/modes/controllers/omfg-rule.ts +647 -0
  771. package/src/modes/controllers/selector-controller.ts +2058 -0
  772. package/src/modes/controllers/session-focus-controller.ts +117 -0
  773. package/src/modes/controllers/ssh-command-controller.ts +385 -0
  774. package/src/modes/controllers/streaming-reveal.ts +399 -0
  775. package/src/modes/controllers/tan-command-controller.ts +243 -0
  776. package/src/modes/controllers/todo-command-controller.ts +487 -0
  777. package/src/modes/controllers/tool-args-reveal.ts +591 -0
  778. package/src/modes/data/emojis.json +1 -0
  779. package/src/modes/emoji-autocomplete.ts +285 -0
  780. package/src/modes/github-ref-autocomplete.ts +75 -0
  781. package/src/modes/gradient-highlight.ts +99 -0
  782. package/src/modes/image-references.ts +137 -0
  783. package/src/modes/index.ts +10 -0
  784. package/src/modes/interactive-mode.ts +5055 -0
  785. package/src/modes/internal-url-autocomplete.ts +158 -0
  786. package/src/modes/loop-limit.ts +192 -0
  787. package/src/modes/magic-keyword-boundary.ts +23 -0
  788. package/src/modes/magic-keywords.ts +42 -0
  789. package/src/modes/markdown-prose.ts +247 -0
  790. package/src/modes/oauth-manual-input.ts +69 -0
  791. package/src/modes/orchestrate.ts +43 -0
  792. package/src/modes/print-mode.ts +282 -0
  793. package/src/modes/prompt-action-autocomplete.ts +322 -0
  794. package/src/modes/queue-input.ts +132 -0
  795. package/src/modes/rpc/host-tools.ts +204 -0
  796. package/src/modes/rpc/host-uris.ts +241 -0
  797. package/src/modes/rpc/rpc-client.ts +1213 -0
  798. package/src/modes/rpc/rpc-frame.ts +316 -0
  799. package/src/modes/rpc/rpc-input.ts +38 -0
  800. package/src/modes/rpc/rpc-messages.ts +127 -0
  801. package/src/modes/rpc/rpc-mode.ts +1519 -0
  802. package/src/modes/rpc/rpc-subagents.ts +265 -0
  803. package/src/modes/rpc/rpc-types.ts +544 -0
  804. package/src/modes/running-subagent-badge.ts +13 -0
  805. package/src/modes/runtime-init.ts +144 -0
  806. package/src/modes/session-observer-registry.ts +223 -0
  807. package/src/modes/session-teardown.ts +82 -0
  808. package/src/modes/setup-version.ts +11 -0
  809. package/src/modes/setup-wizard/index.ts +103 -0
  810. package/src/modes/setup-wizard/lazy.ts +16 -0
  811. package/src/modes/setup-wizard/scenes/glyph.ts +103 -0
  812. package/src/modes/setup-wizard/scenes/model.ts +132 -0
  813. package/src/modes/setup-wizard/scenes/outro.ts +35 -0
  814. package/src/modes/setup-wizard/scenes/providers.ts +105 -0
  815. package/src/modes/setup-wizard/scenes/sign-in.ts +312 -0
  816. package/src/modes/setup-wizard/scenes/splash.ts +201 -0
  817. package/src/modes/setup-wizard/scenes/theme.ts +330 -0
  818. package/src/modes/setup-wizard/scenes/types.ts +65 -0
  819. package/src/modes/setup-wizard/scenes/web-search.ts +153 -0
  820. package/src/modes/setup-wizard/startup-splash.ts +107 -0
  821. package/src/modes/setup-wizard/wizard-overlay.ts +335 -0
  822. package/src/modes/shared.ts +49 -0
  823. package/src/modes/skill-command.ts +91 -0
  824. package/src/modes/theme/dark.json +95 -0
  825. package/src/modes/theme/defaults/alabaster.json +93 -0
  826. package/src/modes/theme/defaults/amethyst.json +96 -0
  827. package/src/modes/theme/defaults/anthracite.json +93 -0
  828. package/src/modes/theme/defaults/basalt.json +91 -0
  829. package/src/modes/theme/defaults/birch.json +95 -0
  830. package/src/modes/theme/defaults/dark-abyss.json +91 -0
  831. package/src/modes/theme/defaults/dark-arctic.json +104 -0
  832. package/src/modes/theme/defaults/dark-aurora.json +95 -0
  833. package/src/modes/theme/defaults/dark-catppuccin.json +107 -0
  834. package/src/modes/theme/defaults/dark-cavern.json +91 -0
  835. package/src/modes/theme/defaults/dark-copper.json +95 -0
  836. package/src/modes/theme/defaults/dark-cosmos.json +90 -0
  837. package/src/modes/theme/defaults/dark-cyberpunk.json +102 -0
  838. package/src/modes/theme/defaults/dark-dracula.json +98 -0
  839. package/src/modes/theme/defaults/dark-eclipse.json +91 -0
  840. package/src/modes/theme/defaults/dark-ember.json +95 -0
  841. package/src/modes/theme/defaults/dark-equinox.json +90 -0
  842. package/src/modes/theme/defaults/dark-forest.json +96 -0
  843. package/src/modes/theme/defaults/dark-github.json +105 -0
  844. package/src/modes/theme/defaults/dark-gruvbox.json +112 -0
  845. package/src/modes/theme/defaults/dark-lavender.json +95 -0
  846. package/src/modes/theme/defaults/dark-lunar.json +89 -0
  847. package/src/modes/theme/defaults/dark-midnight.json +95 -0
  848. package/src/modes/theme/defaults/dark-monochrome.json +94 -0
  849. package/src/modes/theme/defaults/dark-monokai.json +98 -0
  850. package/src/modes/theme/defaults/dark-nebula.json +90 -0
  851. package/src/modes/theme/defaults/dark-nord.json +97 -0
  852. package/src/modes/theme/defaults/dark-ocean.json +101 -0
  853. package/src/modes/theme/defaults/dark-one.json +100 -0
  854. package/src/modes/theme/defaults/dark-poimandres.json +143 -0
  855. package/src/modes/theme/defaults/dark-rainforest.json +91 -0
  856. package/src/modes/theme/defaults/dark-reef.json +91 -0
  857. package/src/modes/theme/defaults/dark-retro.json +92 -0
  858. package/src/modes/theme/defaults/dark-rose-pine.json +96 -0
  859. package/src/modes/theme/defaults/dark-sakura.json +95 -0
  860. package/src/modes/theme/defaults/dark-slate.json +95 -0
  861. package/src/modes/theme/defaults/dark-solarized.json +97 -0
  862. package/src/modes/theme/defaults/dark-solstice.json +90 -0
  863. package/src/modes/theme/defaults/dark-starfall.json +91 -0
  864. package/src/modes/theme/defaults/dark-sunset.json +99 -0
  865. package/src/modes/theme/defaults/dark-swamp.json +90 -0
  866. package/src/modes/theme/defaults/dark-synthwave.json +103 -0
  867. package/src/modes/theme/defaults/dark-taiga.json +91 -0
  868. package/src/modes/theme/defaults/dark-terminal.json +95 -0
  869. package/src/modes/theme/defaults/dark-tokyo-night.json +101 -0
  870. package/src/modes/theme/defaults/dark-tundra.json +91 -0
  871. package/src/modes/theme/defaults/dark-twilight.json +91 -0
  872. package/src/modes/theme/defaults/dark-volcanic.json +91 -0
  873. package/src/modes/theme/defaults/graphite.json +92 -0
  874. package/src/modes/theme/defaults/index.ts +199 -0
  875. package/src/modes/theme/defaults/light-arctic.json +107 -0
  876. package/src/modes/theme/defaults/light-aurora-day.json +91 -0
  877. package/src/modes/theme/defaults/light-canyon.json +91 -0
  878. package/src/modes/theme/defaults/light-catppuccin.json +106 -0
  879. package/src/modes/theme/defaults/light-cirrus.json +90 -0
  880. package/src/modes/theme/defaults/light-coral.json +95 -0
  881. package/src/modes/theme/defaults/light-cyberpunk.json +96 -0
  882. package/src/modes/theme/defaults/light-dawn.json +90 -0
  883. package/src/modes/theme/defaults/light-dunes.json +91 -0
  884. package/src/modes/theme/defaults/light-eucalyptus.json +95 -0
  885. package/src/modes/theme/defaults/light-forest.json +100 -0
  886. package/src/modes/theme/defaults/light-frost.json +95 -0
  887. package/src/modes/theme/defaults/light-github.json +115 -0
  888. package/src/modes/theme/defaults/light-glacier.json +91 -0
  889. package/src/modes/theme/defaults/light-gruvbox.json +108 -0
  890. package/src/modes/theme/defaults/light-haze.json +90 -0
  891. package/src/modes/theme/defaults/light-honeycomb.json +95 -0
  892. package/src/modes/theme/defaults/light-lagoon.json +91 -0
  893. package/src/modes/theme/defaults/light-lavender.json +95 -0
  894. package/src/modes/theme/defaults/light-meadow.json +91 -0
  895. package/src/modes/theme/defaults/light-mint.json +95 -0
  896. package/src/modes/theme/defaults/light-monochrome.json +101 -0
  897. package/src/modes/theme/defaults/light-ocean.json +99 -0
  898. package/src/modes/theme/defaults/light-one.json +99 -0
  899. package/src/modes/theme/defaults/light-opal.json +91 -0
  900. package/src/modes/theme/defaults/light-orchard.json +91 -0
  901. package/src/modes/theme/defaults/light-paper.json +95 -0
  902. package/src/modes/theme/defaults/light-poimandres.json +143 -0
  903. package/src/modes/theme/defaults/light-prism.json +90 -0
  904. package/src/modes/theme/defaults/light-retro.json +98 -0
  905. package/src/modes/theme/defaults/light-sand.json +95 -0
  906. package/src/modes/theme/defaults/light-savanna.json +91 -0
  907. package/src/modes/theme/defaults/light-solarized.json +102 -0
  908. package/src/modes/theme/defaults/light-soleil.json +90 -0
  909. package/src/modes/theme/defaults/light-sunset.json +99 -0
  910. package/src/modes/theme/defaults/light-synthwave.json +98 -0
  911. package/src/modes/theme/defaults/light-tokyo-night.json +111 -0
  912. package/src/modes/theme/defaults/light-wetland.json +91 -0
  913. package/src/modes/theme/defaults/light-zenith.json +89 -0
  914. package/src/modes/theme/defaults/limestone.json +94 -0
  915. package/src/modes/theme/defaults/mahogany.json +97 -0
  916. package/src/modes/theme/defaults/marble.json +93 -0
  917. package/src/modes/theme/defaults/obsidian.json +91 -0
  918. package/src/modes/theme/defaults/onyx.json +91 -0
  919. package/src/modes/theme/defaults/pearl.json +93 -0
  920. package/src/modes/theme/defaults/porcelain.json +91 -0
  921. package/src/modes/theme/defaults/quartz.json +96 -0
  922. package/src/modes/theme/defaults/sandstone.json +95 -0
  923. package/src/modes/theme/defaults/titanium.json +90 -0
  924. package/src/modes/theme/light.json +93 -0
  925. package/src/modes/theme/mermaid-cache.ts +92 -0
  926. package/src/modes/theme/shimmer.ts +305 -0
  927. package/src/modes/theme/theme-schema.json +463 -0
  928. package/src/modes/theme/theme.ts +3171 -0
  929. package/src/modes/turn-budget.ts +31 -0
  930. package/src/modes/types.ts +494 -0
  931. package/src/modes/ultrathink.ts +42 -0
  932. package/src/modes/utils/context-usage.ts +518 -0
  933. package/src/modes/utils/copy-targets.ts +378 -0
  934. package/src/modes/utils/hotkeys-markdown.ts +65 -0
  935. package/src/modes/utils/interactive-context-helpers.ts +31 -0
  936. package/src/modes/utils/keybinding-matchers.ts +86 -0
  937. package/src/modes/utils/tools-markdown.ts +31 -0
  938. package/src/modes/utils/transcript-render-helpers.ts +253 -0
  939. package/src/modes/utils/ui-helpers.ts +976 -0
  940. package/src/modes/warp-events.ts +232 -0
  941. package/src/modes/workflow.ts +55 -0
  942. package/src/plan-mode/approved-plan.ts +194 -0
  943. package/src/plan-mode/model-transition.ts +51 -0
  944. package/src/plan-mode/plan-files.ts +40 -0
  945. package/src/plan-mode/plan-handoff.ts +37 -0
  946. package/src/plan-mode/plan-protection.ts +31 -0
  947. package/src/plan-mode/state.ts +6 -0
  948. package/src/priority.json +60 -0
  949. package/src/prompts/advisor/active-repo-watchdog.md +6 -0
  950. package/src/prompts/advisor/advise-tool.md +3 -0
  951. package/src/prompts/advisor/context-files.md +8 -0
  952. package/src/prompts/advisor/system.md +98 -0
  953. package/src/prompts/agents/designer.md +74 -0
  954. package/src/prompts/agents/frontmatter.md +12 -0
  955. package/src/prompts/agents/init.md +33 -0
  956. package/src/prompts/agents/librarian.md +119 -0
  957. package/src/prompts/agents/reviewer.md +139 -0
  958. package/src/prompts/agents/scout.md +58 -0
  959. package/src/prompts/agents/security-reviewer.md +75 -0
  960. package/src/prompts/agents/task.md +17 -0
  961. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  962. package/src/prompts/bench/cache-prefix.md +3 -0
  963. package/src/prompts/bench/cache-suffix.md +1 -0
  964. package/src/prompts/bench.md +6 -0
  965. package/src/prompts/ci-green-request.md +36 -0
  966. package/src/prompts/dry-balance-bench.md +8 -0
  967. package/src/prompts/goals/goal-budget-limit.md +16 -0
  968. package/src/prompts/goals/goal-continuation.md +28 -0
  969. package/src/prompts/goals/goal-mode-active.md +23 -0
  970. package/src/prompts/goals/goal-mode-context.md +4 -0
  971. package/src/prompts/goals/goal-todo-context.md +12 -0
  972. package/src/prompts/goals/guided-goal-interview.md +43 -0
  973. package/src/prompts/memories/consolidation.md +30 -0
  974. package/src/prompts/memories/consolidation_system.md +4 -0
  975. package/src/prompts/memories/read-path.md +17 -0
  976. package/src/prompts/memories/stage_one_input.md +6 -0
  977. package/src/prompts/memories/stage_one_system.md +21 -0
  978. package/src/prompts/review-custom-request.md +21 -0
  979. package/src/prompts/review-headless-request.md +16 -0
  980. package/src/prompts/review-request.md +68 -0
  981. package/src/prompts/security/scan-coordinator.md +7 -0
  982. package/src/prompts/security/scan-request.md +21 -0
  983. package/src/prompts/security/validate-request.md +8 -0
  984. package/src/prompts/session/launch-completion.md +1 -0
  985. package/src/prompts/skills/autoload.md +8 -0
  986. package/src/prompts/skills/user-invocation.md +11 -0
  987. package/src/prompts/steering/parent-irc.md +5 -0
  988. package/src/prompts/steering/user-interjection.md +6 -0
  989. package/src/prompts/system/active-repo-context.md +4 -0
  990. package/src/prompts/system/agent-creation-architect.md +50 -0
  991. package/src/prompts/system/agent-creation-user.md +6 -0
  992. package/src/prompts/system/auto-continue.md +1 -0
  993. package/src/prompts/system/auto-thinking-difficulty-local.md +14 -0
  994. package/src/prompts/system/auto-thinking-difficulty.md +14 -0
  995. package/src/prompts/system/autolearn-guidance-learn.md +1 -0
  996. package/src/prompts/system/autolearn-guidance.md +7 -0
  997. package/src/prompts/system/autolearn-nudge-autocontinue.md +5 -0
  998. package/src/prompts/system/background-tan-dispatch.md +8 -0
  999. package/src/prompts/system/btw-user.md +8 -0
  1000. package/src/prompts/system/commit-message-system.md +14 -0
  1001. package/src/prompts/system/computer-safety.md +14 -0
  1002. package/src/prompts/system/custom-system-prompt.md +64 -0
  1003. package/src/prompts/system/eager-task.md +7 -0
  1004. package/src/prompts/system/eager-todo.md +18 -0
  1005. package/src/prompts/system/empty-stop-retry.md +4 -0
  1006. package/src/prompts/system/gemini-tool-call-reminder.md +9 -0
  1007. package/src/prompts/system/interrupted-thinking.md +7 -0
  1008. package/src/prompts/system/irc-autoreply.md +6 -0
  1009. package/src/prompts/system/irc-incoming.md +9 -0
  1010. package/src/prompts/system/manual-continue.md +7 -0
  1011. package/src/prompts/system/mcp-xdev-guidance.md +11 -0
  1012. package/src/prompts/system/memory-consolidation-system.md +8 -0
  1013. package/src/prompts/system/memory-extraction-system.md +26 -0
  1014. package/src/prompts/system/mid-run-todo-nudge.md +3 -0
  1015. package/src/prompts/system/omfg-user.md +50 -0
  1016. package/src/prompts/system/orchestrate-notice.md +40 -0
  1017. package/src/prompts/system/personalities/default.md +18 -0
  1018. package/src/prompts/system/personalities/friendly.md +17 -0
  1019. package/src/prompts/system/personalities/pragmatic.md +15 -0
  1020. package/src/prompts/system/plan-mode-active.md +125 -0
  1021. package/src/prompts/system/plan-mode-approved.md +22 -0
  1022. package/src/prompts/system/plan-mode-compact-instructions.md +17 -0
  1023. package/src/prompts/system/plan-mode-reference.md +10 -0
  1024. package/src/prompts/system/plan-mode-subagent.md +33 -0
  1025. package/src/prompts/system/plan-mode-tool-decision-reminder.md +9 -0
  1026. package/src/prompts/system/plan-yolo-handoff.md +5 -0
  1027. package/src/prompts/system/prewalk-checklist.md +7 -0
  1028. package/src/prompts/system/prewalk-continue.md +1 -0
  1029. package/src/prompts/system/prewalk-plan.md +13 -0
  1030. package/src/prompts/system/project-prompt.md +61 -0
  1031. package/src/prompts/system/recap-user.md +9 -0
  1032. package/src/prompts/system/resolve-device-reminder.md +3 -0
  1033. package/src/prompts/system/rewind-report.md +6 -0
  1034. package/src/prompts/system/side-channel-no-tools.md +3 -0
  1035. package/src/prompts/system/snapcompact-context-frames-note.md +1 -0
  1036. package/src/prompts/system/snapcompact-context-stub.md +1 -0
  1037. package/src/prompts/system/snapcompact-system-frames-note.md +1 -0
  1038. package/src/prompts/system/snapcompact-system-stub.md +1 -0
  1039. package/src/prompts/system/snapcompact-toolresult-note.md +1 -0
  1040. package/src/prompts/system/speech-rewrite.md +15 -0
  1041. package/src/prompts/system/subagent-async-pending.md +6 -0
  1042. package/src/prompts/system/subagent-system-prompt.md +73 -0
  1043. package/src/prompts/system/subagent-user-prompt.md +3 -0
  1044. package/src/prompts/system/subagent-yield-reminder.md +23 -0
  1045. package/src/prompts/system/system-prompt.md +263 -0
  1046. package/src/prompts/system/tan-context-switch.md +17 -0
  1047. package/src/prompts/system/task-label.md +23 -0
  1048. package/src/prompts/system/thinking-loop-redirect.md +10 -0
  1049. package/src/prompts/system/title-marker-instruction.md +1 -0
  1050. package/src/prompts/system/title-system.md +16 -0
  1051. package/src/prompts/system/tool-call-loop-redirect.md +8 -0
  1052. package/src/prompts/system/ttsr-interrupt.md +7 -0
  1053. package/src/prompts/system/ttsr-tool-reminder.md +5 -0
  1054. package/src/prompts/system/ultrathink-notice.md +3 -0
  1055. package/src/prompts/system/unexpected-stop-classifier.md +17 -0
  1056. package/src/prompts/system/unexpected-stop-retry.md +4 -0
  1057. package/src/prompts/system/vibe-mode-active.md +26 -0
  1058. package/src/prompts/system/web-search.md +25 -0
  1059. package/src/prompts/system/workflow-notice.md +112 -0
  1060. package/src/prompts/system/xdev-mount-notice.md +20 -0
  1061. package/src/prompts/tools/apply-patch.md +65 -0
  1062. package/src/prompts/tools/ask.md +22 -0
  1063. package/src/prompts/tools/ast-edit.md +11 -0
  1064. package/src/prompts/tools/ast-grep.md +19 -0
  1065. package/src/prompts/tools/async-result.md +8 -0
  1066. package/src/prompts/tools/bash.md +23 -0
  1067. package/src/prompts/tools/browser.md +29 -0
  1068. package/src/prompts/tools/checkpoint.md +15 -0
  1069. package/src/prompts/tools/computer.md +26 -0
  1070. package/src/prompts/tools/debug.md +3 -0
  1071. package/src/prompts/tools/eval.md +45 -0
  1072. package/src/prompts/tools/github.md +22 -0
  1073. package/src/prompts/tools/glob.md +16 -0
  1074. package/src/prompts/tools/goal.md +11 -0
  1075. package/src/prompts/tools/grep.md +13 -0
  1076. package/src/prompts/tools/hub.md +34 -0
  1077. package/src/prompts/tools/image-attachment-describe-system.md +8 -0
  1078. package/src/prompts/tools/image-attachment-describe.md +10 -0
  1079. package/src/prompts/tools/image-gen.md +7 -0
  1080. package/src/prompts/tools/inspect-image-system.md +20 -0
  1081. package/src/prompts/tools/inspect-image.md +22 -0
  1082. package/src/prompts/tools/learn.md +7 -0
  1083. package/src/prompts/tools/lsp-late-diagnostic.md +8 -0
  1084. package/src/prompts/tools/lsp.md +19 -0
  1085. package/src/prompts/tools/manage-skill.md +9 -0
  1086. package/src/prompts/tools/memory-edit.md +12 -0
  1087. package/src/prompts/tools/patch.md +57 -0
  1088. package/src/prompts/tools/read.md +27 -0
  1089. package/src/prompts/tools/recall.md +7 -0
  1090. package/src/prompts/tools/reflect.md +5 -0
  1091. package/src/prompts/tools/replace.md +30 -0
  1092. package/src/prompts/tools/retain.md +6 -0
  1093. package/src/prompts/tools/rewind.md +14 -0
  1094. package/src/prompts/tools/security-publish.md +1 -0
  1095. package/src/prompts/tools/security-scan.md +1 -0
  1096. package/src/prompts/tools/task-async-contract.md +1 -0
  1097. package/src/prompts/tools/task-summary.md +20 -0
  1098. package/src/prompts/tools/task.md +83 -0
  1099. package/src/prompts/tools/todo.md +42 -0
  1100. package/src/prompts/tools/vibe-kill.md +3 -0
  1101. package/src/prompts/tools/vibe-list.md +3 -0
  1102. package/src/prompts/tools/vibe-send.md +9 -0
  1103. package/src/prompts/tools/vibe-spawn.md +10 -0
  1104. package/src/prompts/tools/vibe-turn-result.md +19 -0
  1105. package/src/prompts/tools/vibe-wait.md +8 -0
  1106. package/src/prompts/tools/web-search.md +8 -0
  1107. package/src/prompts/tools/write.md +14 -0
  1108. package/src/registry/agent-lifecycle.ts +505 -0
  1109. package/src/registry/agent-registry.ts +287 -0
  1110. package/src/registry/persisted-agents.ts +423 -0
  1111. package/src/sdk.ts +3820 -0
  1112. package/src/secrets/index.ts +375 -0
  1113. package/src/secrets/obfuscator.ts +2629 -0
  1114. package/src/secrets/regex.ts +21 -0
  1115. package/src/security/auth.ts +98 -0
  1116. package/src/security/cloud.ts +686 -0
  1117. package/src/security/comparison.ts +255 -0
  1118. package/src/security/contracts/ids.ts +111 -0
  1119. package/src/security/contracts/index.ts +4 -0
  1120. package/src/security/contracts/schemas.ts +219 -0
  1121. package/src/security/contracts/types.ts +254 -0
  1122. package/src/security/contracts/validation.ts +69 -0
  1123. package/src/security/coordinator.ts +708 -0
  1124. package/src/security/importers/codex-security.ts +387 -0
  1125. package/src/security/importers/index.ts +2 -0
  1126. package/src/security/importers/sarif.ts +357 -0
  1127. package/src/security/index.ts +13 -0
  1128. package/src/security/preflight.ts +405 -0
  1129. package/src/security/provenance.ts +106 -0
  1130. package/src/security/publication.ts +326 -0
  1131. package/src/security/remediation.ts +93 -0
  1132. package/src/security/resource-output.ts +50 -0
  1133. package/src/security/sarif.ts +78 -0
  1134. package/src/security/store.ts +430 -0
  1135. package/src/session/acp-permission-gate.ts +165 -0
  1136. package/src/session/agent-session-events.ts +67 -0
  1137. package/src/session/agent-session-types.ts +407 -0
  1138. package/src/session/agent-session.ts +9000 -0
  1139. package/src/session/agent-storage.ts +807 -0
  1140. package/src/session/artifacts.ts +154 -0
  1141. package/src/session/async-job-delivery.ts +82 -0
  1142. package/src/session/auth-broker-config.ts +92 -0
  1143. package/src/session/auth-storage.ts +25 -0
  1144. package/src/session/bash-runner.ts +326 -0
  1145. package/src/session/blob-store.ts +295 -0
  1146. package/src/session/checkpoint-entries.ts +81 -0
  1147. package/src/session/claude-session-store.ts +426 -0
  1148. package/src/session/client-bridge.ts +85 -0
  1149. package/src/session/codex-auto-reset.ts +673 -0
  1150. package/src/session/codex-session-store.ts +673 -0
  1151. package/src/session/compact-modes.ts +105 -0
  1152. package/src/session/credential-pin.ts +93 -0
  1153. package/src/session/eval-runner.ts +219 -0
  1154. package/src/session/exit-diagnostics.ts +310 -0
  1155. package/src/session/foreign-session-import.ts +52 -0
  1156. package/src/session/foreign-session-jsonl.ts +29 -0
  1157. package/src/session/foreign-session-store.ts +26 -0
  1158. package/src/session/history-storage.ts +329 -0
  1159. package/src/session/indexed-session-storage.ts +553 -0
  1160. package/src/session/irc-bridge.ts +203 -0
  1161. package/src/session/launch-completion.ts +37 -0
  1162. package/src/session/messages.ts +1304 -0
  1163. package/src/session/model-controls.ts +757 -0
  1164. package/src/session/prewalk.ts +279 -0
  1165. package/src/session/provider-image-budget.ts +86 -0
  1166. package/src/session/queued-messages.ts +99 -0
  1167. package/src/session/redis-session-storage.ts +257 -0
  1168. package/src/session/retry-fallback-chains.ts +455 -0
  1169. package/src/session/role-models.ts +85 -0
  1170. package/src/session/session-advisors.ts +1896 -0
  1171. package/src/session/session-context.ts +584 -0
  1172. package/src/session/session-dump-format.ts +216 -0
  1173. package/src/session/session-entries.ts +306 -0
  1174. package/src/session/session-handoff.ts +325 -0
  1175. package/src/session/session-history-format.ts +459 -0
  1176. package/src/session/session-listing.ts +715 -0
  1177. package/src/session/session-loader.ts +362 -0
  1178. package/src/session/session-maintenance.ts +3115 -0
  1179. package/src/session/session-manager.ts +2747 -0
  1180. package/src/session/session-memory.ts +222 -0
  1181. package/src/session/session-metadata.ts +53 -0
  1182. package/src/session/session-migrations.ts +78 -0
  1183. package/src/session/session-paths.ts +280 -0
  1184. package/src/session/session-persistence.ts +293 -0
  1185. package/src/session/session-provider-boundary.ts +306 -0
  1186. package/src/session/session-stats.ts +349 -0
  1187. package/src/session/session-storage.ts +774 -0
  1188. package/src/session/session-title-slot.ts +141 -0
  1189. package/src/session/session-tools.ts +1385 -0
  1190. package/src/session/session-workspace.ts +53 -0
  1191. package/src/session/settings-stream-fn.ts +79 -0
  1192. package/src/session/shake-types.ts +43 -0
  1193. package/src/session/snapcompact-inline.ts +545 -0
  1194. package/src/session/snapcompact-savings-journal.ts +113 -0
  1195. package/src/session/sql-session-storage.ts +374 -0
  1196. package/src/session/stream-guards.ts +417 -0
  1197. package/src/session/streaming-output.ts +1459 -0
  1198. package/src/session/todo-tracker.ts +380 -0
  1199. package/src/session/tool-choice-queue.ts +305 -0
  1200. package/src/session/ttsr-coordinator.ts +496 -0
  1201. package/src/session/turn-persistence.ts +142 -0
  1202. package/src/session/turn-recovery.ts +2005 -0
  1203. package/src/session/unexpected-stop-classifier.ts +141 -0
  1204. package/src/session/yield-queue.ts +288 -0
  1205. package/src/slash-commands/acp-builtins.ts +70 -0
  1206. package/src/slash-commands/available-commands.ts +105 -0
  1207. package/src/slash-commands/builtin-registry.ts +3149 -0
  1208. package/src/slash-commands/helpers/active-oauth-account.ts +80 -0
  1209. package/src/slash-commands/helpers/collab-qrcode.ts +28 -0
  1210. package/src/slash-commands/helpers/context-report.ts +66 -0
  1211. package/src/slash-commands/helpers/format.ts +46 -0
  1212. package/src/slash-commands/helpers/logout.ts +108 -0
  1213. package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
  1214. package/src/slash-commands/helpers/mcp.ts +533 -0
  1215. package/src/slash-commands/helpers/parse.ts +85 -0
  1216. package/src/slash-commands/helpers/reset-usage.ts +68 -0
  1217. package/src/slash-commands/helpers/security.ts +451 -0
  1218. package/src/slash-commands/helpers/session-pin.ts +44 -0
  1219. package/src/slash-commands/helpers/ssh.ts +196 -0
  1220. package/src/slash-commands/helpers/stats-dashboard.ts +86 -0
  1221. package/src/slash-commands/helpers/todo.ts +285 -0
  1222. package/src/slash-commands/helpers/usage-report.ts +198 -0
  1223. package/src/slash-commands/marketplace-install-parser.ts +99 -0
  1224. package/src/slash-commands/types.ts +139 -0
  1225. package/src/ssh/config-writer.ts +183 -0
  1226. package/src/ssh/connection-manager.ts +667 -0
  1227. package/src/ssh/file-transfer.ts +214 -0
  1228. package/src/ssh/sshfs-mount.ts +163 -0
  1229. package/src/ssh/utils.ts +51 -0
  1230. package/src/startup-splash.ts +19 -0
  1231. package/src/stt/asr-client.ts +401 -0
  1232. package/src/stt/asr-protocol.ts +65 -0
  1233. package/src/stt/asr-worker.ts +603 -0
  1234. package/src/stt/downloader.ts +142 -0
  1235. package/src/stt/endpointer.ts +259 -0
  1236. package/src/stt/index.ts +6 -0
  1237. package/src/stt/models.ts +150 -0
  1238. package/src/stt/sherpa-runtime.ts +71 -0
  1239. package/src/stt/stt-controller.ts +320 -0
  1240. package/src/stt/submit-trigger.ts +74 -0
  1241. package/src/subprocess/worker-client.ts +463 -0
  1242. package/src/subprocess/worker-runtime.ts +494 -0
  1243. package/src/system-prompt.ts +921 -0
  1244. package/src/task/agents.ts +170 -0
  1245. package/src/task/commands.ts +132 -0
  1246. package/src/task/discovery.ts +145 -0
  1247. package/src/task/executor.ts +3431 -0
  1248. package/src/task/index.ts +1515 -0
  1249. package/src/task/isolation-ownership.ts +106 -0
  1250. package/src/task/isolation-runner.ts +444 -0
  1251. package/src/task/label.ts +40 -0
  1252. package/src/task/name-generator.ts +1577 -0
  1253. package/src/task/omp-command.ts +26 -0
  1254. package/src/task/output-manager.ts +115 -0
  1255. package/src/task/parallel.ts +221 -0
  1256. package/src/task/persisted-revive.ts +173 -0
  1257. package/src/task/prewalk.ts +6 -0
  1258. package/src/task/prompt-policy.ts +8 -0
  1259. package/src/task/provider-concurrency.ts +100 -0
  1260. package/src/task/read-only-policy.ts +27 -0
  1261. package/src/task/render.ts +1830 -0
  1262. package/src/task/renderer.ts +14 -0
  1263. package/src/task/repair-args.ts +118 -0
  1264. package/src/task/spawn-policy.ts +72 -0
  1265. package/src/task/structured-subagent.ts +676 -0
  1266. package/src/task/subprocess-tool-registry.ts +88 -0
  1267. package/src/task/types.ts +555 -0
  1268. package/src/task/worktree.ts +967 -0
  1269. package/src/task/yield-assembly.ts +198 -0
  1270. package/src/telemetry-export.ts +504 -0
  1271. package/src/thinking.ts +377 -0
  1272. package/src/tiny/device.ts +111 -0
  1273. package/src/tiny/dtype.ts +101 -0
  1274. package/src/tiny/message-preproc.ts +155 -0
  1275. package/src/tiny/models.ts +268 -0
  1276. package/src/tiny/text.ts +290 -0
  1277. package/src/tiny/title-client.ts +460 -0
  1278. package/src/tiny/title-protocol.ts +56 -0
  1279. package/src/tiny/worker.ts +354 -0
  1280. package/src/tools/acp-bridge.ts +125 -0
  1281. package/src/tools/approval.ts +245 -0
  1282. package/src/tools/ask.ts +1459 -0
  1283. package/src/tools/ast-edit.ts +718 -0
  1284. package/src/tools/ast-grep.ts +526 -0
  1285. package/src/tools/auto-generated-guard.ts +335 -0
  1286. package/src/tools/bash-interactive.ts +435 -0
  1287. package/src/tools/bash-interceptor.ts +148 -0
  1288. package/src/tools/bash-pty-selection.ts +14 -0
  1289. package/src/tools/bash-skill-urls.ts +350 -0
  1290. package/src/tools/bash.ts +1826 -0
  1291. package/src/tools/browser/aria/aria-snapshot.bundle.txt +7 -0
  1292. package/src/tools/browser/aria/aria-snapshot.ts +131 -0
  1293. package/src/tools/browser/attach.ts +219 -0
  1294. package/src/tools/browser/cmux/cmux-tab.ts +1531 -0
  1295. package/src/tools/browser/cmux/rpc.ts +200 -0
  1296. package/src/tools/browser/cmux/socket-client.ts +445 -0
  1297. package/src/tools/browser/launch.ts +939 -0
  1298. package/src/tools/browser/readable.ts +112 -0
  1299. package/src/tools/browser/registry.ts +447 -0
  1300. package/src/tools/browser/relay/bridge.ts +945 -0
  1301. package/src/tools/browser/relay/daemon.ts +117 -0
  1302. package/src/tools/browser/relay/extension-assets/background.js.txt +242 -0
  1303. package/src/tools/browser/relay/extension-assets/manifest.json.txt +14 -0
  1304. package/src/tools/browser/relay/extension-assets/options.html.txt +53 -0
  1305. package/src/tools/browser/relay/extension-assets/options.js.txt +23 -0
  1306. package/src/tools/browser/relay/kind.ts +41 -0
  1307. package/src/tools/browser/relay/protocol.ts +54 -0
  1308. package/src/tools/browser/relay/server.ts +141 -0
  1309. package/src/tools/browser/render.ts +229 -0
  1310. package/src/tools/browser/run-output.ts +76 -0
  1311. package/src/tools/browser/shared-daemon.ts +139 -0
  1312. package/src/tools/browser/tab-protocol.ts +123 -0
  1313. package/src/tools/browser/tab-supervisor.ts +1077 -0
  1314. package/src/tools/browser/tab-worker-entry.ts +29 -0
  1315. package/src/tools/browser/tab-worker.ts +1982 -0
  1316. package/src/tools/browser.ts +487 -0
  1317. package/src/tools/builtin-names.ts +67 -0
  1318. package/src/tools/checkpoint.ts +137 -0
  1319. package/src/tools/computer/exposure.ts +14 -0
  1320. package/src/tools/computer/protocol.ts +69 -0
  1321. package/src/tools/computer/supervisor.ts +409 -0
  1322. package/src/tools/computer/worker-entry.ts +39 -0
  1323. package/src/tools/computer/worker.ts +745 -0
  1324. package/src/tools/computer-renderer.ts +147 -0
  1325. package/src/tools/computer.ts +222 -0
  1326. package/src/tools/conflict-detect.ts +815 -0
  1327. package/src/tools/context.ts +49 -0
  1328. package/src/tools/debug.ts +1121 -0
  1329. package/src/tools/default-renderer.ts +154 -0
  1330. package/src/tools/essential-tools.ts +46 -0
  1331. package/src/tools/eval-backends.ts +34 -0
  1332. package/src/tools/eval-format/index.ts +24 -0
  1333. package/src/tools/eval-format/javascript.ts +952 -0
  1334. package/src/tools/eval-format/julia.ts +446 -0
  1335. package/src/tools/eval-format/python.ts +544 -0
  1336. package/src/tools/eval-format/ruby.ts +380 -0
  1337. package/src/tools/eval-render.ts +784 -0
  1338. package/src/tools/eval.ts +774 -0
  1339. package/src/tools/fetch.ts +1889 -0
  1340. package/src/tools/file-recorder.ts +35 -0
  1341. package/src/tools/fs-cache-invalidation.ts +28 -0
  1342. package/src/tools/gh-cache-invalidation.ts +175 -0
  1343. package/src/tools/gh-format.ts +12 -0
  1344. package/src/tools/gh-renderer.ts +484 -0
  1345. package/src/tools/gh.ts +3958 -0
  1346. package/src/tools/github-cache.ts +663 -0
  1347. package/src/tools/glob.ts +691 -0
  1348. package/src/tools/grep.ts +1920 -0
  1349. package/src/tools/grouped-file-output.ts +210 -0
  1350. package/src/tools/hub/index.ts +579 -0
  1351. package/src/tools/hub/jobs.ts +714 -0
  1352. package/src/tools/hub/launch.ts +690 -0
  1353. package/src/tools/hub/messaging.ts +735 -0
  1354. package/src/tools/hub/types.ts +117 -0
  1355. package/src/tools/image-gen.ts +1689 -0
  1356. package/src/tools/image-providers.ts +50 -0
  1357. package/src/tools/index.ts +736 -0
  1358. package/src/tools/inspect-image-renderer.ts +133 -0
  1359. package/src/tools/inspect-image.ts +316 -0
  1360. package/src/tools/json-tree.ts +260 -0
  1361. package/src/tools/jtd-to-json-schema.ts +219 -0
  1362. package/src/tools/jtd-to-typescript.ts +136 -0
  1363. package/src/tools/jtd-utils.ts +102 -0
  1364. package/src/tools/learn.ts +141 -0
  1365. package/src/tools/list-limit.ts +40 -0
  1366. package/src/tools/manage-skill.ts +102 -0
  1367. package/src/tools/match-line-format.ts +20 -0
  1368. package/src/tools/memory-edit.ts +61 -0
  1369. package/src/tools/memory-recall.ts +102 -0
  1370. package/src/tools/memory-reflect.ts +88 -0
  1371. package/src/tools/memory-render.ts +211 -0
  1372. package/src/tools/memory-retain.ts +89 -0
  1373. package/src/tools/output-meta.ts +860 -0
  1374. package/src/tools/output-schema-validator.ts +307 -0
  1375. package/src/tools/path-utils.ts +1489 -0
  1376. package/src/tools/plan-mode-guard.ts +155 -0
  1377. package/src/tools/puppeteer/00_stealth_tampering.txt +44 -0
  1378. package/src/tools/puppeteer/01_stealth_activity.txt +80 -0
  1379. package/src/tools/puppeteer/02_stealth_hairline.txt +57 -0
  1380. package/src/tools/puppeteer/03_stealth_botd.txt +380 -0
  1381. package/src/tools/puppeteer/04_stealth_iframe.txt +174 -0
  1382. package/src/tools/puppeteer/05_stealth_webgl.txt +233 -0
  1383. package/src/tools/puppeteer/06_stealth_screen.txt +260 -0
  1384. package/src/tools/puppeteer/07_stealth_fonts.txt +99 -0
  1385. package/src/tools/puppeteer/08_stealth_audio.txt +63 -0
  1386. package/src/tools/puppeteer/09_stealth_locale.txt +51 -0
  1387. package/src/tools/puppeteer/10_stealth_plugins.txt +212 -0
  1388. package/src/tools/puppeteer/11_stealth_hardware.txt +59 -0
  1389. package/src/tools/puppeteer/12_stealth_codecs.txt +42 -0
  1390. package/src/tools/puppeteer/13_stealth_worker.txt +235 -0
  1391. package/src/tools/read.ts +3762 -0
  1392. package/src/tools/render-utils.ts +926 -0
  1393. package/src/tools/renderers.ts +133 -0
  1394. package/src/tools/report-tool-issue.ts +568 -0
  1395. package/src/tools/resolve.ts +423 -0
  1396. package/src/tools/review.ts +103 -0
  1397. package/src/tools/run-scope.ts +417 -0
  1398. package/src/tools/security-scan.ts +287 -0
  1399. package/src/tools/shell-tokenize.ts +213 -0
  1400. package/src/tools/sqlite-reader.ts +884 -0
  1401. package/src/tools/terminal-output.ts +141 -0
  1402. package/src/tools/todo.ts +1226 -0
  1403. package/src/tools/tool-errors.ts +62 -0
  1404. package/src/tools/tool-result.ts +102 -0
  1405. package/src/tools/tool-timeouts.ts +38 -0
  1406. package/src/tools/tts.ts +266 -0
  1407. package/src/tools/vibe.ts +608 -0
  1408. package/src/tools/write.ts +1646 -0
  1409. package/src/tools/xdev.ts +560 -0
  1410. package/src/tools/yield.ts +486 -0
  1411. package/src/tts/downloader.ts +64 -0
  1412. package/src/tts/index.ts +10 -0
  1413. package/src/tts/models.ts +137 -0
  1414. package/src/tts/runtime.ts +21 -0
  1415. package/src/tts/speakable.ts +392 -0
  1416. package/src/tts/speech-enhancer.ts +206 -0
  1417. package/src/tts/streaming-player.ts +120 -0
  1418. package/src/tts/tts-client.ts +475 -0
  1419. package/src/tts/tts-protocol.ts +69 -0
  1420. package/src/tts/tts-worker.ts +434 -0
  1421. package/src/tts/vocalizer.ts +419 -0
  1422. package/src/tts/wav.ts +58 -0
  1423. package/src/tui/code-cell.ts +268 -0
  1424. package/src/tui/file-list.ts +55 -0
  1425. package/src/tui/hyperlink.ts +178 -0
  1426. package/src/tui/index.ts +13 -0
  1427. package/src/tui/output-block.ts +268 -0
  1428. package/src/tui/status-line.ts +54 -0
  1429. package/src/tui/tree-list.ts +172 -0
  1430. package/src/tui/types.ts +15 -0
  1431. package/src/tui/utils.ts +103 -0
  1432. package/src/tui/width-aware-text.ts +58 -0
  1433. package/src/utils/active-repo-context.ts +143 -0
  1434. package/src/utils/block-context.ts +312 -0
  1435. package/src/utils/changelog.ts +373 -0
  1436. package/src/utils/clipboard.ts +361 -0
  1437. package/src/utils/command-args.ts +74 -0
  1438. package/src/utils/commit-message-generator.ts +148 -0
  1439. package/src/utils/cpuprofile.ts +235 -0
  1440. package/src/utils/edit-mode.ts +61 -0
  1441. package/src/utils/enhanced-paste.ts +230 -0
  1442. package/src/utils/event-bus.ts +33 -0
  1443. package/src/utils/external-editor.ts +80 -0
  1444. package/src/utils/fetch-timeout.ts +10 -0
  1445. package/src/utils/file-display-mode.ts +44 -0
  1446. package/src/utils/file-mentions.ts +293 -0
  1447. package/src/utils/git.ts +2472 -0
  1448. package/src/utils/image-loading.ts +236 -0
  1449. package/src/utils/image-resize.ts +420 -0
  1450. package/src/utils/image-vision-fallback.ts +196 -0
  1451. package/src/utils/inspect-image-mode.ts +39 -0
  1452. package/src/utils/ipc.ts +38 -0
  1453. package/src/utils/jj.ts +422 -0
  1454. package/src/utils/lang-from-path.ts +251 -0
  1455. package/src/utils/late-cleanup.ts +17 -0
  1456. package/src/utils/local-date.ts +7 -0
  1457. package/src/utils/mac-file-urls.applescript +37 -0
  1458. package/src/utils/markit-cache.ts +166 -0
  1459. package/src/utils/markit.ts +223 -0
  1460. package/src/utils/mupdf-wasm-embed.ts +12 -0
  1461. package/src/utils/open.ts +126 -0
  1462. package/src/utils/profile-tree.ts +111 -0
  1463. package/src/utils/prompt-path.ts +3 -0
  1464. package/src/utils/qrcode.ts +535 -0
  1465. package/src/utils/sample-profile.ts +437 -0
  1466. package/src/utils/session-color.ts +142 -0
  1467. package/src/utils/shell-snapshot-fn-env.sh +63 -0
  1468. package/src/utils/shell-snapshot.ts +326 -0
  1469. package/src/utils/sixel.ts +69 -0
  1470. package/src/utils/thinking-display.ts +163 -0
  1471. package/src/utils/title-generator.ts +597 -0
  1472. package/src/utils/token-rate.ts +72 -0
  1473. package/src/utils/tool-choice.ts +50 -0
  1474. package/src/utils/tools-manager.ts +411 -0
  1475. package/src/utils/turndown.ts +82 -0
  1476. package/src/utils/zip.ts +1106 -0
  1477. package/src/vibe/runtime.ts +1684 -0
  1478. package/src/vibe/state.ts +4 -0
  1479. package/src/web/kagi.ts +305 -0
  1480. package/src/web/parallel.ts +354 -0
  1481. package/src/web/scrapers/artifacthub.ts +207 -0
  1482. package/src/web/scrapers/arxiv.ts +83 -0
  1483. package/src/web/scrapers/aur.ts +162 -0
  1484. package/src/web/scrapers/biorxiv.ts +133 -0
  1485. package/src/web/scrapers/bluesky.ts +262 -0
  1486. package/src/web/scrapers/brew.ts +172 -0
  1487. package/src/web/scrapers/cheatsh.ts +68 -0
  1488. package/src/web/scrapers/chocolatey.ts +196 -0
  1489. package/src/web/scrapers/choosealicense.ts +95 -0
  1490. package/src/web/scrapers/cisa-kev.ts +87 -0
  1491. package/src/web/scrapers/clojars.ts +154 -0
  1492. package/src/web/scrapers/coingecko.ts +177 -0
  1493. package/src/web/scrapers/crates-io.ts +97 -0
  1494. package/src/web/scrapers/crossref.ts +136 -0
  1495. package/src/web/scrapers/devto.ts +147 -0
  1496. package/src/web/scrapers/discogs.ts +306 -0
  1497. package/src/web/scrapers/discourse.ts +197 -0
  1498. package/src/web/scrapers/dockerhub.ts +138 -0
  1499. package/src/web/scrapers/docs-rs.ts +663 -0
  1500. package/src/web/scrapers/fdroid.ts +134 -0
  1501. package/src/web/scrapers/firefox-addons.ts +191 -0
  1502. package/src/web/scrapers/flathub.ts +223 -0
  1503. package/src/web/scrapers/github-gist.ts +58 -0
  1504. package/src/web/scrapers/github.ts +800 -0
  1505. package/src/web/scrapers/gitlab.ts +401 -0
  1506. package/src/web/scrapers/go-pkg.ts +266 -0
  1507. package/src/web/scrapers/hackage.ts +129 -0
  1508. package/src/web/scrapers/hackernews.ts +189 -0
  1509. package/src/web/scrapers/hex.ts +105 -0
  1510. package/src/web/scrapers/huggingface.ts +321 -0
  1511. package/src/web/scrapers/iacr.ts +89 -0
  1512. package/src/web/scrapers/index.ts +252 -0
  1513. package/src/web/scrapers/jetbrains-marketplace.ts +159 -0
  1514. package/src/web/scrapers/lemmy.ts +203 -0
  1515. package/src/web/scrapers/lobsters.ts +175 -0
  1516. package/src/web/scrapers/mastodon.ts +292 -0
  1517. package/src/web/scrapers/maven.ts +138 -0
  1518. package/src/web/scrapers/mdn.ts +173 -0
  1519. package/src/web/scrapers/metacpan.ts +222 -0
  1520. package/src/web/scrapers/musicbrainz.ts +250 -0
  1521. package/src/web/scrapers/npm.ts +98 -0
  1522. package/src/web/scrapers/nuget.ts +183 -0
  1523. package/src/web/scrapers/nvd.ts +222 -0
  1524. package/src/web/scrapers/ollama.ts +239 -0
  1525. package/src/web/scrapers/open-vsx.ts +106 -0
  1526. package/src/web/scrapers/opencorporates.ts +292 -0
  1527. package/src/web/scrapers/openlibrary.ts +336 -0
  1528. package/src/web/scrapers/orcid.ts +286 -0
  1529. package/src/web/scrapers/osv.ts +176 -0
  1530. package/src/web/scrapers/packagist.ts +160 -0
  1531. package/src/web/scrapers/pub-dev.ts +143 -0
  1532. package/src/web/scrapers/pubmed.ts +211 -0
  1533. package/src/web/scrapers/pypi.ts +112 -0
  1534. package/src/web/scrapers/rawg.ts +110 -0
  1535. package/src/web/scrapers/readthedocs.ts +121 -0
  1536. package/src/web/scrapers/reddit.ts +95 -0
  1537. package/src/web/scrapers/repology.ts +251 -0
  1538. package/src/web/scrapers/rfc.ts +201 -0
  1539. package/src/web/scrapers/rubygems.ts +103 -0
  1540. package/src/web/scrapers/searchcode.ts +189 -0
  1541. package/src/web/scrapers/sec-edgar.ts +261 -0
  1542. package/src/web/scrapers/semantic-scholar.ts +171 -0
  1543. package/src/web/scrapers/snapcraft.ts +187 -0
  1544. package/src/web/scrapers/sourcegraph.ts +336 -0
  1545. package/src/web/scrapers/spdx.ts +108 -0
  1546. package/src/web/scrapers/spotify.ts +198 -0
  1547. package/src/web/scrapers/stackoverflow.ts +120 -0
  1548. package/src/web/scrapers/terraform.ts +277 -0
  1549. package/src/web/scrapers/tldr.ts +47 -0
  1550. package/src/web/scrapers/twitter.ts +94 -0
  1551. package/src/web/scrapers/types.ts +354 -0
  1552. package/src/web/scrapers/utils.ts +109 -0
  1553. package/src/web/scrapers/vimeo.ts +133 -0
  1554. package/src/web/scrapers/vscode-marketplace.ts +187 -0
  1555. package/src/web/scrapers/w3c.ts +156 -0
  1556. package/src/web/scrapers/wikidata.ts +344 -0
  1557. package/src/web/scrapers/wikipedia.ts +84 -0
  1558. package/src/web/scrapers/youtube.ts +325 -0
  1559. package/src/web/search/index.ts +393 -0
  1560. package/src/web/search/provider.ts +272 -0
  1561. package/src/web/search/providers/anthropic.ts +406 -0
  1562. package/src/web/search/providers/base.ts +112 -0
  1563. package/src/web/search/providers/brave.ts +181 -0
  1564. package/src/web/search/providers/browser-headers.ts +82 -0
  1565. package/src/web/search/providers/browser-page.ts +125 -0
  1566. package/src/web/search/providers/codex.ts +788 -0
  1567. package/src/web/search/providers/duckduckgo.ts +382 -0
  1568. package/src/web/search/providers/ecosia.ts +183 -0
  1569. package/src/web/search/providers/exa.ts +513 -0
  1570. package/src/web/search/providers/firecrawl.ts +213 -0
  1571. package/src/web/search/providers/gemini.ts +634 -0
  1572. package/src/web/search/providers/google.ts +195 -0
  1573. package/src/web/search/providers/jina.ts +144 -0
  1574. package/src/web/search/providers/kagi.ts +98 -0
  1575. package/src/web/search/providers/kimi.ts +221 -0
  1576. package/src/web/search/providers/mojeek.ts +220 -0
  1577. package/src/web/search/providers/parallel.ts +186 -0
  1578. package/src/web/search/providers/perplexity-auth.ts +142 -0
  1579. package/src/web/search/providers/perplexity.ts +997 -0
  1580. package/src/web/search/providers/public.ts +199 -0
  1581. package/src/web/search/providers/searxng.ts +467 -0
  1582. package/src/web/search/providers/startpage.ts +225 -0
  1583. package/src/web/search/providers/synthetic.ts +126 -0
  1584. package/src/web/search/providers/tavily.ts +244 -0
  1585. package/src/web/search/providers/tinyfish.ts +166 -0
  1586. package/src/web/search/providers/utils.ts +130 -0
  1587. package/src/web/search/providers/xai.ts +372 -0
  1588. package/src/web/search/providers/zai.ts +452 -0
  1589. package/src/web/search/query.ts +850 -0
  1590. package/src/web/search/render.ts +262 -0
  1591. package/src/web/search/types.ts +518 -0
  1592. package/src/web/search/utils.ts +17 -0
  1593. package/src/workspace-tree.ts +326 -0
@@ -0,0 +1,2747 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type {
4
+ ImageContent,
5
+ Message,
6
+ MessageAttribution,
7
+ ServiceTierByFamily,
8
+ TextContent,
9
+ Usage,
10
+ } from "@oh-my-pi/pi-ai";
11
+ import {
12
+ directoryExists,
13
+ getBlobsDir,
14
+ getProjectDir,
15
+ getSessionsDir,
16
+ isEnoent,
17
+ logger,
18
+ stringifyJson,
19
+ toError,
20
+ } from "@oh-my-pi/pi-utils";
21
+ import type { StructuredSubagentSchemaMode } from "../task/types";
22
+ import { ArtifactManager } from "./artifacts";
23
+ import { type BlobPutOptions, type BlobPutResult, BlobStore } from "./blob-store";
24
+ import {
25
+ type BashExecutionMessage,
26
+ type CustomMessage,
27
+ type FileMentionMessage,
28
+ type HookMessage,
29
+ normalizeCustomMessagePayload,
30
+ type PythonExecutionMessage,
31
+ sanitizeRehydratedOpenAIResponsesAssistantMessage,
32
+ stripInternalDetailsFields,
33
+ } from "./messages";
34
+ import { type BuildSessionContextOptions, buildSessionContext, type SessionContext } from "./session-context";
35
+ import {
36
+ type BranchSummaryEntry,
37
+ type CompactionEntry,
38
+ type CredentialPinEntry,
39
+ CURRENT_SESSION_VERSION,
40
+ type CustomEntry,
41
+ type CustomMessageEntry,
42
+ type FileEntry,
43
+ type LabelEntry,
44
+ type ModeChangeEntry,
45
+ type ModelChangeEntry,
46
+ type NewSessionOptions,
47
+ type ResetBoundaryEntry,
48
+ type ServiceTierChangeEntry,
49
+ type SessionEntry,
50
+ type SessionHeader,
51
+ type SessionInitEntry,
52
+ type SessionMessageEntry,
53
+ type SessionTitleSource,
54
+ type SessionTreeNode,
55
+ type ThinkingLevelChangeEntry,
56
+ TITLE_CHANGE_ENTRY_TYPE,
57
+ type TitleChangeEntry,
58
+ type TtsrInjectionEntry,
59
+ type UsageStatistics,
60
+ } from "./session-entries";
61
+ import { findMostRecentSession, listAllSessions, listSessions, type SessionInfo } from "./session-listing";
62
+ import { loadEntriesFromFile, readTitleSlotFromFile, resolveBlobRefsInEntries } from "./session-loader";
63
+ import { generateId, migrateToCurrentVersion } from "./session-migrations";
64
+ import {
65
+ computeDefaultSessionDir,
66
+ readTerminalBreadcrumbEntry,
67
+ resolveManagedSessionRoot,
68
+ writeTerminalBreadcrumb,
69
+ } from "./session-paths";
70
+ import { prepareEntryForPersistence } from "./session-persistence";
71
+ import {
72
+ FileSessionStorage,
73
+ MemorySessionStorage,
74
+ type SessionStorage,
75
+ type SessionStorageWriter,
76
+ } from "./session-storage";
77
+ import { type SessionTitleUpdate, serializeTitleSlot } from "./session-title-slot";
78
+ import {
79
+ additionalWorkspaceDirectories,
80
+ normalizeSessionWorkspace,
81
+ normalizeWorkspaceDirectory,
82
+ } from "./session-workspace";
83
+
84
+ const JSONL_SUFFIX_LENGTH = ".jsonl".length;
85
+ const DRAFT_ONLY_SESSION_MARKER = ".draft-only-session";
86
+
87
+ function mintSessionId(): string {
88
+ return Bun.randomUUIDv7();
89
+ }
90
+
91
+ function nowIso(): string {
92
+ return new Date().toISOString();
93
+ }
94
+
95
+ function fileSafeTimestamp(iso: string): string {
96
+ return iso.replace(/[:.]/g, "-");
97
+ }
98
+
99
+ function artifactsDirectoryFor(sessionFile: string | undefined): string | null {
100
+ return sessionFile ? sessionFile.slice(0, -JSONL_SUFFIX_LENGTH) : null;
101
+ }
102
+
103
+ /**
104
+ * Resolve a breadcrumb's recorded session file to its interactive root. Subagent
105
+ * (and other artifact) sessions live inside a parent session's artifacts dir —
106
+ * `<parent>.jsonl` strips its suffix to `<parent>/`, and a child writes
107
+ * `<parent>/<agentId>.jsonl`. A breadcrumb that points at such a child — a
108
+ * pre-fix poisoned crumb left by a subagent that opened in the parent's TTY, or
109
+ * any nested artifact — must resolve back up to the top-level session so
110
+ * `--continue` resumes the real conversation instead of a subagent transcript.
111
+ */
112
+ function resolveBreadcrumbToInteractiveRoot(sessionFile: string): string {
113
+ let current = path.resolve(sessionFile);
114
+ // Walk up while the containing dir is itself a session's artifacts dir
115
+ // (`<dir>.jsonl` exists). Capped to defend against pathological layouts.
116
+ for (let depth = 0; depth < 8; depth++) {
117
+ const parentSessionFile = `${path.dirname(current)}.jsonl`;
118
+ if (!fs.existsSync(parentSessionFile)) return current;
119
+ current = parentSessionFile;
120
+ }
121
+ return current;
122
+ }
123
+
124
+ function emptyUsageStatistics(): UsageStatistics {
125
+ return {
126
+ input: 0,
127
+ output: 0,
128
+ cacheRead: 0,
129
+ cacheWrite: 0,
130
+ totalTokens: 0,
131
+ orchestrationInput: 0,
132
+ orchestrationOutput: 0,
133
+ orchestrationCacheRead: 0,
134
+ premiumRequests: 0,
135
+ cost: 0,
136
+ };
137
+ }
138
+
139
+ function taskUsageFrom(details: unknown): Usage | undefined {
140
+ if (details === null || typeof details !== "object") return undefined;
141
+ const maybeUsage = (details as Record<string, unknown>).usage;
142
+ return maybeUsage !== null && typeof maybeUsage === "object" ? (maybeUsage as Usage) : undefined;
143
+ }
144
+
145
+ function entryUsage(entry: SessionEntry): Usage | undefined {
146
+ if (entry.type !== "message") return undefined;
147
+ const message = entry.message;
148
+ if (message.role === "assistant") return message.usage;
149
+ if (message.role === "toolResult" && message.toolName === "task") return taskUsageFrom(message.details);
150
+ return undefined;
151
+ }
152
+
153
+ function addUsage(target: UsageStatistics, usage: Usage | undefined): void {
154
+ if (!usage) return;
155
+ target.input += usage.input;
156
+ target.output += usage.output;
157
+ target.cacheRead += usage.cacheRead;
158
+ target.cacheWrite += usage.cacheWrite;
159
+ target.totalTokens += usage.totalTokens;
160
+ target.orchestrationInput += usage.orchestration?.input ?? 0;
161
+ target.orchestrationOutput += usage.orchestration?.output ?? 0;
162
+ target.orchestrationCacheRead += usage.orchestration?.cacheRead ?? 0;
163
+ target.premiumRequests += usage.premiumRequests ?? 0;
164
+ target.cost += usage.cost.total;
165
+ }
166
+
167
+ function isAssistantEntry(entry: SessionEntry): boolean {
168
+ return entry.type === "message" && entry.message.role === "assistant";
169
+ }
170
+
171
+ function isDraftOnlyMetadataEntry(entry: SessionEntry): boolean {
172
+ // Startup-recorded selector state that does not survive as user intent
173
+ // once the draft is cleared. `mode_change` covers the `plan.defaultOnStartup`
174
+ // path (interactive-mode.ts enters plan mode before draft restoration) and
175
+ // `/plan` toggles that leave the session otherwise empty; entries carrying
176
+ // real conversation state — messages, compactions, branch summaries,
177
+ // custom/custom_message, session_init, labels, title/tool selection — never
178
+ // reach this branch and always keep the file resumable.
179
+ switch (entry.type) {
180
+ case "model_change":
181
+ case "thinking_level_change":
182
+ case "service_tier_change":
183
+ case "mode_change":
184
+ case "credential_pin":
185
+ return true;
186
+ default:
187
+ return false;
188
+ }
189
+ }
190
+
191
+ function orderedByTimestamp(a: SessionTreeNode, b: SessionTreeNode): number {
192
+ return new Date(a.entry.timestamp).getTime() - new Date(b.entry.timestamp).getTime();
193
+ }
194
+
195
+ /**
196
+ * Maintains the derived views over a session's entry list: id lookup, the
197
+ * parent→children adjacency, the resolved label map, the active leaf, and the
198
+ * running usage totals. Kept in lockstep with the manager's `#entries` so reads
199
+ * stay O(1)/O(children) instead of rescanning the whole journal.
200
+ */
201
+ class SessionEntryIndex {
202
+ #entriesById = new Map<string, SessionEntry>();
203
+ #children = new Map<string | null, SessionEntry[]>();
204
+ #labels = new Map<string, string>();
205
+ #leaf: string | null = null;
206
+ #usage = emptyUsageStatistics();
207
+
208
+ clear(): void {
209
+ this.#entriesById.clear();
210
+ this.#children.clear();
211
+ this.#labels.clear();
212
+ this.#leaf = null;
213
+ this.#usage = emptyUsageStatistics();
214
+ }
215
+
216
+ rebuild(entries: readonly SessionEntry[]): void {
217
+ this.clear();
218
+ for (const entry of entries) this.insert(entry);
219
+ }
220
+
221
+ insert(entry: SessionEntry): void {
222
+ this.#entriesById.set(entry.id, entry);
223
+ this.#leaf = entry.id;
224
+
225
+ const bucket = this.#children.get(entry.parentId);
226
+ if (bucket) bucket.push(entry);
227
+ else this.#children.set(entry.parentId, [entry]);
228
+
229
+ if (entry.type === "label") {
230
+ if (entry.label) this.#labels.set(entry.targetId, entry.label);
231
+ else this.#labels.delete(entry.targetId);
232
+ }
233
+
234
+ addUsage(this.#usage, entryUsage(entry));
235
+ }
236
+
237
+ has(id: string): boolean {
238
+ return this.#entriesById.has(id);
239
+ }
240
+
241
+ get(id: string): SessionEntry | undefined {
242
+ return this.#entriesById.get(id);
243
+ }
244
+
245
+ /**
246
+ * The live id→entry map. Read-only for callers (lookups + `generateId`
247
+ * collision checks); never mutate it directly — go through `insert`/`rebuild`.
248
+ */
249
+ entriesById(): Map<string, SessionEntry> {
250
+ return this.#entriesById;
251
+ }
252
+
253
+ leafId(): string | null {
254
+ return this.#leaf;
255
+ }
256
+
257
+ leafEntry(): SessionEntry | undefined {
258
+ return this.#leaf ? this.#entriesById.get(this.#leaf) : undefined;
259
+ }
260
+
261
+ setLeaf(id: string | null): void {
262
+ this.#leaf = id;
263
+ }
264
+
265
+ childrenOf(parentId: string): SessionEntry[] {
266
+ return [...(this.#children.get(parentId) ?? [])];
267
+ }
268
+
269
+ labelFor(id: string): string | undefined {
270
+ return this.#labels.get(id);
271
+ }
272
+
273
+ labelsInEffect(): IterableIterator<[string, string]> {
274
+ return this.#labels.entries();
275
+ }
276
+
277
+ usageSnapshot(): UsageStatistics {
278
+ return { ...this.#usage };
279
+ }
280
+
281
+ pathTo(id: string | null | undefined = this.#leaf): SessionEntry[] {
282
+ const branch: SessionEntry[] = [];
283
+ const seen = new Set<string>();
284
+ let cursor = id ? this.#entriesById.get(id) : undefined;
285
+
286
+ while (cursor && !seen.has(cursor.id)) {
287
+ seen.add(cursor.id);
288
+ branch.push(cursor);
289
+ cursor = cursor.parentId ? this.#entriesById.get(cursor.parentId) : undefined;
290
+ }
291
+ branch.reverse();
292
+ return branch;
293
+ }
294
+
295
+ tree(entries: readonly SessionEntry[]): SessionTreeNode[] {
296
+ const nodes = new Map<string, SessionTreeNode>();
297
+ const roots: SessionTreeNode[] = [];
298
+
299
+ for (const entry of entries) {
300
+ nodes.set(entry.id, { entry, children: [], label: this.#labels.get(entry.id) });
301
+ }
302
+
303
+ for (const entry of entries) {
304
+ const node = nodes.get(entry.id)!;
305
+ const parentId = entry.parentId;
306
+ if (parentId === null || parentId === entry.id) {
307
+ roots.push(node);
308
+ continue;
309
+ }
310
+
311
+ const parent = nodes.get(parentId);
312
+ if (parent) parent.children.push(node);
313
+ else roots.push(node);
314
+ }
315
+
316
+ const stack = [...roots];
317
+ while (stack.length > 0) {
318
+ const node = stack.pop()!;
319
+ node.children.sort(orderedByTimestamp);
320
+ stack.push(...node.children);
321
+ }
322
+
323
+ return roots;
324
+ }
325
+ }
326
+
327
+ export type ReadonlySessionManager = Pick<
328
+ SessionManager,
329
+ | "getCwd"
330
+ | "getSessionDir"
331
+ | "getSessionId"
332
+ | "getSessionFile"
333
+ | "getSessionName"
334
+ | "getArtifactsDir"
335
+ | "getArtifactManager"
336
+ | "allocateArtifactPath"
337
+ | "saveArtifact"
338
+ | "getArtifactPath"
339
+ | "getLeafId"
340
+ | "getLeafEntry"
341
+ | "getEntry"
342
+ | "getLabel"
343
+ | "getBranch"
344
+ | "getHeader"
345
+ | "getEntries"
346
+ | "getTree"
347
+ | "getUsageStatistics"
348
+ | "putBlob"
349
+ | "putBlobSync"
350
+ >;
351
+
352
+ interface SessionManagerStateSnapshot {
353
+ cwd: string;
354
+ sessionDir: string;
355
+ sessionId: string;
356
+ sessionName: string | undefined;
357
+ titleSource: SessionTitleSource | undefined;
358
+ sessionFile: string | undefined;
359
+ titleUpdatedAt: string;
360
+ hasTitleSlot: boolean;
361
+ onDisk: boolean;
362
+ needsRewrite: boolean;
363
+ draftOnlySessionCleanupArmed: boolean;
364
+ header: SessionHeader;
365
+ entries: SessionEntry[];
366
+ }
367
+
368
+ interface DiskQueueOptions {
369
+ ignorePriorError?: boolean;
370
+ ignoreEpoch?: boolean;
371
+ epoch?: number;
372
+ }
373
+
374
+ interface AtomicEntryBatch {
375
+ collecting: boolean;
376
+ entryIds: Set<string>;
377
+ deferredNotifications: SessionEntry[];
378
+ preBatchLeafId: string | null;
379
+ externalLeafChanged: boolean;
380
+ externalLeafId: string | null;
381
+ }
382
+
383
+ /**
384
+ * The storage may have published a write that rejected, and an authoritative
385
+ * repair could not be proven durable. Callers must fail closed until recovery.
386
+ */
387
+ export class SessionPersistenceIndeterminateError extends AggregateError {
388
+ readonly operationError: Error;
389
+ readonly recoveryErrors: readonly Error[];
390
+
391
+ constructor(operationError: Error, recoveryErrors: readonly Error[]) {
392
+ super(
393
+ [operationError, ...recoveryErrors],
394
+ `Session persistence is indeterminate after "${operationError.message}" and authoritative repair failed.`,
395
+ );
396
+ this.name = "SessionPersistenceIndeterminateError";
397
+ this.operationError = operationError;
398
+ this.recoveryErrors = [...recoveryErrors];
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Stores and navigates an append-only conversation journal.
404
+ *
405
+ * A session is a JSONL file: one header line followed by entries. Entries form a
406
+ * tree by `(id, parentId)`, and the mutable leaf pointer selects which path is
407
+ * active for future appends and for LLM context construction.
408
+ *
409
+ * Durability is software-crash safe but not power-loss safe: completed entries
410
+ * (user/assistant/toolResult messages, tool_execution_start markers, custom
411
+ * entries) are handed to the OS synchronously in-body on append and never
412
+ * `fsync`'d. In-flight streaming text is intentionally not durable until
413
+ * `message_end` persists the finished message.
414
+ *
415
+ * While an in-place atomic rewrite is publishing, a concurrent completed append
416
+ * supersedes that publish with a synchronous full-body rewrite so the entry is
417
+ * software-crash durable before the append returns; the abandoned atomic's
418
+ * `commitGuard` then refuses to clobber the fresher body.
419
+ *
420
+ * During {@link moveTo}, appends write a full body to the live relocation path
421
+ * (source until rename, destination once the rename has landed) so a crash mid-
422
+ * move still preserves completed entries without recreating a vacated source.
423
+ * A trailing atomic rewrite still rewrites the header cwd after the path is
424
+ * repointed.
425
+ */
426
+ export class SessionManager {
427
+ #cwd: string;
428
+ /** Additional workspace directories beyond cwd (multi-root). Normalized absolute, deduped, excludes cwd. */
429
+ #additionalDirectories: string[] = [];
430
+ #sessionDir: string;
431
+ readonly #persist: boolean;
432
+ readonly #storage: SessionStorage;
433
+ readonly #blobs: BlobStore;
434
+
435
+ #sessionId = "";
436
+ #sessionName: string | undefined;
437
+ #titleSource: SessionTitleSource | undefined;
438
+ #sessionFile: string | undefined;
439
+ #header!: SessionHeader;
440
+ #titleUpdatedAt = "";
441
+ #hasTitleSlot = true;
442
+ #entries: SessionEntry[] = [];
443
+ #index = new SessionEntryIndex();
444
+
445
+ /** File reflects all current entries; appends can go incrementally. */
446
+ #fileIsCurrent = false;
447
+ /** In-memory entries diverged from disk (load-migration/sanitize) → next persist must full-rewrite. */
448
+ #rewriteRequired = false;
449
+ /** Lazy gate crossed (ensureOnDisk / loaded file): every entry must persist from now on. */
450
+ #forceFileCreation = false;
451
+ /**
452
+ * Armed only when this manager observed a draft sidecar lifecycle that
453
+ * materialized an otherwise metadata-only session file. Explicit
454
+ * ensureOnDisk() callers (ACP session/new, handoff) must survive close().
455
+ */
456
+ #draftOnlySessionCleanupArmed = false;
457
+
458
+ /**
459
+ * Collab replication tap: invoked for every appended entry with the
460
+ * in-memory (pre-blob-externalization) entry, so inline images survive.
461
+ */
462
+ onEntryAppended?: (entry: SessionEntry) => void;
463
+
464
+ #turnBudgetTotal: number | null = null;
465
+ #turnBudgetHard = false;
466
+ #turnOutputBaseline = 0;
467
+ #turnEvalOutput = 0;
468
+
469
+ /** The single open append writer; the manager only ever writes one file at a time. */
470
+ #writer: SessionStorageWriter | undefined;
471
+ /** Serializes async disk work (flush/close/atomic rewrite). Appends are synchronous and bypass it. */
472
+ #diskTail: Promise<void> = Promise.resolve();
473
+ #diskFailure: Error | undefined;
474
+ #diskFailureLogged = false;
475
+ /** FIFO reservation for atomic batches and authoritative recovery. */
476
+ #atomicPersistenceTail: Promise<void> = Promise.resolve();
477
+ /** Observer notifications withheld until their entries are proven durable. */
478
+ #pendingDurabilityNotifications: SessionEntry[] = [];
479
+ /** Bumped on every sync rewrite / chain reset so stale queued tasks become no-ops. */
480
+ #diskEpoch = 0;
481
+ /**
482
+ * Epoch of the in-flight atomic rewrite, or `null` when no rewrite is running.
483
+ * The fence in {@link #appendToSessionFile} only applies while this matches
484
+ * `#diskEpoch`: once a synchronous rewrite (`flushSync` → `#rewriteSynchronously`)
485
+ * bumps the epoch, the pending atomic publish is guaranteed to abandon via
486
+ * its `commitGuard`, and appends can safely take the hot path against the
487
+ * freshly-published file.
488
+ */
489
+ #atomicRewriteFenceEpoch: number | null = null;
490
+ /** Set by synchronous appends that land while an atomic replacement is active. */
491
+ #atomicRewriteDirty = false;
492
+ /**
493
+ * Active {@link moveTo} relocation. Concurrent completed appends write a
494
+ * full body to the live path: source while it still exists, destination
495
+ * once rename has landed (source gone). Never recreates a vacated source.
496
+ * `null` outside an active relocation.
497
+ */
498
+ #sessionFileRelocating: { source: string; dest: string } | null = null;
499
+ /** Atomic entry batch currently staged for a full-file commit. */
500
+ #atomicEntryBatch: AtomicEntryBatch | undefined;
501
+
502
+ #artifactManager: ArtifactManager | null = null;
503
+ #artifactManagerSessionFile: string | null = null;
504
+ #adoptedArtifactManager: ArtifactManager | null = null;
505
+ #inMemoryArtifacts: Map<string, string> | null = null;
506
+ #inMemoryArtifactCounter = 0;
507
+
508
+ #suppressBreadcrumb = false;
509
+ /**
510
+ * The last breadcrumb this manager wrote marked a lazy `/new` boundary whose
511
+ * JSONL is not yet on disk. Cleared (and the crumb re-stamped non-fresh) once
512
+ * the session materializes, so a materialized-then-deleted session still falls
513
+ * back to the most-recent session instead of being treated as a fresh crumb.
514
+ */
515
+ #breadcrumbFresh = false;
516
+ #sessionNameChangedCallbacks = new Set<() => void>();
517
+
518
+ private constructor(cwd: string, sessionDir: string, persist: boolean, storage: SessionStorage) {
519
+ this.#cwd = cwd;
520
+ this.#sessionDir = sessionDir;
521
+ this.#persist = persist;
522
+ this.#storage = storage;
523
+ this.#blobs = new BlobStore(getBlobsDir());
524
+
525
+ if (persist && sessionDir) this.#storage.ensureDirSync(sessionDir);
526
+ }
527
+
528
+ #rememberBreadcrumb(cwd: string, sessionFile: string, fresh = false): void {
529
+ this.#breadcrumbFresh = fresh;
530
+ if (!this.#suppressBreadcrumb) writeTerminalBreadcrumb(cwd, sessionFile, fresh);
531
+ }
532
+
533
+ /**
534
+ * Re-stamp a fresh `/new` breadcrumb as non-fresh once the session has
535
+ * materialized on disk. A no-op unless the current breadcrumb is still fresh.
536
+ */
537
+ #materializeBreadcrumb(): void {
538
+ if (!this.#breadcrumbFresh || !this.#sessionFile) return;
539
+ this.#rememberBreadcrumb(this.#cwd, this.#sessionFile, false);
540
+ }
541
+
542
+ #clearDiskError(): void {
543
+ this.#diskFailure = undefined;
544
+ this.#diskFailureLogged = false;
545
+ }
546
+
547
+ #noteDiskFailure(errorLike: unknown): Error {
548
+ const error = toError(errorLike);
549
+ if (!this.#diskFailure) this.#diskFailure = error;
550
+
551
+ if (!this.#diskFailureLogged) {
552
+ this.#diskFailureLogged = true;
553
+ logger.error("Session persistence error.", {
554
+ sessionFile: this.#sessionFile,
555
+ error: error.message,
556
+ stack: error.stack,
557
+ });
558
+ }
559
+
560
+ return this.#diskFailure;
561
+ }
562
+
563
+ #scheduleDiskWork(work: () => Promise<void>, options: DiskQueueOptions = {}): Promise<void> {
564
+ const epoch = options.epoch ?? this.#diskEpoch;
565
+ const scheduled = this.#diskTail
566
+ .catch(() => undefined)
567
+ .then(async () => {
568
+ if (!options.ignoreEpoch && epoch !== this.#diskEpoch) return;
569
+ if (this.#diskFailure && !options.ignorePriorError) throw this.#diskFailure;
570
+ await work();
571
+ });
572
+
573
+ const reported = scheduled.catch(err => {
574
+ throw this.#noteDiskFailure(err);
575
+ });
576
+ this.#diskTail = reported.catch(() => undefined);
577
+ return reported;
578
+ }
579
+
580
+ async #withAtomicPersistenceLock<T>(operation: () => Promise<T>): Promise<T> {
581
+ const predecessor = this.#atomicPersistenceTail;
582
+ const turn = Promise.withResolvers<void>();
583
+ this.#atomicPersistenceTail = predecessor.catch(() => undefined).then(() => turn.promise);
584
+ await predecessor.catch(() => undefined);
585
+ try {
586
+ return await operation();
587
+ } finally {
588
+ turn.resolve();
589
+ }
590
+ }
591
+
592
+ async #drainAndCloseWriter(): Promise<void> {
593
+ try {
594
+ await this.#scheduleDiskWork(
595
+ async () => {
596
+ await this.#closeWriterHandle();
597
+ },
598
+ { ignorePriorError: true, ignoreEpoch: true },
599
+ );
600
+ } finally {
601
+ this.#writer = undefined;
602
+ this.#diskTail = Promise.resolve();
603
+ }
604
+ }
605
+
606
+ #closeWriterEventually(): void {
607
+ const writer = this.#writer;
608
+ this.#writer = undefined;
609
+ if (writer) void writer.close().catch(() => undefined);
610
+ }
611
+
612
+ async #closeWriterHandle(): Promise<void> {
613
+ const writer = this.#writer;
614
+ if (!writer) return;
615
+ this.#writer = undefined;
616
+ await writer.close();
617
+ }
618
+
619
+ #latchIndeterminate(operationError: Error, recoveryErrors: readonly Error[]): SessionPersistenceIndeterminateError {
620
+ const error = new SessionPersistenceIndeterminateError(operationError, recoveryErrors);
621
+ this.#diskFailure = error;
622
+ if (!this.#diskFailureLogged) {
623
+ this.#diskFailureLogged = true;
624
+ logger.error("Session persistence became indeterminate.", {
625
+ sessionFile: this.#sessionFile,
626
+ error: error.message,
627
+ });
628
+ }
629
+ return error;
630
+ }
631
+
632
+ #notifyDurableEntries(entries: readonly SessionEntry[] = []): void {
633
+ const notifications = [...this.#pendingDurabilityNotifications, ...entries];
634
+ this.#pendingDurabilityNotifications = [];
635
+ const seen = new Set<string>();
636
+ for (const entry of notifications) {
637
+ if (seen.has(entry.id)) continue;
638
+ seen.add(entry.id);
639
+ this.#notifyEntryAppended(entry);
640
+ }
641
+ }
642
+
643
+ async #authoritativelyRewriteCurrentStateLocked(operationError: Error): Promise<void> {
644
+ if (!this.#persist || !this.#sessionFile) return;
645
+ const previousDiskTail = this.#diskTail;
646
+ const writer = this.#writer;
647
+ this.#diskEpoch++;
648
+ const epoch = this.#diskEpoch;
649
+ this.#writer = undefined;
650
+ this.#diskTail = Promise.resolve();
651
+ this.#forceFileCreation = true;
652
+ this.#fileIsCurrent = false;
653
+ this.#rewriteRequired = true;
654
+ this.#atomicRewriteFenceEpoch = epoch;
655
+ if (!this.#diskFailure) this.#diskFailure = operationError;
656
+ try {
657
+ await previousDiskTail.catch(() => undefined);
658
+ let closeError: Error | undefined;
659
+ if (writer) {
660
+ try {
661
+ await writer.close();
662
+ } catch (error) {
663
+ closeError = toError(error);
664
+ }
665
+ }
666
+ let drainError: Error | undefined;
667
+ try {
668
+ await this.#storage.drain();
669
+ } catch (error) {
670
+ drainError = toError(error);
671
+ }
672
+ if (writer?.isOpen()) {
673
+ throw this.#latchIndeterminate(operationError, [
674
+ closeError ?? new Error("Failed to close session writer before authoritative repair."),
675
+ ...(drainError ? [drainError] : []),
676
+ ]);
677
+ }
678
+
679
+ do {
680
+ this.#atomicRewriteDirty = false;
681
+ const sessionFile = this.#sessionFile;
682
+ if (!sessionFile) {
683
+ throw this.#latchIndeterminate(operationError, [
684
+ new Error("Session file disappeared during authoritative repair."),
685
+ ]);
686
+ }
687
+ const body = this.#fileBody();
688
+ try {
689
+ await this.#storage.writeTextAtomic(sessionFile, body, {
690
+ commitGuard: () => this.#diskEpoch === epoch,
691
+ });
692
+ } catch (error) {
693
+ const recoveryErrors = [toError(error)];
694
+ try {
695
+ await this.#storage.drain();
696
+ } catch (drainFailure) {
697
+ recoveryErrors.push(toError(drainFailure));
698
+ }
699
+ let actual: string;
700
+ try {
701
+ actual = await this.#storage.readText(sessionFile);
702
+ } catch (readFailure) {
703
+ recoveryErrors.push(toError(readFailure));
704
+ throw this.#latchIndeterminate(operationError, recoveryErrors);
705
+ }
706
+ if (actual !== body) {
707
+ recoveryErrors.push(new Error("Authoritative session repair did not match durable storage."));
708
+ throw this.#latchIndeterminate(operationError, recoveryErrors);
709
+ }
710
+ }
711
+ if (this.#diskEpoch !== epoch) {
712
+ throw this.#latchIndeterminate(operationError, [
713
+ new Error("Authoritative session repair was superseded before verification."),
714
+ ]);
715
+ }
716
+ } while (this.#atomicRewriteDirty);
717
+
718
+ this.#fileIsCurrent = true;
719
+ this.#rewriteRequired = false;
720
+ this.#hasTitleSlot = true;
721
+ this.#clearDiskError();
722
+ } catch (error) {
723
+ if (error instanceof SessionPersistenceIndeterminateError) throw error;
724
+ throw this.#latchIndeterminate(operationError, [toError(error)]);
725
+ } finally {
726
+ if (this.#atomicRewriteFenceEpoch === epoch) this.#atomicRewriteFenceEpoch = null;
727
+ }
728
+ }
729
+
730
+ #appendWriter(): SessionStorageWriter {
731
+ if (!this.#sessionFile) throw new Error("Cannot open a session writer before a session file exists");
732
+
733
+ if (this.#writer?.isOpen()) return this.#writer;
734
+
735
+ this.#writer = this.#storage.openWriter(this.#sessionFile, {
736
+ flags: "a",
737
+ onError: err => this.#noteDiskFailure(err),
738
+ });
739
+ return this.#writer;
740
+ }
741
+
742
+ #lineFor(entry: FileEntry): string {
743
+ return `${stringifyJson(prepareEntryForPersistence(entry, this.#blobs)) ?? "null"}\n`;
744
+ }
745
+
746
+ #titleSlotLine(): string {
747
+ return serializeTitleSlot({
748
+ title: this.#sessionName,
749
+ source: this.#titleSource,
750
+ updatedAt: this.#titleUpdatedAt || this.#header.timestamp,
751
+ });
752
+ }
753
+
754
+ #fileBody(): string {
755
+ let body = this.#titleSlotLine();
756
+ body += this.#lineFor(this.#header);
757
+ for (const entry of this.#entries) body += this.#lineFor(entry);
758
+ return body;
759
+ }
760
+
761
+ #historyContainsAssistantMessage(): boolean {
762
+ return this.#entries.some(isAssistantEntry);
763
+ }
764
+
765
+ #shouldHaveSessionFile(): boolean {
766
+ return this.#forceFileCreation || this.#fileIsCurrent || this.#historyContainsAssistantMessage();
767
+ }
768
+
769
+ /**
770
+ * Live path for concurrent completed appends during {@link moveTo}.
771
+ * Prefers destination once rename has landed (source gone); otherwise
772
+ * source. Never invents a path that does not already exist.
773
+ */
774
+ #liveRelocationWritePath(): string | null {
775
+ const relocating = this.#sessionFileRelocating;
776
+ if (!relocating) return null;
777
+ if (this.#storage.existsSync(relocating.dest)) return relocating.dest;
778
+ if (this.#storage.existsSync(relocating.source)) return relocating.source;
779
+ // Rename in flight with neither path visible (rare cross-device edge):
780
+ // fall back to destination so we do not recreate a vacated source.
781
+ return relocating.dest;
782
+ }
783
+
784
+ /**
785
+ * Synchronously rewrite the whole file (header + entries) and keep no open
786
+ * writer; the next append re-opens one. `writeTextSync` returns with the
787
+ * bytes in the kernel page cache, so the file is software-crash durable.
788
+ *
789
+ * During {@link moveTo}, writes to the live relocation path (source pre-
790
+ * rename, destination post-rename) rather than always `#sessionFile`, so
791
+ * concurrent completed entries are durable without recreating a vacated source.
792
+ */
793
+ #rewriteSynchronously(): void {
794
+ if (!this.#persist || !this.#shouldHaveSessionFile()) return;
795
+ const targetPath = this.#liveRelocationWritePath() ?? this.#sessionFile;
796
+ if (!targetPath) return;
797
+
798
+ try {
799
+ const body = this.#fileBody();
800
+ this.#diskEpoch++;
801
+ this.#diskTail = Promise.resolve();
802
+ this.#closeWriterEventually();
803
+ this.#storage.writeTextSync(targetPath, body);
804
+ // Only mark the manager current when writing the active session path.
805
+ // Mid-move writes update the live relocation path; `#sessionFile` is
806
+ // still the pre-repoint source until moveTo repoints it.
807
+ if (!this.#sessionFileRelocating || targetPath === this.#sessionFile) {
808
+ this.#fileIsCurrent = true;
809
+ this.#materializeBreadcrumb();
810
+ this.#rewriteRequired = false;
811
+ this.#hasTitleSlot = true;
812
+ } else {
813
+ // Destination body is current on disk; in-memory still needs a
814
+ // header-cwd rewrite after repoint, but entries are durable.
815
+ this.#fileIsCurrent = false;
816
+ this.#rewriteRequired = true;
817
+ this.#hasTitleSlot = true;
818
+ }
819
+ } catch (err) {
820
+ this.#noteDiskFailure(err);
821
+ }
822
+ }
823
+
824
+ /**
825
+ * Rewrite the whole file atomically (temp-write + rename, EPERM-safe) on the
826
+ * disk chain. The body is serialized after the writer is closed. The fence
827
+ * is enabled BEFORE `#closeWriterHandle()` and stays active until the last
828
+ * atomic publish returns, so a sync append landing in the close-yield window
829
+ * cannot open a fresh writer that the pending replacement would then detach
830
+ * from the current JSONL path. A `commitGuard` also prevents a superseding
831
+ * synchronous rewrite from being overwritten by the stale body serialized
832
+ * before it ran.
833
+ */
834
+ async #rewriteAtomically(): Promise<void> {
835
+ if (!this.#persist || !this.#sessionFile) return;
836
+
837
+ const startEpoch = this.#diskEpoch;
838
+ await this.#scheduleDiskWork(
839
+ async () => {
840
+ if (await this.#runFencedAtomicRewrite(startEpoch)) {
841
+ this.#fileIsCurrent = true;
842
+ this.#materializeBreadcrumb();
843
+ this.#rewriteRequired = false;
844
+ this.#hasTitleSlot = true;
845
+ }
846
+ },
847
+ { epoch: startEpoch },
848
+ );
849
+ }
850
+
851
+ /**
852
+ * Shared fenced atomic-rewrite loop used by `#rewriteAtomically` and the
853
+ * `#persistTitleChangeEntry` fallback. Holds `#atomicRewriteActive` across
854
+ * the writer close and the full-file replace, and loops on
855
+ * `#atomicRewriteDirty` so any fenced append that lands during the rewrite
856
+ * is captured before the task resolves. Returns `false` when the disk epoch
857
+ * moved (a superseding synchronous rewrite has taken over) so callers skip
858
+ * their post-publish state updates.
859
+ */
860
+ async #runFencedAtomicRewrite(epoch: number): Promise<boolean> {
861
+ this.#atomicRewriteFenceEpoch = epoch;
862
+ try {
863
+ do {
864
+ this.#atomicRewriteDirty = false;
865
+ await this.#closeWriterHandle();
866
+ const sessionFile = this.#sessionFile;
867
+ if (!sessionFile) return false;
868
+ if (this.#diskEpoch !== epoch) return false;
869
+ await this.#storage.writeTextAtomic(sessionFile, this.#fileBody(), {
870
+ commitGuard: () => this.#diskEpoch === epoch,
871
+ });
872
+ if (this.#diskEpoch !== epoch) return false;
873
+ } while (this.#atomicRewriteDirty);
874
+ return true;
875
+ } finally {
876
+ // Only relinquish the fence if we still own it. A superseding
877
+ // synchronous rewrite (`flushSync` → `#rewriteSynchronously`) may
878
+ // have reset `#diskTail`, scheduled a fresh atomic task at the new
879
+ // epoch, and that task may have taken ownership of the fence while
880
+ // this stale rewrite was still awaiting storage. Clearing it here
881
+ // unconditionally would strand appends during the newer publish.
882
+ if (this.#atomicRewriteFenceEpoch === epoch) this.#atomicRewriteFenceEpoch = null;
883
+ }
884
+ }
885
+
886
+ #appendToSessionFile(entry: SessionEntry): void {
887
+ if (!this.#persist || !this.#sessionFile) return;
888
+ if (this.#atomicEntryBatch) {
889
+ this.#fileIsCurrent = false;
890
+ this.#rewriteRequired = true;
891
+ this.#atomicRewriteDirty = true;
892
+ return;
893
+ }
894
+ if (this.#diskFailure) throw this.#diskFailure;
895
+
896
+ // Lazy gate: a brand-new session is not written until it has an assistant
897
+ // message (or someone forced creation), so sessions that never produce
898
+ // output never create a file.
899
+ if (!this.#shouldHaveSessionFile()) {
900
+ this.#fileIsCurrent = false;
901
+ return;
902
+ }
903
+
904
+ // Atomic replacement / move window: do not open a fresh append writer that
905
+ // a Windows EPERM replace could detach from the current JSONL path.
906
+ // - moveTo: write a full body to the live relocation path (source pre-
907
+ // rename, destination post-rename) so completed entries are durable
908
+ // without recreating a vacated source.
909
+ // - in-place atomic fence: supersede the pending publish with a
910
+ // synchronous full-body rewrite; bumping `#diskEpoch` abandons the
911
+ // in-flight atomic via its `commitGuard`.
912
+ if (this.#sessionFileRelocating) {
913
+ this.#rewriteSynchronously();
914
+ return;
915
+ }
916
+ if (this.#atomicRewriteFenceEpoch !== null && this.#atomicRewriteFenceEpoch === this.#diskEpoch) {
917
+ this.#atomicRewriteDirty = true;
918
+ this.#rewriteSynchronously();
919
+ return;
920
+ }
921
+ // Cold/divergent: not on disk yet, or in-memory entries diverged from the
922
+ // file → rewrite the whole file synchronously and keep going.
923
+ if (!this.#fileIsCurrent || this.#rewriteRequired) {
924
+ this.#rewriteSynchronously();
925
+ return;
926
+ }
927
+
928
+ // Hot path: write the entry directly on the writer, outside the async disk
929
+ // chain. Prefer appendSync so write failures latch `#diskFailure` before
930
+ // this call returns (not via a discarded rejected Promise after a later
931
+ // microtask). Callers stay non-throwing here — the core turn loop invokes
932
+ // appendMessage/appendCustomEntry without try/catch; flushSync/close and
933
+ // subsequent appends still throw the latched error. File writers apply
934
+ // each line to the OS page cache before return.
935
+ // A mid-close writer leaves `#writer` undefined, so `#appendWriter` simply
936
+ // opens a fresh append handle and the entry still lands.
937
+ try {
938
+ const writer = this.#appendWriter();
939
+ const line = this.#lineFor(entry);
940
+ if (writer.appendSync) {
941
+ writer.appendSync(line);
942
+ } else {
943
+ void writer.append(line).catch(err => this.#noteDiskFailure(err));
944
+ }
945
+ } catch (err) {
946
+ this.#noteDiskFailure(err);
947
+ }
948
+ }
949
+
950
+ async #persistTitleChangeEntry(entry: TitleChangeEntry, update: SessionTitleUpdate): Promise<void> {
951
+ if (!this.#persist || !this.#sessionFile) return;
952
+ if (this.#diskFailure) throw this.#diskFailure;
953
+
954
+ if (!this.#shouldHaveSessionFile()) {
955
+ this.#fileIsCurrent = false;
956
+ return;
957
+ }
958
+
959
+ // Title changes use their own asynchronous append path rather than
960
+ // #appendToSessionFile. During move, write the full body (including the
961
+ // title entry) to the live relocation path so a crash mid-move still
962
+ // keeps the title change; the trailing rewrite still updates header cwd.
963
+ if (this.#sessionFileRelocating) {
964
+ this.#rewriteSynchronously();
965
+ return;
966
+ }
967
+
968
+ if (
969
+ !this.#fileIsCurrent ||
970
+ this.#rewriteRequired ||
971
+ !this.#hasTitleSlot ||
972
+ !this.#storage.existsSync(this.#sessionFile)
973
+ ) {
974
+ await this.#rewriteAtomically();
975
+ return;
976
+ }
977
+
978
+ const epoch = this.#diskEpoch;
979
+ const line = this.#lineFor(entry);
980
+ await this.#scheduleDiskWork(
981
+ async () => {
982
+ const sessionFile = this.#sessionFile;
983
+ if (!sessionFile) return;
984
+ try {
985
+ await this.#appendWriter().append(line);
986
+ await this.#storage.updateSessionTitle(sessionFile, update);
987
+ if (this.#diskEpoch === epoch) this.#fileIsCurrent = true;
988
+ } catch {
989
+ if (!(await this.#runFencedAtomicRewrite(epoch))) return;
990
+ this.#clearDiskError();
991
+ this.#fileIsCurrent = true;
992
+ this.#rewriteRequired = false;
993
+ this.#hasTitleSlot = true;
994
+ }
995
+ },
996
+ { epoch },
997
+ );
998
+ }
999
+
1000
+ #notifyEntryAppended(entry: SessionEntry): void {
1001
+ const callback = this.onEntryAppended;
1002
+ if (callback) {
1003
+ try {
1004
+ callback(entry);
1005
+ } catch (err) {
1006
+ logger.warn("collab entry hook failed", { error: String(err) });
1007
+ }
1008
+ }
1009
+ }
1010
+
1011
+ #resetToNewSession(options?: NewSessionOptions, forcedSessionFile?: string): string | undefined {
1012
+ this.#diskTail = Promise.resolve();
1013
+ this.#clearDiskError();
1014
+ this.#sessionId = mintSessionId();
1015
+ this.#sessionName = undefined;
1016
+ this.#titleSource = undefined;
1017
+ this.#titleUpdatedAt = "";
1018
+ this.#hasTitleSlot = true;
1019
+
1020
+ const timestamp = nowIso();
1021
+ this.#header = {
1022
+ type: "session",
1023
+ version: CURRENT_SESSION_VERSION,
1024
+ id: this.#sessionId,
1025
+ timestamp,
1026
+ cwd: this.#cwd,
1027
+ parentSession: options?.parentSession,
1028
+ providerPromptCacheKey: options?.providerPromptCacheKey,
1029
+ };
1030
+ const workspace = normalizeSessionWorkspace({
1031
+ cwd: this.#cwd,
1032
+ directories: options?.additionalDirectories ?? [],
1033
+ });
1034
+ this.#additionalDirectories = additionalWorkspaceDirectories(workspace);
1035
+ if (this.#additionalDirectories.length > 0) {
1036
+ this.#header.additionalDirectories = [...this.#additionalDirectories];
1037
+ }
1038
+ this.#titleUpdatedAt = timestamp;
1039
+
1040
+ this.#entries = [];
1041
+ this.#index.clear();
1042
+ this.#fileIsCurrent = false;
1043
+ this.#rewriteRequired = false;
1044
+ this.#forceFileCreation = false;
1045
+ this.#draftOnlySessionCleanupArmed = false;
1046
+ this.#turnBudgetTotal = null;
1047
+ this.#turnBudgetHard = false;
1048
+ this.#turnOutputBaseline = 0;
1049
+ this.#turnEvalOutput = 0;
1050
+ this.#artifactManager = null;
1051
+ this.#artifactManagerSessionFile = null;
1052
+ this.#adoptedArtifactManager = null;
1053
+ this.#inMemoryArtifacts = null;
1054
+ this.#inMemoryArtifactCounter = 0;
1055
+
1056
+ if (this.#persist) {
1057
+ this.#sessionFile =
1058
+ forcedSessionFile ??
1059
+ path.join(this.#sessionDir, `${fileSafeTimestamp(timestamp)}_${this.#sessionId}.jsonl`);
1060
+ this.#rememberBreadcrumb(this.#cwd, this.#sessionFile, true);
1061
+ } else {
1062
+ this.#sessionFile = undefined;
1063
+ }
1064
+
1065
+ return this.#sessionFile;
1066
+ }
1067
+
1068
+ #applyEntries(header: SessionHeader, entries: SessionEntry[]): void {
1069
+ this.#header = header;
1070
+ this.#entries = entries;
1071
+ this.#sessionId = header.id;
1072
+ this.#sessionName = header.title;
1073
+ this.#titleSource = header.titleSource;
1074
+ this.#titleUpdatedAt = header.timestamp;
1075
+ this.#index.rebuild(entries);
1076
+ }
1077
+
1078
+ #freshEntryFields(): { id: string; parentId: string | null; timestamp: string } {
1079
+ return {
1080
+ id: generateId(this.#index),
1081
+ parentId: this.#index.leafId(),
1082
+ timestamp: nowIso(),
1083
+ };
1084
+ }
1085
+
1086
+ #setLeaf(id: string | null): void {
1087
+ this.#index.setLeaf(id);
1088
+ const batch = this.#atomicEntryBatch;
1089
+ if (batch && !batch.collecting) {
1090
+ batch.externalLeafChanged = true;
1091
+ batch.externalLeafId = id;
1092
+ }
1093
+ }
1094
+
1095
+ #recordEntry(entry: SessionEntry): void {
1096
+ this.#entries.push(entry);
1097
+ this.#index.insert(entry);
1098
+ const batch = this.#atomicEntryBatch;
1099
+ if (batch?.collecting) batch.entryIds.add(entry.id);
1100
+ if (batch && !batch.collecting) {
1101
+ batch.externalLeafChanged = true;
1102
+ batch.externalLeafId = entry.id;
1103
+ }
1104
+ this.#appendToSessionFile(entry);
1105
+ if (batch) batch.deferredNotifications.push(entry);
1106
+ else this.#notifyEntryAppended(entry);
1107
+ }
1108
+
1109
+ #rollbackAtomicEntryBatch(batch: AtomicEntryBatch): void {
1110
+ const retainedAncestor = (id: string | null): string | null => {
1111
+ const seen = new Set<string>();
1112
+ while (id && batch.entryIds.has(id) && !seen.has(id)) {
1113
+ seen.add(id);
1114
+ id = this.#index.get(id)?.parentId ?? null;
1115
+ }
1116
+ return id;
1117
+ };
1118
+ const retained = this.#entries.filter(entry => !batch.entryIds.has(entry.id));
1119
+ for (const entry of retained) entry.parentId = retainedAncestor(entry.parentId);
1120
+ const restoredLeaf = retainedAncestor(batch.externalLeafChanged ? batch.externalLeafId : batch.preBatchLeafId);
1121
+ this.#entries = retained;
1122
+ this.#index.rebuild(retained);
1123
+ this.#index.setLeaf(restoredLeaf && this.#index.has(restoredLeaf) ? restoredLeaf : null);
1124
+ }
1125
+
1126
+ #draftPath(): string | null {
1127
+ const artifactsDir = this.getArtifactsDir();
1128
+ return artifactsDir ? path.join(artifactsDir, "draft.txt") : null;
1129
+ }
1130
+
1131
+ #draftOnlySessionMarkerPath(): string | null {
1132
+ const artifactsDir = this.getArtifactsDir();
1133
+ return artifactsDir ? path.join(artifactsDir, DRAFT_ONLY_SESSION_MARKER) : null;
1134
+ }
1135
+
1136
+ #hasDraftOnlySessionMarker(): boolean {
1137
+ const markerPath = this.#draftOnlySessionMarkerPath();
1138
+ return markerPath !== null && this.#storage.existsSync(markerPath);
1139
+ }
1140
+
1141
+ async #writeDraftOnlySessionMarker(): Promise<void> {
1142
+ const markerPath = this.#draftOnlySessionMarkerPath();
1143
+ if (!markerPath) return;
1144
+ await this.#storage.writeText(markerPath, "");
1145
+ }
1146
+
1147
+ async #clearDraftOnlySessionMarker(): Promise<void> {
1148
+ const markerPath = this.#draftOnlySessionMarkerPath();
1149
+ if (!markerPath) return;
1150
+ try {
1151
+ await this.#storage.unlink(markerPath);
1152
+ } catch (err) {
1153
+ if (!isEnoent(err)) throw err;
1154
+ }
1155
+ }
1156
+
1157
+ #artifactManagerForSession(): ArtifactManager | null {
1158
+ if (this.#adoptedArtifactManager) return this.#adoptedArtifactManager;
1159
+
1160
+ const sessionFile = this.#sessionFile;
1161
+ if (!sessionFile) {
1162
+ this.#artifactManager = null;
1163
+ this.#artifactManagerSessionFile = null;
1164
+ return null;
1165
+ }
1166
+
1167
+ if (this.#artifactManager && this.#artifactManagerSessionFile === sessionFile) return this.#artifactManager;
1168
+
1169
+ this.#artifactManager = new ArtifactManager(sessionFile.slice(0, -JSONL_SUFFIX_LENGTH));
1170
+ this.#artifactManagerSessionFile = sessionFile;
1171
+ return this.#artifactManager;
1172
+ }
1173
+
1174
+ #notifySessionNameListeners(): void {
1175
+ for (const callback of [...this.#sessionNameChangedCallbacks]) {
1176
+ try {
1177
+ callback();
1178
+ } catch (err) {
1179
+ logger.warn("SessionManager: session name change hook failed", { error: String(err) });
1180
+ }
1181
+ }
1182
+ }
1183
+
1184
+ static #cleanTitle(raw: string): string {
1185
+ return raw
1186
+ .replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
1187
+ .replace(/ +/g, " ")
1188
+ .trim();
1189
+ }
1190
+
1191
+ /** Puts a binary blob into the blob store and returns the blob reference. */
1192
+ async putBlob(data: Buffer, options?: BlobPutOptions): Promise<BlobPutResult> {
1193
+ return this.#blobs.put(data, options);
1194
+ }
1195
+
1196
+ /** Synchronous variant of {@link putBlob} for rebuild-only render paths. */
1197
+ putBlobSync(data: Buffer, options?: BlobPutOptions): BlobPutResult {
1198
+ return this.#blobs.putSync(data, options);
1199
+ }
1200
+
1201
+ captureState(): SessionManagerStateSnapshot {
1202
+ return {
1203
+ cwd: this.#cwd,
1204
+ sessionDir: this.#sessionDir,
1205
+ sessionId: this.#sessionId,
1206
+ sessionName: this.#sessionName,
1207
+ titleSource: this.#titleSource,
1208
+ titleUpdatedAt: this.#titleUpdatedAt,
1209
+ hasTitleSlot: this.#hasTitleSlot,
1210
+ sessionFile: this.#sessionFile,
1211
+ onDisk: this.#fileIsCurrent,
1212
+ needsRewrite: this.#rewriteRequired,
1213
+ draftOnlySessionCleanupArmed: this.#draftOnlySessionCleanupArmed,
1214
+ // Snapshot header + entries by reference: switch/reload replaces the
1215
+ // active header/array wholesale, so rollback needs no deep clone.
1216
+ header: this.#header,
1217
+ entries: [...this.#entries],
1218
+ };
1219
+ }
1220
+
1221
+ /**
1222
+ * Create an independent manager for the current logical session and branch.
1223
+ * The clone shares the storage backend but owns its entry index and writer, so
1224
+ * callers can finish session-owned work after this manager switches elsewhere.
1225
+ * Set `persist` false when the original session is intentionally being dropped.
1226
+ */
1227
+ cloneCurrentSession(options?: { persist?: boolean }): SessionManager {
1228
+ const persist = options?.persist ?? this.#persist;
1229
+ const clone = new SessionManager(this.#cwd, this.#sessionDir, persist, this.#storage);
1230
+ clone.#suppressBreadcrumb = true;
1231
+ clone.restoreState(this.captureState());
1232
+ if (!persist) {
1233
+ clone.#sessionFile = undefined;
1234
+ clone.#fileIsCurrent = false;
1235
+ clone.#rewriteRequired = false;
1236
+ clone.#forceFileCreation = false;
1237
+ }
1238
+ return clone;
1239
+ }
1240
+
1241
+ restoreState(snapshot: SessionManagerStateSnapshot): void {
1242
+ this.#closeWriterEventually();
1243
+ this.#diskTail = Promise.resolve();
1244
+ this.#clearDiskError();
1245
+
1246
+ this.#cwd = snapshot.cwd;
1247
+ this.#sessionDir = snapshot.sessionDir;
1248
+ this.#sessionFile = snapshot.sessionFile;
1249
+ this.#fileIsCurrent = snapshot.onDisk;
1250
+ this.#rewriteRequired = snapshot.needsRewrite;
1251
+ this.#forceFileCreation = snapshot.onDisk;
1252
+ this.#draftOnlySessionCleanupArmed = snapshot.draftOnlySessionCleanupArmed;
1253
+ this.#applyEntries(snapshot.header, [...snapshot.entries]);
1254
+ this.#additionalDirectories = snapshot.header.additionalDirectories ?? [];
1255
+ this.#sessionName = snapshot.sessionName;
1256
+ this.#titleSource = snapshot.titleSource;
1257
+ this.#titleUpdatedAt = snapshot.titleUpdatedAt;
1258
+ this.#hasTitleSlot = snapshot.hasTitleSlot;
1259
+ this.#artifactManager = null;
1260
+ this.#artifactManagerSessionFile = null;
1261
+ this.#adoptedArtifactManager = null;
1262
+
1263
+ if (this.#sessionFile) this.#rememberBreadcrumb(this.#cwd, this.#sessionFile);
1264
+ }
1265
+
1266
+ /** Switch to a different session file (resume / branch). */
1267
+ async setSessionFile(sessionFile: string): Promise<void> {
1268
+ await this.#drainAndCloseWriter();
1269
+ this.#clearDiskError();
1270
+ this.#draftOnlySessionCleanupArmed = false;
1271
+
1272
+ const resolvedSessionFile = path.resolve(sessionFile);
1273
+ this.#sessionFile = resolvedSessionFile;
1274
+ this.#rememberBreadcrumb(this.#cwd, resolvedSessionFile);
1275
+
1276
+ const titleSlot = await readTitleSlotFromFile(resolvedSessionFile, this.#storage);
1277
+ const fileEntries = await loadEntriesFromFile(resolvedSessionFile, this.#storage);
1278
+ if (fileEntries.length === 0) {
1279
+ // Explicit but empty/missing path (e.g. --session flag): start fresh but
1280
+ // keep the requested path and materialize the header immediately.
1281
+ this.#resetToNewSession(undefined, resolvedSessionFile);
1282
+ this.#forceFileCreation = true;
1283
+ await this.#rewriteAtomically();
1284
+ this.#fileIsCurrent = true;
1285
+ return;
1286
+ }
1287
+
1288
+ const migrated = migrateToCurrentVersion(fileEntries);
1289
+ await resolveBlobRefsInEntries(fileEntries, this.#blobs);
1290
+ // loadEntriesFromFile guarantees entries[0] is a valid session header.
1291
+ const header = fileEntries[0] as SessionHeader;
1292
+
1293
+ // Adopt the loaded session's working directory. Sessions live in a dir
1294
+ // keyed by their cwd, so resuming a session from another project must
1295
+ // re-point cwd/sessionDir at that project — unless that project directory
1296
+ // no longer exists on disk, in which case adopting it (and the process
1297
+ // chdir interactive mode then performs) would fail with ENOENT. Keep the
1298
+ // current cwd so the resumed session stays where the user already is.
1299
+ const headerCwd = header.cwd ? path.resolve(header.cwd) : undefined;
1300
+ if (headerCwd && headerCwd !== path.resolve(this.#cwd) && (await directoryExists(headerCwd))) {
1301
+ this.#cwd = headerCwd;
1302
+ this.#sessionDir = path.dirname(resolvedSessionFile);
1303
+ this.#rememberBreadcrumb(this.#cwd, resolvedSessionFile);
1304
+ }
1305
+
1306
+ this.#applyEntries(header, fileEntries.slice(1) as SessionEntry[]);
1307
+ this.#additionalDirectories = header.additionalDirectories ?? [];
1308
+ this.#titleUpdatedAt = titleSlot?.updatedAt ?? header.timestamp;
1309
+ this.#hasTitleSlot = titleSlot !== undefined;
1310
+ this.#fileIsCurrent = true;
1311
+ this.#rewriteRequired = migrated;
1312
+ this.#forceFileCreation = true;
1313
+ this.#artifactManager = null;
1314
+ this.#artifactManagerSessionFile = null;
1315
+
1316
+ if (this.sanitizeLoadedOpenAIResponsesReplayMetadata()) this.#rewriteRequired = true;
1317
+ }
1318
+
1319
+ /** Start a new session. Drains and closes any existing writer first. */
1320
+ async newSession(options?: NewSessionOptions): Promise<string | undefined> {
1321
+ await this.#drainAndCloseWriter();
1322
+ return this.#resetToNewSession(options);
1323
+ }
1324
+
1325
+ /** Delete a session file and its artifact directory. ENOENT is treated as success. */
1326
+ async dropSession(sessionPath: string): Promise<void> {
1327
+ await this.#drainAndCloseWriter();
1328
+ try {
1329
+ await this.#storage.deleteSessionWithArtifacts(sessionPath);
1330
+ } catch (err) {
1331
+ if (!isEnoent(err)) throw err;
1332
+ }
1333
+ }
1334
+
1335
+ /**
1336
+ * Fork the current session into a new file with the same entries.
1337
+ * @returns the old and new session file paths, or undefined when not persisting.
1338
+ */
1339
+ async fork(): Promise<{ oldSessionFile: string; newSessionFile: string } | undefined> {
1340
+ if (!this.#persist || !this.#sessionFile) return undefined;
1341
+
1342
+ const oldSessionFile = this.#sessionFile;
1343
+ const parentSessionId = this.#sessionId;
1344
+ await this.#drainAndCloseWriter();
1345
+ this.#clearDiskError();
1346
+
1347
+ const timestamp = nowIso();
1348
+ this.#sessionId = mintSessionId();
1349
+ this.#sessionFile = path.join(this.#sessionDir, `${fileSafeTimestamp(timestamp)}_${this.#sessionId}.jsonl`);
1350
+ this.#header = {
1351
+ type: "session",
1352
+ version: CURRENT_SESSION_VERSION,
1353
+ id: this.#sessionId,
1354
+ title: this.#header.title ?? this.#sessionName,
1355
+ titleSource: this.#header.titleSource ?? this.#titleSource,
1356
+ timestamp,
1357
+ cwd: this.#cwd,
1358
+ additionalDirectories: this.#additionalDirectories.length > 0 ? [...this.#additionalDirectories] : undefined,
1359
+ parentSession: parentSessionId,
1360
+ providerPromptCacheKey: this.#header.providerPromptCacheKey ?? parentSessionId,
1361
+ };
1362
+ this.#sessionName = this.#header.title;
1363
+ this.#titleSource = this.#header.titleSource;
1364
+ this.#titleUpdatedAt = timestamp;
1365
+ this.#hasTitleSlot = true;
1366
+ this.#fileIsCurrent = false;
1367
+ this.#rewriteRequired = false;
1368
+ this.#forceFileCreation = true;
1369
+ this.#draftOnlySessionCleanupArmed = false;
1370
+ this.#artifactManager = null;
1371
+ this.#artifactManagerSessionFile = null;
1372
+ this.#rememberBreadcrumb(this.#cwd, this.#sessionFile);
1373
+
1374
+ await this.#rewriteAtomically();
1375
+ return { oldSessionFile, newSessionFile: this.#sessionFile };
1376
+ }
1377
+
1378
+ /**
1379
+ * Move the session to a new working directory: relocate the session file and
1380
+ * artifacts on disk, update internal references, and rewrite the header cwd.
1381
+ */
1382
+ async moveTo(newCwd: string, targetSessionDir?: string): Promise<void> {
1383
+ const resolvedCwd = path.resolve(newCwd);
1384
+ const resolvedTargetDir = targetSessionDir ? path.resolve(targetSessionDir) : undefined;
1385
+ if (
1386
+ resolvedCwd === path.resolve(this.#cwd) &&
1387
+ (!resolvedTargetDir || resolvedTargetDir === path.resolve(this.#sessionDir))
1388
+ ) {
1389
+ return;
1390
+ }
1391
+
1392
+ const managedRoot = resolveManagedSessionRoot(this.#sessionDir, this.#cwd);
1393
+ const nextSessionDir =
1394
+ resolvedTargetDir ??
1395
+ (managedRoot
1396
+ ? computeDefaultSessionDir(resolvedCwd, this.#storage, managedRoot)
1397
+ : computeDefaultSessionDir(resolvedCwd, this.#storage));
1398
+
1399
+ let sessionFileExisted = false;
1400
+ // Track source+dest for concurrent completed appends during relocation
1401
+ // (see `#sessionFileRelocating`). Existence of either path decides the
1402
+ // live write target — not a `#diskEpoch` bump, which would cancel any
1403
+ // disk task already queued at the current epoch (e.g. a header-only
1404
+ // `ensureOnDisk()` materializing rewrite) before the drain below runs it.
1405
+ if (this.#persist && this.#sessionFile) {
1406
+ const source = this.#sessionFile;
1407
+ const dest = path.join(nextSessionDir, path.basename(source));
1408
+ this.#sessionFileRelocating = { source, dest };
1409
+ }
1410
+
1411
+ try {
1412
+ if (this.#persist && this.#sessionFile) {
1413
+ this.#storage.ensureDirSync(nextSessionDir);
1414
+ await this.#drainAndCloseWriter();
1415
+ this.#clearDiskError();
1416
+
1417
+ const oldSessionFile = this.#sessionFile;
1418
+ const newSessionFile = path.join(nextSessionDir, path.basename(oldSessionFile));
1419
+ const oldArtifactsDir = artifactsDirectoryFor(oldSessionFile)!;
1420
+ const newArtifactsDir = artifactsDirectoryFor(newSessionFile)!;
1421
+ const sessionPathChanged = path.resolve(oldSessionFile) !== path.resolve(newSessionFile);
1422
+ const artifactPathChanged = path.resolve(oldArtifactsDir) !== path.resolve(newArtifactsDir);
1423
+ sessionFileExisted = this.#storage.existsSync(oldSessionFile);
1424
+
1425
+ let sessionMoved = false;
1426
+ let artifactsMoved = false;
1427
+
1428
+ try {
1429
+ if (sessionFileExisted && sessionPathChanged) {
1430
+ await fs.promises.rename(oldSessionFile, newSessionFile);
1431
+ sessionMoved = true;
1432
+ }
1433
+
1434
+ if (artifactPathChanged) {
1435
+ try {
1436
+ const artifactStat = await fs.promises.stat(oldArtifactsDir);
1437
+ if (artifactStat.isDirectory()) {
1438
+ await fs.promises.rename(oldArtifactsDir, newArtifactsDir);
1439
+ artifactsMoved = true;
1440
+ }
1441
+ } catch (err) {
1442
+ if (!isEnoent(err)) throw err;
1443
+ }
1444
+ }
1445
+ } catch (err) {
1446
+ if (artifactsMoved) {
1447
+ try {
1448
+ await fs.promises.rename(newArtifactsDir, oldArtifactsDir);
1449
+ } catch (rollbackErr) {
1450
+ throw new Error(
1451
+ `Failed to move artifacts and rollback: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
1452
+ );
1453
+ }
1454
+ }
1455
+
1456
+ if (sessionMoved) {
1457
+ try {
1458
+ await fs.promises.rename(newSessionFile, oldSessionFile);
1459
+ } catch (rollbackErr) {
1460
+ throw new Error(
1461
+ `Failed to move session file and rollback: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
1462
+ );
1463
+ }
1464
+ }
1465
+
1466
+ throw err;
1467
+ }
1468
+
1469
+ if (sessionFileExisted && sessionPathChanged) {
1470
+ this.#header.previousSessionFiles = [
1471
+ ...new Set([...(this.#header.previousSessionFiles ?? []), path.resolve(oldSessionFile)]),
1472
+ ];
1473
+ }
1474
+
1475
+ this.#sessionFile = newSessionFile;
1476
+ this.#artifactManager = null;
1477
+ this.#artifactManagerSessionFile = null;
1478
+ // Path is repointed; hot-path appends may use `#sessionFile` again.
1479
+ this.#sessionFileRelocating = null;
1480
+ }
1481
+
1482
+ this.#cwd = resolvedCwd;
1483
+ this.#sessionDir = nextSessionDir;
1484
+ this.#header.cwd = resolvedCwd;
1485
+ // Re-filter additional roots: the new cwd may have been an additional root,
1486
+ // or it may now contain/subsume one. Re-normalize to keep the invariant
1487
+ // that cwd is never also listed as an additional directory.
1488
+ if (this.#additionalDirectories.length > 0) {
1489
+ this.#additionalDirectories = this.#additionalDirectories.filter(d => d !== resolvedCwd);
1490
+ this.#header.additionalDirectories =
1491
+ this.#additionalDirectories.length > 0 ? this.#additionalDirectories : undefined;
1492
+ }
1493
+
1494
+ // Rewrite at the new location when the file already existed (update cwd) or
1495
+ // there is in-memory output worth materializing; otherwise stay lazy.
1496
+ const hasAssistant = this.#historyContainsAssistantMessage();
1497
+ if (this.#persist && this.#sessionFile && (sessionFileExisted || hasAssistant)) {
1498
+ this.#forceFileCreation = true;
1499
+ await this.#rewriteAtomically();
1500
+ }
1501
+
1502
+ if (this.#sessionFile) this.#rememberBreadcrumb(resolvedCwd, this.#sessionFile);
1503
+ } finally {
1504
+ this.#sessionFileRelocating = null;
1505
+ }
1506
+ }
1507
+
1508
+ /**
1509
+ * Force the session onto disk even with no assistant message yet (ACP
1510
+ * session/new must create a discoverable file immediately).
1511
+ */
1512
+ async ensureOnDisk(): Promise<void> {
1513
+ if (!this.#persist || !this.#sessionFile) return;
1514
+ this.#forceFileCreation = true;
1515
+ if (this.#fileIsCurrent && !this.#rewriteRequired) return;
1516
+ await this.#rewriteAtomically();
1517
+ }
1518
+
1519
+ /** Persist this session's transcript as a newly identified OMP session. */
1520
+ async persistCopy(
1521
+ options?: { sessionDir?: string; suppressBreadcrumb?: boolean },
1522
+ storage: SessionStorage = new FileSessionStorage(),
1523
+ ): Promise<SessionManager> {
1524
+ const sessionDir = options?.sessionDir ?? SessionManager.getDefaultSessionDir(this.#cwd, undefined, storage);
1525
+ const manager = new SessionManager(this.#cwd, sessionDir, true, storage);
1526
+ manager.#suppressBreadcrumb = options?.suppressBreadcrumb === true;
1527
+ manager.#resetToNewSession();
1528
+ manager.#sessionName = this.#sessionName;
1529
+ manager.#titleSource = this.#titleSource;
1530
+ manager.#titleUpdatedAt = this.#titleUpdatedAt;
1531
+ manager.#header.title = this.#sessionName;
1532
+ manager.#header.titleSource = this.#titleSource;
1533
+ manager.#additionalDirectories = [...this.#additionalDirectories];
1534
+ manager.#header.additionalDirectories =
1535
+ manager.#additionalDirectories.length > 0 ? [...manager.#additionalDirectories] : undefined;
1536
+ manager.#entries = structuredClone(this.#entries);
1537
+ manager.#index.rebuild(manager.#entries);
1538
+ manager.#forceFileCreation = true;
1539
+ await manager.#rewriteAtomically();
1540
+ return manager;
1541
+ }
1542
+
1543
+ /**
1544
+ * Stage a synchronous group of entry appends and publish the resulting full
1545
+ * journal with one atomic replace. A failed publish removes only the staged
1546
+ * entries, preserves/reparents entries appended concurrently, restores the
1547
+ * prior durable file view, and clears the failed writer latch for retry.
1548
+ *
1549
+ * The callback MUST be synchronous.
1550
+ */
1551
+ appendEntriesAtomically<T>(append: () => T): Promise<T> {
1552
+ return this.#withAtomicPersistenceLock(() => this.#appendEntriesAtomicallyLocked(append));
1553
+ }
1554
+
1555
+ async #appendEntriesAtomicallyLocked<T>(append: () => T): Promise<T> {
1556
+ if (!this.#persist || !this.#sessionFile) return append();
1557
+ if (this.#atomicEntryBatch) throw new Error("Atomic persistence lock ownership was violated.");
1558
+ try {
1559
+ await this.ensureOnDisk();
1560
+ await this.flush();
1561
+ } catch (error) {
1562
+ const operationError = toError(error);
1563
+ await this.#authoritativelyRewriteCurrentStateLocked(operationError);
1564
+ this.#notifyDurableEntries();
1565
+ throw error;
1566
+ }
1567
+
1568
+ const batch: AtomicEntryBatch = {
1569
+ collecting: true,
1570
+ entryIds: new Set(),
1571
+ deferredNotifications: [],
1572
+ preBatchLeafId: this.#index.leafId(),
1573
+ externalLeafChanged: false,
1574
+ externalLeafId: null,
1575
+ };
1576
+ this.#atomicEntryBatch = batch;
1577
+ let result!: T;
1578
+ try {
1579
+ try {
1580
+ result = append();
1581
+ } finally {
1582
+ batch.collecting = false;
1583
+ }
1584
+ await this.#rewriteAtomically();
1585
+ if (!this.#fileIsCurrent || this.#rewriteRequired) {
1586
+ throw new Error("Atomic session batch was superseded before commit.");
1587
+ }
1588
+ this.#atomicEntryBatch = undefined;
1589
+ this.#notifyDurableEntries(batch.deferredNotifications);
1590
+ return result;
1591
+ } catch (error) {
1592
+ batch.collecting = false;
1593
+ const operationError = toError(error);
1594
+ this.#rollbackAtomicEntryBatch(batch);
1595
+ try {
1596
+ await this.#authoritativelyRewriteCurrentStateLocked(operationError);
1597
+ } catch (repairError) {
1598
+ const retainedNotifications = batch.deferredNotifications.filter(entry => !batch.entryIds.has(entry.id));
1599
+ this.#pendingDurabilityNotifications.push(...retainedNotifications);
1600
+ this.#atomicEntryBatch = undefined;
1601
+ this.#fileIsCurrent = false;
1602
+ this.#rewriteRequired = true;
1603
+ if (repairError instanceof SessionPersistenceIndeterminateError) throw repairError;
1604
+ throw this.#latchIndeterminate(operationError, [toError(repairError)]);
1605
+ }
1606
+ const retainedNotifications = batch.deferredNotifications.filter(entry => !batch.entryIds.has(entry.id));
1607
+ this.#atomicEntryBatch = undefined;
1608
+ this.#notifyDurableEntries(retainedNotifications);
1609
+ throw error;
1610
+ }
1611
+ }
1612
+
1613
+ /**
1614
+ * Replace an uncertain append tail with the authoritative in-memory journal.
1615
+ * Callers must only use this for monotonic recovery where every retained
1616
+ * entry remains intended (for example, an explicit terminal tombstone).
1617
+ */
1618
+ recoverPersistenceFromCurrentState(): Promise<void> {
1619
+ return this.#withAtomicPersistenceLock(async () => {
1620
+ if (!this.#persist || !this.#sessionFile) return;
1621
+ if (this.#atomicEntryBatch) throw new Error("Atomic persistence lock ownership was violated.");
1622
+ const operationError =
1623
+ this.#diskFailure ?? new Error("Authoritative session persistence recovery was requested.");
1624
+ await this.#authoritativelyRewriteCurrentStateLocked(operationError);
1625
+ this.#notifyDurableEntries();
1626
+ });
1627
+ }
1628
+
1629
+ /** Flush pending writes. Call before switching sessions or on shutdown. */
1630
+ async flush(): Promise<void> {
1631
+ if (!this.#persist || !this.#sessionFile) return;
1632
+ await this.#scheduleDiskWork(async () => {
1633
+ if (this.#writer?.isOpen()) await this.#writer.flush();
1634
+ });
1635
+ // Drain any fire-and-forget backing writes (e.g. `writeTextSync` queued
1636
+ // on IndexedSessionStorage during `flushSync`) so callers relying on
1637
+ // flush() see the write durably visible to readers.
1638
+ await this.#storage.drain();
1639
+ if (this.#diskFailure) throw this.#diskFailure;
1640
+ }
1641
+
1642
+ /**
1643
+ * Synchronously makes the current append-only session durable. Avoid rewriting
1644
+ * an already-current file: large restored sessions can contain GiB of compacted
1645
+ * history, and Ctrl+C must not rebuild the whole JSONL string just to flush.
1646
+ */
1647
+ flushSync(): void {
1648
+ if (!this.#persist || !this.#sessionFile) return;
1649
+ if (this.#atomicEntryBatch) throw new Error("Cannot synchronously flush during an atomic session batch.");
1650
+ if (this.#diskFailure) throw this.#diskFailure;
1651
+ if (this.#fileIsCurrent && !this.#rewriteRequired) {
1652
+ this.#writer?.flushSync?.();
1653
+ const writerError = this.#writer?.getError();
1654
+ if (writerError) throw writerError;
1655
+ return;
1656
+ }
1657
+ this.#rewriteSynchronously();
1658
+ if (this.#diskFailure) throw this.#diskFailure;
1659
+ }
1660
+
1661
+ /**
1662
+ * Drop only session files that this manager saw materialized for a draft and
1663
+ * that still contain no durable conversation or extension state. Explicit
1664
+ * ensureOnDisk() records (ACP session/new, handoff) stay resumable.
1665
+ */
1666
+ async #dropIfEmptyAndNoDraft(): Promise<void> {
1667
+ if (!this.#draftOnlySessionCleanupArmed) return;
1668
+ const sessionFile = this.#sessionFile;
1669
+ if (!sessionFile || !this.#storage.existsSync(sessionFile)) {
1670
+ this.#draftOnlySessionCleanupArmed = false;
1671
+ return;
1672
+ }
1673
+ const draftPath = this.#draftPath();
1674
+ if (draftPath && this.#storage.existsSync(draftPath)) return;
1675
+ if (!this.#entries.every(isDraftOnlyMetadataEntry)) {
1676
+ await this.#clearDraftOnlySessionMarker();
1677
+ this.#draftOnlySessionCleanupArmed = false;
1678
+ return;
1679
+ }
1680
+ try {
1681
+ await this.#storage.deleteSessionWithArtifacts(sessionFile);
1682
+ this.#fileIsCurrent = false;
1683
+ this.#forceFileCreation = false;
1684
+ this.#hasTitleSlot = false;
1685
+ this.#draftOnlySessionCleanupArmed = false;
1686
+ } catch (err) {
1687
+ if (!isEnoent(err)) {
1688
+ logger.warn("Failed to drop empty session on close", { sessionFile, error: String(err) });
1689
+ }
1690
+ }
1691
+ }
1692
+
1693
+ /** Flush, then close the append writer. */
1694
+ async close(): Promise<void> {
1695
+ if (!this.#persist) return;
1696
+ await this.#scheduleDiskWork(async () => {
1697
+ const hadWriter = this.#writer !== undefined;
1698
+ await this.#closeWriterHandle();
1699
+ if (hadWriter || (this.#sessionFile && this.#storage.existsSync(this.#sessionFile)))
1700
+ this.#fileIsCurrent = true;
1701
+ });
1702
+ await this.#dropIfEmptyAndNoDraft();
1703
+ // Wait for any queued backing writes (IndexedSessionStorage per-path
1704
+ // tail) to become durable so a graceful shutdown does not exit while
1705
+ // a fire-and-forget publish is still on the wire.
1706
+ await this.#storage.drain();
1707
+ if (this.#diskFailure) throw this.#diskFailure;
1708
+ }
1709
+
1710
+ getCwd(): string {
1711
+ return this.#cwd;
1712
+ }
1713
+
1714
+ /** Additional workspace directories beyond cwd (multi-root), absolute and normalized. */
1715
+ getAdditionalDirectories(): string[] {
1716
+ return [...this.#additionalDirectories];
1717
+ }
1718
+
1719
+ /**
1720
+ * Persist a workspace-directory change to the session header. Respects the
1721
+ * lazy-persistence gate: a session with no durable output yet keeps the
1722
+ * change in memory (the header lands with the first real write), so seeding
1723
+ * roots at launch never materializes an empty resumable session file.
1724
+ */
1725
+ async #persistWorkspaceDirectoriesChange(): Promise<void> {
1726
+ if (!this.#persist || !this.#sessionFile || !this.#shouldHaveSessionFile()) return;
1727
+ this.#rewriteRequired = true;
1728
+ await this.#rewriteAtomically();
1729
+ }
1730
+
1731
+ /**
1732
+ * Add a workspace directory. Normalizes (relative to cwd), dedupes, rejects
1733
+ * the cwd itself, persists to the session header, and triggers an atomic
1734
+ * rewrite so the change survives a crash. Returns the resolved absolute
1735
+ * path or `null` when the directory was already present (no-op).
1736
+ */
1737
+ async addWorkspaceDirectory(directory: string): Promise<string | null> {
1738
+ const resolved = normalizeWorkspaceDirectory(directory, this.#cwd);
1739
+ if (resolved === path.resolve(this.#cwd)) {
1740
+ throw new Error("The current working directory is already the primary workspace root.");
1741
+ }
1742
+ if (this.#additionalDirectories.includes(resolved)) return null;
1743
+ this.#additionalDirectories = [...this.#additionalDirectories, resolved];
1744
+ this.#header.additionalDirectories = this.#additionalDirectories;
1745
+ await this.#persistWorkspaceDirectoriesChange();
1746
+ return resolved;
1747
+ }
1748
+
1749
+ /**
1750
+ * Remove a workspace directory by absolute or cwd-relative path. Persists
1751
+ * the trimmed header. Returns the resolved path that was removed, or
1752
+ * `null` when the directory was not an additional root (no-op).
1753
+ */
1754
+ async removeWorkspaceDirectory(directory: string): Promise<string | null> {
1755
+ const resolved = normalizeWorkspaceDirectory(directory, this.#cwd);
1756
+ const idx = this.#additionalDirectories.findIndex(p => path.resolve(p) === resolved);
1757
+ if (idx === -1) return null;
1758
+ this.#additionalDirectories = this.#additionalDirectories.filter((_, i) => i !== idx);
1759
+ if (this.#additionalDirectories.length === 0) {
1760
+ this.#header.additionalDirectories = undefined;
1761
+ } else {
1762
+ this.#header.additionalDirectories = this.#additionalDirectories;
1763
+ }
1764
+ await this.#persistWorkspaceDirectoriesChange();
1765
+ return resolved;
1766
+ }
1767
+
1768
+ /** Seed additional directories from settings or a passed list. Also called on resumed sessions with --add-dir; persists the updated header when the session file is already durable. No-op when the normalized list is unchanged (avoids rewriting large session files on every startup). */
1769
+ async setAdditionalDirectories(directories: string[]): Promise<void> {
1770
+ const workspace = normalizeSessionWorkspace({ cwd: this.#cwd, directories });
1771
+ const next = additionalWorkspaceDirectories(workspace);
1772
+ if (
1773
+ next.length === this.#additionalDirectories.length &&
1774
+ next.every((d, i) => d === this.#additionalDirectories[i])
1775
+ ) {
1776
+ return;
1777
+ }
1778
+ this.#additionalDirectories = next;
1779
+ if (this.#additionalDirectories.length > 0) {
1780
+ this.#header.additionalDirectories = this.#additionalDirectories;
1781
+ } else {
1782
+ this.#header.additionalDirectories = undefined;
1783
+ }
1784
+ await this.#persistWorkspaceDirectoriesChange();
1785
+ }
1786
+
1787
+ getUsageStatistics(): UsageStatistics {
1788
+ return this.#index.usageSnapshot();
1789
+ }
1790
+
1791
+ /**
1792
+ * Open a new per-turn budget window: snapshot the cumulative output baseline,
1793
+ * reset the eval-subagent counter, and set the (optional) ceiling.
1794
+ */
1795
+ beginTurnBudget(total: number | null, hard: boolean): void {
1796
+ this.#turnBudgetTotal = total;
1797
+ this.#turnBudgetHard = hard;
1798
+ this.#turnOutputBaseline = this.#index.usageSnapshot().output;
1799
+ this.#turnEvalOutput = 0;
1800
+ }
1801
+
1802
+ recordEvalSubagentOutput(output: number): void {
1803
+ if (Number.isFinite(output) && output > 0) this.#turnEvalOutput += output;
1804
+ }
1805
+
1806
+ getTurnBudget(): { total: number | null; spent: number; hard: boolean } {
1807
+ const mainOutput = Math.max(0, this.#index.usageSnapshot().output - this.#turnOutputBaseline);
1808
+ return { total: this.#turnBudgetTotal, spent: mainOutput + this.#turnEvalOutput, hard: this.#turnBudgetHard };
1809
+ }
1810
+
1811
+ getSessionDir(): string {
1812
+ return this.#sessionDir;
1813
+ }
1814
+
1815
+ getSessionId(): string {
1816
+ return this.#sessionId;
1817
+ }
1818
+
1819
+ getSessionFile(): string | undefined {
1820
+ return this.#sessionFile;
1821
+ }
1822
+
1823
+ getArtifactsDir(): string | null {
1824
+ if (this.#adoptedArtifactManager) return this.#adoptedArtifactManager.dir;
1825
+ return artifactsDirectoryFor(this.#sessionFile);
1826
+ }
1827
+
1828
+ adoptArtifactManager(manager: ArtifactManager): void {
1829
+ this.#adoptedArtifactManager = manager;
1830
+ }
1831
+
1832
+ getArtifactManager(): ArtifactManager | null {
1833
+ return this.#artifactManagerForSession();
1834
+ }
1835
+
1836
+ async allocateArtifactPath(toolType: string): Promise<{ id?: string; path?: string }> {
1837
+ return (await this.#artifactManagerForSession()?.allocatePath(toolType)) ?? {};
1838
+ }
1839
+
1840
+ async saveArtifact(content: string, toolType: string): Promise<string | undefined> {
1841
+ const manager = this.#artifactManagerForSession();
1842
+ if (manager) return manager.save(content, toolType);
1843
+
1844
+ // Non-persistent session: keep an in-memory copy so spill truncation works.
1845
+ this.#inMemoryArtifacts ??= new Map();
1846
+ const id = String(this.#inMemoryArtifactCounter++);
1847
+ this.#inMemoryArtifacts.set(id, content);
1848
+ return id;
1849
+ }
1850
+
1851
+ async getArtifactPath(id: string): Promise<string | null> {
1852
+ return (await this.#artifactManagerForSession()?.getPath(id)) ?? null;
1853
+ }
1854
+
1855
+ async saveDraft(text: string): Promise<void> {
1856
+ const draftPath = this.#draftPath();
1857
+ if (!draftPath || !this.#persist) return;
1858
+
1859
+ if (text.length === 0) {
1860
+ try {
1861
+ await this.#storage.unlink(draftPath);
1862
+ } catch (err) {
1863
+ if (!isEnoent(err)) throw err;
1864
+ }
1865
+ return;
1866
+ }
1867
+
1868
+ const sessionFile = this.#sessionFile;
1869
+ const draftWillMaterializeMetadataOnlyFile =
1870
+ sessionFile !== undefined &&
1871
+ !this.#storage.existsSync(sessionFile) &&
1872
+ this.#entries.every(isDraftOnlyMetadataEntry);
1873
+ // Force the header onto disk so resume can find the file this draft attaches to.
1874
+ await this.ensureOnDisk();
1875
+ if (draftWillMaterializeMetadataOnlyFile) {
1876
+ await this.#writeDraftOnlySessionMarker();
1877
+ this.#draftOnlySessionCleanupArmed = true;
1878
+ }
1879
+ await this.#storage.writeText(draftPath, text);
1880
+ }
1881
+
1882
+ async consumeDraft(): Promise<string | null> {
1883
+ const draftPath = this.#draftPath();
1884
+ if (!draftPath) return null;
1885
+
1886
+ let draft: string;
1887
+ try {
1888
+ draft = await this.#storage.readText(draftPath);
1889
+ } catch (err) {
1890
+ if (isEnoent(err)) return null;
1891
+ throw err;
1892
+ }
1893
+
1894
+ try {
1895
+ await this.#storage.unlink(draftPath);
1896
+ } catch (err) {
1897
+ if (!isEnoent(err)) throw err;
1898
+ }
1899
+ if (this.#entries.every(isDraftOnlyMetadataEntry) && this.#hasDraftOnlySessionMarker())
1900
+ this.#draftOnlySessionCleanupArmed = true;
1901
+
1902
+ return draft;
1903
+ }
1904
+
1905
+ /** The source that set the session name: "user" (manual/RPC) or "auto" (generated title). */
1906
+ get titleSource(): SessionTitleSource | undefined {
1907
+ return this.#titleSource;
1908
+ }
1909
+
1910
+ getSessionName(): string | undefined {
1911
+ return this.#sessionName;
1912
+ }
1913
+
1914
+ onSessionNameChanged(cb: () => void): () => void {
1915
+ this.#sessionNameChangedCallbacks.add(cb);
1916
+ return () => {
1917
+ this.#sessionNameChangedCallbacks.delete(cb);
1918
+ };
1919
+ }
1920
+
1921
+ /**
1922
+ * Set the session display name.
1923
+ * @param source "user" for explicit renames; "auto" for generated titles.
1924
+ * Auto titles are ignored once the user has set a name.
1925
+ */
1926
+ async setSessionName(name: string, source: SessionTitleSource = "auto", trigger?: string): Promise<boolean> {
1927
+ if (this.#titleSource === "user" && source === "auto") return false;
1928
+
1929
+ const title = SessionManager.#cleanTitle(name);
1930
+ if (!title) return false;
1931
+
1932
+ const previousTitle = this.#sessionName;
1933
+ const timestamp = nowIso();
1934
+ this.#sessionName = title;
1935
+ this.#titleSource = source;
1936
+ this.#titleUpdatedAt = timestamp;
1937
+ this.#header.title = title;
1938
+ this.#header.titleSource = source;
1939
+
1940
+ const entry: TitleChangeEntry = {
1941
+ type: TITLE_CHANGE_ENTRY_TYPE,
1942
+ ...this.#freshEntryFields(),
1943
+ timestamp,
1944
+ title,
1945
+ source,
1946
+ };
1947
+ if (previousTitle) entry.previousTitle = previousTitle;
1948
+ if (trigger) entry.trigger = trigger;
1949
+ this.#entries.push(entry);
1950
+ this.#index.insert(entry);
1951
+ this.#notifyEntryAppended(entry);
1952
+ await this.#persistTitleChangeEntry(entry, { title, source, updatedAt: timestamp });
1953
+
1954
+ this.#notifySessionNameListeners();
1955
+ return true;
1956
+ }
1957
+
1958
+ /**
1959
+ * Append a foreign (host-authored) entry verbatim, preserving its
1960
+ * `id`/`parentId`. Used by collab guests to mirror the host session.
1961
+ */
1962
+ ingestReplicatedEntry(entry: SessionEntry): void {
1963
+ this.#recordEntry(entry);
1964
+ }
1965
+
1966
+ /**
1967
+ * Snapshot the session for collab replication: the live header plus a deep
1968
+ * copy of every entry (the host mutates entries in place on rewrite paths, so
1969
+ * guests must not share references).
1970
+ */
1971
+ snapshotForReplication(): { header: SessionHeader; entries: SessionEntry[] } {
1972
+ return { header: structuredClone(this.#header), entries: structuredClone(this.#entries) as SessionEntry[] };
1973
+ }
1974
+
1975
+ /**
1976
+ * Append a message as a child of the current leaf, then advance the leaf.
1977
+ * CompactionSummaryMessage / BranchSummaryMessage are rejected here — they are
1978
+ * top-level entries via appendCompaction()/branchWithSummary().
1979
+ */
1980
+ appendMessage(
1981
+ message:
1982
+ | Message
1983
+ | CustomMessage
1984
+ | HookMessage
1985
+ | BashExecutionMessage
1986
+ | PythonExecutionMessage
1987
+ | FileMentionMessage,
1988
+ ): string {
1989
+ const entry: SessionMessageEntry = { type: "message", ...this.#freshEntryFields(), message };
1990
+ this.#recordEntry(entry);
1991
+ return entry.id;
1992
+ }
1993
+
1994
+ /**
1995
+ * Append to a non-active branch without changing the current leaf.
1996
+ * Used by work that retains ownership of a branch across tree navigation.
1997
+ */
1998
+ appendMessageToBranch(
1999
+ message:
2000
+ | Message
2001
+ | CustomMessage
2002
+ | HookMessage
2003
+ | BashExecutionMessage
2004
+ | PythonExecutionMessage
2005
+ | FileMentionMessage,
2006
+ parentId: string | null,
2007
+ ): string {
2008
+ if (parentId !== null && !this.#index.has(parentId)) throw new Error(`Entry ${parentId} not found`);
2009
+ const activeLeafId = this.#index.leafId();
2010
+ const entry: SessionMessageEntry = {
2011
+ type: "message",
2012
+ id: generateId(this.#index),
2013
+ parentId,
2014
+ timestamp: nowIso(),
2015
+ message,
2016
+ };
2017
+ this.#recordEntry(entry);
2018
+ this.#index.setLeaf(activeLeafId);
2019
+ return entry.id;
2020
+ }
2021
+
2022
+ /** Append a thinking level change as child of current leaf, then advance leaf. Returns entry id. */
2023
+ appendThinkingLevelChange(thinkingLevel?: string, configured?: string): string {
2024
+ const entry: ThinkingLevelChangeEntry = {
2025
+ type: "thinking_level_change",
2026
+ ...this.#freshEntryFields(),
2027
+ thinkingLevel: thinkingLevel ?? null,
2028
+ configured: configured ?? null,
2029
+ };
2030
+ this.#recordEntry(entry);
2031
+ return entry.id;
2032
+ }
2033
+
2034
+ appendServiceTierChange(serviceTier: ServiceTierByFamily | null): string {
2035
+ const entry: ServiceTierChangeEntry = { type: "service_tier_change", ...this.#freshEntryFields(), serviceTier };
2036
+ this.#recordEntry(entry);
2037
+ return entry.id;
2038
+ }
2039
+
2040
+ appendModeChange(mode: string, data?: Record<string, unknown>): string {
2041
+ const entry: ModeChangeEntry = { type: "mode_change", ...this.#freshEntryFields(), mode, data };
2042
+ this.#recordEntry(entry);
2043
+ return entry.id;
2044
+ }
2045
+
2046
+ /**
2047
+ * Append a model change as a child of the current leaf, then advance the leaf.
2048
+ * @param model Model in "provider/modelId" format
2049
+ * @param role Optional role (default: "default")
2050
+ * @param resolvedModelIsFallback Whether this transition selected a retry-fallback model
2051
+ */
2052
+ appendModelChange(model: string, role?: string, resolvedModelIsFallback = false): string {
2053
+ const entry: ModelChangeEntry = {
2054
+ type: "model_change",
2055
+ ...this.#freshEntryFields(),
2056
+ model,
2057
+ role,
2058
+ resolvedModelIsFallback,
2059
+ };
2060
+ this.#recordEntry(entry);
2061
+ return entry.id;
2062
+ }
2063
+
2064
+ appendSessionInit(init: {
2065
+ systemPrompt: string;
2066
+ task: string;
2067
+ tools: string[];
2068
+ agent?: string;
2069
+ modelRole?: string;
2070
+ resolvedModel?: string;
2071
+ readOnly?: boolean;
2072
+ outputSchema?: unknown;
2073
+ outputSchemaMode?: StructuredSubagentSchemaMode;
2074
+ restrictToolNames?: boolean;
2075
+ spawns?: string;
2076
+ readSummarize?: boolean;
2077
+ }): string {
2078
+ const entry: SessionInitEntry = { type: "session_init", ...this.#freshEntryFields(), ...init };
2079
+ this.#recordEntry(entry);
2080
+ return entry.id;
2081
+ }
2082
+
2083
+ appendCompaction<T = unknown>(
2084
+ summary: string,
2085
+ shortSummary: string | undefined,
2086
+ firstKeptEntryId: string,
2087
+ tokensBefore: number,
2088
+ details?: T,
2089
+ fromExtension?: boolean,
2090
+ preserveData?: Record<string, unknown>,
2091
+ ): string {
2092
+ const entry: CompactionEntry<T> = {
2093
+ type: "compaction",
2094
+ ...this.#freshEntryFields(),
2095
+ summary,
2096
+ shortSummary,
2097
+ firstKeptEntryId,
2098
+ tokensBefore,
2099
+ details,
2100
+ fromExtension,
2101
+ preserveData,
2102
+ };
2103
+ this.#recordEntry(entry);
2104
+ return entry.id;
2105
+ }
2106
+
2107
+ /**
2108
+ * Append the durable conversation boundary recorded by `/clear`. The
2109
+ * collapsed live transcript and the model-context rebuild start after the
2110
+ * latest one, while the full history stays on disk (the plain
2111
+ * `transcript:true` export walks it unchanged).
2112
+ */
2113
+ appendResetBoundary(): string {
2114
+ const entry: ResetBoundaryEntry = { type: "reset_boundary", ...this.#freshEntryFields() };
2115
+ this.#recordEntry(entry);
2116
+ return entry.id;
2117
+ }
2118
+
2119
+ appendCustomEntry(customType: string, data?: unknown): string {
2120
+ const entry: CustomEntry = { type: "custom", customType, data, ...this.#freshEntryFields() };
2121
+ this.#recordEntry(entry);
2122
+ return entry.id;
2123
+ }
2124
+
2125
+ /**
2126
+ * Rewrite the session file after in-place entry updates (e.g. pruning old tool
2127
+ * outputs). Use sparingly.
2128
+ */
2129
+ async rewriteEntries(): Promise<void> {
2130
+ if (!this.#persist || !this.#sessionFile) return;
2131
+ await this.#rewriteAtomically();
2132
+ }
2133
+
2134
+ /**
2135
+ * Append a custom message entry (for extensions) that participates in LLM context.
2136
+ * @param customType Hook identifier for filtering on reload
2137
+ * @param content Message content (string or TextContent/ImageContent array)
2138
+ * @param display Whether to show in TUI (true = styled display, false = hidden)
2139
+ * @param details Optional extension-specific metadata (not sent to LLM)
2140
+ * @param attribution Who initiated this message for billing/attribution semantics
2141
+ */
2142
+ appendCustomMessageEntry<T = unknown>(
2143
+ customType: string | undefined,
2144
+ content: string | (TextContent | ImageContent)[] | undefined,
2145
+ display: boolean | undefined,
2146
+ details?: T,
2147
+ attribution: MessageAttribution | undefined = "agent",
2148
+ ): string {
2149
+ const normalized = normalizeCustomMessagePayload<T>({ customType, content, display, details, attribution });
2150
+ const entry: CustomMessageEntry<T> = {
2151
+ type: "custom_message",
2152
+ customType: normalized.customType,
2153
+ content: normalized.content,
2154
+ display: normalized.display,
2155
+ // Drop AgentSession-internal transient fields before disk persistence.
2156
+ details: stripInternalDetailsFields(normalized.details),
2157
+ attribution: normalized.attribution,
2158
+ ...this.#freshEntryFields(),
2159
+ };
2160
+ this.#recordEntry(entry);
2161
+ return entry.id;
2162
+ }
2163
+
2164
+ /** Append a TTSR injection entry recording which rules were injected. */
2165
+ appendTtsrInjection(ruleNames: string[]): string {
2166
+ const entry: TtsrInjectionEntry = {
2167
+ type: "ttsr_injection",
2168
+ ...this.#freshEntryFields(),
2169
+ injectedRules: [...ruleNames],
2170
+ };
2171
+ this.#recordEntry(entry);
2172
+ return entry.id;
2173
+ }
2174
+
2175
+ /** All unique TTSR rule names injected on the current branch (root → leaf). */
2176
+ getInjectedTtsrRules(): string[] {
2177
+ const names = new Set<string>();
2178
+ for (const entry of this.getBranch()) {
2179
+ if (entry.type !== "ttsr_injection") continue;
2180
+ for (const name of entry.injectedRules) names.add(name);
2181
+ }
2182
+ return [...names];
2183
+ }
2184
+
2185
+ /** Append a credential pin recording which OAuth account served `provider`. */
2186
+ appendCredentialPin(provider: string, hash: string): string {
2187
+ const entry: CredentialPinEntry = {
2188
+ type: "credential_pin",
2189
+ ...this.#freshEntryFields(),
2190
+ provider,
2191
+ hash,
2192
+ };
2193
+ this.#recordEntry(entry);
2194
+ return entry.id;
2195
+ }
2196
+
2197
+ /**
2198
+ * Latest credential pin per provider on the current branch (root → leaf),
2199
+ * with the effective last-use time of the pinned account.
2200
+ *
2201
+ * Pins are appended only when the serving account *changes*, so a long
2202
+ * session on one account carries a single old pin entry. Any assistant turn
2203
+ * for the same provider after that pin was necessarily served by the pinned
2204
+ * account, so its timestamp advances `lastUsedAt` — a resume seconds after
2205
+ * the last turn seeds a warm sticky instead of a stale one.
2206
+ */
2207
+ getCredentialPins(): Map<string, { hash: string; lastUsedAt: number }> {
2208
+ const pins = new Map<string, { hash: string; lastUsedAt: number }>();
2209
+ for (const entry of this.getBranch()) {
2210
+ if (entry.type === "credential_pin") {
2211
+ pins.set(entry.provider, { hash: entry.hash, lastUsedAt: new Date(entry.timestamp).getTime() });
2212
+ } else if (entry.type === "message" && entry.message.role === "assistant") {
2213
+ const pin = pins.get(entry.message.provider);
2214
+ if (pin) pin.lastUsedAt = Math.max(pin.lastUsedAt, entry.message.timestamp);
2215
+ }
2216
+ }
2217
+ return pins;
2218
+ }
2219
+
2220
+ getLeafId(): string | null {
2221
+ return this.#index.leafId();
2222
+ }
2223
+
2224
+ getLeafEntry(): SessionEntry | undefined {
2225
+ return this.#index.leafEntry();
2226
+ }
2227
+
2228
+ /**
2229
+ * The most recent model role on the current branch, or undefined when no
2230
+ * model change has been recorded.
2231
+ */
2232
+ getLastModelChangeRole(): string | undefined {
2233
+ const branch = this.getBranch();
2234
+ for (let index = branch.length - 1; index >= 0; index--) {
2235
+ const entry = branch[index];
2236
+ if (entry.type === "model_change") return entry.role ?? "default";
2237
+ }
2238
+ return undefined;
2239
+ }
2240
+
2241
+ getEntry(id: string): SessionEntry | undefined {
2242
+ return this.#index.get(id);
2243
+ }
2244
+
2245
+ /** All direct children of an entry. */
2246
+ getChildren(parentId: string): SessionEntry[] {
2247
+ return this.#index.childrenOf(parentId);
2248
+ }
2249
+
2250
+ getLabel(id: string): string | undefined {
2251
+ return this.#index.labelFor(id);
2252
+ }
2253
+
2254
+ /**
2255
+ * Set or clear a label on an entry. Pass undefined/empty to clear.
2256
+ */
2257
+ appendLabelChange(targetId: string, label: string | undefined): string {
2258
+ if (!this.#index.has(targetId)) throw new Error(`Entry ${targetId} not found`);
2259
+
2260
+ const entry: LabelEntry = { type: "label", ...this.#freshEntryFields(), targetId, label };
2261
+ this.#recordEntry(entry);
2262
+ return entry.id;
2263
+ }
2264
+
2265
+ /**
2266
+ * Walk from an entry to root, returning entries in path order. Includes all
2267
+ * entry types; use buildSessionContext() for the resolved LLM messages.
2268
+ */
2269
+ getBranch(fromId?: string): SessionEntry[] {
2270
+ return this.#index.pathTo(fromId ?? this.#index.leafId());
2271
+ }
2272
+
2273
+ /**
2274
+ * Build the session context (LLM messages), or — with `{ transcript: true }` —
2275
+ * the full-history display transcript, from the current leaf path.
2276
+ */
2277
+ buildSessionContext(options?: BuildSessionContextOptions): SessionContext {
2278
+ return buildSessionContext(this.#entries, this.#index.leafId(), this.#index.entriesById(), options);
2279
+ }
2280
+
2281
+ /** Strip stale OpenAI Responses assistant replay metadata from loaded entries. */
2282
+ sanitizeLoadedOpenAIResponsesReplayMetadata(): boolean {
2283
+ let changed = false;
2284
+ for (const entry of this.#entries) {
2285
+ if (entry.type !== "message" || entry.message.role !== "assistant") continue;
2286
+
2287
+ const sanitized = sanitizeRehydratedOpenAIResponsesAssistantMessage(entry.message);
2288
+ if (sanitized === entry.message) continue;
2289
+
2290
+ entry.message = sanitized;
2291
+ changed = true;
2292
+ }
2293
+
2294
+ return changed;
2295
+ }
2296
+
2297
+ getHeader(): SessionHeader | null {
2298
+ return this.#header;
2299
+ }
2300
+
2301
+ /** All session entries (excludes header). Returns a shallow copy. */
2302
+ getEntries(): SessionEntry[] {
2303
+ return [...this.#entries];
2304
+ }
2305
+
2306
+ /**
2307
+ * The session as a tree. A well-formed session has exactly one root; orphaned
2308
+ * entries (broken parent chain) are returned as roots too.
2309
+ */
2310
+ getTree(): SessionTreeNode[] {
2311
+ return this.#index.tree(this.#entries);
2312
+ }
2313
+
2314
+ /**
2315
+ * Move the leaf to an earlier entry so the next append forms a new branch.
2316
+ * Existing entries are never modified or deleted.
2317
+ */
2318
+ branch(branchFromId: string): void {
2319
+ if (!this.#index.has(branchFromId)) throw new Error(`Entry ${branchFromId} not found`);
2320
+ this.#setLeaf(branchFromId);
2321
+ }
2322
+
2323
+ /** Reset the leaf to null so the next append creates a new root entry. */
2324
+ resetLeaf(): void {
2325
+ this.#setLeaf(null);
2326
+ }
2327
+
2328
+ /** Like branch(), but also records a branch_summary of the abandoned path. */
2329
+ branchWithSummary(branchFromId: string | null, summary: string, details?: unknown, fromExtension?: boolean): string {
2330
+ if (branchFromId !== null && !this.#index.has(branchFromId)) throw new Error(`Entry ${branchFromId} not found`);
2331
+
2332
+ this.#setLeaf(branchFromId);
2333
+ const entry: BranchSummaryEntry = {
2334
+ type: "branch_summary",
2335
+ id: generateId(this.#index),
2336
+ parentId: branchFromId,
2337
+ timestamp: nowIso(),
2338
+ fromId: branchFromId ?? "root",
2339
+ summary,
2340
+ details,
2341
+ fromExtension,
2342
+ };
2343
+ this.#recordEntry(entry);
2344
+ return entry.id;
2345
+ }
2346
+
2347
+ /**
2348
+ * Create a new session file containing only the path from root to `leafId`.
2349
+ * Returns the new file path, or undefined when not persisting.
2350
+ */
2351
+ createBranchedSession(leafId: string): string | undefined {
2352
+ const sourceSessionFile = this.#sessionFile;
2353
+ const branchPath = this.getBranch(leafId);
2354
+ if (branchPath.length === 0) throw new Error(`Entry ${leafId} not found`);
2355
+
2356
+ // Drop label entries from the path; recreate them fresh from the resolved map.
2357
+ const entriesToKeep = branchPath.filter(entry => entry.type !== "label");
2358
+ const keptIds = new Set(entriesToKeep.map(entry => entry.id));
2359
+ const labelsToCarry: Array<{ targetId: string; label: string }> = [];
2360
+ for (const [targetId, label] of this.#index.labelsInEffect()) {
2361
+ if (keptIds.has(targetId)) labelsToCarry.push({ targetId, label });
2362
+ }
2363
+
2364
+ const timestamp = nowIso();
2365
+ const newSessionId = mintSessionId();
2366
+ const newSessionFile = path.join(this.#sessionDir, `${fileSafeTimestamp(timestamp)}_${newSessionId}.jsonl`);
2367
+ const header: SessionHeader = {
2368
+ type: "session",
2369
+ version: CURRENT_SESSION_VERSION,
2370
+ id: newSessionId,
2371
+ timestamp,
2372
+ cwd: this.#cwd,
2373
+ title: this.#sessionName,
2374
+ titleSource: this.#titleSource,
2375
+ parentSession: this.#persist ? sourceSessionFile : undefined,
2376
+ additionalDirectories: this.#additionalDirectories.length > 0 ? [...this.#additionalDirectories] : undefined,
2377
+ };
2378
+
2379
+ const labels: LabelEntry[] = [];
2380
+ let parentId = entriesToKeep[entriesToKeep.length - 1]?.id ?? null;
2381
+ for (const carried of labelsToCarry) {
2382
+ const labelEntry: LabelEntry = {
2383
+ type: "label",
2384
+ id: generateId(new Set([...keptIds, ...labels.map(entry => entry.id)])),
2385
+ parentId,
2386
+ timestamp: nowIso(),
2387
+ targetId: carried.targetId,
2388
+ label: carried.label,
2389
+ };
2390
+ labels.push(labelEntry);
2391
+ parentId = labelEntry.id;
2392
+ }
2393
+
2394
+ this.#header = header;
2395
+ this.#entries = [...entriesToKeep, ...labels];
2396
+ this.#sessionId = newSessionId;
2397
+ this.#sessionName = header.title;
2398
+ this.#titleSource = header.titleSource;
2399
+ this.#titleUpdatedAt = timestamp;
2400
+ this.#hasTitleSlot = true;
2401
+ this.#index.rebuild(this.#entries);
2402
+ this.#artifactManager = null;
2403
+ this.#artifactManagerSessionFile = null;
2404
+ this.#forceFileCreation = this.#persist;
2405
+
2406
+ if (!this.#persist) {
2407
+ this.#sessionFile = undefined;
2408
+ this.#fileIsCurrent = false;
2409
+ this.#rewriteRequired = false;
2410
+ return undefined;
2411
+ }
2412
+
2413
+ this.#sessionFile = newSessionFile;
2414
+ this.#rewriteSynchronously();
2415
+ this.#rememberBreadcrumb(this.#cwd, newSessionFile);
2416
+ return newSessionFile;
2417
+ }
2418
+
2419
+ /** Resolve the canonical default session directory for a cwd. */
2420
+ static getDefaultSessionDir(
2421
+ cwd: string,
2422
+ agentDir?: string,
2423
+ storage: SessionStorage = new FileSessionStorage(),
2424
+ ): string {
2425
+ return computeDefaultSessionDir(cwd, storage, getSessionsDir(agentDir));
2426
+ }
2427
+
2428
+ /**
2429
+ * Create a new session.
2430
+ * @param cwd Working directory (stored in the session header)
2431
+ * @param sessionDir Optional session directory; defaults to the cwd-derived dir.
2432
+ */
2433
+ static create(cwd: string, sessionDir?: string, storage: SessionStorage = new FileSessionStorage()): SessionManager {
2434
+ const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
2435
+ const manager = new SessionManager(cwd, dir, true, storage);
2436
+ manager.#resetToNewSession();
2437
+ return manager;
2438
+ }
2439
+
2440
+ /**
2441
+ * Create a fresh empty session file in the default session directory for
2442
+ * `cwd`, writing only the session header. The returned path can be passed to
2443
+ * `setSessionFile` / `AgentSession.switchSession` when a caller explicitly
2444
+ * needs a brand-new persisted session at a cwd-derived path.
2445
+ */
2446
+ static createEmptySessionFile(cwd: string, storage: SessionStorage = new FileSessionStorage()): string {
2447
+ const sessionDir = SessionManager.getDefaultSessionDir(cwd, undefined, storage);
2448
+ const id = mintSessionId();
2449
+ const timestamp = nowIso();
2450
+ const header: SessionHeader = {
2451
+ type: "session",
2452
+ version: CURRENT_SESSION_VERSION,
2453
+ id,
2454
+ timestamp,
2455
+ cwd: path.resolve(cwd),
2456
+ };
2457
+ const file = path.join(sessionDir, `${fileSafeTimestamp(timestamp)}_${id}.jsonl`);
2458
+ storage.writeTextSync(file, `${serializeTitleSlot({ updatedAt: timestamp })}${JSON.stringify(header)}\n`);
2459
+ return file;
2460
+ }
2461
+
2462
+ /**
2463
+ * Fork a session into the current project directory: copy history from another
2464
+ * session file while creating a fresh session file in this sessionDir.
2465
+ *
2466
+ * `options.sessionFile` pins the new session's file path (default: an
2467
+ * auto-named `<timestamp>_<id>.jsonl` in `sessionDir`). Callers that register
2468
+ * the fork as a named agent (e.g. `/tan`) pass `<agentId>.jsonl` so the
2469
+ * persisted-subagent scan keys the agent by the same id the live ref uses.
2470
+ */
2471
+ static async forkFrom(
2472
+ sourcePath: string,
2473
+ cwd: string,
2474
+ sessionDir?: string,
2475
+ storage: SessionStorage = new FileSessionStorage(),
2476
+ options?: { suppressBreadcrumb?: boolean; sessionFile?: string },
2477
+ ): Promise<SessionManager> {
2478
+ const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
2479
+ const manager = new SessionManager(cwd, dir, true, storage);
2480
+ manager.#suppressBreadcrumb = options?.suppressBreadcrumb === true;
2481
+
2482
+ const sourceEntries = structuredClone(await loadEntriesFromFile(sourcePath, storage)) as FileEntry[];
2483
+ migrateToCurrentVersion(sourceEntries);
2484
+ await resolveBlobRefsInEntries(sourceEntries, manager.#blobs);
2485
+
2486
+ const sourceHeader = sourceEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
2487
+ const history = sourceEntries.filter(entry => entry.type !== "session") as SessionEntry[];
2488
+ manager.#resetToNewSession(
2489
+ {
2490
+ parentSession: sourceHeader?.id,
2491
+ providerPromptCacheKey: sourceHeader?.providerPromptCacheKey ?? sourceHeader?.id,
2492
+ },
2493
+ options?.sessionFile,
2494
+ );
2495
+ manager.#header.title = sourceHeader?.title;
2496
+ manager.#header.titleSource = sourceHeader?.titleSource;
2497
+ manager.#additionalDirectories = (sourceHeader?.additionalDirectories ?? []).filter(d => d !== path.resolve(cwd));
2498
+ manager.#header.additionalDirectories =
2499
+ manager.#additionalDirectories.length > 0 ? manager.#additionalDirectories : undefined;
2500
+ manager.#sessionName = manager.#header.title;
2501
+ manager.#titleSource = manager.#header.titleSource;
2502
+ manager.#titleUpdatedAt = nowIso();
2503
+ manager.#hasTitleSlot = true;
2504
+ manager.#entries = history;
2505
+ manager.#index.rebuild(history);
2506
+ manager.sanitizeLoadedOpenAIResponsesReplayMetadata();
2507
+ manager.#forceFileCreation = true;
2508
+ await manager.#rewriteAtomically();
2509
+ return manager;
2510
+ }
2511
+
2512
+ /**
2513
+ * Open a specific session file.
2514
+ * @param sessionDir Optional dir for /new or /branch; defaults to the file's parent.
2515
+ * @param options.initialCwd Cwd to use when the file is empty or missing.
2516
+ */
2517
+ static async open(
2518
+ filePath: string,
2519
+ sessionDir?: string,
2520
+ storage: SessionStorage = new FileSessionStorage(),
2521
+ options?: { initialCwd?: string; suppressBreadcrumb?: boolean },
2522
+ ): Promise<SessionManager> {
2523
+ const loaded = await loadEntriesFromFile(filePath, storage);
2524
+ const header = loaded.find(entry => entry.type === "session") as SessionHeader | undefined;
2525
+ // Resume into the session's recorded cwd only when that directory still
2526
+ // exists. A deleted project dir would make the constructor's #cwd — and the
2527
+ // `setProjectDir` chdir interactive mode runs next — point at (and fail on)
2528
+ // a missing path, so fall back to the launch cwd and anchor /new and /branch
2529
+ // there too, keeping the resumed session where the user already is.
2530
+ const recordedCwd = header?.cwd;
2531
+ const recordedCwdUsable = !!recordedCwd && (await directoryExists(recordedCwd));
2532
+ const cwd = recordedCwdUsable ? recordedCwd : (options?.initialCwd ?? getProjectDir());
2533
+ const dir =
2534
+ sessionDir ??
2535
+ (recordedCwd && !recordedCwdUsable
2536
+ ? SessionManager.getDefaultSessionDir(cwd, undefined, storage)
2537
+ : path.dirname(path.resolve(filePath)));
2538
+ const manager = new SessionManager(cwd, dir, true, storage);
2539
+ manager.#suppressBreadcrumb = options?.suppressBreadcrumb === true;
2540
+ await manager.setSessionFile(filePath);
2541
+ return manager;
2542
+ }
2543
+
2544
+ /**
2545
+ * Lock-free peek for cold subagent revival: returns the recorded working
2546
+ * directory (session header) and the latest `session_init` contract (system
2547
+ * prompt / tools / output schema) WITHOUT taking the single-writer lock that
2548
+ * {@link open} acquires — the caller re-opens for the actual revive. Returns
2549
+ * null when the file can't be read; `init` is null for files written before
2550
+ * `session_init` was recorded (no faithful contract to rebuild from).
2551
+ */
2552
+ static async peekSessionInit(
2553
+ filePath: string,
2554
+ storage: SessionStorage = new FileSessionStorage(),
2555
+ ): Promise<{
2556
+ cwd: string;
2557
+ init: {
2558
+ systemPrompt: string;
2559
+ task: string;
2560
+ tools: string[];
2561
+ agent?: string;
2562
+ modelRole?: string;
2563
+ resolvedModel?: string;
2564
+ outputSchema?: unknown;
2565
+ outputSchemaMode?: StructuredSubagentSchemaMode;
2566
+ restrictToolNames?: boolean;
2567
+ spawns?: string;
2568
+ readSummarize?: boolean;
2569
+ } | null;
2570
+ } | null> {
2571
+ let loaded: FileEntry[];
2572
+ try {
2573
+ loaded = await loadEntriesFromFile(filePath, storage);
2574
+ } catch {
2575
+ return null;
2576
+ }
2577
+ // A missing/empty file has no usable session — nothing to revive from.
2578
+ if (loaded.length === 0) return null;
2579
+ const header = loaded.find(entry => entry.type === "session") as SessionHeader | undefined;
2580
+ let init: {
2581
+ systemPrompt: string;
2582
+ task: string;
2583
+ tools: string[];
2584
+ agent?: string;
2585
+ modelRole?: string;
2586
+ resolvedModel?: string;
2587
+ outputSchema?: unknown;
2588
+ outputSchemaMode?: StructuredSubagentSchemaMode;
2589
+ restrictToolNames?: boolean;
2590
+ spawns?: string;
2591
+ readSummarize?: boolean;
2592
+ } | null = null;
2593
+ for (let index = loaded.length - 1; index >= 0; index--) {
2594
+ const entry = loaded[index];
2595
+ if (entry.type === "session_init") {
2596
+ init = {
2597
+ systemPrompt: entry.systemPrompt,
2598
+ task: entry.task,
2599
+ tools: entry.tools,
2600
+ agent: entry.agent,
2601
+ modelRole: entry.modelRole,
2602
+ resolvedModel: entry.resolvedModel,
2603
+ outputSchema: entry.outputSchema,
2604
+ outputSchemaMode: entry.outputSchemaMode,
2605
+ restrictToolNames: entry.restrictToolNames,
2606
+ readSummarize: entry.readSummarize,
2607
+ spawns: entry.spawns,
2608
+ };
2609
+ break;
2610
+ }
2611
+ }
2612
+ return { cwd: header?.cwd ?? getProjectDir(), init };
2613
+ }
2614
+
2615
+ /** Continue the most recent session, or create a new one if none exists. */
2616
+ static async continueRecent(
2617
+ cwd: string,
2618
+ sessionDir?: string,
2619
+ storage: SessionStorage = new FileSessionStorage(),
2620
+ ): Promise<SessionManager> {
2621
+ const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
2622
+ const resolvedCwd = path.resolve(cwd);
2623
+ const breadcrumb = await readTerminalBreadcrumbEntry();
2624
+ let chosenSession: string | null | undefined;
2625
+
2626
+ if (breadcrumb) {
2627
+ // A fresh `/new` boundary whose JSONL was never materialized (lazy
2628
+ // new-session persistence, then a process exit before any assistant
2629
+ // output). Honor the boundary: start fresh rather than falling back to
2630
+ // findMostRecentSession(), which would resurrect the pre-`/new`
2631
+ // transcript. A materialized (or genuinely stale/deleted) crumb reports
2632
+ // exists=false only when fresh, so this never masks a real stale crumb.
2633
+ if (breadcrumb.fresh && !breadcrumb.exists) {
2634
+ const manager = new SessionManager(cwd, dir, true, storage);
2635
+ manager.#resetToNewSession();
2636
+ return manager;
2637
+ }
2638
+
2639
+ // Recover stale crumbs: a subagent open (pre-fix) may have pointed this
2640
+ // terminal's breadcrumb at an artifact child; resume the parent instead.
2641
+ breadcrumb.sessionFile = resolveBreadcrumbToInteractiveRoot(breadcrumb.sessionFile);
2642
+ const breadcrumbCwd = path.resolve(breadcrumb.cwd);
2643
+ if (breadcrumbCwd === resolvedCwd) {
2644
+ chosenSession = breadcrumb.sessionFile;
2645
+ } else {
2646
+ // The terminal's last session started in a different cwd. If that cwd is
2647
+ // gone (worktree move/rename) and this location has no sessions of its
2648
+ // own, re-root the moved session here instead of starting fresh. When an
2649
+ // explicit sessionDir is reused across the move, the stale breadcrumb file
2650
+ // may be the newest entry there; prefer a genuine current-cwd session.
2651
+ let newestInTargetDir = await findMostRecentSession(dir, storage);
2652
+ const breadcrumbFile = path.resolve(breadcrumb.sessionFile);
2653
+ const breadcrumbCwdMissing = !fs.existsSync(breadcrumbCwd);
2654
+ const newestIsBreadcrumb = newestInTargetDir ? path.resolve(newestInTargetDir) === breadcrumbFile : false;
2655
+ let currentProjectAlreadyHasSession = false;
2656
+
2657
+ if (breadcrumbCwdMissing && newestIsBreadcrumb) {
2658
+ const localSession = (await SessionManager.list(cwd, dir, storage)).find(
2659
+ session =>
2660
+ path.resolve(session.path) !== breadcrumbFile &&
2661
+ session.cwd &&
2662
+ path.resolve(session.cwd) === resolvedCwd,
2663
+ );
2664
+ if (localSession) {
2665
+ newestInTargetDir = localSession.path;
2666
+ currentProjectAlreadyHasSession = true;
2667
+ }
2668
+ }
2669
+
2670
+ const looksLikeMovedProject =
2671
+ breadcrumbCwdMissing &&
2672
+ (newestInTargetDir === null || (newestIsBreadcrumb && !currentProjectAlreadyHasSession));
2673
+ if (looksLikeMovedProject) {
2674
+ logger.info("Re-rooting moved session", { from: breadcrumbCwd, to: resolvedCwd });
2675
+ // Anchor at the gone breadcrumb cwd so the moveTo below relocates the
2676
+ // session: open() now falls back to the launch cwd for a missing
2677
+ // recorded cwd, which would no-op moveTo when it equals `cwd`.
2678
+ const manager = await SessionManager.open(breadcrumb.sessionFile, undefined, storage, {
2679
+ initialCwd: breadcrumbCwd,
2680
+ });
2681
+ await manager.moveTo(cwd, sessionDir);
2682
+ return manager;
2683
+ }
2684
+
2685
+ chosenSession = newestInTargetDir;
2686
+ }
2687
+ }
2688
+
2689
+ if (chosenSession === undefined) chosenSession = await findMostRecentSession(dir, storage);
2690
+
2691
+ const manager = new SessionManager(cwd, dir, true, storage);
2692
+ if (chosenSession) await manager.setSessionFile(chosenSession);
2693
+ else manager.#resetToNewSession();
2694
+ return manager;
2695
+ }
2696
+
2697
+ /** Create an in-memory session (no file persistence). */
2698
+ static inMemory(
2699
+ cwd: string = getProjectDir(),
2700
+ storage: SessionStorage = new MemorySessionStorage(),
2701
+ ): SessionManager {
2702
+ const manager = new SessionManager(cwd, "", false, storage);
2703
+ manager.#resetToNewSession();
2704
+ return manager;
2705
+ }
2706
+
2707
+ /**
2708
+ * List sessions for a project directory.
2709
+ * @param sessionDir Optional dir; defaults to the cwd-derived dir.
2710
+ */
2711
+ static async list(
2712
+ cwd: string,
2713
+ sessionDir?: string,
2714
+ storage: SessionStorage = new FileSessionStorage(),
2715
+ ): Promise<SessionInfo[]> {
2716
+ const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
2717
+ return listSessions(dir, storage);
2718
+ }
2719
+
2720
+ /** List all sessions across all project directories. */
2721
+ static listAll(storage: SessionStorage = new FileSessionStorage()): Promise<SessionInfo[]> {
2722
+ return listAllSessions(storage);
2723
+ }
2724
+ }
2725
+
2726
+ /**
2727
+ * If the current session was created by `/move` and contains no real
2728
+ * user/assistant messages, delete it so empty move sessions don't accumulate.
2729
+ */
2730
+ export async function cleanupEmptyMoveSession(
2731
+ sessionManager: SessionManager,
2732
+ movedFromEmptySessionFile: string | undefined,
2733
+ ): Promise<void> {
2734
+ const sessionFile = sessionManager.getSessionFile();
2735
+ if (!sessionFile || !movedFromEmptySessionFile) return;
2736
+ if (path.resolve(sessionFile) !== path.resolve(movedFromEmptySessionFile)) return;
2737
+ const entries = sessionManager.getEntries();
2738
+ const hasRealMessages = entries.some(
2739
+ e => e.type === "message" && (e.message.role === "user" || e.message.role === "assistant"),
2740
+ );
2741
+ if (hasRealMessages) return;
2742
+ try {
2743
+ await sessionManager.dropSession(sessionFile);
2744
+ } catch (err) {
2745
+ logger.warn("Failed to clean up empty move session", { sessionFile, error: String(err) });
2746
+ }
2747
+ }