pi-agent-zh 17.1.8

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