@sayknow-cli/coding-agent 0.3.16 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1474) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/package.json +127 -110
  3. package/scripts/build-sdk-package-smoke.ts +132 -0
  4. package/scripts/capture-notifications-settings-showcase.ts +435 -0
  5. package/scripts/compile-args.ts +5 -2
  6. package/scripts/g005-daemon-smoke.ts +4 -5
  7. package/scripts/g011-daemon-path-smoke.ts +5 -5
  8. package/scripts/generate-hotkeys-docs.ts +74 -0
  9. package/scripts/generate-sdk-adapter-parity-manifest.ts +163 -0
  10. package/scripts/generate-sdk-operation-inventory.ts +914 -0
  11. package/scripts/generate-telegram-baseline-manifest.ts +128 -0
  12. package/scripts/manifest-schema.ts +124 -0
  13. package/scripts/run-test-manifest.ts +153 -0
  14. package/scripts/run-threat-model-check.ts +217 -0
  15. package/scripts/verify-skc-sdk-canonicalization.ts +3647 -0
  16. package/scripts/verify-skc-sdk-rename.ts +90 -0
  17. package/src/append-only-mode.ts +1 -1
  18. package/src/async/job-manager.ts +455 -69
  19. package/src/cli/args.ts +43 -12
  20. package/src/cli/auth-gateway-cli.ts +2 -2
  21. package/src/cli/config-cli.ts +77 -3
  22. package/src/cli/daemon-cli.ts +63 -14
  23. package/src/cli/launch-disposition.ts +42 -0
  24. package/src/cli/list-models.ts +1 -1
  25. package/src/cli/notify-cli.ts +373 -283
  26. package/src/cli/session-picker.ts +20 -3
  27. package/src/cli/setup-cli.ts +1 -1
  28. package/src/cli/worktree-cli.ts +9 -287
  29. package/src/cli.ts +31 -11
  30. package/src/commands/config.ts +1 -1
  31. package/src/commands/daemon.ts +50 -6
  32. package/src/commands/gc.ts +11 -1
  33. package/src/commands/harness.ts +127 -142
  34. package/src/commands/launch.ts +11 -6
  35. package/src/commands/mcp-serve.ts +97 -13
  36. package/src/commands/mcp.ts +2 -2
  37. package/src/commands/notify.ts +44 -7
  38. package/src/commands/sdk.ts +453 -0
  39. package/src/commands/ultragoal.ts +4 -1
  40. package/src/commands/worktree.ts +10 -53
  41. package/src/commit/agentic/agent.ts +1 -0
  42. package/src/config/atomic-yaml-patch.ts +490 -0
  43. package/src/config/file-lock.ts +105 -43
  44. package/src/config/keybindings.ts +64 -20
  45. package/src/config/model-auth.ts +9 -0
  46. package/src/config/model-bindings-applier.ts +139 -0
  47. package/src/config/model-discovery-manager.ts +224 -0
  48. package/src/config/model-equivalence.ts +66 -0
  49. package/src/config/model-profile-activation.ts +233 -86
  50. package/src/config/model-profiles.ts +52 -49
  51. package/src/config/model-registry.ts +5 -4
  52. package/src/config/model-resolver.ts +487 -410
  53. package/src/config/model-selector-value.ts +50 -0
  54. package/src/config/models-config-schema.ts +17 -23
  55. package/src/config/settings-schema.ts +372 -21
  56. package/src/config/settings.ts +1120 -182
  57. package/src/coordinator-mcp/model-preset.ts +74 -20
  58. package/src/coordinator-mcp/policy.ts +76 -11
  59. package/src/coordinator-mcp/question-gate-codec.ts +274 -0
  60. package/src/coordinator-mcp/question-state.ts +778 -0
  61. package/src/coordinator-mcp/server.ts +3120 -3111
  62. package/src/daemon/builtin.ts +8 -5
  63. package/src/daemon/control-types.ts +1 -1
  64. package/src/daemon/runtime.ts +2 -2
  65. package/src/debug/index.ts +1 -1
  66. package/src/defaults/skc/skills/team/SKILL.md +27 -13
  67. package/src/defaults/skc/skills/ultragoal/SKILL.md +0 -2
  68. package/src/discovery/helpers.ts +1 -50
  69. package/src/discovery/mcp-json.ts +327 -20
  70. package/src/edit/index.ts +64 -0
  71. package/src/eval/js/shared/runtime.ts +1 -1
  72. package/src/eval/py/env.ts +26 -0
  73. package/src/eval/py/index.ts +2 -2
  74. package/src/eval/py/kernel.ts +5 -3
  75. package/src/export/html/template.generated.ts +1 -1
  76. package/src/export/html/template.js +52 -18
  77. package/src/extensibility/custom-tools/types.ts +1 -1
  78. package/src/extensibility/extensions/compact-handler.ts +3 -3
  79. package/src/extensibility/extensions/get-commands-handler.ts +2 -2
  80. package/src/extensibility/extensions/loader.ts +77 -5
  81. package/src/extensibility/extensions/runner.ts +124 -7
  82. package/src/extensibility/extensions/types.ts +215 -18
  83. package/src/extensibility/hooks/runner.ts +2 -2
  84. package/src/extensibility/hooks/types.ts +1 -1
  85. package/src/extensibility/plugins/marketplace/manager.ts +94 -10
  86. package/src/extensibility/runtime-skill-discovery.ts +33 -13
  87. package/src/extensibility/shared-events.ts +2 -0
  88. package/src/extensibility/skc-plugins/installer.ts +16 -16
  89. package/src/extensibility/slash-commands.ts +3 -3
  90. package/src/goals/continuation-timeout-guard.ts +79 -0
  91. package/src/goals/runtime.ts +7 -5
  92. package/src/goals/state.ts +18 -0
  93. package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
  94. package/src/harness-control-plane/operate.ts +9 -9
  95. package/src/harness-control-plane/owner.ts +341 -41
  96. package/src/harness-control-plane/phase-rollup.ts +4 -7
  97. package/src/harness-control-plane/receipt-ingest.ts +5 -0
  98. package/src/harness-control-plane/receipt-spool.ts +51 -7
  99. package/src/harness-control-plane/receipts.ts +0 -1
  100. package/src/harness-control-plane/sdk-transport.ts +395 -0
  101. package/src/harness-control-plane/session-lease.ts +12 -130
  102. package/src/harness-control-plane/session-transport.ts +94 -0
  103. package/src/harness-control-plane/types.ts +1 -1
  104. package/src/hindsight/backend.ts +26 -27
  105. package/src/hindsight/client.ts +65 -2
  106. package/src/hindsight/state.ts +101 -32
  107. package/src/hooks/skill-state.ts +17 -11
  108. package/src/i18n/messages/en.ts +1 -0
  109. package/src/internal-urls/agent-protocol.ts +76 -5
  110. package/src/internal-urls/docs-index.generated.ts +44 -37
  111. package/src/internal-urls/local-protocol.ts +420 -18
  112. package/src/lsp/client.ts +43 -7
  113. package/src/lsp/config.ts +145 -26
  114. package/src/lsp/index.ts +7 -91
  115. package/src/lsp/lspmux.ts +39 -10
  116. package/src/lsp/path-trust.ts +81 -0
  117. package/src/main.ts +451 -236
  118. package/src/memories/index.ts +1 -0
  119. package/src/memories/storage.ts +6 -2
  120. package/src/modes/DESIGN.md +258 -0
  121. package/src/modes/acp/acp-agent.ts +1236 -2612
  122. package/src/modes/acp/acp-client-bridge.ts +2 -20
  123. package/src/modes/acp/acp-event-mapper.ts +18 -2
  124. package/src/modes/acp/acp-mode.ts +8 -36
  125. package/src/modes/acp/permission-mode.ts +25 -0
  126. package/src/modes/acp/startup-options.ts +6 -0
  127. package/src/modes/action-registry.ts +165 -0
  128. package/src/modes/bridge/bridge-mode.ts +6 -4
  129. package/src/modes/components/agent-dashboard.ts +37 -17
  130. package/src/modes/components/ansi-display-validator.ts +54 -0
  131. package/src/modes/components/chrome.ts +40 -0
  132. package/src/modes/components/command-palette.ts +168 -0
  133. package/src/modes/components/custom-editor.ts +105 -33
  134. package/src/modes/components/custom-provider-wizard.ts +77 -15
  135. package/src/modes/components/index.ts +2 -1
  136. package/src/modes/components/model-selector.ts +93 -35
  137. package/src/modes/components/notifications-settings-editor.ts +1403 -0
  138. package/src/modes/components/pet-capability.ts +115 -0
  139. package/src/modes/components/pet-selector.ts +15 -23
  140. package/src/modes/components/plan-preview-overlay.ts +295 -0
  141. package/src/modes/components/queue-mode-selector.ts +10 -27
  142. package/src/modes/components/queue-pane.ts +79 -0
  143. package/src/modes/components/read-tool-group.ts +13 -0
  144. package/src/modes/components/sayknow-pet-widget.ts +169 -20
  145. package/src/modes/components/session-observer-overlay.ts +213 -775
  146. package/src/modes/components/sessions-dashboard.ts +181 -0
  147. package/src/modes/components/settings-selector.ts +89 -12
  148. package/src/modes/components/show-images-selector.ts +10 -24
  149. package/src/modes/components/status-line/types.ts +1 -1
  150. package/src/modes/components/status-line.ts +1 -969
  151. package/src/modes/components/tasks-pane.ts +73 -0
  152. package/src/modes/components/theme-selector.ts +11 -31
  153. package/src/modes/components/tool-execution.ts +22 -0
  154. package/src/modes/components/tool-status-header.ts +1056 -0
  155. package/src/modes/components/tool-transcript-format.ts +393 -0
  156. package/src/modes/components/transcript-viewer-overlay.ts +446 -0
  157. package/src/modes/components/welcome.ts +4 -4
  158. package/src/modes/controllers/btw-controller.ts +1 -0
  159. package/src/modes/controllers/command-controller.ts +25 -17
  160. package/src/modes/controllers/event-controller.ts +14 -18
  161. package/src/modes/controllers/extension-ui-controller.ts +441 -37
  162. package/src/modes/controllers/goal-mode-controller.ts +493 -0
  163. package/src/modes/controllers/input-controller.ts +889 -263
  164. package/src/modes/controllers/mode-gate.ts +24 -0
  165. package/src/modes/controllers/plan-mode-controller.ts +519 -0
  166. package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
  167. package/src/modes/controllers/selector-controller.ts +957 -54
  168. package/src/modes/controllers/stt-controller.ts +125 -0
  169. package/src/modes/index.ts +0 -22
  170. package/src/modes/interactive-mode.ts +376 -1130
  171. package/src/modes/jobs-observer.ts +3 -0
  172. package/src/modes/prompt-action-autocomplete.ts +7 -0
  173. package/src/modes/rpc/rpc-mode.ts +3 -1
  174. package/src/modes/runtime-init.ts +320 -6
  175. package/src/modes/shared/agent-wire/approval-gate.ts +9 -11
  176. package/src/modes/shared/agent-wire/command-dispatch.ts +3 -1
  177. package/src/modes/shared/agent-wire/deep-interview-gate.ts +65 -111
  178. package/src/modes/shared/agent-wire/event-contract.ts +7 -8
  179. package/src/modes/shared/agent-wire/event-envelope.ts +9 -17
  180. package/src/modes/shared/agent-wire/event-observation.ts +16 -7
  181. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +1331 -89
  182. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +19 -17
  183. package/src/modes/shared/agent-wire/workflow-gate-types.ts +407 -0
  184. package/src/modes/tasks-aggregator.ts +199 -0
  185. package/src/modes/transcript-item-registry.ts +229 -0
  186. package/src/modes/types.ts +63 -21
  187. package/src/modes/utils/ui-helpers.ts +17 -9
  188. package/src/notifications/index.ts +8 -2
  189. package/src/plan-mode/approved-plan.ts +3 -1
  190. package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
  191. package/src/prompts/agent-fragments/restricted-bash.md +1 -0
  192. package/src/prompts/agents/architect.md +3 -8
  193. package/src/prompts/agents/critic.md +3 -8
  194. package/src/prompts/agents/executor.md +3 -0
  195. package/src/prompts/agents/planner.md +4 -12
  196. package/src/prompts/system/custom-system-prompt.md +2 -24
  197. package/src/prompts/system/plan-mode-approved.md +8 -0
  198. package/src/prompts/system/system-prompt.md +18 -94
  199. package/src/prompts/tools/bash.md +4 -4
  200. package/src/prompts/tools/browser.md +15 -52
  201. package/src/prompts/tools/find.md +2 -2
  202. package/src/prompts/tools/hashline.md +13 -55
  203. package/src/prompts/tools/lsp.md +3 -3
  204. package/src/prompts/tools/read.md +3 -6
  205. package/src/prompts/tools/search-tool-bm25.md +0 -2
  206. package/src/prompts/tools/search.md +2 -4
  207. package/src/prompts/tools/skill-discovery.md +1 -2
  208. package/src/prompts/tools/task.md +1 -1
  209. package/src/runtime-mcp/client.ts +94 -19
  210. package/src/runtime-mcp/config.ts +27 -15
  211. package/src/runtime-mcp/discoverable-tool-metadata.ts +16 -13
  212. package/src/runtime-mcp/manager.ts +359 -106
  213. package/src/runtime-mcp/oauth-discovery.ts +116 -26
  214. package/src/runtime-mcp/oauth-flow.ts +42 -12
  215. package/src/runtime-mcp/transports/http.ts +49 -19
  216. package/src/runtime-mcp/transports/stdio.ts +47 -28
  217. package/src/runtime-mcp/types.ts +10 -0
  218. package/src/sdk/acp/adapter.ts +418 -0
  219. package/src/sdk/acp/index.ts +2 -0
  220. package/src/sdk/broker/broker.ts +877 -0
  221. package/src/sdk/broker/discovery.ts +238 -0
  222. package/src/sdk/broker/ensure.ts +393 -0
  223. package/src/sdk/broker/identity.ts +42 -0
  224. package/src/sdk/broker/index.ts +8 -0
  225. package/src/sdk/broker/internal-source-marker-2178.txt +1 -0
  226. package/src/sdk/broker/internal-source.bunfig.toml +1 -0
  227. package/src/sdk/broker/internal.ts +10 -0
  228. package/src/sdk/broker/lifecycle-codec.ts +11 -0
  229. package/src/sdk/broker/lifecycle-ledger.ts +583 -0
  230. package/src/sdk/broker/lifecycle.ts +3413 -0
  231. package/src/sdk/broker/process-incarnation.ts +151 -0
  232. package/src/sdk/broker/runtime.ts +164 -0
  233. package/src/sdk/broker/session-index.ts +678 -0
  234. package/src/sdk/broker/state-version.ts +47 -0
  235. package/src/sdk/broker/transport.ts +125 -0
  236. package/src/sdk/bus/attachment-registry.ts +23 -0
  237. package/src/sdk/bus/chat-adapters.ts +147 -0
  238. package/src/sdk/bus/chat-command-policy.ts +107 -0
  239. package/src/sdk/bus/chat-daemon-cli.ts +278 -0
  240. package/src/sdk/bus/chat-daemon-control.ts +1101 -0
  241. package/src/sdk/bus/chat-daemon-runtime.ts +527 -0
  242. package/src/sdk/bus/chat-effect-journal.ts +363 -0
  243. package/src/sdk/bus/config-commands.ts +192 -0
  244. package/src/sdk/bus/config.ts +568 -0
  245. package/src/sdk/bus/conversation-store.ts +367 -0
  246. package/src/sdk/bus/daemon-paths.ts +34 -0
  247. package/src/sdk/bus/discord-conversation.ts +85 -0
  248. package/src/sdk/bus/discord-daemon.ts +2292 -0
  249. package/src/sdk/bus/discord-live-provider.ts +521 -0
  250. package/src/sdk/bus/discord-provider.ts +56 -0
  251. package/src/sdk/bus/engine.ts +100 -0
  252. package/src/sdk/bus/helpers.ts +135 -0
  253. package/src/sdk/bus/html-format.ts +518 -0
  254. package/src/sdk/bus/index.ts +4961 -0
  255. package/src/sdk/bus/lifecycle-commands.ts +291 -0
  256. package/src/sdk/bus/lifecycle-control-runtime.ts +1409 -0
  257. package/src/sdk/bus/lifecycle-orchestrator.ts +458 -0
  258. package/src/sdk/bus/managed-daemon.ts +163 -0
  259. package/src/sdk/bus/native-runtime-compatibility.ts +42 -0
  260. package/src/sdk/bus/natives-augment.d.ts +63 -0
  261. package/src/sdk/bus/notification-orchestration.ts +535 -0
  262. package/src/sdk/bus/notification-service.ts +1174 -0
  263. package/src/sdk/bus/operator-runtime.ts +171 -0
  264. package/src/sdk/bus/rate-limit-pool.ts +276 -0
  265. package/src/sdk/bus/recent-activity.ts +279 -0
  266. package/src/sdk/bus/reply-sent-store.ts +134 -0
  267. package/src/sdk/bus/rich-document.ts +258 -0
  268. package/src/sdk/bus/rich-draft.ts +107 -0
  269. package/src/sdk/bus/rich-render.ts +135 -0
  270. package/src/sdk/bus/session-control.ts +439 -0
  271. package/src/sdk/bus/slack-conversation.ts +74 -0
  272. package/src/sdk/bus/slack-daemon.ts +2144 -0
  273. package/src/sdk/bus/slack-live-provider.ts +380 -0
  274. package/src/sdk/bus/slack-provider.ts +76 -0
  275. package/src/sdk/bus/telegram-cli.ts +194 -0
  276. package/src/sdk/bus/telegram-daemon-cli.ts +292 -0
  277. package/src/sdk/bus/telegram-daemon-contract.ts +27 -0
  278. package/src/sdk/bus/telegram-daemon-control.ts +642 -0
  279. package/src/sdk/bus/telegram-daemon.ts +6675 -0
  280. package/src/sdk/bus/telegram-reference.ts +436 -0
  281. package/src/sdk/bus/telegram-setup.ts +663 -0
  282. package/src/sdk/bus/threaded-inbound.ts +141 -0
  283. package/src/sdk/bus/threaded-render.ts +294 -0
  284. package/src/sdk/bus/topic-registry.ts +364 -0
  285. package/src/sdk/cli/index.ts +1 -0
  286. package/src/sdk/cli/session-cli.ts +275 -0
  287. package/src/sdk/client/client.ts +1 -0
  288. package/src/sdk/client/discovery.ts +108 -0
  289. package/src/sdk/client/index.ts +2 -0
  290. package/src/sdk/host/control/dispatch.ts +388 -0
  291. package/src/sdk/host/control/index.ts +10 -0
  292. package/src/sdk/host/control/operations.ts +68 -0
  293. package/src/sdk/host/events.ts +91 -0
  294. package/src/sdk/host/host.ts +338 -0
  295. package/src/sdk/host/index.ts +4 -0
  296. package/src/sdk/host/query/cursor.ts +189 -0
  297. package/src/sdk/host/query/handlers.ts +531 -0
  298. package/src/sdk/host/query/index.ts +3 -0
  299. package/src/sdk/host/query/revision-store.ts +1149 -0
  300. package/src/sdk/host/reverse-leases.ts +325 -0
  301. package/src/sdk/host/types.ts +29 -0
  302. package/src/sdk/index.ts +14 -0
  303. package/src/sdk/lifecycle-session.ts +39 -0
  304. package/src/sdk/mcp/index.ts +1 -0
  305. package/src/sdk/mcp/server.ts +332 -0
  306. package/src/sdk/models.ts +168 -0
  307. package/src/sdk/protocol/adapter-validation.ts +48 -0
  308. package/src/sdk/protocol/operation-inventory.generated.json +4719 -0
  309. package/src/sdk/protocol/operation-registry.ts +312 -0
  310. package/src/sdk/session-directory.ts +141 -0
  311. package/src/sdk/session.ts +2869 -0
  312. package/src/sdk/startup-capability.ts +189 -0
  313. package/src/secrets/index.ts +53 -10
  314. package/src/secrets/obfuscator.ts +21 -18
  315. package/src/secrets/regex.ts +2 -0
  316. package/src/session/agent-session.ts +5602 -2138
  317. package/src/session/agent-storage.ts +18 -0
  318. package/src/session/artifacts.ts +188 -23
  319. package/src/session/cache-economics.ts +25 -8
  320. package/src/session/context-estimation.ts +7 -2
  321. package/src/session/default-model-selection.ts +93 -0
  322. package/src/session/fallback-chain-controller.ts +186 -0
  323. package/src/session/internal/managed-session-scope.ts +2402 -0
  324. package/src/session/internal/managed-session-storage.ts +1559 -0
  325. package/src/session/messages.ts +91 -2
  326. package/src/session/session-manager-internal.ts +33 -0
  327. package/src/session/session-manager.ts +2578 -392
  328. package/src/session/session-storage.ts +550 -40
  329. package/src/session/streaming-output.ts +6 -0
  330. package/src/session/volatile-context-pruning.ts +78 -0
  331. package/src/session/yield-queue.ts +20 -0
  332. package/src/setup/credential-auto-import.ts +291 -35
  333. package/src/setup/hermes/templates/operator-instructions.v1.md +4 -6
  334. package/src/setup/provider-onboarding.ts +36 -61
  335. package/src/setup/provider-presets.json +5 -0
  336. package/src/skc-runtime/deep-interview-recorder.ts +70 -3
  337. package/src/skc-runtime/deep-interview-state.ts +441 -5
  338. package/src/skc-runtime/gc-render.ts +19 -1
  339. package/src/skc-runtime/gc-runtime.ts +68 -3
  340. package/src/skc-runtime/goal-mode-request.ts +55 -8
  341. package/src/skc-runtime/launch-worktree.ts +5 -5
  342. package/src/skc-runtime/linux-proc.ts +71 -0
  343. package/src/skc-runtime/managed-owner-admission.ts +352 -0
  344. package/src/skc-runtime/managed-owner-supervisor.ts +162 -0
  345. package/src/skc-runtime/psmux-detect.ts +82 -4
  346. package/src/skc-runtime/ralplan-runtime.ts +74 -90
  347. package/src/skc-runtime/session-resolution.ts +18 -3
  348. package/src/skc-runtime/session-state-sidecar.ts +47 -138
  349. package/src/skc-runtime/state-argv.ts +292 -0
  350. package/src/skc-runtime/state-migrations.ts +1 -1
  351. package/src/skc-runtime/state-runtime.ts +387 -451
  352. package/src/skc-runtime/state-writer.ts +36 -40
  353. package/src/skc-runtime/team-launch.ts +319 -0
  354. package/src/skc-runtime/team-notify.ts +326 -0
  355. package/src/skc-runtime/team-runtime.ts +8 -2
  356. package/src/skc-runtime/team-store.ts +828 -0
  357. package/src/skc-runtime/team-workers.ts +962 -0
  358. package/src/skc-runtime/tmux-owner-isolation-cli.ts +55 -28
  359. package/src/skc-runtime/tmux-owner-isolation.ts +136 -11
  360. package/src/skc-runtime/tmux-sessions.ts +23 -34
  361. package/src/skc-runtime/ultragoal-change-set.ts +172 -0
  362. package/src/skc-runtime/ultragoal-evidence.ts +874 -0
  363. package/src/skc-runtime/ultragoal-guard.ts +136 -18
  364. package/src/skc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
  365. package/src/skc-runtime/ultragoal-pipeline.ts +306 -0
  366. package/src/skc-runtime/ultragoal-receipt-freshness.ts +102 -1
  367. package/src/skc-runtime/ultragoal-runtime.ts +325 -1322
  368. package/src/skc-runtime/windows-powershell-command.ts +58 -0
  369. package/src/skc-runtime/workflow-cli-common.ts +30 -0
  370. package/src/skc-runtime/workflow-manifest.generated.json +58 -0
  371. package/src/skc-runtime/workflow-manifest.ts +18 -0
  372. package/src/skill-state/active-state.ts +3 -13
  373. package/src/skill-state/workflow-mutation-guard.ts +661 -0
  374. package/src/skill-state/workflow-state-contract.ts +6 -41
  375. package/src/slash-commands/builtin-registry.ts +115 -41
  376. package/src/system-prompt.ts +37 -44
  377. package/src/task/agents.ts +19 -1
  378. package/src/task/executor.ts +223 -45
  379. package/src/task/index.ts +124 -56
  380. package/src/thinking.ts +21 -6
  381. package/src/tool-discovery/tool-index.ts +20 -134
  382. package/src/tools/ask-answer-registry.ts +16 -2
  383. package/src/tools/ast-edit.ts +1 -1
  384. package/src/tools/bash-allowed-prefixes.ts +79 -29
  385. package/src/tools/bash-skill-urls.ts +14 -3
  386. package/src/tools/bash.ts +40 -2
  387. package/src/tools/browser/STEALTH-THIRD-PARTY-NOTICES.md +26 -0
  388. package/src/tools/browser/benchmark/detector-report.ts +157 -0
  389. package/src/tools/browser/benchmark/run-suite.ts +39 -0
  390. package/src/tools/browser/dead-tab-recovery.ts +78 -0
  391. package/src/tools/browser/launch.ts +127 -20
  392. package/src/tools/browser/profile-discovery.ts +63 -0
  393. package/src/tools/browser/profile-posture.ts +59 -0
  394. package/src/tools/browser/profile-reuse.ts +102 -0
  395. package/src/tools/browser/profile-warmup.ts +109 -0
  396. package/src/tools/browser/readable.ts +1 -1
  397. package/src/tools/browser/registry.ts +87 -11
  398. package/src/tools/browser/screenshot-format.ts +34 -0
  399. package/src/tools/browser/tab-protocol.ts +4 -0
  400. package/src/tools/browser/tab-supervisor.ts +392 -142
  401. package/src/tools/browser/tab-worker-entry.ts +33 -14
  402. package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
  403. package/src/tools/browser/tab-worker-smoke.ts +87 -0
  404. package/src/tools/browser/tab-worker.ts +8 -6
  405. package/src/tools/browser.ts +6 -0
  406. package/src/tools/computer.ts +10 -3
  407. package/src/tools/cron.ts +30 -16
  408. package/src/tools/fetch.ts +11 -4
  409. package/src/tools/index.ts +88 -32
  410. package/src/tools/output-meta.ts +19 -7
  411. package/src/tools/path-utils.ts +42 -35
  412. package/src/tools/plan-mode-guard.ts +1 -0
  413. package/src/tools/puppeteer/14_stealth_permissions.txt +57 -0
  414. package/src/tools/puppeteer/15_stealth_webrtc.txt +60 -0
  415. package/src/tools/read.ts +226 -65
  416. package/src/tools/resource-gc.ts +125 -36
  417. package/src/tools/search-tool-bm25.ts +11 -74
  418. package/src/tools/sqlite-reader.ts +3 -2
  419. package/src/tools/subagent-render.ts +19 -8
  420. package/src/tools/subagent.ts +129 -34
  421. package/src/tools/telegram-send.ts +2 -2
  422. package/src/utils/event-bus.ts +9 -4
  423. package/src/utils/file-mentions.ts +4 -4
  424. package/src/utils/image-loading.ts +85 -34
  425. package/src/utils/image-resize.ts +16 -9
  426. package/src/utils/pasted-image-loading.ts +514 -0
  427. package/src/utils/pasted-image-path.ts +223 -98
  428. package/src/web/scrapers/html-to-markdown.ts +68 -0
  429. package/src/web/scrapers/readthedocs.ts +1 -1
  430. package/src/web/scrapers/types.ts +1 -68
  431. package/src/web/search/index.ts +3 -3
  432. package/src/web/search/providers/openai-compatible.ts +76 -23
  433. package/dist/types/append-only-mode.d.ts +0 -31
  434. package/dist/types/async/index.d.ts +0 -2
  435. package/dist/types/async/job-manager.d.ts +0 -328
  436. package/dist/types/async/support.d.ts +0 -2
  437. package/dist/types/autoresearch/dashboard.d.ts +0 -4
  438. package/dist/types/autoresearch/git.d.ts +0 -36
  439. package/dist/types/autoresearch/helpers.d.ts +0 -24
  440. package/dist/types/autoresearch/index.d.ts +0 -2
  441. package/dist/types/autoresearch/state.d.ts +0 -17
  442. package/dist/types/autoresearch/storage.d.ts +0 -142
  443. package/dist/types/autoresearch/tools/init-experiment.d.ts +0 -31
  444. package/dist/types/autoresearch/tools/log-experiment.d.ts +0 -23
  445. package/dist/types/autoresearch/tools/run-experiment.d.ts +0 -8
  446. package/dist/types/autoresearch/tools/update-notes.d.ts +0 -12
  447. package/dist/types/autoresearch/types.d.ts +0 -154
  448. package/dist/types/build-metadata.d.ts +0 -7
  449. package/dist/types/capability/context-file.d.ts +0 -17
  450. package/dist/types/capability/extension-module.d.ts +0 -15
  451. package/dist/types/capability/extension.d.ts +0 -28
  452. package/dist/types/capability/fs.d.ts +0 -20
  453. package/dist/types/capability/hook.d.ts +0 -19
  454. package/dist/types/capability/index.d.ts +0 -80
  455. package/dist/types/capability/instruction.d.ts +0 -17
  456. package/dist/types/capability/mcp.d.ts +0 -49
  457. package/dist/types/capability/prompt.d.ts +0 -15
  458. package/dist/types/capability/rule.d.ts +0 -69
  459. package/dist/types/capability/settings.d.ts +0 -15
  460. package/dist/types/capability/skill.d.ts +0 -36
  461. package/dist/types/capability/slash-command.d.ts +0 -17
  462. package/dist/types/capability/ssh.d.ts +0 -23
  463. package/dist/types/capability/system-prompt.d.ts +0 -15
  464. package/dist/types/capability/tool.d.ts +0 -19
  465. package/dist/types/capability/types.d.ts +0 -154
  466. package/dist/types/cli/agents-cli.d.ts +0 -12
  467. package/dist/types/cli/args.d.ts +0 -57
  468. package/dist/types/cli/auth-broker-cli.d.ts +0 -25
  469. package/dist/types/cli/auth-gateway-cli.d.ts +0 -18
  470. package/dist/types/cli/classify-install-target.d.ts +0 -8
  471. package/dist/types/cli/commands/init-xdg.d.ts +0 -1
  472. package/dist/types/cli/completion-cli.d.ts +0 -41
  473. package/dist/types/cli/config-cli.d.ts +0 -23
  474. package/dist/types/cli/daemon-cli.d.ts +0 -27
  475. package/dist/types/cli/fast-help.d.ts +0 -1
  476. package/dist/types/cli/file-processor.d.ts +0 -11
  477. package/dist/types/cli/fixture-report.d.ts +0 -20
  478. package/dist/types/cli/fixture-report.test.d.ts +0 -1
  479. package/dist/types/cli/grep-cli.d.ts +0 -17
  480. package/dist/types/cli/initial-message.d.ts +0 -17
  481. package/dist/types/cli/list-models.d.ts +0 -36
  482. package/dist/types/cli/local-provider-smoke.d.ts +0 -59
  483. package/dist/types/cli/malloc-env-guard.d.ts +0 -65
  484. package/dist/types/cli/malloc-env-guard.test.d.ts +0 -1
  485. package/dist/types/cli/mcp-cli.d.ts +0 -25
  486. package/dist/types/cli/migrate-cli.d.ts +0 -20
  487. package/dist/types/cli/nofile-limit.d.ts +0 -11
  488. package/dist/types/cli/notify-cli.d.ts +0 -39
  489. package/dist/types/cli/plugin-cli.d.ts +0 -34
  490. package/dist/types/cli/read-cli.d.ts +0 -4
  491. package/dist/types/cli/session-picker.d.ts +0 -4
  492. package/dist/types/cli/setup-cli.d.ts +0 -73
  493. package/dist/types/cli/shell-cli.d.ts +0 -8
  494. package/dist/types/cli/skills-cli.d.ts +0 -9
  495. package/dist/types/cli/ssh-cli.d.ts +0 -21
  496. package/dist/types/cli/stats-cli.d.ts +0 -17
  497. package/dist/types/cli/telegram-cli.d.ts +0 -13
  498. package/dist/types/cli/update-cli.d.ts +0 -108
  499. package/dist/types/cli/web-search-cli.d.ts +0 -32
  500. package/dist/types/cli/worktree-cli.d.ts +0 -26
  501. package/dist/types/cli.d.ts +0 -140
  502. package/dist/types/commands/acp.d.ts +0 -12
  503. package/dist/types/commands/agents.d.ts +0 -34
  504. package/dist/types/commands/auth-broker.d.ts +0 -54
  505. package/dist/types/commands/auth-gateway.d.ts +0 -32
  506. package/dist/types/commands/codex-native-hook.d.ts +0 -6
  507. package/dist/types/commands/commit.d.ts +0 -27
  508. package/dist/types/commands/completion.d.ts +0 -29
  509. package/dist/types/commands/config.d.ts +0 -33
  510. package/dist/types/commands/contribution-prep.d.ts +0 -18
  511. package/dist/types/commands/coordinator.d.ts +0 -19
  512. package/dist/types/commands/daemon.d.ts +0 -45
  513. package/dist/types/commands/deep-interview.d.ts +0 -54
  514. package/dist/types/commands/gc.d.ts +0 -26
  515. package/dist/types/commands/grep.d.ts +0 -42
  516. package/dist/types/commands/harness.d.ts +0 -43
  517. package/dist/types/commands/launch.d.ts +0 -131
  518. package/dist/types/commands/local-provider.d.ts +0 -34
  519. package/dist/types/commands/mcp-serve.d.ts +0 -24
  520. package/dist/types/commands/mcp.d.ts +0 -70
  521. package/dist/types/commands/migrate.d.ts +0 -33
  522. package/dist/types/commands/notify.d.ts +0 -47
  523. package/dist/types/commands/plugin.d.ts +0 -58
  524. package/dist/types/commands/ralplan.d.ts +0 -7
  525. package/dist/types/commands/read.d.ts +0 -15
  526. package/dist/types/commands/rlm.d.ts +0 -10
  527. package/dist/types/commands/session.d.ts +0 -30
  528. package/dist/types/commands/setup.d.ts +0 -105
  529. package/dist/types/commands/shell.d.ts +0 -21
  530. package/dist/types/commands/skills.d.ts +0 -26
  531. package/dist/types/commands/ssh.d.ts +0 -48
  532. package/dist/types/commands/state.d.ts +0 -7
  533. package/dist/types/commands/stats.d.ts +0 -25
  534. package/dist/types/commands/team.d.ts +0 -24
  535. package/dist/types/commands/telegram.d.ts +0 -22
  536. package/dist/types/commands/ultragoal.d.ts +0 -8
  537. package/dist/types/commands/update.d.ts +0 -20
  538. package/dist/types/commands/web-search.d.ts +0 -87
  539. package/dist/types/commands/worktree.d.ts +0 -34
  540. package/dist/types/commit/agentic/agent.d.ts +0 -31
  541. package/dist/types/commit/agentic/fallback.d.ts +0 -5
  542. package/dist/types/commit/agentic/index.d.ts +0 -2
  543. package/dist/types/commit/agentic/state.d.ts +0 -58
  544. package/dist/types/commit/agentic/tools/analyze-file.d.ts +0 -19
  545. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +0 -10
  546. package/dist/types/commit/agentic/tools/git-hunk.d.ts +0 -9
  547. package/dist/types/commit/agentic/tools/git-overview.d.ts +0 -9
  548. package/dist/types/commit/agentic/tools/index.d.ts +0 -16
  549. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +0 -28
  550. package/dist/types/commit/agentic/tools/propose-commit.d.ts +0 -36
  551. package/dist/types/commit/agentic/tools/recent-commits.d.ts +0 -7
  552. package/dist/types/commit/agentic/tools/schemas.d.ts +0 -27
  553. package/dist/types/commit/agentic/tools/split-commit.d.ts +0 -53
  554. package/dist/types/commit/agentic/topo-sort.d.ts +0 -4
  555. package/dist/types/commit/agentic/trivial.d.ts +0 -7
  556. package/dist/types/commit/agentic/validation.d.ts +0 -20
  557. package/dist/types/commit/analysis/conventional.d.ts +0 -22
  558. package/dist/types/commit/analysis/index.d.ts +0 -4
  559. package/dist/types/commit/analysis/scope.d.ts +0 -6
  560. package/dist/types/commit/analysis/summary.d.ts +0 -19
  561. package/dist/types/commit/analysis/validation.d.ts +0 -8
  562. package/dist/types/commit/changelog/detect.d.ts +0 -2
  563. package/dist/types/commit/changelog/generate.d.ts +0 -30
  564. package/dist/types/commit/changelog/index.d.ts +0 -30
  565. package/dist/types/commit/changelog/parse.d.ts +0 -2
  566. package/dist/types/commit/cli.d.ts +0 -3
  567. package/dist/types/commit/git/diff.d.ts +0 -5
  568. package/dist/types/commit/index.d.ts +0 -4
  569. package/dist/types/commit/map-reduce/index.d.ts +0 -28
  570. package/dist/types/commit/map-reduce/map-phase.d.ts +0 -17
  571. package/dist/types/commit/map-reduce/reduce-phase.d.ts +0 -13
  572. package/dist/types/commit/map-reduce/utils.d.ts +0 -2
  573. package/dist/types/commit/message.d.ts +0 -2
  574. package/dist/types/commit/model-selection.d.ts +0 -15
  575. package/dist/types/commit/pipeline.d.ts +0 -5
  576. package/dist/types/commit/shared-llm.d.ts +0 -54
  577. package/dist/types/commit/types.d.ts +0 -78
  578. package/dist/types/commit/utils/exclusions.d.ts +0 -4
  579. package/dist/types/commit/utils.d.ts +0 -20
  580. package/dist/types/config/config-file.d.ts +0 -58
  581. package/dist/types/config/file-lock-gc.d.ts +0 -5
  582. package/dist/types/config/file-lock.d.ts +0 -36
  583. package/dist/types/config/keybindings.d.ts +0 -364
  584. package/dist/types/config/model-equivalence.d.ts +0 -24
  585. package/dist/types/config/model-profile-activation.d.ts +0 -65
  586. package/dist/types/config/model-profiles.d.ts +0 -39
  587. package/dist/types/config/model-registry.d.ts +0 -473
  588. package/dist/types/config/model-resolver.d.ts +0 -217
  589. package/dist/types/config/models-config-schema.d.ts +0 -700
  590. package/dist/types/config/prompt-templates.d.ts +0 -32
  591. package/dist/types/config/resolve-config-value.d.ts +0 -17
  592. package/dist/types/config/settings-schema.d.ts +0 -4223
  593. package/dist/types/config/settings.d.ts +0 -158
  594. package/dist/types/config/skill-settings-defaults.d.ts +0 -14
  595. package/dist/types/config/telegram-autostart.d.ts +0 -14
  596. package/dist/types/config/telegram-env-bridge.d.ts +0 -18
  597. package/dist/types/config.d.ts +0 -66
  598. package/dist/types/coordinator/contract.d.ts +0 -4
  599. package/dist/types/coordinator-mcp/model-preset.d.ts +0 -45
  600. package/dist/types/coordinator-mcp/policy.d.ts +0 -27
  601. package/dist/types/coordinator-mcp/safety.d.ts +0 -26
  602. package/dist/types/coordinator-mcp/server.d.ts +0 -139
  603. package/dist/types/coordinator-mcp/session-reaper.d.ts +0 -53
  604. package/dist/types/cursor.d.ts +0 -24
  605. package/dist/types/daemon/builtin.d.ts +0 -20
  606. package/dist/types/daemon/control-types.d.ts +0 -67
  607. package/dist/types/daemon/operator-contract.d.ts +0 -47
  608. package/dist/types/daemon/runtime.d.ts +0 -25
  609. package/dist/types/dap/client.d.ts +0 -39
  610. package/dist/types/dap/config.d.ts +0 -6
  611. package/dist/types/dap/index.d.ts +0 -4
  612. package/dist/types/dap/session.d.ts +0 -108
  613. package/dist/types/dap/types.d.ts +0 -524
  614. package/dist/types/debug/crash-diagnostics.d.ts +0 -45
  615. package/dist/types/debug/index.d.ts +0 -15
  616. package/dist/types/debug/log-formatting.d.ts +0 -4
  617. package/dist/types/debug/log-viewer.d.ts +0 -68
  618. package/dist/types/debug/profiler.d.ts +0 -24
  619. package/dist/types/debug/raw-sse-buffer.d.ts +0 -44
  620. package/dist/types/debug/raw-sse.d.ts +0 -16
  621. package/dist/types/debug/report-bundle.d.ts +0 -55
  622. package/dist/types/debug/runtime-gauges.d.ts +0 -6
  623. package/dist/types/debug/system-info.d.ts +0 -26
  624. package/dist/types/deep-interview/plaintext-gate-guard.d.ts +0 -11
  625. package/dist/types/deep-interview/render-middleware.d.ts +0 -6
  626. package/dist/types/defaults/skc/extensions/grok-build/index.d.ts +0 -1
  627. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/index.d.ts +0 -1
  628. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.d.ts +0 -29
  629. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +0 -27
  630. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/provider/billing.d.ts +0 -8
  631. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/provider/register.d.ts +0 -5
  632. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/provider/stream.d.ts +0 -10
  633. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/provider/usage.d.ts +0 -2
  634. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +0 -2
  635. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/shared/errors.d.ts +0 -38
  636. package/dist/types/defaults/skc-defaults.d.ts +0 -64
  637. package/dist/types/defaults/skc-grok-cli.d.ts +0 -5
  638. package/dist/types/discovery/agents-md.d.ts +0 -1
  639. package/dist/types/discovery/agents.d.ts +0 -12
  640. package/dist/types/discovery/builtin.d.ts +0 -1
  641. package/dist/types/discovery/claude-plugins.d.ts +0 -1
  642. package/dist/types/discovery/claude.d.ts +0 -1
  643. package/dist/types/discovery/cline.d.ts +0 -1
  644. package/dist/types/discovery/codex.d.ts +0 -1
  645. package/dist/types/discovery/cursor.d.ts +0 -19
  646. package/dist/types/discovery/gemini.d.ts +0 -1
  647. package/dist/types/discovery/github.d.ts +0 -1
  648. package/dist/types/discovery/helpers.d.ts +0 -271
  649. package/dist/types/discovery/index.d.ts +0 -48
  650. package/dist/types/discovery/mcp-json.d.ts +0 -1
  651. package/dist/types/discovery/opencode.d.ts +0 -1
  652. package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
  653. package/dist/types/discovery/ssh.d.ts +0 -1
  654. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -1
  655. package/dist/types/discovery/vscode.d.ts +0 -1
  656. package/dist/types/discovery/windsurf.d.ts +0 -15
  657. package/dist/types/edit/apply-patch/index.d.ts +0 -35
  658. package/dist/types/edit/apply-patch/parser.d.ts +0 -34
  659. package/dist/types/edit/diff.d.ts +0 -75
  660. package/dist/types/edit/file-read-cache.d.ts +0 -25
  661. package/dist/types/edit/index.d.ts +0 -58
  662. package/dist/types/edit/modes/apply-patch.d.ts +0 -24
  663. package/dist/types/edit/modes/patch.d.ts +0 -99
  664. package/dist/types/edit/modes/replace.d.ts +0 -149
  665. package/dist/types/edit/normalize.d.ts +0 -43
  666. package/dist/types/edit/notebook.d.ts +0 -26
  667. package/dist/types/edit/read-file.d.ts +0 -8
  668. package/dist/types/edit/renderer.d.ts +0 -110
  669. package/dist/types/edit/streaming.d.ts +0 -66
  670. package/dist/types/eval/backend.d.ts +0 -40
  671. package/dist/types/eval/index.d.ts +0 -4
  672. package/dist/types/eval/js/context-manager.d.ts +0 -27
  673. package/dist/types/eval/js/executor.d.ts +0 -29
  674. package/dist/types/eval/js/index.d.ts +0 -10
  675. package/dist/types/eval/js/shared/helpers.d.ts +0 -38
  676. package/dist/types/eval/js/shared/indirect-eval.d.ts +0 -14
  677. package/dist/types/eval/js/shared/prelude.d.ts +0 -1
  678. package/dist/types/eval/js/shared/rewrite-imports.d.ts +0 -6
  679. package/dist/types/eval/js/shared/runtime.d.ts +0 -47
  680. package/dist/types/eval/js/shared/types.d.ts +0 -24
  681. package/dist/types/eval/js/tool-bridge.d.ts +0 -18
  682. package/dist/types/eval/js/worker-core.d.ts +0 -5
  683. package/dist/types/eval/js/worker-entry.d.ts +0 -1
  684. package/dist/types/eval/js/worker-protocol.d.ts +0 -77
  685. package/dist/types/eval/py/display.d.ts +0 -25
  686. package/dist/types/eval/py/executor.d.ts +0 -88
  687. package/dist/types/eval/py/index.d.ts +0 -10
  688. package/dist/types/eval/py/kernel.d.ts +0 -65
  689. package/dist/types/eval/py/prelude.d.ts +0 -1
  690. package/dist/types/eval/py/runtime.d.ts +0 -29
  691. package/dist/types/eval/py/tool-bridge.d.ts +0 -20
  692. package/dist/types/eval/types.d.ts +0 -52
  693. package/dist/types/exa/factory.d.ts +0 -13
  694. package/dist/types/exa/index.d.ts +0 -20
  695. package/dist/types/exa/mcp-client.d.ts +0 -45
  696. package/dist/types/exa/render.d.ts +0 -19
  697. package/dist/types/exa/researcher.d.ts +0 -9
  698. package/dist/types/exa/search.d.ts +0 -9
  699. package/dist/types/exa/types.d.ts +0 -155
  700. package/dist/types/exa/websets.d.ts +0 -9
  701. package/dist/types/exec/bash-executor.d.ts +0 -64
  702. package/dist/types/exec/exec.d.ts +0 -25
  703. package/dist/types/exec/idle-timeout-watchdog.d.ts +0 -18
  704. package/dist/types/exec/non-interactive-env.d.ts +0 -1
  705. package/dist/types/export/custom-share.d.ts +0 -20
  706. package/dist/types/export/html/index.d.ts +0 -10
  707. package/dist/types/export/html/template.generated.d.ts +0 -1
  708. package/dist/types/export/html/template.macro.d.ts +0 -5
  709. package/dist/types/export/ttsr.d.ts +0 -55
  710. package/dist/types/extensibility/custom-commands/bundled/ci-green/index.d.ts +0 -9
  711. package/dist/types/extensibility/custom-commands/index.d.ts +0 -2
  712. package/dist/types/extensibility/custom-commands/loader.d.ts +0 -29
  713. package/dist/types/extensibility/custom-commands/types.d.ts +0 -106
  714. package/dist/types/extensibility/custom-tools/index.d.ts +0 -6
  715. package/dist/types/extensibility/custom-tools/loader.d.ts +0 -69
  716. package/dist/types/extensibility/custom-tools/types.d.ts +0 -229
  717. package/dist/types/extensibility/custom-tools/wrapper.d.ts +0 -24
  718. package/dist/types/extensibility/extensions/compact-handler.d.ts +0 -26
  719. package/dist/types/extensibility/extensions/get-commands-handler.d.ts +0 -29
  720. package/dist/types/extensibility/extensions/index.d.ts +0 -9
  721. package/dist/types/extensibility/extensions/loader.d.ts +0 -43
  722. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +0 -35
  723. package/dist/types/extensibility/extensions/runner.d.ts +0 -135
  724. package/dist/types/extensibility/extensions/types.d.ts +0 -920
  725. package/dist/types/extensibility/extensions/wrapper.d.ts +0 -55
  726. package/dist/types/extensibility/hooks/index.d.ts +0 -5
  727. package/dist/types/extensibility/hooks/loader.d.ts +0 -89
  728. package/dist/types/extensibility/hooks/runner.d.ts +0 -126
  729. package/dist/types/extensibility/hooks/tool-wrapper.d.ts +0 -25
  730. package/dist/types/extensibility/hooks/types.d.ts +0 -431
  731. package/dist/types/extensibility/plugins/doctor.d.ts +0 -3
  732. package/dist/types/extensibility/plugins/git-url.d.ts +0 -34
  733. package/dist/types/extensibility/plugins/index.d.ts +0 -8
  734. package/dist/types/extensibility/plugins/installer.d.ts +0 -5
  735. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +0 -2
  736. package/dist/types/extensibility/plugins/loader.d.ts +0 -31
  737. package/dist/types/extensibility/plugins/manager.d.ts +0 -65
  738. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +0 -41
  739. package/dist/types/extensibility/plugins/marketplace/fetcher.d.ts +0 -48
  740. package/dist/types/extensibility/plugins/marketplace/index.d.ts +0 -6
  741. package/dist/types/extensibility/plugins/marketplace/manager.d.ts +0 -57
  742. package/dist/types/extensibility/plugins/marketplace/registry.d.ts +0 -34
  743. package/dist/types/extensibility/plugins/marketplace/source-resolver.d.ts +0 -28
  744. package/dist/types/extensibility/plugins/marketplace/types.d.ts +0 -130
  745. package/dist/types/extensibility/plugins/parser.d.ts +0 -52
  746. package/dist/types/extensibility/plugins/security-scanner.d.ts +0 -31
  747. package/dist/types/extensibility/plugins/types.d.ts +0 -149
  748. package/dist/types/extensibility/runtime-skill-discovery.d.ts +0 -20
  749. package/dist/types/extensibility/shared-events.d.ts +0 -281
  750. package/dist/types/extensibility/skc-plugins/activation.d.ts +0 -14
  751. package/dist/types/extensibility/skc-plugins/compiler.d.ts +0 -19
  752. package/dist/types/extensibility/skc-plugins/constrained-hooks.d.ts +0 -29
  753. package/dist/types/extensibility/skc-plugins/index.d.ts +0 -18
  754. package/dist/types/extensibility/skc-plugins/injection.d.ts +0 -40
  755. package/dist/types/extensibility/skc-plugins/installer.d.ts +0 -13
  756. package/dist/types/extensibility/skc-plugins/loader.d.ts +0 -3
  757. package/dist/types/extensibility/skc-plugins/mcp-policy.d.ts +0 -26
  758. package/dist/types/extensibility/skc-plugins/observability.d.ts +0 -27
  759. package/dist/types/extensibility/skc-plugins/paths.d.ts +0 -8
  760. package/dist/types/extensibility/skc-plugins/prompt-appendix.d.ts +0 -16
  761. package/dist/types/extensibility/skc-plugins/registry.d.ts +0 -32
  762. package/dist/types/extensibility/skc-plugins/runtime-adapters.d.ts +0 -64
  763. package/dist/types/extensibility/skc-plugins/schema.d.ts +0 -3
  764. package/dist/types/extensibility/skc-plugins/session-validation.d.ts +0 -42
  765. package/dist/types/extensibility/skc-plugins/state.d.ts +0 -9
  766. package/dist/types/extensibility/skc-plugins/tools.d.ts +0 -8
  767. package/dist/types/extensibility/skc-plugins/types.d.ts +0 -220
  768. package/dist/types/extensibility/skc-plugins/validation.d.ts +0 -11
  769. package/dist/types/extensibility/skills.d.ts +0 -82
  770. package/dist/types/extensibility/slash-commands.d.ts +0 -48
  771. package/dist/types/extensibility/tool-proxy.d.ts +0 -4
  772. package/dist/types/extensibility/typebox.d.ts +0 -155
  773. package/dist/types/extensibility/utils.d.ts +0 -12
  774. package/dist/types/goals/index.d.ts +0 -3
  775. package/dist/types/goals/runtime.d.ts +0 -63
  776. package/dist/types/goals/state.d.ts +0 -32
  777. package/dist/types/goals/tools/goal-tool.d.ts +0 -62
  778. package/dist/types/harness-control-plane/classifier.d.ts +0 -13
  779. package/dist/types/harness-control-plane/control-endpoint.d.ts +0 -31
  780. package/dist/types/harness-control-plane/finalize.d.ts +0 -60
  781. package/dist/types/harness-control-plane/gc-adapter.d.ts +0 -3
  782. package/dist/types/harness-control-plane/operate.d.ts +0 -35
  783. package/dist/types/harness-control-plane/owner.d.ts +0 -53
  784. package/dist/types/harness-control-plane/phase-rollup.d.ts +0 -23
  785. package/dist/types/harness-control-plane/preserve.d.ts +0 -19
  786. package/dist/types/harness-control-plane/receipt-ingest.d.ts +0 -19
  787. package/dist/types/harness-control-plane/receipt-spool.d.ts +0 -19
  788. package/dist/types/harness-control-plane/receipts.d.ts +0 -149
  789. package/dist/types/harness-control-plane/rpc-adapter.d.ts +0 -69
  790. package/dist/types/harness-control-plane/seams.d.ts +0 -21
  791. package/dist/types/harness-control-plane/session-lease.d.ts +0 -65
  792. package/dist/types/harness-control-plane/state-machine.d.ts +0 -24
  793. package/dist/types/harness-control-plane/storage.d.ts +0 -81
  794. package/dist/types/harness-control-plane/types.d.ts +0 -187
  795. package/dist/types/hashline/anchors.d.ts +0 -20
  796. package/dist/types/hashline/apply.d.ts +0 -14
  797. package/dist/types/hashline/constants.d.ts +0 -19
  798. package/dist/types/hashline/diff-preview.d.ts +0 -2
  799. package/dist/types/hashline/diff.d.ts +0 -17
  800. package/dist/types/hashline/execute.d.ts +0 -4
  801. package/dist/types/hashline/hash.d.ts +0 -123
  802. package/dist/types/hashline/index.d.ts +0 -13
  803. package/dist/types/hashline/input.d.ts +0 -11
  804. package/dist/types/hashline/parser.d.ts +0 -7
  805. package/dist/types/hashline/prefixes.d.ts +0 -7
  806. package/dist/types/hashline/recovery.d.ts +0 -27
  807. package/dist/types/hashline/stream.d.ts +0 -2
  808. package/dist/types/hashline/types.d.ts +0 -77
  809. package/dist/types/hindsight/backend.d.ts +0 -13
  810. package/dist/types/hindsight/bank.d.ts +0 -54
  811. package/dist/types/hindsight/client.d.ts +0 -224
  812. package/dist/types/hindsight/config.d.ts +0 -51
  813. package/dist/types/hindsight/content.d.ts +0 -70
  814. package/dist/types/hindsight/index.d.ts +0 -8
  815. package/dist/types/hindsight/mental-models.d.ts +0 -125
  816. package/dist/types/hindsight/state.d.ts +0 -99
  817. package/dist/types/hindsight/transcript.d.ts +0 -28
  818. package/dist/types/hooks/codex-native-hooks-config.d.ts +0 -29
  819. package/dist/types/hooks/native-skill-hook.d.ts +0 -28
  820. package/dist/types/hooks/skill-keywords.d.ts +0 -18
  821. package/dist/types/hooks/skill-state.d.ts +0 -93
  822. package/dist/types/i18n/index.d.ts +0 -38
  823. package/dist/types/i18n/messages/de.d.ts +0 -2
  824. package/dist/types/i18n/messages/de.messages.d.ts +0 -1
  825. package/dist/types/i18n/messages/de.settings.d.ts +0 -1
  826. package/dist/types/i18n/messages/en.d.ts +0 -81
  827. package/dist/types/i18n/messages/es.d.ts +0 -2
  828. package/dist/types/i18n/messages/es.messages.d.ts +0 -1
  829. package/dist/types/i18n/messages/es.settings.d.ts +0 -1
  830. package/dist/types/i18n/messages/fr.d.ts +0 -2
  831. package/dist/types/i18n/messages/fr.messages.d.ts +0 -1
  832. package/dist/types/i18n/messages/fr.settings.d.ts +0 -1
  833. package/dist/types/i18n/messages/ja.d.ts +0 -2
  834. package/dist/types/i18n/messages/ja.messages.d.ts +0 -1
  835. package/dist/types/i18n/messages/ja.settings.d.ts +0 -1
  836. package/dist/types/i18n/messages/ko.d.ts +0 -2
  837. package/dist/types/i18n/messages/ko.messages.d.ts +0 -1
  838. package/dist/types/i18n/messages/ko.settings.d.ts +0 -1
  839. package/dist/types/i18n/messages/zh.d.ts +0 -2
  840. package/dist/types/i18n/messages/zh.messages.d.ts +0 -1
  841. package/dist/types/i18n/messages/zh.settings.d.ts +0 -1
  842. package/dist/types/index.d.ts +0 -33
  843. package/dist/types/internal-urls/agent-protocol.d.ts +0 -12
  844. package/dist/types/internal-urls/artifact-protocol.d.ts +0 -6
  845. package/dist/types/internal-urls/docs-index.generated.d.ts +0 -2
  846. package/dist/types/internal-urls/index.d.ts +0 -20
  847. package/dist/types/internal-urls/issue-pr-protocol.d.ts +0 -17
  848. package/dist/types/internal-urls/json-query.d.ts +0 -30
  849. package/dist/types/internal-urls/local-protocol.d.ts +0 -43
  850. package/dist/types/internal-urls/mcp-protocol.d.ts +0 -12
  851. package/dist/types/internal-urls/memory-protocol.d.ts +0 -23
  852. package/dist/types/internal-urls/parse.d.ts +0 -19
  853. package/dist/types/internal-urls/registry-helpers.d.ts +0 -11
  854. package/dist/types/internal-urls/router.d.ts +0 -14
  855. package/dist/types/internal-urls/rule-protocol.d.ts +0 -6
  856. package/dist/types/internal-urls/skc-protocol.d.ts +0 -12
  857. package/dist/types/internal-urls/skill-protocol.d.ts +0 -13
  858. package/dist/types/internal-urls/types.d.ts +0 -109
  859. package/dist/types/lsp/client.d.ts +0 -66
  860. package/dist/types/lsp/clients/biome-client.d.ts +0 -16
  861. package/dist/types/lsp/clients/index.d.ts +0 -19
  862. package/dist/types/lsp/clients/lsp-linter-client.d.ts +0 -16
  863. package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -20
  864. package/dist/types/lsp/config.d.ts +0 -65
  865. package/dist/types/lsp/edits.d.ts +0 -23
  866. package/dist/types/lsp/index.d.ts +0 -130
  867. package/dist/types/lsp/lspmux.d.ts +0 -58
  868. package/dist/types/lsp/render.d.ts +0 -32
  869. package/dist/types/lsp/startup-events.d.ts +0 -12
  870. package/dist/types/lsp/types.d.ts +0 -306
  871. package/dist/types/lsp/utils.d.ts +0 -108
  872. package/dist/types/main.d.ts +0 -139
  873. package/dist/types/memories/index.d.ts +0 -28
  874. package/dist/types/memories/storage.d.ts +0 -110
  875. package/dist/types/memory-backend/index.d.ts +0 -4
  876. package/dist/types/memory-backend/local-backend.d.ts +0 -9
  877. package/dist/types/memory-backend/off-backend.d.ts +0 -7
  878. package/dist/types/memory-backend/resolve.d.ts +0 -15
  879. package/dist/types/memory-backend/types.d.ts +0 -61
  880. package/dist/types/migrate/action-planner.d.ts +0 -11
  881. package/dist/types/migrate/adapters/claude-code.d.ts +0 -2
  882. package/dist/types/migrate/adapters/codex.d.ts +0 -5
  883. package/dist/types/migrate/adapters/index.d.ts +0 -45
  884. package/dist/types/migrate/adapters/opencode.d.ts +0 -2
  885. package/dist/types/migrate/executor.d.ts +0 -2
  886. package/dist/types/migrate/mcp-mapper.d.ts +0 -20
  887. package/dist/types/migrate/report.d.ts +0 -18
  888. package/dist/types/migrate/skill-normalizer.d.ts +0 -27
  889. package/dist/types/migrate/types.d.ts +0 -126
  890. package/dist/types/modes/acp/acp-agent.d.ts +0 -78
  891. package/dist/types/modes/acp/acp-client-bridge.d.ts +0 -9
  892. package/dist/types/modes/acp/acp-event-mapper.d.ts +0 -36
  893. package/dist/types/modes/acp/acp-mode.d.ts +0 -17
  894. package/dist/types/modes/acp/index.d.ts +0 -2
  895. package/dist/types/modes/acp/terminal-auth.d.ts +0 -6
  896. package/dist/types/modes/bridge/auth.d.ts +0 -12
  897. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +0 -9
  898. package/dist/types/modes/bridge/bridge-mode.d.ts +0 -47
  899. package/dist/types/modes/bridge/bridge-ui-context.d.ts +0 -88
  900. package/dist/types/modes/bridge/event-stream.d.ts +0 -8
  901. package/dist/types/modes/components/agent-dashboard.d.ts +0 -21
  902. package/dist/types/modes/components/assistant-message.d.ts +0 -20
  903. package/dist/types/modes/components/bash-execution.d.ts +0 -29
  904. package/dist/types/modes/components/bordered-loader.d.ts +0 -11
  905. package/dist/types/modes/components/branch-summary-message.d.ts +0 -13
  906. package/dist/types/modes/components/btw-panel.d.ts +0 -16
  907. package/dist/types/modes/components/compaction-summary-message.d.ts +0 -13
  908. package/dist/types/modes/components/countdown-timer.d.ts +0 -14
  909. package/dist/types/modes/components/custom-editor.d.ts +0 -69
  910. package/dist/types/modes/components/custom-message.d.ts +0 -15
  911. package/dist/types/modes/components/custom-model-preset-wizard.d.ts +0 -12
  912. package/dist/types/modes/components/custom-provider-wizard.d.ts +0 -10
  913. package/dist/types/modes/components/diff.d.ts +0 -11
  914. package/dist/types/modes/components/dynamic-border.d.ts +0 -14
  915. package/dist/types/modes/components/eval-execution.d.ts +0 -23
  916. package/dist/types/modes/components/execution-shared.d.ts +0 -46
  917. package/dist/types/modes/components/extensions/extension-dashboard.d.ts +0 -26
  918. package/dist/types/modes/components/extensions/extension-list.d.ts +0 -39
  919. package/dist/types/modes/components/extensions/index.d.ts +0 -8
  920. package/dist/types/modes/components/extensions/inspector-panel.d.ts +0 -8
  921. package/dist/types/modes/components/extensions/state-manager.d.ts +0 -50
  922. package/dist/types/modes/components/extensions/types.d.ts +0 -151
  923. package/dist/types/modes/components/footer.d.ts +0 -30
  924. package/dist/types/modes/components/history-search.d.ts +0 -7
  925. package/dist/types/modes/components/hook-editor.d.ts +0 -18
  926. package/dist/types/modes/components/hook-input.d.ts +0 -15
  927. package/dist/types/modes/components/hook-message.d.ts +0 -15
  928. package/dist/types/modes/components/hook-selector.d.ts +0 -59
  929. package/dist/types/modes/components/index.d.ts +0 -35
  930. package/dist/types/modes/components/irc-sidebar.d.ts +0 -32
  931. package/dist/types/modes/components/jobs-overlay-model.d.ts +0 -31
  932. package/dist/types/modes/components/jobs-overlay.d.ts +0 -30
  933. package/dist/types/modes/components/keybinding-hints.d.ts +0 -40
  934. package/dist/types/modes/components/login-dialog.d.ts +0 -32
  935. package/dist/types/modes/components/message-frame.d.ts +0 -42
  936. package/dist/types/modes/components/model-selector.d.ts +0 -60
  937. package/dist/types/modes/components/oauth-selector.d.ts +0 -17
  938. package/dist/types/modes/components/pet-selector.d.ts +0 -11
  939. package/dist/types/modes/components/plugin-selector.d.ts +0 -26
  940. package/dist/types/modes/components/plugin-settings.d.ts +0 -66
  941. package/dist/types/modes/components/provider-onboarding-selector.d.ts +0 -7
  942. package/dist/types/modes/components/queue-mode-selector.d.ts +0 -9
  943. package/dist/types/modes/components/queued-message-selector.d.ts +0 -12
  944. package/dist/types/modes/components/read-tool-group.d.ts +0 -30
  945. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +0 -26
  946. package/dist/types/modes/components/sayknow-pet-widget.d.ts +0 -59
  947. package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
  948. package/dist/types/modes/components/session-selector.d.ts +0 -43
  949. package/dist/types/modes/components/settings-defs.d.ts +0 -50
  950. package/dist/types/modes/components/settings-selector.d.ts +0 -64
  951. package/dist/types/modes/components/show-images-selector.d.ts +0 -9
  952. package/dist/types/modes/components/skill-hud/render.d.ts +0 -2
  953. package/dist/types/modes/components/skill-message.d.ts +0 -10
  954. package/dist/types/modes/components/status-line/context-thresholds.d.ts +0 -4
  955. package/dist/types/modes/components/status-line/git-utils.d.ts +0 -28
  956. package/dist/types/modes/components/status-line/index.d.ts +0 -4
  957. package/dist/types/modes/components/status-line/presets.d.ts +0 -3
  958. package/dist/types/modes/components/status-line/segments.d.ts +0 -5
  959. package/dist/types/modes/components/status-line/separators.d.ts +0 -3
  960. package/dist/types/modes/components/status-line/token-rate.d.ts +0 -10
  961. package/dist/types/modes/components/status-line/types.d.ts +0 -83
  962. package/dist/types/modes/components/status-line.d.ts +0 -94
  963. package/dist/types/modes/components/theme-selector.d.ts +0 -10
  964. package/dist/types/modes/components/thinking-selector.d.ts +0 -15
  965. package/dist/types/modes/components/todo-reminder.d.ts +0 -13
  966. package/dist/types/modes/components/tool-execution.d.ts +0 -55
  967. package/dist/types/modes/components/tree-selector.d.ts +0 -31
  968. package/dist/types/modes/components/ttsr-notification.d.ts +0 -13
  969. package/dist/types/modes/components/two-column-body.d.ts +0 -9
  970. package/dist/types/modes/components/user-message-selector.d.ts +0 -28
  971. package/dist/types/modes/components/user-message.d.ts +0 -8
  972. package/dist/types/modes/components/visual-truncate.d.ts +0 -19
  973. package/dist/types/modes/components/welcome.d.ts +0 -46
  974. package/dist/types/modes/controllers/btw-controller.d.ts +0 -10
  975. package/dist/types/modes/controllers/command-controller-shared.d.ts +0 -47
  976. package/dist/types/modes/controllers/command-controller.d.ts +0 -40
  977. package/dist/types/modes/controllers/event-controller.d.ts +0 -21
  978. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -80
  979. package/dist/types/modes/controllers/input-controller.d.ts +0 -42
  980. package/dist/types/modes/controllers/runtime-mcp-command-controller.d.ts +0 -10
  981. package/dist/types/modes/controllers/selector-controller.d.ts +0 -68
  982. package/dist/types/modes/controllers/ssh-command-controller.d.ts +0 -10
  983. package/dist/types/modes/controllers/todo-command-controller.d.ts +0 -7
  984. package/dist/types/modes/emoji-autocomplete.d.ts +0 -16
  985. package/dist/types/modes/index.d.ts +0 -10
  986. package/dist/types/modes/interactive-mode.d.ts +0 -297
  987. package/dist/types/modes/irc-observation-ledger.d.ts +0 -22
  988. package/dist/types/modes/jobs-observer.d.ts +0 -57
  989. package/dist/types/modes/oauth-manual-input.d.ts +0 -8
  990. package/dist/types/modes/print-mode.d.ts +0 -44
  991. package/dist/types/modes/prompt-action-autocomplete.d.ts +0 -50
  992. package/dist/types/modes/rpc/host-tools.d.ts +0 -1
  993. package/dist/types/modes/rpc/host-uris.d.ts +0 -1
  994. package/dist/types/modes/rpc/rpc-client.d.ts +0 -299
  995. package/dist/types/modes/rpc/rpc-mode.d.ts +0 -86
  996. package/dist/types/modes/rpc/rpc-socket-security.d.ts +0 -10
  997. package/dist/types/modes/rpc/rpc-types.d.ts +0 -810
  998. package/dist/types/modes/runtime-init.d.ts +0 -21
  999. package/dist/types/modes/session-observer-registry.d.ts +0 -26
  1000. package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +0 -57
  1001. package/dist/types/modes/shared/agent-wire/command-contract.d.ts +0 -18
  1002. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +0 -37
  1003. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +0 -2
  1004. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +0 -102
  1005. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +0 -103
  1006. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +0 -60
  1007. package/dist/types/modes/shared/agent-wire/event-observation.d.ts +0 -37
  1008. package/dist/types/modes/shared/agent-wire/handshake.d.ts +0 -56
  1009. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +0 -16
  1010. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +0 -17
  1011. package/dist/types/modes/shared/agent-wire/protocol.d.ts +0 -25
  1012. package/dist/types/modes/shared/agent-wire/responses.d.ts +0 -4
  1013. package/dist/types/modes/shared/agent-wire/scopes.d.ts +0 -18
  1014. package/dist/types/modes/shared/agent-wire/session-registry.d.ts +0 -25
  1015. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +0 -42
  1016. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +0 -27
  1017. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +0 -29
  1018. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +0 -68
  1019. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +0 -166
  1020. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +0 -127
  1021. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +0 -154
  1022. package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +0 -39
  1023. package/dist/types/modes/shared.d.ts +0 -15
  1024. package/dist/types/modes/slash-command-visibility.d.ts +0 -1
  1025. package/dist/types/modes/theme/defaults/index.d.ts +0 -652
  1026. package/dist/types/modes/theme/mermaid-cache.d.ts +0 -9
  1027. package/dist/types/modes/theme/shimmer.d.ts +0 -38
  1028. package/dist/types/modes/theme/theme.d.ts +0 -275
  1029. package/dist/types/modes/tmux-scroll.d.ts +0 -18
  1030. package/dist/types/modes/types.d.ts +0 -318
  1031. package/dist/types/modes/utils/abort-message.d.ts +0 -4
  1032. package/dist/types/modes/utils/context-usage.d.ts +0 -36
  1033. package/dist/types/modes/utils/hotkeys-markdown.d.ts +0 -5
  1034. package/dist/types/modes/utils/injected-user-submission.d.ts +0 -52
  1035. package/dist/types/modes/utils/irc-message.d.ts +0 -38
  1036. package/dist/types/modes/utils/keybinding-matchers.d.ts +0 -10
  1037. package/dist/types/modes/utils/terminal-bell.d.ts +0 -3
  1038. package/dist/types/modes/utils/tools-markdown.d.ts +0 -5
  1039. package/dist/types/modes/utils/ui-helpers.d.ts +0 -75
  1040. package/dist/types/notifications/attachment-registry.d.ts +0 -17
  1041. package/dist/types/notifications/chat-adapters.d.ts +0 -9
  1042. package/dist/types/notifications/config-commands.d.ts +0 -64
  1043. package/dist/types/notifications/config.d.ts +0 -132
  1044. package/dist/types/notifications/daemon-paths.d.ts +0 -17
  1045. package/dist/types/notifications/engine.d.ts +0 -59
  1046. package/dist/types/notifications/helpers.d.ts +0 -55
  1047. package/dist/types/notifications/html-format.d.ts +0 -75
  1048. package/dist/types/notifications/index.d.ts +0 -219
  1049. package/dist/types/notifications/lifecycle-commands.d.ts +0 -69
  1050. package/dist/types/notifications/lifecycle-control-runtime.d.ts +0 -107
  1051. package/dist/types/notifications/lifecycle-orchestrator.d.ts +0 -144
  1052. package/dist/types/notifications/managed-daemon.d.ts +0 -48
  1053. package/dist/types/notifications/notification-service.d.ts +0 -144
  1054. package/dist/types/notifications/operator-runtime.d.ts +0 -52
  1055. package/dist/types/notifications/rate-limit-pool.d.ts +0 -113
  1056. package/dist/types/notifications/recent-activity.d.ts +0 -39
  1057. package/dist/types/notifications/reply-sent-store.d.ts +0 -53
  1058. package/dist/types/notifications/rich-draft.d.ts +0 -68
  1059. package/dist/types/notifications/rich-render.d.ts +0 -90
  1060. package/dist/types/notifications/telegram-cli.d.ts +0 -19
  1061. package/dist/types/notifications/telegram-daemon-cli.d.ts +0 -28
  1062. package/dist/types/notifications/telegram-daemon-control.d.ts +0 -70
  1063. package/dist/types/notifications/telegram-daemon.d.ts +0 -558
  1064. package/dist/types/notifications/telegram-reference.d.ts +0 -136
  1065. package/dist/types/notifications/threaded-inbound.d.ts +0 -77
  1066. package/dist/types/notifications/threaded-render.d.ts +0 -88
  1067. package/dist/types/notifications/topic-registry.d.ts +0 -89
  1068. package/dist/types/plan-mode/approved-plan.d.ts +0 -49
  1069. package/dist/types/plan-mode/state.d.ts +0 -6
  1070. package/dist/types/registry/agent-registry.d.ts +0 -64
  1071. package/dist/types/reminders/star-reminder.d.ts +0 -115
  1072. package/dist/types/research-plan/index.d.ts +0 -1
  1073. package/dist/types/research-plan/ledger.d.ts +0 -33
  1074. package/dist/types/rlm/artifacts.d.ts +0 -9
  1075. package/dist/types/rlm/complete-research-tool.d.ts +0 -35
  1076. package/dist/types/rlm/data-context.d.ts +0 -6
  1077. package/dist/types/rlm/index.d.ts +0 -47
  1078. package/dist/types/rlm/notebook.d.ts +0 -12
  1079. package/dist/types/rlm/preset.d.ts +0 -23
  1080. package/dist/types/rlm/python-tool.d.ts +0 -16
  1081. package/dist/types/rlm/report.d.ts +0 -14
  1082. package/dist/types/rlm/types.d.ts +0 -37
  1083. package/dist/types/runtime/process-lifecycle.d.ts +0 -108
  1084. package/dist/types/runtime-api-key.d.ts +0 -7
  1085. package/dist/types/runtime-credential-selector.d.ts +0 -7
  1086. package/dist/types/runtime-mcp/client.d.ts +0 -74
  1087. package/dist/types/runtime-mcp/config-writer.d.ts +0 -89
  1088. package/dist/types/runtime-mcp/config.d.ts +0 -77
  1089. package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
  1090. package/dist/types/runtime-mcp/index.d.ts +0 -18
  1091. package/dist/types/runtime-mcp/json-rpc.d.ts +0 -22
  1092. package/dist/types/runtime-mcp/loader.d.ts +0 -45
  1093. package/dist/types/runtime-mcp/manager.d.ts +0 -195
  1094. package/dist/types/runtime-mcp/oauth-discovery.d.ts +0 -40
  1095. package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -59
  1096. package/dist/types/runtime-mcp/render.d.ts +0 -25
  1097. package/dist/types/runtime-mcp/smithery-auth.d.ts +0 -16
  1098. package/dist/types/runtime-mcp/smithery-connect.d.ts +0 -38
  1099. package/dist/types/runtime-mcp/smithery-registry.d.ts +0 -51
  1100. package/dist/types/runtime-mcp/tool-bridge.d.ts +0 -86
  1101. package/dist/types/runtime-mcp/tool-cache.d.ts +0 -8
  1102. package/dist/types/runtime-mcp/transports/http.d.ts +0 -36
  1103. package/dist/types/runtime-mcp/transports/index.d.ts +0 -5
  1104. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -29
  1105. package/dist/types/runtime-mcp/types.d.ts +0 -349
  1106. package/dist/types/sdk.d.ts +0 -256
  1107. package/dist/types/secrets/index.d.ts +0 -9
  1108. package/dist/types/secrets/obfuscator.d.ts +0 -23
  1109. package/dist/types/secrets/regex.d.ts +0 -2
  1110. package/dist/types/session/agent-session.d.ts +0 -1238
  1111. package/dist/types/session/agent-storage.d.ts +0 -101
  1112. package/dist/types/session/artifacts.d.ts +0 -64
  1113. package/dist/types/session/auth-broker-config.d.ts +0 -13
  1114. package/dist/types/session/auth-storage.d.ts +0 -6
  1115. package/dist/types/session/blob-store.d.ts +0 -160
  1116. package/dist/types/session/cache-economics.d.ts +0 -79
  1117. package/dist/types/session/client-bridge.d.ts +0 -88
  1118. package/dist/types/session/context-estimation.d.ts +0 -31
  1119. package/dist/types/session/contribution-prep.d.ts +0 -47
  1120. package/dist/types/session/file-mention-pruning.d.ts +0 -8
  1121. package/dist/types/session/history-storage.d.ts +0 -16
  1122. package/dist/types/session/messages.d.ts +0 -169
  1123. package/dist/types/session/provider-safety-stop.d.ts +0 -5
  1124. package/dist/types/session/session-dump-format.d.ts +0 -22
  1125. package/dist/types/session/session-manager.d.ts +0 -781
  1126. package/dist/types/session/session-storage.d.ts +0 -230
  1127. package/dist/types/session/streaming-output.d.ts +0 -209
  1128. package/dist/types/session/tool-choice-queue.d.ts +0 -84
  1129. package/dist/types/session/yield-queue.d.ts +0 -24
  1130. package/dist/types/setup/credential-auto-import.d.ts +0 -63
  1131. package/dist/types/setup/credential-import.d.ts +0 -82
  1132. package/dist/types/setup/hermes-setup.d.ts +0 -78
  1133. package/dist/types/setup/host-plugin-setup.d.ts +0 -39
  1134. package/dist/types/setup/model-onboarding-guidance.d.ts +0 -9
  1135. package/dist/types/setup/provider-onboarding.d.ts +0 -52
  1136. package/dist/types/skc-runtime/cli-write-receipt.d.ts +0 -24
  1137. package/dist/types/skc-runtime/deep-interview-ambiguity.d.ts +0 -39
  1138. package/dist/types/skc-runtime/deep-interview-recorder.d.ts +0 -114
  1139. package/dist/types/skc-runtime/deep-interview-runtime.d.ts +0 -36
  1140. package/dist/types/skc-runtime/deep-interview-state.d.ts +0 -127
  1141. package/dist/types/skc-runtime/gc-render.d.ts +0 -6
  1142. package/dist/types/skc-runtime/gc-runtime.d.ts +0 -134
  1143. package/dist/types/skc-runtime/goal-mode-request.d.ts +0 -53
  1144. package/dist/types/skc-runtime/launch-tmux.d.ts +0 -78
  1145. package/dist/types/skc-runtime/launch-worktree.d.ts +0 -46
  1146. package/dist/types/skc-runtime/ledger-event-renderer.d.ts +0 -68
  1147. package/dist/types/skc-runtime/psmux-detect.d.ts +0 -78
  1148. package/dist/types/skc-runtime/ralplan-runtime.d.ts +0 -26
  1149. package/dist/types/skc-runtime/restricted-role-agent-bash.d.ts +0 -3
  1150. package/dist/types/skc-runtime/session-layout.d.ts +0 -59
  1151. package/dist/types/skc-runtime/session-resolution.d.ts +0 -47
  1152. package/dist/types/skc-runtime/session-state-sidecar.d.ts +0 -70
  1153. package/dist/types/skc-runtime/state-graph.d.ts +0 -4
  1154. package/dist/types/skc-runtime/state-migrations.d.ts +0 -33
  1155. package/dist/types/skc-runtime/state-renderer.d.ts +0 -70
  1156. package/dist/types/skc-runtime/state-runtime.d.ts +0 -38
  1157. package/dist/types/skc-runtime/state-schema.d.ts +0 -319
  1158. package/dist/types/skc-runtime/state-validation.d.ts +0 -6
  1159. package/dist/types/skc-runtime/state-writer.d.ts +0 -244
  1160. package/dist/types/skc-runtime/team-gc.d.ts +0 -7
  1161. package/dist/types/skc-runtime/team-runtime.d.ts +0 -355
  1162. package/dist/types/skc-runtime/tmux-common.d.ts +0 -111
  1163. package/dist/types/skc-runtime/tmux-gc.d.ts +0 -7
  1164. package/dist/types/skc-runtime/tmux-owner-isolation-cli.d.ts +0 -5
  1165. package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +0 -309
  1166. package/dist/types/skc-runtime/tmux-sessions.d.ts +0 -94
  1167. package/dist/types/skc-runtime/ultragoal-guard.d.ts +0 -93
  1168. package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +0 -42
  1169. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +0 -410
  1170. package/dist/types/skc-runtime/workflow-command-ref.d.ts +0 -43
  1171. package/dist/types/skc-runtime/workflow-manifest.d.ts +0 -54
  1172. package/dist/types/skill-state/active-state.d.ts +0 -126
  1173. package/dist/types/skill-state/canonical-skills.d.ts +0 -3
  1174. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +0 -38
  1175. package/dist/types/skill-state/initial-phase.d.ts +0 -12
  1176. package/dist/types/skill-state/workflow-hud.d.ts +0 -83
  1177. package/dist/types/skill-state/workflow-state-contract.d.ts +0 -57
  1178. package/dist/types/skill-state/workflow-state-version.d.ts +0 -3
  1179. package/dist/types/slash-commands/acp-builtins.d.ts +0 -18
  1180. package/dist/types/slash-commands/builtin-registry.d.ts +0 -24
  1181. package/dist/types/slash-commands/helpers/context-report.d.ts +0 -7
  1182. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +0 -82
  1183. package/dist/types/slash-commands/helpers/format.d.ts +0 -10
  1184. package/dist/types/slash-commands/helpers/mcp.d.ts +0 -3
  1185. package/dist/types/slash-commands/helpers/parse.d.ts +0 -33
  1186. package/dist/types/slash-commands/helpers/ssh.d.ts +0 -3
  1187. package/dist/types/slash-commands/helpers/todo.d.ts +0 -3
  1188. package/dist/types/slash-commands/helpers/usage-report.d.ts +0 -7
  1189. package/dist/types/slash-commands/types.d.ts +0 -120
  1190. package/dist/types/ssh/config-writer.d.ts +0 -49
  1191. package/dist/types/ssh/connection-manager.d.ts +0 -33
  1192. package/dist/types/ssh/ssh-executor.d.ts +0 -37
  1193. package/dist/types/ssh/sshfs-mount.d.ts +0 -6
  1194. package/dist/types/ssh/utils.d.ts +0 -2
  1195. package/dist/types/stt/downloader.d.ts +0 -9
  1196. package/dist/types/stt/index.d.ts +0 -3
  1197. package/dist/types/stt/recorder.d.ts +0 -13
  1198. package/dist/types/stt/setup.d.ts +0 -18
  1199. package/dist/types/stt/stt-controller.d.ts +0 -16
  1200. package/dist/types/stt/transcriber.d.ts +0 -16
  1201. package/dist/types/system-prompt.d.ts +0 -118
  1202. package/dist/types/task/agents.d.ts +0 -28
  1203. package/dist/types/task/commands.d.ts +0 -35
  1204. package/dist/types/task/discovery.d.ts +0 -19
  1205. package/dist/types/task/executor.d.ts +0 -127
  1206. package/dist/types/task/fork-context-advisory.d.ts +0 -13
  1207. package/dist/types/task/fork-context-budget.d.ts +0 -7
  1208. package/dist/types/task/id.d.ts +0 -7
  1209. package/dist/types/task/index.d.ts +0 -43
  1210. package/dist/types/task/name-generator.d.ts +0 -16
  1211. package/dist/types/task/output-manager.d.ts +0 -36
  1212. package/dist/types/task/parallel.d.ts +0 -34
  1213. package/dist/types/task/receipt.d.ts +0 -89
  1214. package/dist/types/task/render.d.ts +0 -28
  1215. package/dist/types/task/roi-reconciliation.d.ts +0 -27
  1216. package/dist/types/task/simple-mode.d.ts +0 -8
  1217. package/dist/types/task/skc-command.d.ts +0 -7
  1218. package/dist/types/task/spawn-gate.d.ts +0 -29
  1219. package/dist/types/task/subprocess-tool-registry.d.ts +0 -72
  1220. package/dist/types/task/token-log.d.ts +0 -30
  1221. package/dist/types/task/token-log.test.d.ts +0 -1
  1222. package/dist/types/task/types.d.ts +0 -516
  1223. package/dist/types/task/worktree.d.ts +0 -94
  1224. package/dist/types/thinking-metadata.d.ts +0 -16
  1225. package/dist/types/thinking.d.ts +0 -22
  1226. package/dist/types/tool-discovery/tool-index.d.ts +0 -112
  1227. package/dist/types/tools/archive-reader.d.ts +0 -40
  1228. package/dist/types/tools/ask-answer-registry.d.ts +0 -18
  1229. package/dist/types/tools/ask.d.ts +0 -123
  1230. package/dist/types/tools/ast-edit.d.ts +0 -77
  1231. package/dist/types/tools/ast-grep.d.ts +0 -69
  1232. package/dist/types/tools/auto-generated-guard.d.ts +0 -17
  1233. package/dist/types/tools/bash-allowed-prefixes.d.ts +0 -10
  1234. package/dist/types/tools/bash-command-fixup.d.ts +0 -11
  1235. package/dist/types/tools/bash-interactive.d.ts +0 -16
  1236. package/dist/types/tools/bash-interceptor.d.ts +0 -24
  1237. package/dist/types/tools/bash-pty-selection.d.ts +0 -7
  1238. package/dist/types/tools/bash-skill-urls.d.ts +0 -31
  1239. package/dist/types/tools/bash.d.ts +0 -157
  1240. package/dist/types/tools/bisect.d.ts +0 -81
  1241. package/dist/types/tools/browser/actions.d.ts +0 -54
  1242. package/dist/types/tools/browser/attach.d.ts +0 -50
  1243. package/dist/types/tools/browser/launch.d.ts +0 -62
  1244. package/dist/types/tools/browser/readable.d.ts +0 -17
  1245. package/dist/types/tools/browser/registry.d.ts +0 -57
  1246. package/dist/types/tools/browser/render.d.ts +0 -53
  1247. package/dist/types/tools/browser/tab-protocol.d.ts +0 -144
  1248. package/dist/types/tools/browser/tab-supervisor.d.ts +0 -109
  1249. package/dist/types/tools/browser/tab-worker-entry.d.ts +0 -1
  1250. package/dist/types/tools/browser/tab-worker.d.ts +0 -19
  1251. package/dist/types/tools/browser.d.ts +0 -211
  1252. package/dist/types/tools/calculator.d.ts +0 -76
  1253. package/dist/types/tools/checkpoint.d.ts +0 -63
  1254. package/dist/types/tools/composer-bash-policy.d.ts +0 -14
  1255. package/dist/types/tools/computer/render.d.ts +0 -17
  1256. package/dist/types/tools/computer-gc.d.ts +0 -23
  1257. package/dist/types/tools/computer.d.ts +0 -465
  1258. package/dist/types/tools/conflict-detect.d.ts +0 -205
  1259. package/dist/types/tools/context.d.ts +0 -19
  1260. package/dist/types/tools/cron.d.ts +0 -91
  1261. package/dist/types/tools/debug.d.ts +0 -209
  1262. package/dist/types/tools/eval.d.ts +0 -108
  1263. package/dist/types/tools/fetch.d.ts +0 -84
  1264. package/dist/types/tools/file-recorder.d.ts +0 -13
  1265. package/dist/types/tools/find.d.ts +0 -95
  1266. package/dist/types/tools/fs-cache-invalidation.d.ts +0 -15
  1267. package/dist/types/tools/gh-format.d.ts +0 -6
  1268. package/dist/types/tools/gh-renderer.d.ts +0 -25
  1269. package/dist/types/tools/gh.d.ts +0 -356
  1270. package/dist/types/tools/github-cache.d.ts +0 -103
  1271. package/dist/types/tools/grouped-file-output.d.ts +0 -36
  1272. package/dist/types/tools/hindsight-recall.d.ts +0 -21
  1273. package/dist/types/tools/hindsight-reflect.d.ts +0 -23
  1274. package/dist/types/tools/hindsight-retain.d.ts +0 -27
  1275. package/dist/types/tools/image-gen.d.ts +0 -79
  1276. package/dist/types/tools/index.d.ts +0 -371
  1277. package/dist/types/tools/irc.d.ts +0 -79
  1278. package/dist/types/tools/job.d.ts +0 -78
  1279. package/dist/types/tools/json-tree.d.ts +0 -23
  1280. package/dist/types/tools/jtd-to-json-schema.d.ts +0 -29
  1281. package/dist/types/tools/jtd-to-typescript.d.ts +0 -26
  1282. package/dist/types/tools/jtd-utils.d.ts +0 -42
  1283. package/dist/types/tools/list-limit.d.ts +0 -12
  1284. package/dist/types/tools/match-line-format.d.ts +0 -11
  1285. package/dist/types/tools/monitor.d.ts +0 -54
  1286. package/dist/types/tools/output-meta.d.ts +0 -204
  1287. package/dist/types/tools/path-utils.d.ts +0 -147
  1288. package/dist/types/tools/plan-mode-guard.d.ts +0 -18
  1289. package/dist/types/tools/read.d.ts +0 -85
  1290. package/dist/types/tools/recipe/index.d.ts +0 -45
  1291. package/dist/types/tools/recipe/render.d.ts +0 -36
  1292. package/dist/types/tools/recipe/runner.d.ts +0 -60
  1293. package/dist/types/tools/recipe/runners/cargo.d.ts +0 -16
  1294. package/dist/types/tools/recipe/runners/index.d.ts +0 -2
  1295. package/dist/types/tools/recipe/runners/just.d.ts +0 -2
  1296. package/dist/types/tools/recipe/runners/make.d.ts +0 -2
  1297. package/dist/types/tools/recipe/runners/pkg.d.ts +0 -2
  1298. package/dist/types/tools/recipe/runners/task.d.ts +0 -2
  1299. package/dist/types/tools/render-mermaid.d.ts +0 -37
  1300. package/dist/types/tools/render-utils.d.ts +0 -158
  1301. package/dist/types/tools/renderers.d.ts +0 -26
  1302. package/dist/types/tools/resolve.d.ts +0 -80
  1303. package/dist/types/tools/resource-gc.d.ts +0 -54
  1304. package/dist/types/tools/review.d.ts +0 -63
  1305. package/dist/types/tools/search-tool-bm25.d.ts +0 -65
  1306. package/dist/types/tools/search.d.ts +0 -93
  1307. package/dist/types/tools/skill-discovery.d.ts +0 -40
  1308. package/dist/types/tools/skill.d.ts +0 -48
  1309. package/dist/types/tools/sqlite-reader.d.ts +0 -94
  1310. package/dist/types/tools/ssh.d.ts +0 -67
  1311. package/dist/types/tools/subagent-render.d.ts +0 -31
  1312. package/dist/types/tools/subagent.d.ts +0 -117
  1313. package/dist/types/tools/telegram-send.d.ts +0 -39
  1314. package/dist/types/tools/todo-write.d.ts +0 -120
  1315. package/dist/types/tools/tool-errors.d.ts +0 -33
  1316. package/dist/types/tools/tool-result.d.ts +0 -30
  1317. package/dist/types/tools/tool-timeouts.d.ts +0 -56
  1318. package/dist/types/tools/ultragoal-ask-guard.d.ts +0 -12
  1319. package/dist/types/tools/vim.d.ts +0 -58
  1320. package/dist/types/tools/write.d.ts +0 -59
  1321. package/dist/types/tools/yield.d.ts +0 -25
  1322. package/dist/types/tui/code-cell.d.ts +0 -32
  1323. package/dist/types/tui/file-list.d.ts +0 -22
  1324. package/dist/types/tui/hyperlink.d.ts +0 -42
  1325. package/dist/types/tui/index.d.ts +0 -11
  1326. package/dist/types/tui/output-block.d.ts +0 -28
  1327. package/dist/types/tui/status-line.d.ts +0 -18
  1328. package/dist/types/tui/tree-list.d.ts +0 -19
  1329. package/dist/types/tui/types.d.ts +0 -13
  1330. package/dist/types/tui/utils.d.ts +0 -36
  1331. package/dist/types/utils/changelog.d.ts +0 -37
  1332. package/dist/types/utils/clipboard.d.ts +0 -22
  1333. package/dist/types/utils/cmux-workspace.d.ts +0 -52
  1334. package/dist/types/utils/command-args.d.ts +0 -9
  1335. package/dist/types/utils/commit-message-generator.d.ts +0 -7
  1336. package/dist/types/utils/edit-mode.d.ts +0 -13
  1337. package/dist/types/utils/event-bus.d.ts +0 -6
  1338. package/dist/types/utils/external-editor.d.ts +0 -19
  1339. package/dist/types/utils/file-display-mode.d.ts +0 -27
  1340. package/dist/types/utils/file-mentions.d.ts +0 -25
  1341. package/dist/types/utils/git.d.ts +0 -393
  1342. package/dist/types/utils/image-loading.d.ts +0 -26
  1343. package/dist/types/utils/image-resize.d.ts +0 -39
  1344. package/dist/types/utils/lang-from-path.d.ts +0 -8
  1345. package/dist/types/utils/linkedom.d.ts +0 -4
  1346. package/dist/types/utils/markit.d.ts +0 -7
  1347. package/dist/types/utils/open.d.ts +0 -2
  1348. package/dist/types/utils/pasted-image-path.d.ts +0 -32
  1349. package/dist/types/utils/session-color.d.ts +0 -10
  1350. package/dist/types/utils/shell-snapshot.d.ts +0 -5
  1351. package/dist/types/utils/sixel.d.ts +0 -21
  1352. package/dist/types/utils/title-generator.d.ts +0 -31
  1353. package/dist/types/utils/tool-choice.d.ts +0 -20
  1354. package/dist/types/utils/tools-manager.d.ts +0 -9
  1355. package/dist/types/vim/buffer.d.ts +0 -41
  1356. package/dist/types/vim/commands.d.ts +0 -6
  1357. package/dist/types/vim/engine.d.ts +0 -47
  1358. package/dist/types/vim/parser.d.ts +0 -3
  1359. package/dist/types/vim/render.d.ts +0 -25
  1360. package/dist/types/vim/types.d.ts +0 -182
  1361. package/dist/types/web/insane/bridge.d.ts +0 -103
  1362. package/dist/types/web/insane/url-guard.d.ts +0 -25
  1363. package/dist/types/web/kagi.d.ts +0 -23
  1364. package/dist/types/web/parallel.d.ts +0 -58
  1365. package/dist/types/web/scrapers/artifacthub.d.ts +0 -6
  1366. package/dist/types/web/scrapers/arxiv.d.ts +0 -5
  1367. package/dist/types/web/scrapers/aur.d.ts +0 -5
  1368. package/dist/types/web/scrapers/biorxiv.d.ts +0 -5
  1369. package/dist/types/web/scrapers/bluesky.d.ts +0 -5
  1370. package/dist/types/web/scrapers/brew.d.ts +0 -5
  1371. package/dist/types/web/scrapers/cheatsh.d.ts +0 -8
  1372. package/dist/types/web/scrapers/chocolatey.d.ts +0 -5
  1373. package/dist/types/web/scrapers/choosealicense.d.ts +0 -2
  1374. package/dist/types/web/scrapers/cisa-kev.d.ts +0 -5
  1375. package/dist/types/web/scrapers/clojars.d.ts +0 -5
  1376. package/dist/types/web/scrapers/coingecko.d.ts +0 -5
  1377. package/dist/types/web/scrapers/crates-io.d.ts +0 -5
  1378. package/dist/types/web/scrapers/crossref.d.ts +0 -2
  1379. package/dist/types/web/scrapers/devto.d.ts +0 -5
  1380. package/dist/types/web/scrapers/discogs.d.ts +0 -8
  1381. package/dist/types/web/scrapers/discourse.d.ts +0 -5
  1382. package/dist/types/web/scrapers/dockerhub.d.ts +0 -5
  1383. package/dist/types/web/scrapers/docs-rs.d.ts +0 -2
  1384. package/dist/types/web/scrapers/fdroid.d.ts +0 -5
  1385. package/dist/types/web/scrapers/firefox-addons.d.ts +0 -2
  1386. package/dist/types/web/scrapers/flathub.d.ts +0 -2
  1387. package/dist/types/web/scrapers/github-gist.d.ts +0 -5
  1388. package/dist/types/web/scrapers/github.d.ts +0 -12
  1389. package/dist/types/web/scrapers/gitlab.d.ts +0 -5
  1390. package/dist/types/web/scrapers/go-pkg.d.ts +0 -5
  1391. package/dist/types/web/scrapers/hackage.d.ts +0 -5
  1392. package/dist/types/web/scrapers/hackernews.d.ts +0 -2
  1393. package/dist/types/web/scrapers/hex.d.ts +0 -5
  1394. package/dist/types/web/scrapers/huggingface.d.ts +0 -2
  1395. package/dist/types/web/scrapers/iacr.d.ts +0 -5
  1396. package/dist/types/web/scrapers/index.d.ts +0 -84
  1397. package/dist/types/web/scrapers/jetbrains-marketplace.d.ts +0 -2
  1398. package/dist/types/web/scrapers/lemmy.d.ts +0 -2
  1399. package/dist/types/web/scrapers/lobsters.d.ts +0 -5
  1400. package/dist/types/web/scrapers/mastodon.d.ts +0 -5
  1401. package/dist/types/web/scrapers/maven.d.ts +0 -6
  1402. package/dist/types/web/scrapers/mdn.d.ts +0 -2
  1403. package/dist/types/web/scrapers/metacpan.d.ts +0 -5
  1404. package/dist/types/web/scrapers/musicbrainz.d.ts +0 -5
  1405. package/dist/types/web/scrapers/npm.d.ts +0 -5
  1406. package/dist/types/web/scrapers/nuget.d.ts +0 -5
  1407. package/dist/types/web/scrapers/nvd.d.ts +0 -5
  1408. package/dist/types/web/scrapers/ollama.d.ts +0 -2
  1409. package/dist/types/web/scrapers/open-vsx.d.ts +0 -5
  1410. package/dist/types/web/scrapers/opencorporates.d.ts +0 -5
  1411. package/dist/types/web/scrapers/openlibrary.d.ts +0 -5
  1412. package/dist/types/web/scrapers/orcid.d.ts +0 -5
  1413. package/dist/types/web/scrapers/osv.d.ts +0 -5
  1414. package/dist/types/web/scrapers/packagist.d.ts +0 -5
  1415. package/dist/types/web/scrapers/pub-dev.d.ts +0 -5
  1416. package/dist/types/web/scrapers/pubmed.d.ts +0 -5
  1417. package/dist/types/web/scrapers/pypi.d.ts +0 -5
  1418. package/dist/types/web/scrapers/rawg.d.ts +0 -2
  1419. package/dist/types/web/scrapers/readthedocs.d.ts +0 -2
  1420. package/dist/types/web/scrapers/reddit.d.ts +0 -5
  1421. package/dist/types/web/scrapers/repology.d.ts +0 -5
  1422. package/dist/types/web/scrapers/rfc.d.ts +0 -5
  1423. package/dist/types/web/scrapers/rubygems.d.ts +0 -5
  1424. package/dist/types/web/scrapers/searchcode.d.ts +0 -2
  1425. package/dist/types/web/scrapers/sec-edgar.d.ts +0 -5
  1426. package/dist/types/web/scrapers/semantic-scholar.d.ts +0 -2
  1427. package/dist/types/web/scrapers/snapcraft.d.ts +0 -2
  1428. package/dist/types/web/scrapers/sourcegraph.d.ts +0 -2
  1429. package/dist/types/web/scrapers/spdx.d.ts +0 -5
  1430. package/dist/types/web/scrapers/spotify.d.ts +0 -8
  1431. package/dist/types/web/scrapers/stackoverflow.d.ts +0 -6
  1432. package/dist/types/web/scrapers/terraform.d.ts +0 -5
  1433. package/dist/types/web/scrapers/tldr.d.ts +0 -7
  1434. package/dist/types/web/scrapers/twitter.d.ts +0 -5
  1435. package/dist/types/web/scrapers/types.d.ts +0 -83
  1436. package/dist/types/web/scrapers/utils.d.ts +0 -32
  1437. package/dist/types/web/scrapers/vimeo.d.ts +0 -5
  1438. package/dist/types/web/scrapers/vscode-marketplace.d.ts +0 -5
  1439. package/dist/types/web/scrapers/w3c.d.ts +0 -2
  1440. package/dist/types/web/scrapers/wikidata.d.ts +0 -5
  1441. package/dist/types/web/scrapers/wikipedia.d.ts +0 -5
  1442. package/dist/types/web/scrapers/youtube.d.ts +0 -13
  1443. package/dist/types/web/search/index.d.ts +0 -118
  1444. package/dist/types/web/search/provider.d.ts +0 -55
  1445. package/dist/types/web/search/providers/anthropic.d.ts +0 -32
  1446. package/dist/types/web/search/providers/base.d.ts +0 -90
  1447. package/dist/types/web/search/providers/brave.d.ts +0 -27
  1448. package/dist/types/web/search/providers/codex.d.ts +0 -35
  1449. package/dist/types/web/search/providers/duckduckgo.d.ts +0 -57
  1450. package/dist/types/web/search/providers/exa.d.ts +0 -52
  1451. package/dist/types/web/search/providers/gemini.d.ts +0 -57
  1452. package/dist/types/web/search/providers/insane.d.ts +0 -53
  1453. package/dist/types/web/search/providers/jina.d.ts +0 -26
  1454. package/dist/types/web/search/providers/kagi.d.ts +0 -24
  1455. package/dist/types/web/search/providers/kimi.d.ts +0 -27
  1456. package/dist/types/web/search/providers/openai-compatible.d.ts +0 -9
  1457. package/dist/types/web/search/providers/parallel.d.ts +0 -15
  1458. package/dist/types/web/search/providers/perplexity.d.ts +0 -38
  1459. package/dist/types/web/search/providers/searxng.d.ts +0 -44
  1460. package/dist/types/web/search/providers/synthetic.d.ts +0 -21
  1461. package/dist/types/web/search/providers/tavily.d.ts +0 -29
  1462. package/dist/types/web/search/providers/text-citations.d.ts +0 -23
  1463. package/dist/types/web/search/providers/utils.d.ts +0 -88
  1464. package/dist/types/web/search/providers/xai.d.ts +0 -64
  1465. package/dist/types/web/search/providers/zai.d.ts +0 -28
  1466. package/dist/types/web/search/render.d.ts +0 -36
  1467. package/dist/types/web/search/types.d.ts +0 -372
  1468. package/dist/types/web/search/utils.d.ts +0 -4
  1469. package/dist/types/workflow/workflow-intent-diff.d.ts +0 -25
  1470. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +0 -50
  1471. package/dist/types/workflow/workflow-intent-report-schema.d.ts +0 -2
  1472. package/dist/types/workflow/workflow-intent-report.d.ts +0 -48
  1473. package/dist/types/workspace-tree.d.ts +0 -42
  1474. package/src/sdk.ts +0 -2623
@@ -4,6 +4,7 @@ import * as fs from "node:fs";
4
4
  import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { type AgentMessage, canContinuePersistedHistory } from "@sayknow-cli/agent-core";
7
+ import type { ConfiguredModelChainEntry as SharedConfiguredModelChainEntry } from "@sayknow-cli/agent-core/compaction";
7
8
  import type {
8
9
  ImageContent,
9
10
  Message,
@@ -13,6 +14,7 @@ import type {
13
14
  TextContent,
14
15
  Usage,
15
16
  } from "@sayknow-cli/ai";
17
+ import * as native from "@sayknow-cli/natives";
16
18
  import { getTerminalId } from "@sayknow-cli/tui";
17
19
  import {
18
20
  getBlobsDir,
@@ -30,6 +32,7 @@ import {
30
32
  toError,
31
33
  } from "@sayknow-cli/utils";
32
34
  import type { TtsrInjectionRecord } from "../export/ttsr";
35
+ import type { ManagedLegacyLocalMigrationSource } from "../internal-urls/local-protocol";
33
36
  import { writeTextAtomic } from "../skc-runtime/state-writer";
34
37
 
35
38
  import * as git from "../utils/git";
@@ -51,6 +54,25 @@ import {
51
54
  resolveResidentImageDataUrlSync,
52
55
  resolveTextBlobSync,
53
56
  } from "./blob-store";
57
+ import {
58
+ canonicalizeTrustedPath,
59
+ deleteManagedSessionCandidate,
60
+ listManagedCandidates,
61
+ type ManagedMigrationPolicy,
62
+ type ManagedScope,
63
+ managedDirectoryAuthorityForScope,
64
+ managedRootForScope,
65
+ openManagedCandidateForWrite,
66
+ prepareManagedSessionScopeForWriteSync,
67
+ resolveManagedScope,
68
+ } from "./internal/managed-session-scope";
69
+ import {
70
+ type ManagedDirectoryRoot,
71
+ type ManagedFileSnapshot,
72
+ ManagedSessionDescendantStore,
73
+ retainManagedDirectoryAuthority,
74
+ } from "./internal/managed-session-storage";
75
+
54
76
  import {
55
77
  type BashExecutionMessage,
56
78
  type CustomMessage,
@@ -63,7 +85,9 @@ import {
63
85
  sanitizeRehydratedOpenAIResponsesAssistantMessage,
64
86
  stripInternalDetailsFields,
65
87
  } from "./messages";
88
+ import { type SessionManagerReadAccess, sessionManagerReadCapability } from "./session-manager-internal";
66
89
  import type {
90
+ ManagedSessionSecurityContext,
67
91
  SessionStorage,
68
92
  SessionStorageSnapshot,
69
93
  SessionStorageStat,
@@ -72,9 +96,15 @@ import type {
72
96
  VerifiedSessionDeleteResult,
73
97
  VerifiedSessionDeleteTarget,
74
98
  } from "./session-storage";
75
- import { FileSessionStorage, MemorySessionStorage } from "./session-storage";
99
+ import { createManagedSessionSecurityContext, FileSessionStorage, MemorySessionStorage } from "./session-storage";
100
+
101
+ export const CURRENT_SESSION_VERSION = 5;
102
+
103
+ /**
104
+ * Version 5 separates persisted MCP and discovered-built-in selection authority.
105
+ * Version 4 patch records remain readable; older writers must not edit v5 sessions.
106
+ */
76
107
 
77
- export const CURRENT_SESSION_VERSION = 3;
78
108
  function isUnderProjectSkc(cwd: string, targetPath: string): boolean {
79
109
  const relative = path.relative(path.join(path.resolve(cwd), ".skc"), path.resolve(targetPath));
80
110
  return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
@@ -186,6 +216,15 @@ export function getSessionMessageViewportAnchorId(message: AgentMessage): string
186
216
  return sessionMessageViewportAnchorIds.get(message);
187
217
  }
188
218
 
219
+ /** Returns registered viewport anchors for durable user messages in session order. */
220
+ export function getUserMessageViewportAnchorIds(messages: readonly AgentMessage[]): string[] {
221
+ return messages.flatMap(message => {
222
+ if (message.role !== "user" || message.synthetic) return [];
223
+ const anchorId = getSessionMessageViewportAnchorId(message);
224
+ return anchorId ? [anchorId] : [];
225
+ });
226
+ }
227
+
189
228
  export function transferSessionMessageIdentity(source: AgentMessage[], target: AgentMessage[]): void {
190
229
  if (source.length !== target.length) {
191
230
  throw new Error(
@@ -221,6 +260,14 @@ export interface ModelChangeEntry extends SessionEntryBase {
221
260
  thinkingLevel?: string | null;
222
261
  }
223
262
 
263
+ /** Persisted configured fallback chain for one model role. */
264
+ export type ConfiguredModelChainEntry = SharedConfiguredModelChainEntry;
265
+
266
+ export type ConfiguredModelChain = Pick<
267
+ ConfiguredModelChainEntry,
268
+ "role" | "entries" | "origin" | "identity" | "explicitHead" | "cleared"
269
+ >;
270
+
224
271
  export interface ServiceTierChangeEntry extends SessionEntryBase {
225
272
  type: "service_tier_change";
226
273
  serviceTier: ServiceTier | null;
@@ -289,6 +336,18 @@ export interface MCPToolSelectionEntry extends SessionEntryBase {
289
336
  type: "mcp_tool_selection";
290
337
  /** MCP tool names selected for visibility in discovery mode. */
291
338
  selectedToolNames: string[];
339
+ /** Legacy v4 combined built-in authority, retained for read compatibility. */
340
+ selectedDiscoveredBuiltinToolNames?: string[];
341
+ /** Correlates the ordered MCP and built-in entries emitted by one combined activation. */
342
+ mutationCorrelationId?: string;
343
+ }
344
+
345
+ /** Persisted discovered-built-in selection state, independent of MCP authority. */
346
+ export interface DiscoveredBuiltinToolSelectionEntry extends SessionEntryBase {
347
+ type: "discovered_builtin_tool_selection";
348
+ selectedToolNames: string[];
349
+ /** Correlates the ordered MCP and built-in entries emitted by one combined activation. */
350
+ mutationCorrelationId?: string;
292
351
  }
293
352
 
294
353
  /** Session init entry - captures initial context for subagent sessions (debugging/replay). */
@@ -352,12 +411,48 @@ export type SessionEntry =
352
411
  | LabelEntry
353
412
  | TtsrInjectionEntry
354
413
  | MCPToolSelectionEntry
414
+ | DiscoveredBuiltinToolSelectionEntry
355
415
  | SessionInitEntry
356
- | ModeChangeEntry;
416
+ | ModeChangeEntry
417
+ | ConfiguredModelChainEntry;
357
418
 
358
- /** Raw file entry (includes header) */
419
+ /** Append-only replacement for mutable fields on the session header. */
420
+ export interface HeaderPatchRecord {
421
+ type: "header_patch";
422
+ patch: Partial<Pick<SessionHeader, "title" | "titleSource" | "cwd">>;
423
+ }
424
+
425
+ /** Append-only replacement for replay metadata on one existing session entry. */
426
+ export interface EntryPatchRecord {
427
+ type: "entry_patch";
428
+ entryId: string;
429
+ patch: Partial<Pick<SessionMessageEntry, "message">>;
430
+ }
431
+
432
+ export type SessionPatchRecord = HeaderPatchRecord | EntryPatchRecord;
433
+
434
+ /** Resolved file entries; patch records are applied by parseSessionEntries(). */
359
435
  export type FileEntry = SessionHeader | SessionEntry;
360
436
 
437
+ export type DefaultModelSelectionStage = {
438
+ readonly entryRevision: number;
439
+ readonly headerExportRevision: number;
440
+ readonly sessionId: string;
441
+ readonly sessionFile: string | undefined;
442
+ readonly entries: readonly FileEntry[];
443
+ readonly tempPath: string | undefined;
444
+ readonly persistsToExistingFile: boolean;
445
+ };
446
+
447
+ export type DefaultModelSelectionPromotion =
448
+ | { readonly kind: "promoted" }
449
+ | { readonly kind: "not_promoted"; readonly error?: Error }
450
+ | { readonly kind: "unknown"; readonly error: Error };
451
+
452
+ type SessionFileReplacementSyncOutcome =
453
+ | { readonly kind: "replaced" }
454
+ | { readonly kind: "restored_previous"; readonly error: Error };
455
+
361
456
  /** Tree node for getTree() - defensive copy of session structure */
362
457
  export interface SessionTreeNode {
363
458
  entry: SessionEntry;
@@ -372,6 +467,9 @@ export interface SessionContext {
372
467
  serviceTier?: ServiceTier;
373
468
  /** Model roles: { default: "provider/modelId", small: "provider/modelId", ... } */
374
469
  models: Record<string, string>;
470
+ /** Configured fallback chains for model roles on the active branch. */
471
+ configuredModelChains: Record<string, ConfiguredModelChain>;
472
+
375
473
  /** Names of TTSR rules that have been injected this session */
376
474
  injectedTtsrRules: string[];
377
475
  /** Rich TTSR rule injection records for repeat resume. */
@@ -381,8 +479,12 @@ export interface SessionContext {
381
479
 
382
480
  /** MCP tool names selected through discovery for this session branch. */
383
481
  selectedMCPToolNames: string[];
482
+ /** Built-in discoverable tool names activated through discovery, when explicitly persisted. */
483
+ selectedDiscoveredBuiltinToolNames?: string[];
384
484
  /** Whether this branch contains an explicit persisted MCP selection entry. */
385
485
  hasPersistedMCPToolSelection: boolean;
486
+ /** Whether this branch contains an explicit persisted discovered-built-in selection entry. */
487
+ hasPersistedDiscoveredBuiltinToolSelection?: boolean;
386
488
  /** Active mode (e.g. "plan") or "none" if no special mode is active */
387
489
  mode: string;
388
490
  /** Mode-specific data from the last mode_change entry */
@@ -413,11 +515,32 @@ export interface ResumeTailTerminal {
413
515
 
414
516
  export interface ResumeTailError {
415
517
  kind: "error";
416
- reason: "missing" | "malformed" | "unstable" | "read-failed";
518
+ reason: "missing" | "malformed" | "unstable" | "read-failed" | "legacy_migration_disabled";
417
519
  }
418
520
 
419
- export type ResumeTailInspection = ResumeTailResumable | ResumeTailTerminal | ResumeTailError;
521
+ export type SessionDirectoryMigrationPolicy = ManagedMigrationPolicy;
522
+
523
+ export class SessionManagedStorageError extends Error {
524
+ readonly code = "managed_storage_unsupported";
525
+
526
+ constructor() {
527
+ super(
528
+ "Default managed session storage requires FileSessionStorage; provide an explicit session directory for custom storage.",
529
+ );
530
+ this.name = "SessionManagedStorageError";
531
+ }
532
+ }
420
533
 
534
+ export class SessionMigrationPolicyError extends Error {
535
+ readonly code = "legacy_migration_disabled";
536
+
537
+ constructor() {
538
+ super("Legacy session migration is disabled for this workspace.");
539
+ this.name = "SessionMigrationPolicyError";
540
+ }
541
+ }
542
+
543
+ export type ResumeTailInspection = ResumeTailResumable | ResumeTailTerminal | ResumeTailError;
421
544
  export interface StrictSessionOpenSuccess {
422
545
  kind: "opened";
423
546
  manager: SessionManager;
@@ -425,12 +548,47 @@ export interface StrictSessionOpenSuccess {
425
548
 
426
549
  export interface StrictSessionOpenFailure {
427
550
  kind: "error";
428
- reason: ResumeTailError["reason"] | "identity-mismatch";
551
+ reason: ResumeTailError["reason"] | "identity-mismatch" | "migration-required";
552
+ }
553
+
554
+ /** Exact transcript bytes and authority captured from one descriptor-bound read. */
555
+ export interface CapturedSessionTranscriptSnapshot {
556
+ sourcePath: string;
557
+ identity: ResumeSessionIdentity;
558
+ content: Uint8Array;
559
+ storage: SessionStorage;
429
560
  }
430
561
 
562
+ export type StrictSessionCaptureResult =
563
+ | { kind: "captured"; snapshot: CapturedSessionTranscriptSnapshot }
564
+ | ResumeTailError;
565
+
566
+ export type StrictSessionForkResult = { kind: "forked"; manager: SessionManager } | StrictSessionOpenFailure;
567
+
431
568
  /** Result of opening an inspected session without create-or-rewrite fallback. */
432
569
  export type StrictSessionOpenResult = StrictSessionOpenSuccess | StrictSessionOpenFailure;
433
570
 
571
+ /**
572
+ * Capability returned only by a strict recovery hydration open. It represents
573
+ * immutable transcript authority and is consumed by the promotion seam.
574
+ */
575
+ export interface RecoveryHydrationContext {
576
+ readonly identity: ResumeSessionIdentity;
577
+ }
578
+
579
+ export interface RecoveryHydrationOpenSuccess {
580
+ readonly kind: "hydrated";
581
+ readonly manager: SessionManager;
582
+ readonly context: RecoveryHydrationContext;
583
+ }
584
+
585
+ export type RecoveryHydrationOpenResult = RecoveryHydrationOpenSuccess | StrictSessionOpenFailure;
586
+
587
+ export interface RecoveryHydrationPromotionFence {
588
+ /** The caller has durably published ownership and acquired its writer lease. */
589
+ readonly ownershipReady: true;
590
+ }
591
+
434
592
  export interface SessionInfo {
435
593
  path: string;
436
594
  id: string;
@@ -442,6 +600,8 @@ export interface SessionInfo {
442
600
  created: Date;
443
601
  modified: Date;
444
602
  messageCount: number;
603
+ /** True when messageCount was counted from only the bounded list prefix. */
604
+ messageCountIsEstimate?: boolean;
445
605
  /** File size in bytes on disk; used for compact list rendering. */
446
606
  size: number;
447
607
  firstMessage: string;
@@ -498,30 +658,100 @@ export type SessionManagerCloseOutcome =
498
658
  | { kind: "close_failed_retryable"; error: Error }
499
659
  | { kind: "close_unknown"; error: Error };
500
660
 
501
- export type ReadonlySessionManager = Pick<
502
- SessionManager,
503
- | "getCwd"
504
- | "getSessionDir"
505
- | "getSessionId"
506
- | "getSessionFile"
507
- | "getSessionName"
508
- | "getArtifactsDir"
509
- | "getArtifactManager"
510
- | "allocateArtifactPath"
511
- | "saveArtifact"
512
- | "getArtifactPath"
513
- | "getLeafId"
514
- | "getLeafEntry"
515
- | "getEntry"
516
- | "getLabel"
517
- | "getBranch"
518
- | "getHeader"
519
- | "getEntries"
520
- | "getTree"
521
- | "getUsageStatistics"
522
- | "putBlob"
661
+ /** Read-only session state made available to extensions and custom tools. */
662
+ /** Frozen read-only session facade made available to extensions and custom tools. */
663
+ export interface ReadonlySessionManager {
664
+ getCwd(): string;
665
+ getSessionDir(): string;
666
+ getSessionId(): string;
667
+ getSessionFile(): string | undefined;
668
+ getSessionName(): string | undefined;
669
+ getArtifactsDir(): string | null;
670
+ getArtifactPath(id: string): Promise<string | null>;
671
+ getLeafId(): string | null;
672
+ getLeafEntry(): SessionEntry | undefined;
673
+ getEntry(id: string): SessionEntry | undefined;
674
+ getLabel(id: string): string | undefined;
675
+ getBranch(fromId?: string): SessionEntry[];
676
+ getHeader(): SessionHeader | null;
677
+ getEntries(): SessionEntry[];
678
+ getTree(): SessionTreeNode[];
679
+ getUsageStatistics(): UsageStatistics;
680
+ }
681
+
682
+ function cloneAndFreezeSnapshot<T>(value: T): T {
683
+ const cloned = cloneJsonSemantic(value);
684
+ const freeze = (candidate: unknown, seen = new WeakSet<object>()): void => {
685
+ if (candidate === null || typeof candidate !== "object" || seen.has(candidate)) return;
686
+ seen.add(candidate);
687
+ for (const child of Object.values(candidate)) freeze(child, seen);
688
+ Object.freeze(candidate);
689
+ };
690
+ freeze(cloned);
691
+ return cloned;
692
+ }
693
+
694
+ /** Creates an immutable facade that never exposes SessionManager mutation authority. */
695
+ export function createReadonlySessionManager(manager: SessionManager): ReadonlySessionManager {
696
+ const facade = Object.freeze({
697
+ getCwd: () => manager.getCwd(),
698
+ getSessionDir: () => manager.getSessionDir(),
699
+ getSessionId: () => manager.getSessionId(),
700
+ getSessionFile: () => manager.getSessionFile(),
701
+ getSessionName: () => manager.getSessionName(),
702
+ getArtifactsDir: () => manager.getArtifactsDir(),
703
+ getArtifactPath: (id: string) => manager.getArtifactPath(id),
704
+ getLeafId: () => manager.getLeafId(),
705
+ getLeafEntry: () => {
706
+ const entry = manager.getLeafEntry();
707
+ return entry === undefined ? undefined : cloneAndFreezeSnapshot(entry);
708
+ },
709
+ getEntry: (id: string) => {
710
+ const entry = manager.getEntry(id);
711
+ return entry === undefined ? undefined : cloneAndFreezeSnapshot(entry);
712
+ },
713
+ getLabel: (id: string) => manager.getLabel(id),
714
+ getBranch: (fromId?: string) => cloneAndFreezeSnapshot(manager.getBranch(fromId)),
715
+ getHeader: () => {
716
+ const header = manager.getHeader();
717
+ return header === null ? null : cloneAndFreezeSnapshot(header);
718
+ },
719
+ getEntries: () => cloneAndFreezeSnapshot(manager.getEntries()),
720
+ getTree: () => cloneAndFreezeSnapshot(manager.getTree()),
721
+ getUsageStatistics: () => cloneAndFreezeSnapshot(manager.getUsageStatistics()),
722
+ });
723
+ readonlySessionArtifactCapabilities.set(facade, sessionArtifactCapability(manager)!);
724
+ return facade;
725
+ }
726
+
727
+ /** Internal artifact-writing capability. Read-only facades expose it only through a private weak-map lookup. */
728
+ export type SessionArtifactCapability = Readonly<
729
+ Pick<SessionManager, "allocateArtifactPath" | "saveArtifact" | "putBlob">
523
730
  >;
524
731
 
732
+ const sessionArtifactCapabilities = new WeakMap<SessionManager, SessionArtifactCapability>();
733
+ const readonlySessionArtifactCapabilities = new WeakMap<ReadonlySessionManager, SessionArtifactCapability>();
734
+
735
+ /**
736
+ * Returns the artifact capability for a concrete persistence owner or one of its
737
+ * immutable read-only facades. Structural lookalikes remain unauthorized.
738
+ */
739
+ export function sessionArtifactCapability(value: unknown): SessionArtifactCapability | undefined {
740
+ if (value instanceof SessionManager) {
741
+ let capability = sessionArtifactCapabilities.get(value);
742
+ if (!capability) {
743
+ capability = Object.freeze({
744
+ allocateArtifactPath: value.allocateArtifactPath.bind(value),
745
+ saveArtifact: value.saveArtifact.bind(value),
746
+ putBlob: value.putBlob.bind(value),
747
+ });
748
+ sessionArtifactCapabilities.set(value, capability);
749
+ }
750
+ return capability;
751
+ }
752
+ if (value === null || typeof value !== "object") return undefined;
753
+ return readonlySessionArtifactCapabilities.get(value as ReadonlySessionManager);
754
+ }
525
755
  function createSessionId(): string {
526
756
  return Bun.randomUUIDv7();
527
757
  }
@@ -597,18 +827,17 @@ function migrateV2ToV3(entries: FileEntry[]): void {
597
827
  }
598
828
 
599
829
  /**
600
- * Run all necessary migrations to bring entries to current version.
601
- * Mutates entries in place. Returns true if any migration was applied.
830
+ * Run all necessary migrations to bring entries to the current semantic version.
831
+ * Version 4 adds trailing patch records, so v3 transcripts must be atomically upgraded before they can receive one.
602
832
  */
603
833
  function migrateToCurrentVersion(entries: FileEntry[]): boolean {
604
834
  const header = entries.find(e => e.type === "session") as SessionHeader | undefined;
605
835
  const version = header?.version ?? 1;
606
-
607
836
  if (version >= CURRENT_SESSION_VERSION) return false;
608
-
609
837
  if (version < 2) migrateV1ToV2(entries);
610
838
  if (version < 3) migrateV2ToV3(entries);
611
-
839
+ const migratedHeader = entries.find(entry => entry.type === "session") as SessionHeader | undefined;
840
+ if (migratedHeader) migratedHeader.version = CURRENT_SESSION_VERSION;
612
841
  return true;
613
842
  }
614
843
 
@@ -617,131 +846,136 @@ export function migrateSessionEntries(entries: FileEntry[]): void {
617
846
  migrateToCurrentVersion(entries);
618
847
  }
619
848
 
620
- const migratedSessionRoots = new Set<string>();
849
+ function resolveManagedSessionRoot(sessionDir: string, cwd: string): string | undefined {
850
+ const sessionsRoot = path.dirname(sessionDir);
851
+ const resolved = resolveManagedScope({ cwd, agentDir: path.resolve(sessionsRoot, ".."), sessionsRoot });
852
+ if (resolved.kind === "error") return undefined;
853
+ return path.resolve(sessionDir) === path.resolve(resolved.scope.directoryPath) ? sessionsRoot : undefined;
854
+ }
621
855
 
622
856
  /**
623
- * Merge or rename a legacy session directory into its canonical target.
624
- * Best effort: callers decide whether migration failures should surface.
857
+ * Resolve `target` through the real path of its nearest existing ancestor,
858
+ * re-appending any not-yet-created trailing segments. Symlink/reparse components
859
+ * in the existing prefix are canonicalized (so aliases cannot escape a containment
860
+ * check or alias another file), while a missing leaf (e.g. an uncommitted session
861
+ * transcript) is still resolved deterministically. Non-ENOENT errors (ELOOP,
862
+ * EACCES, ...) surface rather than silently falling back to the lexical path.
625
863
  */
626
- function migrateSessionDirPath(oldPath: string, newPath: string): void {
627
- // Session-dir lifecycle migration: moves/removes whole directories, not file content writes.
628
- const existing = fs.statSync(newPath, { throwIfNoEntry: false });
629
- if (existing?.isDirectory()) {
630
- for (const file of fs.readdirSync(oldPath)) {
631
- const src = path.join(oldPath, file);
632
- const dst = path.join(newPath, file);
633
- if (!fs.existsSync(dst)) {
634
- fs.renameSync(src, dst);
635
- }
864
+ function canonicalizeThroughExistingAncestor(target: string): string {
865
+ const resolved = path.resolve(target);
866
+ const tail: string[] = [];
867
+ let current = resolved;
868
+ while (true) {
869
+ try {
870
+ const real = fs.realpathSync(current);
871
+ return tail.length === 0 ? real : path.join(real, ...tail.reverse());
872
+ } catch (err) {
873
+ if (!isEnoent(err)) throw err;
874
+ const parent = path.dirname(current);
875
+ if (parent === current) return resolved;
876
+ tail.push(path.basename(current));
877
+ current = parent;
636
878
  }
637
- fs.rmSync(oldPath, { recursive: true, force: true });
638
- return;
639
- }
640
- if (existing) {
641
- fs.rmSync(newPath, { recursive: true, force: true });
642
879
  }
643
- fs.renameSync(oldPath, newPath);
644
- }
645
-
646
- function encodeLegacyAbsoluteSessionDirName(cwd: string): string {
647
- const resolvedCwd = path.resolve(cwd);
648
- return `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
649
- }
650
-
651
- function encodeRelativeSessionDirName(prefix: string, root: string, cwd: string): string {
652
- const relative = path.relative(root, cwd).replace(/[/\\:]/g, "-");
653
- return relative ? (prefix.endsWith("-") ? `${prefix}${relative}` : `${prefix}-${relative}`) : prefix;
654
880
  }
655
881
 
656
- function getDefaultSessionDirName(cwd: string): { encodedDirName: string; resolvedCwd: string } {
657
- const resolvedCwd = path.resolve(cwd);
658
- const canonicalCwd = resolveEquivalentPath(resolvedCwd);
659
- const home = resolveEquivalentPath(os.homedir());
660
- const tempRoot = resolveEquivalentPath(os.tmpdir());
661
- const encodedDirName = pathIsWithin(home, canonicalCwd)
662
- ? encodeRelativeSessionDirName("-", home, canonicalCwd)
663
- : pathIsWithin(tempRoot, canonicalCwd)
664
- ? encodeRelativeSessionDirName("-tmp", tempRoot, canonicalCwd)
665
- : encodeLegacyAbsoluteSessionDirName(canonicalCwd);
666
- return { encodedDirName, resolvedCwd };
667
- }
668
-
669
- /** Default and legacy directory candidates for maintenance-free resume discovery. */
670
- function getReadOnlyDefaultSessionDirs(cwd: string, sessionsRoot: string = getSessionsDir()): string[] {
671
- const { encodedDirName, resolvedCwd } = getDefaultSessionDirName(cwd);
672
- const canonicalCwd = resolveEquivalentPath(resolvedCwd);
673
- return [
674
- path.join(sessionsRoot, encodedDirName),
675
- path.join(sessionsRoot, encodeLegacyAbsoluteSessionDirName(canonicalCwd)),
676
- path.join(sessionsRoot, encodeLegacyAbsoluteSessionDirName(resolvedCwd)),
677
- ].filter((dir, index, dirs) => dirs.indexOf(dir) === index);
882
+ function isSupportedSessionVersion(version: unknown): boolean {
883
+ return (
884
+ version === undefined ||
885
+ (typeof version === "number" && Number.isInteger(version) && version <= CURRENT_SESSION_VERSION)
886
+ );
678
887
  }
679
888
 
680
- /**
681
- * Migrate old `--<home-encoded>-*--` session dirs to the new `-*` format.
682
- * Runs once per sessions root on first access, best-effort.
683
- */
684
- function migrateHomeSessionDirs(sessionsRoot: string): void {
685
- if (migratedSessionRoots.has(sessionsRoot)) return;
686
- migratedSessionRoots.add(sessionsRoot);
687
-
688
- const home = os.homedir();
689
- const homeEncoded = home.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-");
690
- const oldPrefix = `--${homeEncoded}-`;
691
- const oldExact = `--${homeEncoded}--`;
692
-
693
- let entries: string[];
694
- try {
695
- entries = fs.readdirSync(sessionsRoot);
696
- } catch {
697
- return;
698
- }
699
-
700
- for (const entry of entries) {
701
- let remainder: string;
702
- if (entry === oldExact) {
703
- remainder = "";
704
- } else if (entry.startsWith(oldPrefix) && entry.endsWith("--")) {
705
- remainder = entry.slice(oldPrefix.length, -2);
706
- } else {
889
+ /** Exported for compaction.test.ts */
890
+ export function parseSessionEntries(content: string): FileEntry[] {
891
+ const records = parseJsonlLenient<FileEntry | SessionPatchRecord>(content);
892
+ const rawHeader = records.find((record): record is SessionHeader => record.type === "session");
893
+ if (!isSupportedSessionVersion(rawHeader?.version)) {
894
+ throw new Error(`Unsupported session version: ${String(rawHeader?.version)}`);
895
+ }
896
+ const entries: FileEntry[] = [];
897
+ const entriesById = new Map<string, SessionEntry>();
898
+ let header: SessionHeader | undefined;
899
+
900
+ for (const record of records) {
901
+ // Patch records are valid in v4 and v5 only. Ignore patches before a valid
902
+ // header so malformed prefixes cannot affect later records.
903
+ if (record.type === "header_patch") {
904
+ if (header?.version !== undefined && header.version >= 4 && isHeaderPatchRecord(record))
905
+ applyHeaderPatch(header, record.patch);
707
906
  continue;
708
907
  }
709
-
710
- const newName = remainder ? `-${remainder}` : "-";
711
- const oldPath = path.join(sessionsRoot, entry);
712
- const newPath = path.join(sessionsRoot, newName);
713
-
714
- try {
715
- migrateSessionDirPath(oldPath, newPath);
716
- } catch {
717
- // Best effort
908
+ if (record.type === "entry_patch") {
909
+ if (header?.version !== undefined && header.version >= 4 && isEntryPatchRecord(record)) {
910
+ const entry = entriesById.get(record.entryId);
911
+ if (entry?.type === "message" && record.patch.message) entry.message = record.patch.message;
912
+ }
913
+ continue;
718
914
  }
915
+ entries.push(record);
916
+ if (record.type === "session") header ??= record;
917
+ else entriesById.set(record.id, record);
719
918
  }
919
+ return entries;
720
920
  }
721
921
 
722
- function migrateLegacyAbsoluteSessionDir(cwd: string, sessionDir: string, sessionsRoot: string): void {
723
- const legacyDir = path.join(sessionsRoot, encodeLegacyAbsoluteSessionDirName(cwd));
724
- if (legacyDir === sessionDir || !fs.existsSync(legacyDir)) return;
922
+ function isHeaderPatchRecord(record: SessionPatchRecord): record is HeaderPatchRecord {
923
+ if (
924
+ record.type !== "header_patch" ||
925
+ !isRecord(record.patch) ||
926
+ !Object.keys(record).every(key => key === "type" || key === "patch")
927
+ )
928
+ return false;
929
+ const keys = Object.keys(record.patch);
930
+ if (!keys.every(key => key === "cwd" || key === "title" || key === "titleSource")) return false;
931
+ const { cwd, title, titleSource } = record.patch;
932
+ return (
933
+ (cwd === undefined || typeof cwd === "string") &&
934
+ (title === undefined || typeof title === "string") &&
935
+ (titleSource === undefined || titleSource === "auto" || titleSource === "user")
936
+ );
937
+ }
725
938
 
726
- try {
727
- migrateSessionDirPath(legacyDir, sessionDir);
728
- } catch {
729
- // Best effort
730
- }
939
+ function applyHeaderPatch(header: SessionHeader, patch: HeaderPatchRecord["patch"]): void {
940
+ if (patch.cwd !== undefined) header.cwd = patch.cwd;
941
+ if (patch.title !== undefined) header.title = patch.title;
942
+ if (patch.titleSource !== undefined) header.titleSource = patch.titleSource;
731
943
  }
732
944
 
733
- function resolveManagedSessionRoot(sessionDir: string, cwd: string): string | undefined {
734
- const currentDirName = path.basename(sessionDir);
735
- const { encodedDirName } = getDefaultSessionDirName(cwd);
736
- if (currentDirName !== encodedDirName && currentDirName !== encodeLegacyAbsoluteSessionDirName(cwd)) {
737
- return undefined;
738
- }
739
- return path.dirname(sessionDir);
945
+ function isEntryPatchRecord(record: SessionPatchRecord): record is EntryPatchRecord {
946
+ return (
947
+ record.type === "entry_patch" &&
948
+ typeof record.entryId === "string" &&
949
+ isRecord(record.patch) &&
950
+ Object.keys(record).every(key => key === "type" || key === "entryId" || key === "patch") &&
951
+ Object.keys(record.patch).every(key => key === "message") &&
952
+ (record.patch.message === undefined || isRecord(record.patch.message))
953
+ );
740
954
  }
741
955
 
742
- /** Exported for compaction.test.ts */
743
- export function parseSessionEntries(content: string): FileEntry[] {
744
- return parseJsonlLenient<FileEntry>(content);
956
+ function normalizeConfiguredModelChainEntry(entry: unknown): ConfiguredModelChain | undefined {
957
+ if (!entry || typeof entry !== "object") return undefined;
958
+ const candidate = entry as Partial<ConfiguredModelChainEntry>;
959
+ if (typeof candidate.role !== "string" || candidate.role.length === 0) return undefined;
960
+ if (candidate.cleared === true) {
961
+ return {
962
+ role: candidate.role,
963
+ entries: [],
964
+ origin: typeof candidate.origin === "string" ? candidate.origin : "session",
965
+ explicitHead: candidate.explicitHead === true,
966
+ cleared: true,
967
+ };
968
+ }
969
+ if (!Array.isArray(candidate.entries)) return undefined;
970
+ const entries = candidate.entries.filter((model): model is string => typeof model === "string" && model.length > 0);
971
+ if (entries.length === 0) return undefined;
972
+ return {
973
+ role: candidate.role,
974
+ entries,
975
+ origin: typeof candidate.origin === "string" ? candidate.origin : "session",
976
+ ...(typeof candidate.identity === "string" ? { identity: candidate.identity } : {}),
977
+ explicitHead: candidate.explicitHead === true,
978
+ };
745
979
  }
746
980
 
747
981
  export function getLatestCompactionEntry(entries: SessionEntry[]): CompactionEntry | null {
@@ -781,12 +1015,15 @@ export function buildSessionContext(
781
1015
  thinkingLevel: "off",
782
1016
  serviceTier: undefined,
783
1017
  models: {},
1018
+ configuredModelChains: {},
1019
+
784
1020
  injectedTtsrRules: [],
785
1021
  injectedTtsrRuleRecords: [],
786
1022
  ttsrMessageCount: 0,
787
1023
 
788
1024
  selectedMCPToolNames: [],
789
1025
  hasPersistedMCPToolSelection: false,
1026
+ hasPersistedDiscoveredBuiltinToolSelection: false,
790
1027
  mode: "none",
791
1028
  };
792
1029
  }
@@ -804,11 +1041,14 @@ export function buildSessionContext(
804
1041
  thinkingLevel: "off",
805
1042
  serviceTier: undefined,
806
1043
  models: {},
1044
+ configuredModelChains: {},
1045
+
807
1046
  injectedTtsrRules: [],
808
1047
  injectedTtsrRuleRecords: [],
809
1048
  ttsrMessageCount: 0,
810
1049
  selectedMCPToolNames: [],
811
1050
  hasPersistedMCPToolSelection: false,
1051
+ hasPersistedDiscoveredBuiltinToolSelection: false,
812
1052
  mode: "none",
813
1053
  };
814
1054
  }
@@ -829,6 +1069,8 @@ export function buildSessionContext(
829
1069
  let thinkingLevel: string | undefined = "off";
830
1070
  let serviceTier: ServiceTier | undefined;
831
1071
  const models: Record<string, string> = {};
1072
+ const configuredModelChains: Record<string, ConfiguredModelChain> = {};
1073
+
832
1074
  let compaction: CompactionEntry | null = null;
833
1075
  const injectedTtsrRulesSet = new Set<string>();
834
1076
  const injectedTtsrRuleRecords = new Map<string, TtsrInjectionRecord>();
@@ -836,6 +1078,8 @@ export function buildSessionContext(
836
1078
 
837
1079
  let selectedMCPToolNames: string[] = [];
838
1080
  let hasPersistedMCPToolSelection = false;
1081
+ let selectedDiscoveredBuiltinToolNames: string[] | undefined;
1082
+ let hasPersistedDiscoveredBuiltinToolSelection = false;
839
1083
  let mode = "none";
840
1084
  let modeData: Record<string, unknown> | undefined;
841
1085
  // Track whether an explicit `model_change` with role="default" has been
@@ -859,6 +1103,13 @@ export function buildSessionContext(
859
1103
  hasExplicitDefaultModel = true;
860
1104
  }
861
1105
  }
1106
+ } else if (entry.type === "configured_model_chain") {
1107
+ const configuredChain = normalizeConfiguredModelChainEntry(entry);
1108
+ if (configuredChain?.cleared) {
1109
+ delete configuredModelChains[configuredChain.role];
1110
+ } else if (configuredChain) {
1111
+ configuredModelChains[configuredChain.role] = configuredChain;
1112
+ }
862
1113
  } else if (entry.type === "service_tier_change") {
863
1114
  serviceTier = entry.serviceTier ?? undefined;
864
1115
  } else if (entry.type === "message" && entry.message.role === "assistant") {
@@ -889,7 +1140,14 @@ export function buildSessionContext(
889
1140
  }
890
1141
  } else if (entry.type === "mcp_tool_selection") {
891
1142
  selectedMCPToolNames = [...entry.selectedToolNames];
1143
+ if (entry.selectedDiscoveredBuiltinToolNames !== undefined) {
1144
+ selectedDiscoveredBuiltinToolNames = [...entry.selectedDiscoveredBuiltinToolNames];
1145
+ hasPersistedDiscoveredBuiltinToolSelection = true;
1146
+ }
892
1147
  hasPersistedMCPToolSelection = true;
1148
+ } else if (entry.type === "discovered_builtin_tool_selection") {
1149
+ selectedDiscoveredBuiltinToolNames = [...entry.selectedToolNames];
1150
+ hasPersistedDiscoveredBuiltinToolSelection = true;
893
1151
  } else if (entry.type === "mode_change") {
894
1152
  mode = entry.mode;
895
1153
  modeData = entry.data;
@@ -897,6 +1155,16 @@ export function buildSessionContext(
897
1155
  }
898
1156
 
899
1157
  const injectedTtsrRules = Array.from(injectedTtsrRulesSet);
1158
+ for (const [role, model] of Object.entries(models)) {
1159
+ if (role in configuredModelChains) continue;
1160
+ configuredModelChains[role] = {
1161
+ role,
1162
+ entries: [model],
1163
+ origin: "legacy_session",
1164
+ explicitHead: true,
1165
+ };
1166
+ }
1167
+
900
1168
  const injectedTtsrRuleRecordsArray = Array.from(injectedTtsrRuleRecords.values());
901
1169
 
902
1170
  // Build messages and collect corresponding entries
@@ -996,12 +1264,16 @@ export function buildSessionContext(
996
1264
  thinkingLevel,
997
1265
  serviceTier,
998
1266
  models,
1267
+ configuredModelChains,
1268
+
999
1269
  injectedTtsrRules,
1000
1270
  injectedTtsrRuleRecords: injectedTtsrRuleRecordsArray,
1001
1271
  ttsrMessageCount,
1002
1272
 
1003
1273
  selectedMCPToolNames,
1274
+ selectedDiscoveredBuiltinToolNames,
1004
1275
  hasPersistedMCPToolSelection,
1276
+ hasPersistedDiscoveredBuiltinToolSelection,
1005
1277
  mode,
1006
1278
  modeData,
1007
1279
  };
@@ -1014,31 +1286,183 @@ function cloneSessionContext(context: SessionContext): SessionContext {
1014
1286
  ...context,
1015
1287
  messages,
1016
1288
  models: { ...context.models },
1289
+ configuredModelChains: Object.fromEntries(
1290
+ Object.entries(context.configuredModelChains ?? {}).map(([role, chain]) => [
1291
+ role,
1292
+ { ...chain, entries: [...chain.entries] },
1293
+ ]),
1294
+ ),
1295
+
1017
1296
  injectedTtsrRules: [...context.injectedTtsrRules],
1018
1297
  injectedTtsrRuleRecords: context.injectedTtsrRuleRecords?.map(record => ({ ...record })),
1019
1298
  ttsrMessageCount: context.ttsrMessageCount,
1020
1299
 
1021
1300
  selectedMCPToolNames: [...context.selectedMCPToolNames],
1301
+ selectedDiscoveredBuiltinToolNames: context.selectedDiscoveredBuiltinToolNames
1302
+ ? [...context.selectedDiscoveredBuiltinToolNames]
1303
+ : undefined,
1022
1304
  modeData: cloneJsonSemantic(context.modeData),
1023
1305
  };
1024
1306
  }
1025
1307
 
1026
- /**
1027
- * Compute the default session directory for a cwd.
1028
- * Classifies cwd by canonical location so symlink/alias paths resolve to the
1029
- * same home-relative or temp-root directory names as their real targets.
1030
- */
1308
+ function freezeInternalReadSnapshot<T>(value: T): T {
1309
+ if (process.env.NODE_ENV !== "test" && process.env.NODE_ENV !== "development") return value;
1310
+ const copy = cloneJsonSemantic(value);
1311
+ const freeze = (item: unknown): void => {
1312
+ if (!item || typeof item !== "object" || Object.isFrozen(item)) return;
1313
+ for (const child of Object.values(item as Record<string, unknown>)) freeze(child);
1314
+ Object.freeze(item);
1315
+ };
1316
+ freeze(copy);
1317
+ return copy;
1318
+ }
1319
+
1320
+ /** Resolve and prepare the default v2 session scope before any managed writer exists. */
1031
1321
  function computeDefaultSessionDir(
1032
1322
  cwd: string,
1033
1323
  storage: SessionStorage,
1034
1324
  sessionsRoot: string = getSessionsDir(),
1035
1325
  ): string {
1036
- const { encodedDirName, resolvedCwd } = getDefaultSessionDirName(cwd);
1037
- migrateHomeSessionDirs(sessionsRoot);
1038
- const sessionDir = path.join(sessionsRoot, encodedDirName);
1039
- migrateLegacyAbsoluteSessionDir(resolvedCwd, sessionDir, sessionsRoot);
1040
- storage.ensureDirSync(sessionDir);
1041
- return sessionDir;
1326
+ if (!(storage instanceof FileSessionStorage)) throw new SessionManagedStorageError();
1327
+ const resolved = resolveManagedScope({ cwd, agentDir: path.resolve(sessionsRoot, ".."), sessionsRoot });
1328
+ if (resolved.kind === "error") throw new Error(`Could not resolve managed session scope: ${resolved.message}`);
1329
+ const prepared = prepareManagedSessionScopeForWriteSync(resolved.scope);
1330
+ if (prepared.kind === "error") throw new Error(`Could not prepare managed session scope: ${prepared.message}`);
1331
+ return prepared.scope.directoryPath;
1332
+ }
1333
+
1334
+ /** A session directory's authority is distinct from its string path. */
1335
+ export type SessionDestination =
1336
+ | { readonly kind: "managed"; readonly directory: string; readonly securityContext: ManagedSessionSecurityContext }
1337
+ | { readonly kind: "explicit"; readonly directory: string };
1338
+ export type SessionDestinationInput = string | SessionDestination | undefined;
1339
+
1340
+ interface ForkArtifactPublication {
1341
+ readonly snapshot: native.NativeDirectoryTreeSnapshot;
1342
+ readonly store: ManagedSessionDescendantStore;
1343
+ readonly cleanupStore: ManagedSessionDescendantStore;
1344
+ readonly cleanupRelativePath: string;
1345
+ }
1346
+
1347
+ function retainedTreeSnapshotEquals(
1348
+ left: native.NativeDirectoryTreeSnapshot,
1349
+ right: native.NativeDirectoryTreeSnapshot,
1350
+ ): boolean {
1351
+ return (
1352
+ left.rootDev === right.rootDev &&
1353
+ left.rootIno === right.rootIno &&
1354
+ left.entries.length === right.entries.length &&
1355
+ left.entries.every((entry, index) => {
1356
+ const other = right.entries[index];
1357
+ return (
1358
+ other !== undefined &&
1359
+ entry.relativePath === other.relativePath &&
1360
+ entry.kind === other.kind &&
1361
+ entry.dev === other.dev &&
1362
+ entry.ino === other.ino &&
1363
+ entry.size === other.size &&
1364
+ entry.mtimeNs === other.mtimeNs &&
1365
+ entry.ctimeNs === other.ctimeNs &&
1366
+ entry.sha256 === other.sha256
1367
+ );
1368
+ })
1369
+ );
1370
+ }
1371
+
1372
+ function retainedTreeSnapshotEqualsAfterRename(
1373
+ left: native.NativeDirectoryTreeSnapshot,
1374
+ right: native.NativeDirectoryTreeSnapshot,
1375
+ ): boolean {
1376
+ return (
1377
+ left.rootDev === right.rootDev &&
1378
+ left.rootIno === right.rootIno &&
1379
+ left.entries.length === right.entries.length &&
1380
+ left.entries.every((entry, index) => {
1381
+ const other = right.entries[index];
1382
+ return (
1383
+ other !== undefined &&
1384
+ entry.relativePath === other.relativePath &&
1385
+ entry.kind === other.kind &&
1386
+ entry.dev === other.dev &&
1387
+ entry.ino === other.ino &&
1388
+ entry.size === other.size &&
1389
+ entry.mtimeNs === other.mtimeNs &&
1390
+ (entry.relativePath === "" || entry.ctimeNs === other.ctimeNs) &&
1391
+ entry.sha256 === other.sha256
1392
+ );
1393
+ })
1394
+ );
1395
+ }
1396
+
1397
+ function managedFileSnapshotEquals(left: ManagedFileSnapshot | null, right: ManagedFileSnapshot | null): boolean {
1398
+ return (
1399
+ left !== null &&
1400
+ right !== null &&
1401
+ left.identity.dev === right.identity.dev &&
1402
+ left.identity.ino === right.identity.ino &&
1403
+ left.identity.size === right.identity.size &&
1404
+ left.identity.mtimeNs === right.identity.mtimeNs &&
1405
+ left.identity.ctimeNs === right.identity.ctimeNs &&
1406
+ left.identity.sha256 === right.identity.sha256 &&
1407
+ left.bytes.equals(right.bytes)
1408
+ );
1409
+ }
1410
+ const trustedSessionDestinations = new WeakSet<SessionDestination>();
1411
+
1412
+ function managedStoreFromContext(
1413
+ securityContext: ManagedSessionSecurityContext,
1414
+ directory: string,
1415
+ ): ManagedSessionDescendantStore {
1416
+ return new ManagedSessionDescendantStore(
1417
+ securityContext.rootAuthority,
1418
+ directory,
1419
+ securityContext.retainedAuthority
1420
+ ? { authority: securityContext.retainedAuthority, authorityBaseDir: directory }
1421
+ : undefined,
1422
+ );
1423
+ }
1424
+
1425
+ function freezeManagedDestination(scope: ManagedScope): SessionDestination {
1426
+ const rootAuthority = managedRootForScope(scope);
1427
+ const securityContext = createManagedSessionSecurityContext({
1428
+ agentDir: scope.agentDir,
1429
+ sessionsRoot: scope.sessionsRoot,
1430
+ sessionDir: scope.directoryPath,
1431
+ rootAuthority,
1432
+ retainedAuthority: managedDirectoryAuthorityForScope(scope),
1433
+ });
1434
+ const destination = Object.freeze({ kind: "managed" as const, directory: scope.directoryPath, securityContext });
1435
+ trustedSessionDestinations.add(destination);
1436
+ return destination;
1437
+ }
1438
+
1439
+ function explicitDestination(directory: string): SessionDestination {
1440
+ const destination = Object.freeze({ kind: "explicit" as const, directory });
1441
+ trustedSessionDestinations.add(destination);
1442
+ return destination;
1443
+ }
1444
+
1445
+ function managedDestination(cwd: string, storage: SessionStorage, agentDir?: string): SessionDestination {
1446
+ if (!(storage instanceof FileSessionStorage)) throw new SessionManagedStorageError();
1447
+ const sessionsRoot = getSessionsDir(agentDir);
1448
+ const resolved = resolveManagedScope({ cwd, agentDir: agentDir ?? path.resolve(sessionsRoot, ".."), sessionsRoot });
1449
+ if (resolved.kind === "error") throw new Error(`Could not resolve managed session scope: ${resolved.message}`);
1450
+ const prepared = prepareManagedSessionScopeForWriteSync(resolved.scope);
1451
+ if (prepared.kind === "error") throw new Error(`Could not prepare managed session scope: ${prepared.message}`);
1452
+ return freezeManagedDestination(prepared.scope);
1453
+ }
1454
+
1455
+ function destinationFor(
1456
+ cwd: string,
1457
+ input: SessionDestinationInput,
1458
+ storage: SessionStorage,
1459
+ agentDir?: string,
1460
+ ): SessionDestination {
1461
+ if (typeof input === "object") {
1462
+ if (!trustedSessionDestinations.has(input)) throw new Error("Untrusted session destination authority");
1463
+ return input;
1464
+ }
1465
+ return input === undefined ? managedDestination(cwd, storage, agentDir) : explicitDestination(input);
1042
1466
  }
1043
1467
 
1044
1468
  // =============================================================================
@@ -1110,9 +1534,39 @@ async function readTerminalBreadcrumb(cwd: string): Promise<string | null> {
1110
1534
  return null;
1111
1535
  }
1112
1536
 
1113
- // Verify the session file still exists
1114
- const stat = fs.statSync(sessionFile, { throwIfNoEntry: false });
1115
- if (stat?.isFile()) return sessionFile;
1537
+ const inspected = inspectResumeSessionFile(sessionFile, new FileSessionStorage());
1538
+ if ("kind" in inspected) {
1539
+ if (inspected.reason !== "missing") return null;
1540
+ const sessionsRoot = getSessionsDir();
1541
+ const resolved = resolveManagedScope({
1542
+ cwd: breadcrumbCwd,
1543
+ agentDir: path.resolve(sessionsRoot, ".."),
1544
+ sessionsRoot,
1545
+ });
1546
+ if (resolved.kind !== "resolved") return null;
1547
+ const listing = listManagedCandidates(resolved.scope);
1548
+ if (listing.kind !== "complete") return null;
1549
+ const migrated = listing.owned.filter(
1550
+ candidate => path.basename(candidate.path) === path.basename(sessionFile),
1551
+ );
1552
+ return migrated.length === 1 ? migrated[0]!.path : null;
1553
+ }
1554
+ const header = inspected.entries[0] as SessionHeader;
1555
+ if (resolveEquivalentPath(header.cwd) !== resolveEquivalentPath(breadcrumbCwd)) return null;
1556
+ const sessionsRoot = getSessionsDir();
1557
+ const resolved = resolveManagedScope({
1558
+ cwd: header.cwd,
1559
+ agentDir: path.resolve(sessionsRoot, ".."),
1560
+ sessionsRoot,
1561
+ });
1562
+ if (resolved.kind !== "resolved") return null;
1563
+ const listing = listManagedCandidates(resolved.scope);
1564
+ if (listing.kind !== "complete") return null;
1565
+ const exact = listing.owned.find(candidate => path.resolve(candidate.path) === path.resolve(sessionFile));
1566
+ if (exact) return exact.path;
1567
+ const byIdentity = listing.owned.find(candidate => candidate.sessionId === header.id);
1568
+ if (byIdentity) return byIdentity.path;
1569
+ return pathIsWithin(sessionsRoot, path.resolve(sessionFile)) ? null : path.resolve(sessionFile);
1116
1570
  } catch (err) {
1117
1571
  if (!isEnoent(err)) logger.debug("Terminal breadcrumb read failed", { err });
1118
1572
  // Breadcrumb doesn't exist or is corrupt — fall through
@@ -1132,7 +1586,7 @@ export async function loadEntriesFromFile(
1132
1586
  if (isEnoent(err)) return [];
1133
1587
  throw err;
1134
1588
  }
1135
- const entries = parseJsonlLenient<FileEntry>(content);
1589
+ const entries = parseSessionEntries(content);
1136
1590
 
1137
1591
  // Validate session header
1138
1592
  if (entries.length === 0) return entries;
@@ -1171,6 +1625,7 @@ function sameResumeStat(left: SessionStorageStat, right: SessionStorageStat): bo
1171
1625
 
1172
1626
  interface ResumeInspectionSnapshot {
1173
1627
  identity: ResumeSessionIdentity;
1628
+ content: Uint8Array;
1174
1629
  entries: FileEntry[];
1175
1630
  context: SessionContext;
1176
1631
  migrationApplied: boolean;
@@ -1218,6 +1673,9 @@ function hasStrictSessionSchema(entries: FileEntry[]): boolean {
1218
1673
  )
1219
1674
  return false;
1220
1675
  break;
1676
+ case "configured_model_chain":
1677
+ if (!normalizeConfiguredModelChainEntry(value)) return false;
1678
+ break;
1221
1679
  case "compaction":
1222
1680
  if (
1223
1681
  typeof value.summary !== "string" ||
@@ -1251,7 +1709,19 @@ function hasStrictSessionSchema(entries: FileEntry[]): boolean {
1251
1709
  case "mcp_tool_selection":
1252
1710
  if (
1253
1711
  !Array.isArray(value.selectedToolNames) ||
1254
- !value.selectedToolNames.every(name => typeof name === "string")
1712
+ !value.selectedToolNames.every(name => typeof name === "string") ||
1713
+ (value.selectedDiscoveredBuiltinToolNames !== undefined &&
1714
+ (!Array.isArray(value.selectedDiscoveredBuiltinToolNames) ||
1715
+ !value.selectedDiscoveredBuiltinToolNames.every(name => typeof name === "string"))) ||
1716
+ (value.mutationCorrelationId !== undefined && typeof value.mutationCorrelationId !== "string")
1717
+ )
1718
+ return false;
1719
+ break;
1720
+ case "discovered_builtin_tool_selection":
1721
+ if (
1722
+ !Array.isArray(value.selectedToolNames) ||
1723
+ !value.selectedToolNames.every(name => typeof name === "string") ||
1724
+ (value.mutationCorrelationId !== undefined && typeof value.mutationCorrelationId !== "string")
1255
1725
  )
1256
1726
  return false;
1257
1727
  break;
@@ -1344,7 +1814,7 @@ function inspectResumeSessionFile(
1344
1814
  mtimeNs: snapshot.mtimeNs,
1345
1815
  sha256: crypto.createHash("sha256").update(bytes).digest("hex"),
1346
1816
  };
1347
- return { identity, entries, context, migrationApplied };
1817
+ return { identity, content: Uint8Array.from(bytes), entries, context, migrationApplied };
1348
1818
  } catch {
1349
1819
  return { kind: "error", reason: "malformed" };
1350
1820
  }
@@ -1577,11 +2047,22 @@ async function getSortedSessions(sessionDir: string, storage: SessionStorage): P
1577
2047
  await Promise.all(
1578
2048
  files.map(async (path: string) => {
1579
2049
  try {
1580
- const content = await storage.readTextPrefix(path, 4096);
2050
+ const buffer = Buffer.allocUnsafe(SESSION_LIST_PREFIX_BYTES);
2051
+ const content = await readSessionListPrefix(path, storage, buffer);
1581
2052
  const entries = parseJsonlLenient<Record<string, unknown>>(content);
1582
2053
  if (entries.length === 0) return;
1583
2054
  const header = entries[0] as Record<string, unknown>;
1584
- if (header.type !== "session" || typeof header.id !== "string") return;
2055
+ if (
2056
+ header.type !== "session" ||
2057
+ typeof header.id !== "string" ||
2058
+ !isSupportedSessionVersion(header.version)
2059
+ )
2060
+ return;
2061
+ if (typeof header.version === "number" && header.version >= 4) {
2062
+ for (const patch of await readSessionListTrailingPatches(path, storage, buffer)) {
2063
+ applySessionListHeaderPatch(header as unknown as SessionListHeader, patch);
2064
+ }
2065
+ }
1585
2066
  const mtime = storage.statSync(path).mtimeMs;
1586
2067
  const firstPrompt = header.title ? undefined : extractFirstUserPrompt(entries);
1587
2068
  sessions.push(new RecentSessionInfo(path, mtime, header, firstPrompt));
@@ -1618,21 +2099,223 @@ function formatTimeAgo(date: Date): string {
1618
2099
  return date.toLocaleDateString();
1619
2100
  }
1620
2101
 
1621
- async function movePathAcrossDevicesSafe(source: string, destination: string): Promise<void> {
2102
+ async function syncPathAndParent(filePath: string): Promise<void> {
2103
+ const file = await fs.promises.open(filePath, "r");
2104
+ try {
2105
+ await file.sync();
2106
+ } finally {
2107
+ await file.close();
2108
+ }
2109
+ if (process.platform !== "win32") {
2110
+ const parent = await fs.promises.open(path.dirname(filePath), "r");
2111
+ try {
2112
+ await parent.sync();
2113
+ } finally {
2114
+ await parent.close();
2115
+ }
2116
+ }
2117
+ }
2118
+
2119
+ interface SessionMoveDirectoryHandle {
2120
+ sync(): Promise<void>;
2121
+ close(): Promise<void>;
2122
+ }
2123
+
2124
+ export async function syncSessionMoveDirectory(
2125
+ directory: string,
2126
+ platform: NodeJS.Platform = process.platform,
2127
+ openDirectory: (directory: string) => Promise<SessionMoveDirectoryHandle> = value => fs.promises.open(value, "r"),
2128
+ ): Promise<void> {
2129
+ if (platform === "win32") return;
2130
+ const handle = await openDirectory(directory);
2131
+ try {
2132
+ await handle.sync();
2133
+ } finally {
2134
+ await handle.close();
2135
+ }
2136
+ }
2137
+
2138
+ /** Durably flush every copied file and directory before an EXDEV source removal. */
2139
+ async function syncCopiedTree(treePath: string): Promise<void> {
2140
+ const stat = await fs.promises.lstat(treePath);
2141
+ if (stat.isSymbolicLink()) throw new Error("Refusing to fsync a copied symbolic link during a cross-device move");
2142
+ if (stat.isFile()) {
2143
+ const file = await fs.promises.open(treePath, "r");
2144
+ try {
2145
+ await file.sync();
2146
+ } finally {
2147
+ await file.close();
2148
+ }
2149
+ return;
2150
+ }
2151
+ if (!stat.isDirectory()) throw new Error("Copied cross-device move contains an unsupported filesystem entry");
2152
+ const directory = await fs.promises.opendir(treePath);
2153
+ try {
2154
+ for (;;) {
2155
+ const entry = await directory.read();
2156
+ if (entry === null) break;
2157
+ await syncCopiedTree(path.join(treePath, entry.name));
2158
+ }
2159
+ } finally {
2160
+ await directory.close();
2161
+ }
2162
+ if (process.platform !== "win32") {
2163
+ const handle = await fs.promises.open(treePath, "r");
2164
+ try {
2165
+ await handle.sync();
2166
+ } finally {
2167
+ await handle.close();
2168
+ }
2169
+ }
2170
+ }
2171
+
2172
+ type CrossDeviceTreeIdentity = string;
2173
+
2174
+ /** Captures content plus topology; a later capture must match before source removal. */
2175
+ async function captureCrossDeviceTreeIdentity(treePath: string): Promise<CrossDeviceTreeIdentity> {
2176
+ const before = await fs.promises.lstat(treePath, { bigint: true });
2177
+ if (before.isSymbolicLink()) throw new Error("Refusing to move a symbolic link across devices");
2178
+ if (before.isFile()) {
2179
+ const bytes = await fs.promises.readFile(treePath);
2180
+ const after = await fs.promises.lstat(treePath, { bigint: true });
2181
+ if (
2182
+ before.dev !== after.dev ||
2183
+ before.ino !== after.ino ||
2184
+ before.size !== after.size ||
2185
+ before.mtimeNs !== after.mtimeNs
2186
+ )
2187
+ throw new Error("Cross-device move source changed while its identity was captured");
2188
+ return `file:${before.dev}:${before.ino}:${before.size}:${before.mtimeNs}:${crypto.createHash("sha256").update(bytes).digest("hex")}`;
2189
+ }
2190
+ if (!before.isDirectory()) throw new Error("Cross-device move contains an unsupported filesystem entry");
2191
+ const directory = await fs.promises.opendir(treePath);
2192
+ const entries: string[] = [];
1622
2193
  try {
1623
- await fs.promises.rename(source, destination);
2194
+ for (;;) {
2195
+ const entry = await directory.read();
2196
+ if (entry === null) break;
2197
+ entries.push(entry.name);
2198
+ }
2199
+ } finally {
2200
+ await directory.close();
2201
+ }
2202
+ entries.sort();
2203
+ const children = await Promise.all(
2204
+ entries.map(
2205
+ async name => `${JSON.stringify(name)}=${await captureCrossDeviceTreeIdentity(path.join(treePath, name))}`,
2206
+ ),
2207
+ );
2208
+ const after = await fs.promises.lstat(treePath, { bigint: true });
2209
+ if (
2210
+ before.dev !== after.dev ||
2211
+ before.ino !== after.ino ||
2212
+ before.size !== after.size ||
2213
+ before.mtimeNs !== after.mtimeNs
2214
+ )
2215
+ throw new Error("Cross-device move source topology changed while its identity was captured");
2216
+ return `directory:${before.dev}:${before.ino}:${before.size}:${before.mtimeNs}:[${children.join(",")}]`;
2217
+ }
2218
+
2219
+ async function removeCrossDeviceSourceIfUnchanged(source: string, expected: CrossDeviceTreeIdentity): Promise<void> {
2220
+ if ((await captureCrossDeviceTreeIdentity(source)) !== expected)
2221
+ throw new Error("Cross-device move source changed before removal");
2222
+ const stat = await fs.promises.lstat(source);
2223
+ if (stat.isDirectory()) await fs.promises.rm(source, { recursive: true, force: false });
2224
+ else await fs.promises.unlink(source);
2225
+ }
2226
+
2227
+ /**
2228
+ * An EXDEV retirement must be driven by a native descriptor/handle-bound
2229
+ * snapshot-copy-verify transaction. Do not fall back to pathname copy/remove:
2230
+ * that can retire a replacement after a crash or concurrent rename.
2231
+ */
2232
+ class CrossDeviceMoveUnsupportedError extends Error {
2233
+ constructor(source: string, destination: string, cause?: unknown) {
2234
+ super(
2235
+ `Cross-device session move is unavailable for ${source} -> ${destination}: native atomic detach/copy/verify support is required.`,
2236
+ { cause },
2237
+ );
2238
+ this.name = "CrossDeviceMoveUnsupportedError";
2239
+ }
2240
+ }
2241
+
2242
+ class PathMoveCompletedWithSyncFailure extends Error {
2243
+ constructor(readonly cause: unknown) {
2244
+ super("Path move completed but final source-directory sync failed.", { cause });
2245
+ }
2246
+ }
2247
+
2248
+ async function movePathAcrossDevicesSafe(source: string, destination: string): Promise<void> {
2249
+ const sourceIdentity = await captureCrossDeviceTreeIdentity(source);
2250
+ const renamed = native.renameNoReplacePath(source, destination);
2251
+ if (renamed.ok) {
2252
+ try {
2253
+ if ((await captureCrossDeviceTreeIdentity(destination)) !== sourceIdentity)
2254
+ throw new Error("Atomic session rename did not preserve the captured source identity");
2255
+ await syncSessionMoveDirectory(path.dirname(destination));
2256
+ if (path.dirname(source) !== path.dirname(destination)) await syncSessionMoveDirectory(path.dirname(source));
2257
+ } catch (error) {
2258
+ throw new PathMoveCompletedWithSyncFailure(error);
2259
+ }
1624
2260
  return;
1625
- } catch (error) {
1626
- if (!hasFsCode(error, "EXDEV")) throw error;
1627
2261
  }
1628
- const stat = await fs.promises.stat(source);
1629
- if (stat.isDirectory()) {
2262
+ if (renamed.code === "quarantine_collision") {
2263
+ const error = new Error(`Session move destination already exists: ${destination}`) as NodeJS.ErrnoException;
2264
+ error.code = "EEXIST";
2265
+ throw error;
2266
+ }
2267
+ if (renamed.code !== "atomic_unavailable")
2268
+ throw new Error(`Atomic session rename failed: ${renamed.code ?? "unknown"}`);
2269
+ const sourceStat = await fs.promises.lstat(source);
2270
+ if (!sourceStat.isDirectory()) {
2271
+ let destinationCreated = false;
2272
+ let sourceRemoved = false;
2273
+ try {
2274
+ try {
2275
+ await fs.promises.link(source, destination);
2276
+ destinationCreated = true;
2277
+ } catch (error) {
2278
+ if (hasFsCode(error, "EXDEV")) throw new CrossDeviceMoveUnsupportedError(source, destination, error);
2279
+ if (
2280
+ !hasFsCode(error, "EPERM") &&
2281
+ !hasFsCode(error, "EACCES") &&
2282
+ !hasFsCode(error, "ENOTSUP") &&
2283
+ !hasFsCode(error, "EOPNOTSUPP")
2284
+ )
2285
+ throw error;
2286
+ await fs.promises.copyFile(source, destination, fs.constants.COPYFILE_EXCL);
2287
+ destinationCreated = true;
2288
+ }
2289
+ await syncPathAndParent(destination);
2290
+ await removeCrossDeviceSourceIfUnchanged(source, sourceIdentity);
2291
+ sourceRemoved = true;
2292
+ await syncSessionMoveDirectory(path.dirname(source));
2293
+ return;
2294
+ } catch (error) {
2295
+ if (!sourceRemoved && destinationCreated) await fs.promises.unlink(destination).catch(() => undefined);
2296
+ if (sourceRemoved && !(error instanceof PathMoveCompletedWithSyncFailure))
2297
+ throw new PathMoveCompletedWithSyncFailure(error);
2298
+ throw error;
2299
+ }
2300
+ }
2301
+
2302
+ const destinationParent = await fs.promises.lstat(path.dirname(destination));
2303
+ if (sourceStat.dev !== destinationParent.dev) throw new CrossDeviceMoveUnsupportedError(source, destination);
2304
+
2305
+ try {
1630
2306
  await fs.promises.cp(source, destination, { recursive: true, force: false, errorOnExist: true });
1631
- await fs.promises.rm(source, { recursive: true, force: false });
1632
- return;
2307
+ await syncCopiedTree(destination);
2308
+ await removeCrossDeviceSourceIfUnchanged(source, sourceIdentity);
2309
+ try {
2310
+ await syncSessionMoveDirectory(path.dirname(source));
2311
+ } catch (error) {
2312
+ throw new PathMoveCompletedWithSyncFailure(error);
2313
+ }
2314
+ } catch (error) {
2315
+ if (!(error instanceof PathMoveCompletedWithSyncFailure))
2316
+ await fs.promises.rm(destination, { recursive: true, force: true }).catch(() => undefined);
2317
+ throw error;
1633
2318
  }
1634
- await fs.promises.copyFile(source, destination, fs.constants.COPYFILE_EXCL);
1635
- await fs.promises.unlink(source);
1636
2319
  }
1637
2320
 
1638
2321
  const MAX_PERSIST_CHARS = 500_000;
@@ -1984,19 +2667,12 @@ function cloneJsonSemantic<T>(value: T): T {
1984
2667
  return cloned as T;
1985
2668
  }
1986
2669
 
1987
- function cloneAgentMessage<T extends AgentMessage>(message: T): T {
1988
- const cloned = {
1989
- ...message,
1990
- ...("content" in message ? { content: cloneJsonSemantic(message.content) } : {}),
1991
- ...("providerPayload" in message ? { providerPayload: cloneJsonSemantic(message.providerPayload) } : {}),
1992
- } as T;
1993
- transferSessionMessageIdentity([message], [cloned]);
1994
- return cloned;
1995
- }
1996
-
1997
2670
  function cloneSessionEntry(entry: SessionEntry): SessionEntry {
1998
- if (entry.type !== "message") return { ...entry };
1999
- return { ...entry, message: cloneAgentMessage(entry.message) } as SessionEntry;
2671
+ const cloned = cloneJsonSemantic(entry);
2672
+ if (entry.type === "message" && cloned.type === "message") {
2673
+ transferSessionMessageIdentity([entry.message], [cloned.message]);
2674
+ }
2675
+ return cloned;
2000
2676
  }
2001
2677
 
2002
2678
  function materializeProviderVisibleEntrySync(entry: SessionEntry, stores: ResidentBlobStores): SessionEntry {
@@ -2580,11 +3256,16 @@ class NdjsonFileWriter {
2580
3256
  #onError: ((err: Error) => void) | undefined;
2581
3257
  #closeDrained = false;
2582
3258
 
2583
- constructor(storage: SessionStorage, path: string, options?: { flags?: "a" | "w"; onError?: (err: Error) => void }) {
3259
+ constructor(
3260
+ storage: SessionStorage,
3261
+ path: string,
3262
+ options?: { flags?: "a" | "w"; onError?: (err: Error) => void; securityContext?: ManagedSessionSecurityContext },
3263
+ ) {
2584
3264
  this.#onError = options?.onError;
2585
3265
  this.#writer = storage.openWriter(path, {
2586
3266
  flags: options?.flags ?? "a",
2587
3267
  onError: (err: Error) => this.#recordError(err),
3268
+ securityContext: options?.securityContext,
2588
3269
  });
2589
3270
  }
2590
3271
 
@@ -2618,7 +3299,7 @@ class NdjsonFileWriter {
2618
3299
  }
2619
3300
 
2620
3301
  /** Queue a write. Returns a promise so callers can await if needed. */
2621
- write(entry: FileEntry): Promise<void> {
3302
+ write(entry: FileEntry | SessionPatchRecord): Promise<void> {
2622
3303
  if (this.#closed || this.#closing) throw new Error("Writer closed");
2623
3304
  if (this.#error) throw this.#error;
2624
3305
  const line = `${JSON.stringify(entry)}\n`;
@@ -2634,7 +3315,7 @@ class NdjsonFileWriter {
2634
3315
  * queue. Use only when no concurrent async write is in flight (the session-manager
2635
3316
  * persist path enforces this via `#flushed`/`#needsFullRewriteOnNextPersist`).
2636
3317
  */
2637
- writeSync(entry: FileEntry): void {
3318
+ writeSync(entry: FileEntry | SessionPatchRecord): void {
2638
3319
  if (this.#closed || this.#closing) throw new Error("Writer closed");
2639
3320
  if (this.#error) throw this.#error;
2640
3321
  const line = `${JSON.stringify(entry)}\n`;
@@ -2829,6 +3510,14 @@ function extractTextFromContent(content: Message["content"]): string {
2829
3510
  }
2830
3511
 
2831
3512
  const SESSION_LIST_PREFIX_BYTES = 4096;
3513
+ const SESSION_LIST_TRAILING_PATCH_BYTES = 4096;
3514
+ // Hard cap on how many trailing bytes the listers may scan for header patches.
3515
+ // moveTo folds cwd patches into line 1 via a full atomic rewrite, so only a
3516
+ // recent title patch can live unfolded near EOF; anything buried deeper is
3517
+ // folded on the next natural full rewrite. Keeping this bounded preserves the
3518
+ // O(SESSION_LIST_PREFIX_BYTES) listing cost that SESSION_LIST_PREFIX_BYTES exists for.
3519
+ const SESSION_LIST_TRAILING_PATCH_SCAN_CAP = SESSION_LIST_TRAILING_PATCH_BYTES * 4;
3520
+ const SESSION_NAME_MAX_CHARS = 1_000;
2832
3521
  const SESSION_LIST_PARALLEL_THRESHOLD = 64;
2833
3522
  const SESSION_LIST_MAX_WORKERS = 16;
2834
3523
  const sessionListPrefixDecoder = new TextDecoder("utf-8", { fatal: false });
@@ -2848,7 +3537,70 @@ async function readSessionListPrefix(file: string, storage: SessionStorage, buff
2848
3537
  }
2849
3538
  }
2850
3539
 
2851
- function decodeJsonStringFragment(value: string): string {
3540
+ async function readSessionListTrailingPatches(
3541
+ file: string,
3542
+ storage: SessionStorage,
3543
+ buffer: Buffer,
3544
+ ): Promise<HeaderPatchRecord["patch"][]> {
3545
+ if (!(storage instanceof FileSessionStorage)) {
3546
+ const content = await storage.readText(file);
3547
+ const entries = parseSessionEntries(content);
3548
+ const header = entries[0] as SessionHeader | undefined;
3549
+ return header?.type === "session" ? [{ cwd: header.cwd, title: header.title }] : [];
3550
+ }
3551
+
3552
+ const size = storage.statSync(file).size;
3553
+ if (size <= SESSION_LIST_PREFIX_BYTES) return [];
3554
+ const latest: HeaderPatchRecord["patch"] = {};
3555
+ let position = size;
3556
+ const scanFloor = Math.max(0, size - SESSION_LIST_TRAILING_PATCH_SCAN_CAP);
3557
+ let trailingFragment = Buffer.alloc(0);
3558
+ const chunkSize = Math.min(buffer.byteLength, SESSION_LIST_TRAILING_PATCH_BYTES);
3559
+ const handle = await fs.promises.open(file, "r");
3560
+ try {
3561
+ while (position > scanFloor && (latest.cwd === undefined || latest.title === undefined)) {
3562
+ const start = Math.max(0, position - chunkSize);
3563
+ const length = position - start;
3564
+ const { bytesRead } = await handle.read(buffer, 0, length, start);
3565
+ const combined = Buffer.concat([buffer.subarray(0, bytesRead), trailingFragment]);
3566
+ let complete = combined;
3567
+ if (start > 0) {
3568
+ const firstNewline = combined.indexOf(0x0a);
3569
+ if (firstNewline === -1) {
3570
+ trailingFragment = combined;
3571
+ position = start;
3572
+ continue;
3573
+ }
3574
+ trailingFragment = combined.subarray(0, firstNewline);
3575
+ complete = combined.subarray(firstNewline + 1);
3576
+ }
3577
+ const lines = complete.toString("utf8").split("\n");
3578
+ for (let index = lines.length - 1; index >= 0; index--) {
3579
+ const line = lines[index];
3580
+ if (!line) continue;
3581
+ try {
3582
+ const record = JSON.parse(line) as SessionPatchRecord;
3583
+ if (!isHeaderPatchRecord(record)) continue;
3584
+ if (latest.cwd === undefined && typeof record.patch.cwd === "string") latest.cwd = record.patch.cwd;
3585
+ if (latest.title === undefined && typeof record.patch.title === "string")
3586
+ latest.title = record.patch.title;
3587
+ } catch {
3588
+ // Ignore malformed or partial records exactly as the canonical loader does.
3589
+ }
3590
+ }
3591
+ position = start;
3592
+ }
3593
+ return latest.cwd === undefined && latest.title === undefined ? [] : [latest];
3594
+ } finally {
3595
+ await handle.close();
3596
+ }
3597
+ }
3598
+
3599
+ function applySessionListHeaderPatch(header: SessionListHeader, patch: HeaderPatchRecord["patch"]): void {
3600
+ if (typeof patch.cwd === "string") header.cwd = patch.cwd;
3601
+ if (typeof patch.title === "string") header.title = patch.title;
3602
+ }
3603
+ function decodeJsonStringFragment(value: string): string {
2852
3604
  const safeValue = value.endsWith("\\") ? value.slice(0, -1) : value;
2853
3605
  try {
2854
3606
  return JSON.parse(`"${safeValue}"`) as string;
@@ -2931,6 +3683,8 @@ function extractFirstUserMessageFromPrefix(content: string): string | undefined
2931
3683
  interface SessionListHeader {
2932
3684
  type: "session";
2933
3685
  id: string;
3686
+ version?: number;
3687
+
2934
3688
  cwd?: string;
2935
3689
  title?: string;
2936
3690
  parentSession?: string;
@@ -2946,6 +3700,7 @@ function parseSessionListHeader(
2946
3700
  return {
2947
3701
  type: "session",
2948
3702
  id: parsedHeader.id,
3703
+ version: typeof parsedHeader.version === "number" ? parsedHeader.version : undefined,
2949
3704
  cwd: typeof parsedHeader.cwd === "string" ? parsedHeader.cwd : undefined,
2950
3705
  title: typeof parsedHeader.title === "string" ? parsedHeader.title : undefined,
2951
3706
  parentSession: typeof parsedHeader.parentSession === "string" ? parsedHeader.parentSession : undefined,
@@ -2963,6 +3718,7 @@ function parseSessionListHeader(
2963
3718
  return {
2964
3719
  type: "session",
2965
3720
  id,
3721
+ version: Number(extractStringProperty(firstLine, "version")) || undefined,
2966
3722
  cwd: extractStringProperty(firstLine, "cwd"),
2967
3723
  title: extractStringProperty(firstLine, "title"),
2968
3724
  parentSession: extractStringProperty(firstLine, "parentSession"),
@@ -2986,9 +3742,14 @@ async function collectSessionFromFile(
2986
3742
  ): Promise<SessionInfo | undefined> {
2987
3743
  try {
2988
3744
  const content = await readSessionListPrefix(file, storage, buffer);
2989
- const entries = parseJsonlLenient<Record<string, unknown>>(content);
3745
+ const entries = parseSessionEntries(content).map(entry => entry as unknown as Record<string, unknown>);
2990
3746
  const header = parseSessionListHeader(content, entries);
2991
3747
  if (!header) return undefined;
3748
+ if (typeof header.version === "number" && header.version >= 4 && header.version <= CURRENT_SESSION_VERSION) {
3749
+ for (const patch of await readSessionListTrailingPatches(file, storage, buffer)) {
3750
+ applySessionListHeaderPatch(header, patch);
3751
+ }
3752
+ }
2992
3753
 
2993
3754
  let parsedMessageCount = 0;
2994
3755
  let firstMessage = "";
@@ -3031,6 +3792,7 @@ async function collectSessionFromFile(
3031
3792
  created: new Date(header.timestamp ?? ""),
3032
3793
  modified: stats.mtime,
3033
3794
  messageCount,
3795
+ messageCountIsEstimate: stats.size > SESSION_LIST_PREFIX_BYTES,
3034
3796
  size: stats.size,
3035
3797
  firstMessage: firstMessage || "(no messages)",
3036
3798
  allMessagesText: allMessages.length > 0 ? allMessages.join(" ") : firstMessage,
@@ -3131,6 +3893,7 @@ interface SessionManagerStateSnapshot {
3131
3893
  titleSource: "auto" | "user" | undefined;
3132
3894
  sessionFile: string | undefined;
3133
3895
  flushed: boolean;
3896
+ ensuredOnDisk: boolean;
3134
3897
  needsFullRewriteOnNextPersist: boolean;
3135
3898
  fileEntries: FileEntry[];
3136
3899
  materializedFileEntries: FileEntry[];
@@ -3146,6 +3909,7 @@ export class SessionManager {
3146
3909
  #flushed: boolean = false;
3147
3910
  #needsFullRewriteOnNextPersist: boolean = false;
3148
3911
  #ensuredOnDisk: boolean = false;
3912
+ #recoveryHydrationContext: RecoveryHydrationContext | undefined;
3149
3913
  #fileEntries: FileEntry[] = [];
3150
3914
  #byId: Map<string, SessionEntry> = new Map();
3151
3915
  #labelsById: Map<string, string> = new Map();
@@ -3163,8 +3927,10 @@ export class SessionManager {
3163
3927
  #persistChain: Promise<void> = Promise.resolve();
3164
3928
  #persistError: Error | undefined;
3165
3929
  #persistErrorReported = false;
3930
+
3166
3931
  #artifactManager: ArtifactManager | null = null;
3167
3932
  #artifactManagerSessionFile: string | null = null;
3933
+ #managedTranscriptStoreCache: { directory: string; store: ManagedSessionDescendantStore } | null = null;
3168
3934
  // When set, take precedence over the lazily-derived per-session manager.
3169
3935
  // Subagents adopt the parent's manager so artifact IDs are unique across the
3170
3936
  // whole agent tree and all files land in the parent's artifacts dir.
@@ -3185,7 +3951,7 @@ export class SessionManager {
3185
3951
  #replayMetadataRevision = 0;
3186
3952
  #materializedEntriesRevision = -1;
3187
3953
  #materializedEntriesCache: SessionEntry[] | undefined;
3188
- #sessionContextCache: WeakRef<SessionContext> | undefined;
3954
+ #sessionContextCache: SessionContext | undefined;
3189
3955
  #sessionContextEntryRevision = -1;
3190
3956
  #sessionContextLeafRevision = -1;
3191
3957
  #sessionContextReplayMetadataRevision = -1;
@@ -3199,17 +3965,30 @@ export class SessionManager {
3199
3965
  #getEntriesMaterializerCallCount = 0;
3200
3966
  #materializedEntriesCachePopulateCount = 0;
3201
3967
  #pathOnlyContextBuildCount = 0;
3968
+ #internalReadAccess: SessionManagerReadAccess = {
3969
+ getEntries: () => freezeInternalReadSnapshot(this.#getMaterializedEntriesInternal()),
3970
+ getSessionContext: () => this.#getSessionContextForRead(),
3971
+ getTree: () => this.#getTree(freezeInternalReadSnapshot(this.#getMaterializedEntriesInternal())),
3972
+ };
3202
3973
 
3203
3974
  private constructor(
3204
3975
  private cwd: string,
3205
3976
  private sessionDir: string,
3206
3977
  private readonly persist: boolean,
3207
3978
  private readonly storage: SessionStorage,
3979
+ private destination: SessionDestination = explicitDestination(sessionDir),
3208
3980
  ) {
3209
3981
  this.#blobStore = persist ? new BlobStore(getBlobsDir()) : this.#residentTextBlobStore;
3210
3982
  this.#residentImageBlobStore = this.#blobStore;
3211
3983
  if (persist && sessionDir) {
3212
3984
  this.storage.ensureDirSync(sessionDir);
3985
+ // Canonicalize the trusted session directory (single choke point for every
3986
+ // creation path: create/open/moveTo/fork/SDK) so benign ancestor symlinks
3987
+ // (e.g. macOS `/var -> /private/var`, a symlinked `$HOME`) are resolved to a
3988
+ // symlink-free root before the strict owner-only and reparse guards run.
3989
+ if (this.storage instanceof FileSessionStorage) {
3990
+ this.sessionDir = canonicalizeTrustedPath(sessionDir);
3991
+ }
3213
3992
  }
3214
3993
  // Note: call _initSession() or _initSessionFile() after construction
3215
3994
  }
@@ -3302,7 +4081,12 @@ export class SessionManager {
3302
4081
 
3303
4082
  #resetResidentTextBlobStore(): void {
3304
4083
  this.#disposeResidentTextBlobStore();
3305
- if (this.persist && this.#sessionFile && this.storage instanceof FileSessionStorage) {
4084
+ if (
4085
+ this.destination.kind === "explicit" &&
4086
+ this.persist &&
4087
+ this.#sessionFile &&
4088
+ this.storage instanceof FileSessionStorage
4089
+ ) {
3306
4090
  this.#residentTextBlobStore = new EphemeralBlobStore(this.#residentCacheDir(this.#sessionFile));
3307
4091
  }
3308
4092
  }
@@ -3323,6 +4107,7 @@ export class SessionManager {
3323
4107
  titleSource: this.#titleSource,
3324
4108
  sessionFile: this.#sessionFile,
3325
4109
  flushed: this.#flushed,
4110
+ ensuredOnDisk: this.#ensuredOnDisk,
3326
4111
  needsFullRewriteOnNextPersist: this.#needsFullRewriteOnNextPersist,
3327
4112
  // Snapshot entry objects by reference: switch/reload replaces the active entry array,
3328
4113
  // so rollback does not need structured cloning of extension/custom details.
@@ -3335,15 +4120,23 @@ export class SessionManager {
3335
4120
 
3336
4121
  restoreState(snapshot: SessionManagerStateSnapshot): void {
3337
4122
  const restoredFileEntries = [...snapshot.materializedFileEntries];
4123
+ const retainsPersistWriter =
4124
+ this.#persistWriter?.isOpen() === true &&
4125
+ this.#sessionId === snapshot.sessionId &&
4126
+ this.#sessionFile === snapshot.sessionFile &&
4127
+ this.#persistWriterPath === snapshot.sessionFile;
3338
4128
  this.#sessionId = snapshot.sessionId;
3339
4129
  this.#sessionName = snapshot.sessionName;
3340
4130
  this.#titleSource = snapshot.titleSource;
3341
4131
  this.#sessionFile = snapshot.sessionFile;
3342
4132
  this.#flushed = snapshot.flushed;
4133
+ this.#ensuredOnDisk = snapshot.ensuredOnDisk;
3343
4134
  this.#needsFullRewriteOnNextPersist = snapshot.needsFullRewriteOnNextPersist;
3344
4135
  this.#fileEntries = restoredFileEntries;
3345
- this.#persistWriter = undefined;
3346
- this.#persistWriterPath = undefined;
4136
+ if (!retainsPersistWriter) {
4137
+ this.#persistWriter = undefined;
4138
+ this.#persistWriterPath = undefined;
4139
+ }
3347
4140
  this.#persistChain = Promise.resolve();
3348
4141
  this.#persistError = undefined;
3349
4142
  this.#persistErrorReported = false;
@@ -3364,19 +4157,19 @@ export class SessionManager {
3364
4157
  }
3365
4158
 
3366
4159
  async #hydrateExistingSession(sessionFile: string, entries: FileEntry[], migrationApplied: boolean): Promise<void> {
3367
- const header = entries[0] as SessionHeader;
3368
- this.#sessionFile = path.resolve(sessionFile);
4160
+ const ownedEntries = structuredClone(entries) as FileEntry[];
4161
+ const header = ownedEntries[0] as SessionHeader;
4162
+ this.#sessionFile = this.storage instanceof FileSessionStorage ? path.resolve(sessionFile) : sessionFile;
3369
4163
  this.#sessionId = header.id;
3370
4164
  this.#sessionName = header.title;
3371
4165
  this.#titleSource = header.titleSource;
3372
4166
  this.#needsFullRewriteOnNextPersist = migrationApplied;
3373
- await resolveBlobRefsInEntries(entries, this.#blobStore);
3374
- this.#fileEntries = entries;
4167
+ await resolveBlobRefsInEntries(ownedEntries, this.#blobStore);
4168
+ this.#fileEntries = ownedEntries;
3375
4169
  this.#resetResidentTextBlobStore();
3376
4170
  this.#fileEntries = this.#fileEntries.map(entry =>
3377
4171
  prepareEntryForResidentSync(entry, this.#residentBlobStores()),
3378
4172
  );
3379
- this.sanitizeLoadedOpenAIResponsesReplayMetadata();
3380
4173
  this.#buildIndex();
3381
4174
  this.#bumpAllRevisions();
3382
4175
  this.#flushed = true;
@@ -3394,7 +4187,7 @@ export class SessionManager {
3394
4187
  await this.#closePersistWriter();
3395
4188
  this.#persistError = undefined;
3396
4189
  this.#persistErrorReported = false;
3397
- this.#sessionFile = path.resolve(sessionFile);
4190
+ this.#sessionFile = this.storage instanceof FileSessionStorage ? path.resolve(sessionFile) : sessionFile;
3398
4191
  writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
3399
4192
  this.#fileEntries = await loadEntriesFromFile(this.#sessionFile, this.storage);
3400
4193
  if (this.#fileEntries.length > 0) {
@@ -3410,12 +4203,12 @@ export class SessionManager {
3410
4203
  this.#fileEntries = this.#fileEntries.map(entry =>
3411
4204
  prepareEntryForResidentSync(entry, this.#residentBlobStores()),
3412
4205
  );
3413
- this.sanitizeLoadedOpenAIResponsesReplayMetadata();
3414
4206
 
3415
4207
  this.#buildIndex();
3416
4208
  this.#bumpAllRevisions();
3417
4209
  this.#flushed = true;
3418
4210
  this.#ensuredOnDisk = true;
4211
+ await this.#sanitizeLoadedOpenAIResponsesReplayMetadataAndPersist();
3419
4212
  } else {
3420
4213
  const explicitPath = this.#sessionFile;
3421
4214
  this.#newSessionSync();
@@ -3437,11 +4230,75 @@ export class SessionManager {
3437
4230
  return sessionFile;
3438
4231
  }
3439
4232
 
3440
- /** Delete a session file and its artifacts. Drains the persist writer first to avoid EPERM on Windows. ENOENT is treated as success. */
4233
+ /** Tombstone and exact-delete managed transcripts, detaching the active transcript first. */
3441
4234
  async dropSession(sessionPath: string): Promise<void> {
4235
+ const requestedPath = path.resolve(sessionPath);
4236
+ if (this.#sessionFile && path.resolve(this.#sessionFile) === requestedPath) {
4237
+ await this.newSession();
4238
+ }
4239
+ await this.#closePersistWriter();
4240
+ const managedRoot = resolveManagedSessionRoot(this.sessionDir, this.cwd);
4241
+ try {
4242
+ if (managedRoot) {
4243
+ if (!pathIsWithin(managedRoot, requestedPath))
4244
+ throw new Error("Managed session deletion is limited to this manager's configured session root.");
4245
+ const resolved = resolveManagedScope({
4246
+ cwd: this.cwd,
4247
+ agentDir: path.resolve(managedRoot, ".."),
4248
+ sessionsRoot: managedRoot,
4249
+ });
4250
+ if (resolved.kind !== "resolved")
4251
+ throw new Error("Managed session storage could not be resolved for this manager.");
4252
+ const listing = listManagedCandidates(resolved.scope);
4253
+ if (listing.kind !== "complete")
4254
+ throw new Error("Managed session storage could not be verified; refusing deletion.");
4255
+ const candidate = listing.owned.find(candidate => path.resolve(candidate.path) === requestedPath);
4256
+ if (!candidate) throw new Error("Managed session deletion requires exact logical authorization.");
4257
+ const deleted = await deleteManagedSessionCandidate(resolved.scope, candidate);
4258
+ if (deleted.kind === "error") throw new Error(`Could not delete managed session: ${deleted.message}`);
4259
+ } else {
4260
+ await this.storage.deleteSessionWithArtifacts(sessionPath);
4261
+ }
4262
+ } catch (err) {
4263
+ if (isEnoent(err)) return;
4264
+ throw err;
4265
+ }
4266
+ }
4267
+
4268
+ /**
4269
+ * Exact-delete a session transcript and its artifacts by path WITHOUT requiring
4270
+ * managed logical authorization. This is only for discarding an UNCOMMITTED
4271
+ * successor that a transaction (e.g. handoff) created via `newSession()` but
4272
+ * never durably authorized — such a session has no managed candidate listing
4273
+ * yet, so `dropSession` would refuse it and leak its resident-cache/artifact
4274
+ * root. Bounded to this manager's configured session root and refuses to touch
4275
+ * the active session; tolerates missing files.
4276
+ */
4277
+ async discardUncommittedSession(sessionPath: string): Promise<void> {
4278
+ // Close the writer FIRST so the canonical checks and the delete run with no
4279
+ // intervening await, eliminating the check/use window in which a parent
4280
+ // directory could be swapped for a symlink between authorization and deletion.
3442
4281
  await this.#closePersistWriter();
4282
+ // Canonicalize the candidate (and the active file / configured dir) through
4283
+ // their nearest existing ancestor so symlink/reparse components cannot alias
4284
+ // the active session or escape the configured session directory, even when
4285
+ // the successor transcript itself does not yet exist.
4286
+ const canonicalCandidate = canonicalizeThroughExistingAncestor(path.resolve(sessionPath));
4287
+ if (this.#sessionFile) {
4288
+ const canonicalActive = canonicalizeThroughExistingAncestor(path.resolve(this.#sessionFile));
4289
+ if (canonicalActive === canonicalCandidate) {
4290
+ throw new Error("Refusing to discard the active session as uncommitted.");
4291
+ }
4292
+ }
4293
+ // Least-authority containment: only within this manager's configured session
4294
+ // directory (canonical), where newSession() allocates the successor. The
4295
+ // broader managed root (parent of sessionDir) is deliberately NOT accepted.
4296
+ const canonicalSessionDir = canonicalizeThroughExistingAncestor(path.resolve(this.sessionDir));
4297
+ if (!pathIsWithin(canonicalSessionDir, canonicalCandidate)) {
4298
+ throw new Error("Uncommitted session discard is limited to this manager's configured session directory.");
4299
+ }
3443
4300
  try {
3444
- await this.storage.deleteSessionWithArtifacts(sessionPath);
4301
+ await this.storage.deleteSessionWithArtifacts(canonicalCandidate);
3445
4302
  } catch (err) {
3446
4303
  if (isEnoent(err)) return;
3447
4304
  throw err;
@@ -3461,6 +4318,9 @@ export class SessionManager {
3461
4318
  const oldSessionFile = this.#sessionFile;
3462
4319
  const oldSessionId = this.#sessionId;
3463
4320
  const materializedEntries = materializeResidentEntriesForReadSync(this.#fileEntries, this.#residentBlobStores());
4321
+ const oldFileEntries = this.#fileEntries;
4322
+ const oldSessionName = this.#sessionName;
4323
+ const oldTitleSource = this.#titleSource;
3464
4324
 
3465
4325
  // Close the current writer
3466
4326
  await this.#closePersistWriter();
@@ -3496,13 +4356,169 @@ export class SessionManager {
3496
4356
  this.#reexternalizeFileEntriesForResidentStore();
3497
4357
  this.#bumpAllRevisions();
3498
4358
 
3499
- // Write the new session file
4359
+ // Write the new session file, import artifacts, and adopt only after the
4360
+ // retained destination manifest is complete.
3500
4361
  this.#flushed = false;
3501
- await this.#rewriteFile();
4362
+ let forkArtifactPublication: ForkArtifactPublication | undefined;
4363
+ let forkTranscriptPublication: ManagedFileSnapshot | null = null;
4364
+ let forkTranscriptStore: ManagedSessionDescendantStore | undefined;
4365
+ try {
4366
+ forkArtifactPublication = await this.copyArtifactsForFork(oldSessionFile, this.#sessionFile);
4367
+ await this.#rewriteFile();
4368
+ if (this.destination.kind === "managed") {
4369
+ forkTranscriptStore = this.#managedTranscriptStore();
4370
+ forkTranscriptPublication = forkTranscriptStore.readExpected(path.basename(this.#sessionFile));
4371
+ if (!forkTranscriptPublication) throw new Error("managed_fork_transcript_publish_missing");
4372
+ }
4373
+ forkArtifactPublication?.store.verifyRootSecurity();
4374
+ forkArtifactPublication?.store.assertBound();
4375
+ if (forkArtifactPublication) {
4376
+ const adoptedSnapshot = forkArtifactPublication.store.fsyncTree();
4377
+ if (!retainedTreeSnapshotEquals(adoptedSnapshot, forkArtifactPublication.snapshot)) {
4378
+ throw new Error("artifact_destination_changed_during_transcript_publication");
4379
+ }
4380
+ }
4381
+ if (forkTranscriptStore && forkTranscriptPublication) {
4382
+ const terminalTranscript = forkTranscriptStore.readExpected(path.basename(this.#sessionFile));
4383
+ if (!managedFileSnapshotEquals(terminalTranscript, forkTranscriptPublication))
4384
+ throw new Error("managed_fork_transcript_changed");
4385
+ }
4386
+ } catch (error) {
4387
+ let failure = error;
4388
+ const failedSessionFile = this.#sessionFile;
4389
+ if (this.destination.kind === "managed") {
4390
+ try {
4391
+ if (forkTranscriptStore && forkTranscriptPublication)
4392
+ forkTranscriptStore.removeExpected(path.basename(failedSessionFile), forkTranscriptPublication);
4393
+ if (forkArtifactPublication)
4394
+ forkArtifactPublication.cleanupStore.removeTreeExpected(
4395
+ forkArtifactPublication.cleanupRelativePath,
4396
+ forkArtifactPublication.snapshot,
4397
+ );
4398
+ } catch (cleanupError) {
4399
+ failure = new Error(`Failed to clean up managed fork destination: ${toError(cleanupError).message}`, {
4400
+ cause: toError(error),
4401
+ });
4402
+ }
4403
+ } else {
4404
+ try {
4405
+ await this.storage.unlink(failedSessionFile);
4406
+ } catch {
4407
+ // Preserve ambiguous cleanup evidence while restoring the active manager.
4408
+ }
4409
+ }
4410
+ this.#sessionId = oldSessionId;
4411
+ this.#sessionFile = oldSessionFile;
4412
+ this.#fileEntries = oldFileEntries;
4413
+ this.#sessionName = oldSessionName;
4414
+ this.#titleSource = oldTitleSource;
4415
+ this.#persistChain = Promise.resolve();
4416
+ this.#persistError = undefined;
4417
+ this.#persistErrorReported = false;
4418
+ this.#resetResidentTextBlobStore();
4419
+ this.#reexternalizeFileEntriesForResidentStore();
4420
+ this.#bumpAllRevisions();
4421
+ throw failure;
4422
+ }
3502
4423
 
3503
4424
  return { oldSessionFile, newSessionFile: this.#sessionFile };
3504
4425
  }
3505
4426
 
4427
+ async copyArtifactsForFork(
4428
+ oldSessionFile: string,
4429
+ newSessionFile: string,
4430
+ ): Promise<ForkArtifactPublication | undefined> {
4431
+ const sourceDir = oldSessionFile.slice(0, -6);
4432
+ const finalDestinationDir = newSessionFile.slice(0, -6);
4433
+ if (this.destination.kind !== "managed") {
4434
+ try {
4435
+ const sourceStat = fs.lstatSync(sourceDir);
4436
+ if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink()) throw new Error("unsafe_artifacts");
4437
+ } catch (error) {
4438
+ if (isEnoent(error)) return;
4439
+ throw error;
4440
+ }
4441
+ await fs.promises.cp(sourceDir, finalDestinationDir, { recursive: true });
4442
+ return undefined;
4443
+ }
4444
+
4445
+ const sourceName = path.basename(sourceDir);
4446
+ const destinationName = path.basename(finalDestinationDir);
4447
+ const stagingName = `.${destinationName}.${crypto.randomUUID()}.fork-staging`;
4448
+ const parentStore = this.#managedTranscriptStore(oldSessionFile);
4449
+ let sourceSnapshot: native.NativeDirectoryTreeSnapshot;
4450
+ try {
4451
+ sourceSnapshot = parentStore.captureTree(sourceName);
4452
+ } catch (error) {
4453
+ if (error instanceof Error && error.message === "not_found") return;
4454
+ throw error;
4455
+ }
4456
+ let stagingSnapshot: native.NativeDirectoryTreeSnapshot | undefined;
4457
+ let publishedSnapshot: native.NativeDirectoryTreeSnapshot | undefined;
4458
+ try {
4459
+ parentStore.ensureDirectory(stagingName);
4460
+ stagingSnapshot = parentStore.captureTree(stagingName);
4461
+ for (const entry of sourceSnapshot.entries) {
4462
+ if (entry.relativePath === "") continue;
4463
+ const destinationRelativePath = path.posix.join(stagingName, entry.relativePath);
4464
+ if (entry.kind === "directory") {
4465
+ parentStore.ensureDirectory(destinationRelativePath);
4466
+ } else if (entry.kind === "file") {
4467
+ const sourceFile = parentStore.readExpected(path.posix.join(sourceName, entry.relativePath));
4468
+ if (
4469
+ !sourceFile ||
4470
+ sourceFile.identity.dev.toString() !== entry.dev ||
4471
+ sourceFile.identity.ino.toString() !== entry.ino ||
4472
+ sourceFile.identity.size.toString() !== entry.size ||
4473
+ sourceFile.identity.mtimeNs.toString() !== entry.mtimeNs ||
4474
+ crypto.createHash("sha256").update(sourceFile.bytes).digest("hex") !== entry.sha256
4475
+ ) {
4476
+ throw new Error("artifact_source_changed");
4477
+ }
4478
+ await parentStore.publishNoReplace(destinationRelativePath, sourceFile.bytes);
4479
+ } else {
4480
+ throw new Error("unsafe_artifacts");
4481
+ }
4482
+ stagingSnapshot = parentStore.captureTree(stagingName);
4483
+ }
4484
+ if (JSON.stringify(parentStore.captureTree(sourceName)) !== JSON.stringify(sourceSnapshot))
4485
+ throw new Error("artifact_source_changed");
4486
+ parentStore.moveTreeNoReplace(stagingName, destinationName, stagingSnapshot);
4487
+ publishedSnapshot = parentStore.captureTree(destinationName);
4488
+ const comparable = (tree: native.NativeDirectoryTreeSnapshot) =>
4489
+ tree.entries.map(entry => ({
4490
+ relativePath: entry.relativePath,
4491
+ kind: entry.kind,
4492
+ size: entry.kind === "file" ? entry.size : undefined,
4493
+ sha256: entry.sha256,
4494
+ }));
4495
+ if (JSON.stringify(comparable(publishedSnapshot)) !== JSON.stringify(comparable(sourceSnapshot)))
4496
+ throw new Error("artifact_destination_terminal_mismatch");
4497
+ const publishedStore = parentStore.deriveSubtree(destinationName);
4498
+ publishedStore.verifyRootSecurity();
4499
+ const retainedPublishedSnapshot = publishedStore.captureTree("");
4500
+ if (!retainedTreeSnapshotEqualsAfterRename(retainedPublishedSnapshot, publishedSnapshot))
4501
+ throw new Error("artifact_destination_retained_identity_changed");
4502
+ publishedSnapshot = retainedPublishedSnapshot;
4503
+ return {
4504
+ snapshot: publishedSnapshot,
4505
+ store: publishedStore,
4506
+ cleanupStore: parentStore,
4507
+ cleanupRelativePath: destinationName,
4508
+ };
4509
+ } catch (error) {
4510
+ try {
4511
+ if (publishedSnapshot) parentStore.removeTreeExpected(destinationName, publishedSnapshot);
4512
+ else if (stagingSnapshot) parentStore.removeTreeExpected(stagingName, stagingSnapshot);
4513
+ } catch (cleanupError) {
4514
+ throw new Error(`Failed to clean up managed fork artifacts: ${toError(cleanupError).message}`, {
4515
+ cause: toError(error),
4516
+ });
4517
+ }
4518
+ throw error;
4519
+ }
4520
+ }
4521
+
3506
4522
  /**
3507
4523
  * Move the session to a new working directory.
3508
4524
  * Moves session files and artifacts on disk, updates all internal references,
@@ -3511,12 +4527,29 @@ export class SessionManager {
3511
4527
  async moveTo(newCwd: string): Promise<void> {
3512
4528
  const resolvedCwd = path.resolve(newCwd);
3513
4529
  if (resolvedCwd === this.cwd) return;
4530
+ const previousCwd = this.cwd;
4531
+ const previousSessionDir = this.sessionDir;
4532
+ const previousSessionFile = this.#sessionFile;
4533
+ const previousDestination = this.destination;
4534
+ const previousArtifactDir = previousSessionFile?.slice(0, -6);
3514
4535
 
3515
- const managedSessionsRoot = resolveManagedSessionRoot(this.sessionDir, this.cwd);
3516
- const newSessionDir = managedSessionsRoot
3517
- ? computeDefaultSessionDir(resolvedCwd, this.storage, managedSessionsRoot)
3518
- : computeDefaultSessionDir(resolvedCwd, this.storage);
4536
+ const nextDestination = (() => {
4537
+ if (this.destination.kind === "managed") {
4538
+ return managedDestination(resolvedCwd, this.storage, this.destination.securityContext.agentDir);
4539
+ }
4540
+ if (!(this.storage instanceof FileSessionStorage)) return this.destination;
4541
+ const managedSessionsRoot = resolveManagedSessionRoot(this.sessionDir, this.cwd);
4542
+ const directory = managedSessionsRoot
4543
+ ? computeDefaultSessionDir(resolvedCwd, this.storage, managedSessionsRoot)
4544
+ : computeDefaultSessionDir(resolvedCwd, this.storage);
4545
+ return explicitDestination(directory);
4546
+ })();
4547
+ const newSessionDir = nextDestination.directory;
3519
4548
  let hadSessionFile = false;
4549
+ const managedMove = this.destination.kind === "managed" && nextDestination.kind === "managed";
4550
+ let managedSourceStore: ManagedSessionDescendantStore | undefined;
4551
+ let managedDestinationStore: ManagedSessionDescendantStore | undefined;
4552
+ let rollbackManagedMove: (() => Promise<void>) | undefined;
3520
4553
 
3521
4554
  if (this.persist && this.#sessionFile) {
3522
4555
  // Close the persist writer before moving files
@@ -3529,7 +4562,11 @@ export class SessionManager {
3529
4562
  const newSessionFile = path.join(newSessionDir, path.basename(oldSessionFile));
3530
4563
  const oldArtifactDir = oldSessionFile.slice(0, -6); // strip .jsonl
3531
4564
  const newArtifactDir = newSessionFile.slice(0, -6);
3532
- hadSessionFile = this.storage.existsSync(oldSessionFile);
4565
+ let managedTranscript!: ReturnType<ManagedSessionDescendantStore["readExpected"]>;
4566
+ let managedArtifacts: native.NativeDirectoryTreeSnapshot | undefined;
4567
+ let managedPublishedTranscript!: ReturnType<ManagedSessionDescendantStore["readExpected"]>;
4568
+ let managedPublishedArtifacts: native.NativeDirectoryTreeSnapshot | undefined;
4569
+ hadSessionFile = managedMove ? false : this.storage.existsSync(oldSessionFile);
3533
4570
  let movedSessionFile = false;
3534
4571
  let movedArtifactDir = false;
3535
4572
  const materializedEntries = materializeResidentEntriesForReadSync(
@@ -3552,46 +4589,184 @@ export class SessionManager {
3552
4589
  }
3553
4590
  throw error;
3554
4591
  };
4592
+ const copyManagedTree = async (
4593
+ source: ManagedSessionDescendantStore,
4594
+ sourceRelative: string,
4595
+ destination: ManagedSessionDescendantStore,
4596
+ destinationRelative: string,
4597
+ snapshot: native.NativeDirectoryTreeSnapshot,
4598
+ ): Promise<void> => {
4599
+ destination.ensureDirectory(destinationRelative);
4600
+ for (const entry of snapshot.entries) {
4601
+ if (entry.relativePath === "") continue;
4602
+ const target = path.posix.join(destinationRelative, entry.relativePath);
4603
+ if (entry.kind === "directory") destination.ensureDirectory(target);
4604
+ else {
4605
+ const file = source.readExpected(path.posix.join(sourceRelative, entry.relativePath));
4606
+ if (
4607
+ !file ||
4608
+ file.identity.dev.toString() !== entry.dev ||
4609
+ file.identity.ino.toString() !== entry.ino ||
4610
+ file.identity.size.toString() !== entry.size ||
4611
+ file.identity.mtimeNs.toString() !== entry.mtimeNs ||
4612
+ crypto.createHash("sha256").update(file.bytes).digest("hex") !== entry.sha256
4613
+ )
4614
+ throw new Error("artifact_source_changed");
4615
+ await destination.publishNoReplace(target, file.bytes);
4616
+ }
4617
+ }
4618
+ const imported = destination.captureTree(destinationRelative);
4619
+ const comparable = (tree: native.NativeDirectoryTreeSnapshot) =>
4620
+ tree.entries.map(entry => ({
4621
+ relativePath: entry.relativePath,
4622
+ kind: entry.kind,
4623
+ size: entry.size,
4624
+ sha256: entry.sha256,
4625
+ }));
4626
+ if (JSON.stringify(comparable(imported)) !== JSON.stringify(comparable(snapshot)))
4627
+ throw new Error("artifact_destination_mismatch");
4628
+ destination.fsyncTree();
4629
+ };
3555
4630
  this.#disposeResidentTextBlobStore();
3556
-
3557
4631
  try {
3558
- // Guard: session file may not exist yet (no assistant messages persisted)
3559
- if (hadSessionFile) {
3560
- await movePathAcrossDevicesSafe(oldSessionFile, newSessionFile);
3561
- movedSessionFile = true;
3562
- }
3563
-
3564
- try {
3565
- const stat = await fs.promises.stat(oldArtifactDir);
3566
- if (stat.isDirectory()) {
3567
- await movePathAcrossDevicesSafe(oldArtifactDir, newArtifactDir);
3568
- movedArtifactDir = true;
4632
+ if (managedMove) {
4633
+ if (previousDestination.kind !== "managed" || nextDestination.kind !== "managed")
4634
+ throw new Error("managed_move_destination_unavailable");
4635
+ managedSourceStore = this.#managedTranscriptStore(oldSessionFile);
4636
+ managedDestinationStore = managedStoreFromContext(nextDestination.securityContext, newSessionDir);
4637
+ rollbackManagedMove = async () => {
4638
+ if (!managedSourceStore || !managedDestinationStore)
4639
+ throw new Error("managed_move_authority_unavailable");
4640
+ if (managedArtifacts && managedPublishedArtifacts) {
4641
+ await copyManagedTree(
4642
+ managedDestinationStore,
4643
+ path.basename(newArtifactDir),
4644
+ managedSourceStore,
4645
+ path.basename(oldArtifactDir),
4646
+ managedPublishedArtifacts,
4647
+ );
4648
+ }
4649
+ if (managedTranscript)
4650
+ await managedSourceStore.publishNoReplace(path.basename(oldSessionFile), managedTranscript.bytes);
4651
+ if (managedPublishedArtifacts)
4652
+ managedDestinationStore.removeTreeExpected(
4653
+ path.basename(newArtifactDir),
4654
+ managedPublishedArtifacts,
4655
+ );
4656
+ if (managedPublishedTranscript)
4657
+ managedDestinationStore.removeExpected(path.basename(newSessionFile), managedPublishedTranscript);
4658
+ };
4659
+ managedTranscript = managedSourceStore.readExpected(path.basename(oldSessionFile));
4660
+ hadSessionFile = managedTranscript !== null;
4661
+ if (managedTranscript) {
4662
+ await managedDestinationStore.publishNoReplace(
4663
+ path.basename(newSessionFile),
4664
+ managedTranscript.bytes,
4665
+ );
4666
+ managedPublishedTranscript = managedDestinationStore.readExpected(path.basename(newSessionFile));
4667
+ if (!managedPublishedTranscript) throw new Error("managed_transcript_publish_missing");
4668
+ movedSessionFile = true;
3569
4669
  }
3570
- } catch (err) {
3571
- if (!isEnoent(err)) throw err;
3572
- }
3573
- } catch (err) {
3574
- if (movedArtifactDir) {
3575
4670
  try {
3576
- await fs.promises.rename(newArtifactDir, oldArtifactDir);
3577
- } catch (rollbackErr) {
3578
- restoreResidentStateAndThrow(
3579
- new Error(
3580
- `Failed to move artifacts and rollback: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
3581
- ),
4671
+ managedArtifacts = managedSourceStore.captureTree(path.basename(oldArtifactDir));
4672
+ await copyManagedTree(
4673
+ managedSourceStore,
4674
+ path.basename(oldArtifactDir),
4675
+ managedDestinationStore,
4676
+ path.basename(newArtifactDir),
4677
+ managedArtifacts,
3582
4678
  );
4679
+ managedPublishedArtifacts = managedDestinationStore.captureTree(path.basename(newArtifactDir));
4680
+ movedArtifactDir = true;
4681
+ } catch (error) {
4682
+ if (!(error instanceof Error) || error.message !== "not_found") throw error;
4683
+ }
4684
+ if (managedPublishedTranscript) {
4685
+ const terminalTranscript = managedDestinationStore.readExpected(path.basename(newSessionFile));
4686
+ if (!managedFileSnapshotEquals(terminalTranscript, managedPublishedTranscript))
4687
+ throw new Error("managed_move_destination_transcript_changed");
4688
+ }
4689
+ if (managedPublishedArtifacts) {
4690
+ const terminalArtifacts = managedDestinationStore.captureTree(path.basename(newArtifactDir));
4691
+ if (!retainedTreeSnapshotEquals(terminalArtifacts, managedPublishedArtifacts))
4692
+ throw new Error("managed_move_destination_artifacts_changed");
4693
+ }
4694
+ if (managedArtifacts)
4695
+ managedSourceStore.removeTreeExpected(path.basename(oldArtifactDir), managedArtifacts);
4696
+ if (managedTranscript)
4697
+ managedSourceStore.removeExpected(path.basename(oldSessionFile), managedTranscript);
4698
+ } else {
4699
+ if (hadSessionFile && oldSessionFile !== newSessionFile) {
4700
+ await movePathAcrossDevicesSafe(oldSessionFile, newSessionFile);
4701
+ movedSessionFile = true;
4702
+ }
4703
+ try {
4704
+ const stat = await fs.promises.stat(oldArtifactDir);
4705
+ if (stat.isDirectory() && oldArtifactDir !== newArtifactDir) {
4706
+ await movePathAcrossDevicesSafe(oldArtifactDir, newArtifactDir);
4707
+ movedArtifactDir = true;
4708
+ }
4709
+ } catch (err) {
4710
+ if (!isEnoent(err)) throw err;
3583
4711
  }
3584
4712
  }
3585
- if (movedSessionFile) {
4713
+ } catch (err) {
4714
+ if (managedMove && managedDestinationStore) {
3586
4715
  try {
3587
- await fs.promises.rename(newSessionFile, oldSessionFile);
4716
+ if (managedArtifacts && !managedPublishedArtifacts) {
4717
+ try {
4718
+ managedPublishedArtifacts = managedDestinationStore.captureTree(path.basename(newArtifactDir));
4719
+ } catch (captureError) {
4720
+ if (!(captureError instanceof Error) || captureError.message !== "not_found")
4721
+ throw captureError;
4722
+ }
4723
+ }
4724
+ if (managedSourceStore && managedArtifacts && managedPublishedArtifacts) {
4725
+ let sourceArtifacts: native.NativeDirectoryTreeSnapshot | undefined;
4726
+ try {
4727
+ sourceArtifacts = managedSourceStore.captureTree(path.basename(oldArtifactDir));
4728
+ } catch (captureError) {
4729
+ if (!(captureError instanceof Error) || captureError.message !== "not_found")
4730
+ throw captureError;
4731
+ }
4732
+ if (!sourceArtifacts || !retainedTreeSnapshotEquals(sourceArtifacts, managedArtifacts)) {
4733
+ if (sourceArtifacts)
4734
+ managedSourceStore.removeTreeExpected(path.basename(oldArtifactDir), sourceArtifacts);
4735
+ await copyManagedTree(
4736
+ managedDestinationStore,
4737
+ path.basename(newArtifactDir),
4738
+ managedSourceStore,
4739
+ path.basename(oldArtifactDir),
4740
+ managedPublishedArtifacts,
4741
+ );
4742
+ const restored = managedSourceStore.captureTree(path.basename(oldArtifactDir));
4743
+ if (!retainedTreeSnapshotEqualsAfterRename(restored, managedArtifacts))
4744
+ throw new Error("managed_move_source_restore_mismatch");
4745
+ }
4746
+ }
4747
+ if (
4748
+ managedSourceStore &&
4749
+ managedTranscript &&
4750
+ !managedSourceStore.readExpected(path.basename(oldSessionFile))
4751
+ )
4752
+ await managedSourceStore.publishNoReplace(path.basename(oldSessionFile), managedTranscript.bytes);
4753
+ if (managedPublishedArtifacts)
4754
+ managedDestinationStore.removeTreeExpected(
4755
+ path.basename(newArtifactDir),
4756
+ managedPublishedArtifacts,
4757
+ );
4758
+ if (managedPublishedTranscript)
4759
+ managedDestinationStore.removeExpected(path.basename(newSessionFile), managedPublishedTranscript);
3588
4760
  } catch (rollbackErr) {
3589
4761
  restoreResidentStateAndThrow(
3590
- new Error(
3591
- `Failed to move session file and rollback: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
3592
- ),
4762
+ new Error(`Failed to rollback managed move: ${toError(rollbackErr).message}`, {
4763
+ cause: toError(err),
4764
+ }),
3593
4765
  );
3594
4766
  }
4767
+ } else {
4768
+ if (movedArtifactDir) await movePathAcrossDevicesSafe(newArtifactDir, oldArtifactDir);
4769
+ if (movedSessionFile) await movePathAcrossDevicesSafe(newSessionFile, oldSessionFile);
3595
4770
  }
3596
4771
  restoreResidentStateAndThrow(err);
3597
4772
  }
@@ -3602,34 +4777,92 @@ export class SessionManager {
3602
4777
  this.#bumpAllRevisions();
3603
4778
  }
3604
4779
 
3605
- // Update cwd and sessionDir after the move succeeds.
4780
+ // Update cwd and sessionDir after the physical move succeeds, but roll the move back if metadata persistence fails.
3606
4781
  this.cwd = resolvedCwd;
3607
4782
  this.sessionDir = newSessionDir;
4783
+ this.destination = nextDestination;
4784
+ this.#managedTranscriptStoreCache =
4785
+ managedMove && managedDestinationStore
4786
+ ? { directory: path.resolve(newSessionDir), store: managedDestinationStore }
4787
+ : null;
3608
4788
 
3609
- // Update the session header in fileEntries
3610
- const header = this.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
3611
- if (header) {
3612
- header.cwd = resolvedCwd;
3613
- this.#headerExportRevision++;
3614
- }
3615
-
3616
- // Rewrite the session file at its new location with updated header.
3617
- // hadSessionFile: file existed before move → must rewrite to update cwd
3618
- // hasAssistant: assistant messages in memory but file missing → recreate from memory
3619
- // Neither true → fresh session, never written → preserve lazy-persist
3620
4789
  const hasAssistant = this.#fileEntries.some(e => e.type === "message" && e.message.role === "assistant");
3621
- if (this.persist && this.#sessionFile && (hadSessionFile || hasAssistant)) {
3622
- await this.#rewriteFile();
4790
+ try {
4791
+ if (this.persist && this.#sessionFile && hadSessionFile) {
4792
+ await this.#appendHeaderPatch({ cwd: resolvedCwd });
4793
+ await this.#rewriteFile();
4794
+ } else if (this.persist && this.#sessionFile && hasAssistant) {
4795
+ await this.#appendHeaderPatch({ cwd: resolvedCwd });
4796
+ await this.#rewriteFile();
4797
+ } else {
4798
+ await this.#appendHeaderPatch({ cwd: resolvedCwd });
4799
+ }
4800
+ } catch (error) {
4801
+ const failedSessionFile = this.#sessionFile;
4802
+ const failedArtifactDir = failedSessionFile?.slice(0, -6);
4803
+ try {
4804
+ await this.#closePersistWriter().catch(() => {});
4805
+ this.#persistChain = Promise.resolve();
4806
+ this.#persistError = undefined;
4807
+ this.#persistErrorReported = false;
4808
+ if (rollbackManagedMove) {
4809
+ await rollbackManagedMove();
4810
+ this.#sessionFile = previousSessionFile;
4811
+ this.cwd = previousCwd;
4812
+ this.sessionDir = previousSessionDir;
4813
+ this.destination = previousDestination;
4814
+ this.#managedTranscriptStoreCache =
4815
+ managedMove && managedSourceStore
4816
+ ? { directory: path.resolve(previousSessionDir), store: managedSourceStore }
4817
+ : null;
4818
+ const header = this.#fileEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
4819
+ if (header) applyHeaderPatch(header, { cwd: previousCwd });
4820
+ this.#headerExportRevision++;
4821
+ throw error;
4822
+ }
4823
+ if (
4824
+ this.storage instanceof FileSessionStorage &&
4825
+ failedArtifactDir &&
4826
+ previousArtifactDir &&
4827
+ failedArtifactDir !== previousArtifactDir &&
4828
+ fs.existsSync(failedArtifactDir)
4829
+ ) {
4830
+ await movePathAcrossDevicesSafe(failedArtifactDir, previousArtifactDir);
4831
+ }
4832
+ if (
4833
+ this.storage instanceof FileSessionStorage &&
4834
+ failedSessionFile &&
4835
+ previousSessionFile &&
4836
+ failedSessionFile !== previousSessionFile &&
4837
+ fs.existsSync(failedSessionFile)
4838
+ ) {
4839
+ await movePathAcrossDevicesSafe(failedSessionFile, previousSessionFile);
4840
+ }
4841
+ this.#sessionFile = previousSessionFile;
4842
+ this.cwd = previousCwd;
4843
+ this.sessionDir = previousSessionDir;
4844
+ this.destination = previousDestination;
4845
+ this.#managedTranscriptStoreCache = null;
4846
+ const header = this.#fileEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
4847
+ if (header) applyHeaderPatch(header, { cwd: previousCwd });
4848
+ this.#headerExportRevision++;
4849
+ if (previousSessionFile && this.storage.existsSync(previousSessionFile)) await this.#rewriteFile();
4850
+ } catch (rollbackError) {
4851
+ throw new Error(`Failed to rollback session move: ${toError(rollbackError).message}`, {
4852
+ cause: toError(error),
4853
+ });
4854
+ }
4855
+ throw error;
3623
4856
  }
3624
4857
 
3625
- // Update terminal breadcrumb
4858
+ // Update terminal breadcrumb only after the durable cwd transition succeeds.
3626
4859
  if (this.#sessionFile) {
3627
4860
  writeTerminalBreadcrumb(resolvedCwd, this.#sessionFile);
3628
4861
  }
3629
4862
  }
3630
4863
 
3631
4864
  /** Sync version for initial creation (no existing writer to close) */
3632
- #newSessionSync(options?: NewSessionOptions): string | undefined {
4865
+ #newSessionSync(options?: NewSessionOptions, writeBreadcrumb = true): string | undefined {
3633
4866
  this.#persistChain = Promise.resolve();
3634
4867
  this.#persistError = undefined;
3635
4868
  this.#persistErrorReported = false;
@@ -3660,11 +4893,14 @@ export class SessionManager {
3660
4893
  this.#usageStatistics = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, premiumRequests: 0, cost: 0 };
3661
4894
  this.#inMemoryArtifacts = null;
3662
4895
  this.#inMemoryArtifactCounter = 0;
4896
+ this.#artifactManager = null;
4897
+ this.#artifactManagerSessionFile = null;
4898
+ this.#adoptedArtifactManager = null;
3663
4899
 
3664
4900
  if (this.persist) {
3665
4901
  const fileTimestamp = timestamp.replace(/[:.]/g, "-");
3666
4902
  this.#sessionFile = path.join(this.getSessionDir(), `${fileTimestamp}_${this.#sessionId}.jsonl`);
3667
- writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
4903
+ if (writeBreadcrumb) writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
3668
4904
  }
3669
4905
  this.#resetResidentTextBlobStore();
3670
4906
  return this.#sessionFile;
@@ -3738,6 +4974,10 @@ export class SessionManager {
3738
4974
  #ensurePersistWriter(): NdjsonFileWriter | undefined {
3739
4975
  if (!this.persist || !this.#sessionFile) return undefined;
3740
4976
  if (this.#persistError) throw this.#persistError;
4977
+ if (this.destination.kind === "managed") {
4978
+ this.#managedTranscriptStore();
4979
+ return undefined;
4980
+ }
3741
4981
  if (this.#persistWriter && this.#persistWriterPath === this.#sessionFile) {
3742
4982
  if (this.#persistWriter.isOpen()) return this.#persistWriter;
3743
4983
  // Cached writer for the current file is mid-close (queued
@@ -3788,7 +5028,11 @@ export class SessionManager {
3788
5028
  // shared `*.bak` glob on both real and in-memory storage backends and promote it back to
3789
5029
  // the primary on the next session-dir scan.
3790
5030
 
3791
- #replaceSessionFileAfterEpermSync(tempPath: string, targetPath: string, renameError: unknown): void {
5031
+ #replaceSessionFileAfterEpermSync(
5032
+ tempPath: string,
5033
+ targetPath: string,
5034
+ renameError: unknown,
5035
+ ): SessionFileReplacementSyncOutcome {
3792
5036
  const dir = path.resolve(targetPath, "..");
3793
5037
  const backupPath = path.join(dir, `${path.basename(targetPath)}.${Snowflake.next()}.bak`);
3794
5038
  try {
@@ -3796,7 +5040,7 @@ export class SessionManager {
3796
5040
  } catch (err) {
3797
5041
  if (isEnoent(err)) {
3798
5042
  this.storage.renameSync(tempPath, targetPath);
3799
- return;
5043
+ return { kind: "replaced" };
3800
5044
  }
3801
5045
  throw toError(renameError);
3802
5046
  }
@@ -3815,7 +5059,7 @@ export class SessionManager {
3815
5059
  { cause: originalError },
3816
5060
  );
3817
5061
  }
3818
- throw replaceError;
5062
+ return { kind: "restored_previous", error: replaceError };
3819
5063
  }
3820
5064
 
3821
5065
  try {
@@ -3829,6 +5073,7 @@ export class SessionManager {
3829
5073
  });
3830
5074
  }
3831
5075
  }
5076
+ return { kind: "replaced" };
3832
5077
  }
3833
5078
 
3834
5079
  async #replaceSessionFileAfterEperm(tempPath: string, targetPath: string, renameError: unknown): Promise<void> {
@@ -3883,21 +5128,44 @@ export class SessionManager {
3883
5128
  }
3884
5129
  }
3885
5130
 
3886
- #replaceSessionFileSync(tempPath: string, targetPath: string): void {
5131
+ #replaceSessionFileSync(tempPath: string, targetPath: string): SessionFileReplacementSyncOutcome {
3887
5132
  try {
3888
5133
  this.storage.renameSync(tempPath, targetPath);
5134
+ return { kind: "replaced" };
3889
5135
  } catch (err) {
3890
5136
  if (hasFsCode(err, "EPERM")) {
3891
- this.#replaceSessionFileAfterEpermSync(tempPath, targetPath, err);
3892
- return;
5137
+ return this.#replaceSessionFileAfterEpermSync(tempPath, targetPath, err);
3893
5138
  }
3894
5139
 
3895
5140
  throw toError(err);
3896
5141
  }
3897
5142
  }
3898
5143
 
5144
+ #managedTranscriptStore(sessionFile = this.#sessionFile): ManagedSessionDescendantStore {
5145
+ if (this.destination.kind !== "managed" || !sessionFile) {
5146
+ throw new Error("Managed transcript authority is unavailable");
5147
+ }
5148
+ const sessionDir = path.resolve(path.dirname(sessionFile));
5149
+ if (sessionDir !== path.resolve(this.destination.directory)) {
5150
+ throw new Error("Managed transcript escaped its session directory");
5151
+ }
5152
+ if (this.#managedTranscriptStoreCache) {
5153
+ if (this.#managedTranscriptStoreCache.directory !== sessionDir)
5154
+ throw new Error("Managed transcript store transition was not verified");
5155
+ return this.#managedTranscriptStoreCache.store;
5156
+ }
5157
+ const store = managedStoreFromContext(this.destination.securityContext, sessionDir);
5158
+ this.#managedTranscriptStoreCache = { directory: sessionDir, store };
5159
+ return store;
5160
+ }
5161
+
3899
5162
  #writeEntriesAtomicallySync(entries: FileEntry[]): void {
3900
5163
  if (!this.#sessionFile) return;
5164
+ if (this.destination.kind === "managed") {
5165
+ const bytes = Buffer.from(`${entries.map(entry => JSON.stringify(entry)).join("\n")}\n`, "utf8");
5166
+ this.#managedTranscriptStore().replaceSync(path.basename(this.#sessionFile), bytes);
5167
+ return;
5168
+ }
3901
5169
  const dir = path.resolve(this.#sessionFile, "..");
3902
5170
  const tempPath = path.join(dir, `.${path.basename(this.#sessionFile)}.${Snowflake.next()}.tmp`);
3903
5171
  const writer = new NdjsonFileWriter(this.storage, tempPath, { flags: "w" });
@@ -3906,7 +5174,8 @@ export class SessionManager {
3906
5174
  writer.writeSync(entry);
3907
5175
  }
3908
5176
  writer.closeSync();
3909
- this.#replaceSessionFileSync(tempPath, this.#sessionFile);
5177
+ const replacement = this.#replaceSessionFileSync(tempPath, this.#sessionFile);
5178
+ if (replacement.kind === "restored_previous") throw replacement.error;
3910
5179
  } catch (err) {
3911
5180
  // closeSync is now truthful and may throw; wrap the best-effort cleanup so
3912
5181
  // the original error (write/close failure) is the one surfaced, not the
@@ -3923,6 +5192,11 @@ export class SessionManager {
3923
5192
  }
3924
5193
  async #writeEntriesAtomically(entries: FileEntry[]): Promise<void> {
3925
5194
  if (!this.#sessionFile) return;
5195
+ if (this.destination.kind === "managed") {
5196
+ const bytes = Buffer.from(`${entries.map(entry => JSON.stringify(entry)).join("\n")}\n`, "utf8");
5197
+ await this.#managedTranscriptStore().replace(path.basename(this.#sessionFile), bytes);
5198
+ return;
5199
+ }
3926
5200
  const dir = path.resolve(this.#sessionFile, "..");
3927
5201
  const tempPath = path.join(dir, `.${path.basename(this.#sessionFile)}.${Snowflake.next()}.tmp`);
3928
5202
  const writer = new NdjsonFileWriter(this.storage, tempPath, { flags: "w" });
@@ -3949,19 +5223,58 @@ export class SessionManager {
3949
5223
  }
3950
5224
  }
3951
5225
 
3952
- async #rewriteFile(): Promise<void> {
3953
- if (!this.persist || !this.#sessionFile) return;
3954
- await this.#queuePersistTask(async () => {
3955
- await this.#closePersistWriterInternal();
3956
- const entries = await Promise.all(
3957
- materializeResidentEntriesForPersistenceSync(this.#fileEntries, this.#residentBlobStores()).map(entry =>
5226
+ async #writeStagedDefaultModelSelection(
5227
+ entries: readonly FileEntry[],
5228
+ sessionFile: string,
5229
+ ): Promise<string | undefined> {
5230
+ if (this.destination.kind === "managed") return undefined;
5231
+ if (!this.storage.existsSync(sessionFile)) return undefined;
5232
+ const dir = path.resolve(sessionFile, "..");
5233
+ const tempPath = path.join(dir, `.${path.basename(sessionFile)}.${Snowflake.next()}.default-selection.tmp`);
5234
+ const writer = new NdjsonFileWriter(this.storage, tempPath, {
5235
+ flags: "w",
5236
+ });
5237
+ try {
5238
+ const persistedEntries = await Promise.all(
5239
+ materializeResidentEntriesForPersistenceSync([...entries], this.#residentBlobStores()).map(entry =>
3958
5240
  prepareEntryForPersistence(entry, this.#blobStore),
3959
5241
  ),
3960
5242
  );
3961
- await this.#writeEntriesAtomically(entries);
3962
- this.#needsFullRewriteOnNextPersist = false;
3963
- this.#flushed = true;
3964
- this.#ensuredOnDisk = true;
5243
+ for (const entry of persistedEntries) {
5244
+ await writer.write(entry);
5245
+ }
5246
+ await writer.flush();
5247
+ await writer.fsync();
5248
+ await writer.close();
5249
+ return tempPath;
5250
+ } catch (error) {
5251
+ try {
5252
+ await writer.close();
5253
+ } catch {}
5254
+ try {
5255
+ await this.storage.unlink(tempPath);
5256
+ } catch {}
5257
+ throw toError(error);
5258
+ }
5259
+ }
5260
+
5261
+ async #rewriteFileContents(): Promise<void> {
5262
+ if (!this.persist || !this.#sessionFile) return;
5263
+ await this.#closePersistWriterInternal();
5264
+ const entries = await Promise.all(
5265
+ materializeResidentEntriesForPersistenceSync(this.#fileEntries, this.#residentBlobStores()).map(entry =>
5266
+ prepareEntryForPersistence(entry, this.#blobStore),
5267
+ ),
5268
+ );
5269
+ await this.#writeEntriesAtomically(entries);
5270
+ this.#needsFullRewriteOnNextPersist = false;
5271
+ this.#flushed = true;
5272
+ this.#ensuredOnDisk = true;
5273
+ }
5274
+
5275
+ async #rewriteFile(): Promise<void> {
5276
+ await this.#queuePersistTask(async () => {
5277
+ await this.#rewriteFileContents();
3965
5278
  });
3966
5279
  }
3967
5280
 
@@ -3981,6 +5294,121 @@ export class SessionManager {
3981
5294
  return this.persist;
3982
5295
  }
3983
5296
 
5297
+ async stageDefaultModelSelection(
5298
+ model: string,
5299
+ thinkingLevel: string | undefined,
5300
+ options?: { readonly appendThinkingLevel: boolean },
5301
+ ): Promise<DefaultModelSelectionStage> {
5302
+ const entryRevision = this.#entryRevision;
5303
+ const headerExportRevision = this.#headerExportRevision;
5304
+ const sessionId = this.#sessionId;
5305
+ const sessionFile = this.#sessionFile;
5306
+ const entryIds = new Map(this.#byId);
5307
+ let parentId = this.#leafId;
5308
+ const entries = [...this.#fileEntries];
5309
+ const temporaryEntry: ModelChangeEntry = {
5310
+ type: "model_change",
5311
+ id: generateId(entryIds),
5312
+ parentId,
5313
+ timestamp: new Date().toISOString(),
5314
+ model,
5315
+ role: "temporary",
5316
+ };
5317
+ entryIds.set(temporaryEntry.id, temporaryEntry);
5318
+ entries.push(temporaryEntry);
5319
+ parentId = temporaryEntry.id;
5320
+ if (options?.appendThinkingLevel) {
5321
+ const thinkingEntry: ThinkingLevelChangeEntry = {
5322
+ type: "thinking_level_change",
5323
+ id: generateId(entryIds),
5324
+ parentId,
5325
+ timestamp: new Date().toISOString(),
5326
+ thinkingLevel: thinkingLevel ?? null,
5327
+ };
5328
+ entryIds.set(thinkingEntry.id, thinkingEntry);
5329
+ entries.push(thinkingEntry);
5330
+ parentId = thinkingEntry.id;
5331
+ }
5332
+ const modelEntry: ModelChangeEntry = {
5333
+ type: "model_change",
5334
+ id: generateId(entryIds),
5335
+ parentId,
5336
+ timestamp: new Date().toISOString(),
5337
+ model,
5338
+ role: "default",
5339
+ };
5340
+ entries.push(modelEntry);
5341
+ const persistsToExistingFile = this.persist && sessionFile !== undefined && this.storage.existsSync(sessionFile);
5342
+ const tempPath = persistsToExistingFile
5343
+ ? await this.#writeStagedDefaultModelSelection(entries, sessionFile)
5344
+ : undefined;
5345
+ return {
5346
+ entryRevision,
5347
+ headerExportRevision,
5348
+ sessionId,
5349
+ sessionFile,
5350
+ entries,
5351
+ tempPath,
5352
+ persistsToExistingFile,
5353
+ };
5354
+ }
5355
+
5356
+ promoteDefaultModelSelection(stage: DefaultModelSelectionStage): DefaultModelSelectionPromotion {
5357
+ if (
5358
+ stage.entryRevision !== this.#entryRevision ||
5359
+ stage.headerExportRevision !== this.#headerExportRevision ||
5360
+ stage.sessionId !== this.#sessionId ||
5361
+ stage.sessionFile !== this.#sessionFile
5362
+ ) {
5363
+ return { kind: "not_promoted" };
5364
+ }
5365
+ if (stage.persistsToExistingFile) {
5366
+ if (!this.#sessionFile) return { kind: "unknown", error: new Error("Missing staged session replacement") };
5367
+ if (this.destination.kind === "managed") {
5368
+ try {
5369
+ const persistedEntries = materializeResidentEntriesForPersistenceSync(
5370
+ [...stage.entries],
5371
+ this.#residentBlobStores(),
5372
+ ).map(entry => prepareEntryForPersistenceSync(entry, this.#blobStore));
5373
+ this.#writeEntriesAtomicallySync(persistedEntries);
5374
+ } catch (error) {
5375
+ return { kind: "unknown", error: toError(error) };
5376
+ }
5377
+ } else {
5378
+ if (!stage.tempPath) return { kind: "unknown", error: new Error("Missing staged session replacement") };
5379
+ try {
5380
+ this.#closePersistWriterInternalSync();
5381
+ const replacement = this.#replaceSessionFileSync(stage.tempPath, this.#sessionFile);
5382
+ if (replacement.kind === "restored_previous") {
5383
+ return { kind: "not_promoted", error: replacement.error };
5384
+ }
5385
+ } catch (error) {
5386
+ if (this.#persistWriter?.getCloseState() === "close_failed_retryable") {
5387
+ return { kind: "not_promoted", error: new Error("Session replacement could not be completed.") };
5388
+ }
5389
+ return { kind: "unknown", error: toError(error) };
5390
+ }
5391
+ }
5392
+ }
5393
+ this.#fileEntries = [...stage.entries];
5394
+ this.#resetResidentTextBlobStore();
5395
+ this.#reexternalizeFileEntriesForResidentStore();
5396
+ this.#needsFullRewriteOnNextPersist = false;
5397
+ this.#flushed = stage.persistsToExistingFile;
5398
+ this.#ensuredOnDisk = stage.persistsToExistingFile;
5399
+ this.#bumpAllRevisions();
5400
+ return { kind: "promoted" };
5401
+ }
5402
+
5403
+ async discardDefaultModelSelectionStage(stage: DefaultModelSelectionStage): Promise<void> {
5404
+ if (!stage.tempPath) return;
5405
+ try {
5406
+ await this.storage.unlink(stage.tempPath);
5407
+ } catch (error) {
5408
+ if (!isEnoent(error)) throw toError(error);
5409
+ }
5410
+ }
5411
+
3984
5412
  /**
3985
5413
  * Force-persist all current entries to disk, even when no assistant message exists yet.
3986
5414
  * Used by ACP mode where session/new must create a discoverable session immediately.
@@ -4107,15 +5535,62 @@ export class SessionManager {
4107
5535
  * Returns the session artifacts directory path (session file path without .jsonl).
4108
5536
  * Returns null when the session is not persisted to a file.
4109
5537
  * When this session has adopted an external ArtifactManager (subagent case),
4110
- * returns that manager's directory so reads/writes land in the shared parent
4111
- * dir instead of a private (non-existent) subdir.
5538
+ * never exposes that managed directory as a pathname. Reads and writes use the
5539
+ * adopted manager capability directly.
4112
5540
  */
4113
5541
  getArtifactsDir(): string | null {
4114
- if (this.#adoptedArtifactManager) return this.#adoptedArtifactManager.dir;
5542
+ if (this.#adoptedArtifactManager) return null;
4115
5543
  const sessionFile = this.#sessionFile;
4116
5544
  return sessionFile ? sessionFile.slice(0, -6) : null;
4117
5545
  }
4118
5546
 
5547
+ isManagedDestination(): boolean {
5548
+ return this.destination.kind === "managed";
5549
+ }
5550
+
5551
+ /** Supplies opaque retained authority for mandatory managed legacy local migration. */
5552
+ getManagedLegacyLocalMigrationSource(): ManagedLegacyLocalMigrationSource | null {
5553
+ if (this.destination.kind !== "managed" || !this.#sessionFile || this.#adoptedArtifactManager) return null;
5554
+ const store = this.#managedTranscriptStore();
5555
+ return {
5556
+ capture: async () => {
5557
+ let snapshot: native.NativeDirectoryTreeSnapshot;
5558
+ try {
5559
+ snapshot = store.captureTree("local");
5560
+ } catch (error) {
5561
+ if (error instanceof Error && error.message === "not_found") return null;
5562
+ throw error;
5563
+ }
5564
+ let totalBytes = 0;
5565
+ for (const entry of snapshot.entries) if (entry.kind === "file") totalBytes += Number(entry.size);
5566
+ if (totalBytes > 64 * 1024 * 1024) throw new Error("Legacy local:// migration exceeds the safe size limit");
5567
+ const entries = snapshot.entries.map(entry => {
5568
+ if (entry.kind === "directory") return { relativePath: entry.relativePath, kind: "directory" as const };
5569
+ const captured = store.readExpected(path.posix.join("local", entry.relativePath));
5570
+ if (
5571
+ !captured ||
5572
+ captured.identity.dev.toString() !== entry.dev ||
5573
+ captured.identity.ino.toString() !== entry.ino ||
5574
+ captured.identity.size.toString() !== entry.size ||
5575
+ crypto.createHash("sha256").update(captured.bytes).digest("hex") !== entry.sha256
5576
+ )
5577
+ throw new Error("Legacy local:// migration source changed during capture");
5578
+ return {
5579
+ relativePath: entry.relativePath,
5580
+ kind: "file" as const,
5581
+ bytes: captured.bytes,
5582
+ sha256: entry.sha256,
5583
+ };
5584
+ });
5585
+ const verified = store.captureTree("local");
5586
+ if (JSON.stringify(verified) !== JSON.stringify(snapshot))
5587
+ throw new Error("Legacy local:// migration source changed during capture");
5588
+ return { snapshot, entries };
5589
+ },
5590
+ retire: snapshot => store.removeTreeExpected("local", snapshot),
5591
+ };
5592
+ }
5593
+
4119
5594
  /**
4120
5595
  * Adopt an externally-owned ArtifactManager. Used by subagents to share
4121
5596
  * the parent session's artifact directory and ID counter.
@@ -4151,7 +5626,12 @@ export class SessionManager {
4151
5626
  return this.#artifactManager;
4152
5627
  }
4153
5628
 
4154
- const manager = new ArtifactManager(sessionFile.slice(0, -6));
5629
+ const artifactDir = sessionFile.slice(0, -6);
5630
+ let artifactStorage: string | ManagedSessionDescendantStore = artifactDir;
5631
+ if (this.destination.kind === "managed") {
5632
+ artifactStorage = this.#managedTranscriptStore(sessionFile).deriveSubtree(path.basename(artifactDir));
5633
+ }
5634
+ const manager = new ArtifactManager(artifactStorage);
4155
5635
  this.#artifactManager = manager;
4156
5636
  this.#artifactManagerSessionFile = sessionFile;
4157
5637
  return manager;
@@ -4192,21 +5672,43 @@ export class SessionManager {
4192
5672
  }
4193
5673
 
4194
5674
  /**
4195
- * Path to the unsent-input draft sidecar for the current session. Lives inside
4196
- * the artifacts directory so it is removed together with the session on
4197
- * `dropSession`. Returns null when the session has no on-disk identity.
5675
+ * Path to the unsent-input draft sidecar for an explicit session. It lives
5676
+ * inside the artifacts directory so `dropSession` removes it with its owner.
5677
+ * Managed drafts use a retained descendant-store capability instead.
4198
5678
  */
4199
5679
  #getDraftPath(): string | null {
5680
+ if (this.destination.kind === "managed") return null;
4200
5681
  const dir = this.getArtifactsDir();
4201
5682
  return dir ? path.join(dir, "draft.txt") : null;
4202
5683
  }
4203
5684
 
5685
+ /** Retained authority for this managed session's private draft sidecar. */
5686
+ #getManagedDraftStore(): ManagedSessionDescendantStore | null {
5687
+ if (this.destination.kind !== "managed" || !this.#sessionFile) return null;
5688
+ return this.#managedTranscriptStore().deriveSubtree(path.basename(this.#sessionFile.slice(0, -6)));
5689
+ }
5690
+
4204
5691
  /**
4205
5692
  * Persist (or clear) the current editor draft so the next resume of this
4206
5693
  * session can restore it. Empty text deletes any stale draft. No-op when the
4207
5694
  * session is not persisted.
4208
5695
  */
4209
5696
  async saveDraft(text: string): Promise<void> {
5697
+ if (this.destination.kind === "managed") {
5698
+ if (text.length === 0) {
5699
+ await this.#getManagedDraftStore()?.remove("draft.txt");
5700
+ return;
5701
+ }
5702
+ // Force the session header onto disk so resume can find the file we are
5703
+ // attaching this draft to. Without this, a session whose first message
5704
+ // never produced an assistant reply would persist a draft next to a
5705
+ // session file that does not exist on disk.
5706
+ await this.ensureOnDisk();
5707
+ const store = this.#getManagedDraftStore();
5708
+ if (!store) return;
5709
+ await store.replace("draft.txt", Buffer.from(text, "utf8"));
5710
+ return;
5711
+ }
4210
5712
  const draftPath = this.#getDraftPath();
4211
5713
  if (!draftPath || !this.persist) return;
4212
5714
  if (text.length === 0) {
@@ -4222,7 +5724,9 @@ export class SessionManager {
4222
5724
  // never produced an assistant reply would persist a draft next to a
4223
5725
  // session file that does not exist on disk.
4224
5726
  await this.ensureOnDisk();
4225
- await this.storage.writeText(draftPath, text);
5727
+ const artifactManager = this.#getOrCreateArtifactManager();
5728
+ if (!artifactManager) return;
5729
+ await artifactManager.replaceNamed("draft.txt", text);
4226
5730
  }
4227
5731
 
4228
5732
  /**
@@ -4231,6 +5735,10 @@ export class SessionManager {
4231
5735
  * sidecar so a subsequent resume does not re-restore the same text.
4232
5736
  */
4233
5737
  async consumeDraft(): Promise<string | null> {
5738
+ if (this.destination.kind === "managed") {
5739
+ const draft = await this.#getManagedDraftStore()?.consume("draft.txt");
5740
+ return draft ? Buffer.from(draft).toString("utf8") : null;
5741
+ }
4234
5742
  const draftPath = this.#getDraftPath();
4235
5743
  if (!draftPath) return null;
4236
5744
  let text: string;
@@ -4262,7 +5770,8 @@ export class SessionManager {
4262
5770
  return name
4263
5771
  .replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
4264
5772
  .replace(/ +/g, " ")
4265
- .trim();
5773
+ .trim()
5774
+ .slice(0, SESSION_NAME_MAX_CHARS);
4266
5775
  }
4267
5776
 
4268
5777
  /**
@@ -4279,23 +5788,51 @@ export class SessionManager {
4279
5788
 
4280
5789
  this.#sessionName = sanitized;
4281
5790
  this.#titleSource = source;
5791
+ await this.#appendHeaderPatch({ title: sanitized, titleSource: source });
5792
+ return true;
5793
+ }
4282
5794
 
4283
- // Update the in-memory header (so first flush includes title)
4284
- const header = this.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
4285
- if (header) {
4286
- header.title = sanitized;
4287
- header.titleSource = source;
4288
- }
5795
+ async #appendHeaderPatch(patch: HeaderPatchRecord["patch"]): Promise<void> {
5796
+ const header = this.#fileEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
5797
+ if (!header) return;
5798
+ applyHeaderPatch(header, patch);
4289
5799
  this.#headerExportRevision++;
5800
+ await this.#persistPatch({ type: "header_patch", patch });
5801
+ }
4290
5802
 
4291
- // Update the session file header with the title (if already flushed)
4292
- const sessionFile = this.#sessionFile;
4293
- if (this.persist && sessionFile && this.storage.existsSync(sessionFile)) {
4294
- await this.#rewriteFile();
4295
- }
4296
- return true;
5803
+ #appendManagedRecordSync(record: FileEntry | SessionPatchRecord): void {
5804
+ if (!this.#sessionFile) throw new Error("Managed transcript path is unavailable");
5805
+ const store = this.#managedTranscriptStore();
5806
+ const relativePath = path.basename(this.#sessionFile);
5807
+ store.appendSync(relativePath, Buffer.from(`${JSON.stringify(record)}\n`, "utf8"));
4297
5808
  }
4298
5809
 
5810
+ async #persistPatch(record: SessionPatchRecord): Promise<void> {
5811
+ if (!this.persist || !this.#sessionFile || !this.storage.existsSync(this.#sessionFile)) return;
5812
+ await this.#queuePersistTask(async () => {
5813
+ const header = this.#fileEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
5814
+ if (this.#needsFullRewriteOnNextPersist || !this.#flushed || (header?.version ?? 1) < CURRENT_SESSION_VERSION)
5815
+ return this.#rewriteFileContents();
5816
+ const persistedRecord =
5817
+ record.type === "entry_patch"
5818
+ ? (prepareEntryForPersistenceSync(
5819
+ materializeResidentEntryForPersistenceSync(
5820
+ record as unknown as FileEntry,
5821
+ this.#residentBlobStores(),
5822
+ new Map(),
5823
+ ),
5824
+ this.#blobStore,
5825
+ ) as unknown as SessionPatchRecord)
5826
+ : record;
5827
+ if (this.destination.kind === "managed") {
5828
+ this.#appendManagedRecordSync(persistedRecord);
5829
+ return;
5830
+ }
5831
+ const writer = this.#ensurePersistWriter();
5832
+ if (!writer) return this.#rewriteFileContents();
5833
+ await writer.write(persistedRecord);
5834
+ });
5835
+ }
4299
5836
  _persist(entry: SessionEntry): void {
4300
5837
  if (!this.persist || !this.#sessionFile) return;
4301
5838
  if (this.#persistError) throw this.#persistError;
@@ -4308,6 +5845,7 @@ export class SessionManager {
4308
5845
  if (!hasAssistant) {
4309
5846
  // Mark as not flushed so when assistant arrives, all entries get written.
4310
5847
  this.#flushed = false;
5848
+
4311
5849
  this.#ensuredOnDisk = false;
4312
5850
  return;
4313
5851
  }
@@ -4334,6 +5872,16 @@ export class SessionManager {
4334
5872
  // the synchronous blob-store path so blob bytes are durable before the JSONL
4335
5873
  // line referencing them is written.
4336
5874
  try {
5875
+ if (this.destination.kind === "managed") {
5876
+ const materializedEntry = materializeResidentEntryForPersistenceSync(
5877
+ entry,
5878
+ this.#residentBlobStores(),
5879
+ new Map(),
5880
+ );
5881
+ const persistedEntry = prepareEntryForPersistenceSync(materializedEntry, this.#blobStore);
5882
+ this.#appendManagedRecordSync(persistedEntry);
5883
+ return;
5884
+ }
4337
5885
  const writer = this.#ensurePersistWriter();
4338
5886
  if (!writer) {
4339
5887
  // `#ensurePersistWriter` returns undefined here only when the cached
@@ -4389,6 +5937,24 @@ export class SessionManager {
4389
5937
  }
4390
5938
  }
4391
5939
 
5940
+ /** Append a configured fallback chain as child of current leaf, then advance leaf. Returns entry id. */
5941
+ appendConfiguredModelChain(chain: ConfiguredModelChain): string {
5942
+ const entry: ConfiguredModelChainEntry = {
5943
+ type: "configured_model_chain",
5944
+ id: generateId(this.#byId),
5945
+ parentId: this.#leafId,
5946
+ timestamp: new Date().toISOString(),
5947
+ role: chain.role,
5948
+ entries: [...chain.entries],
5949
+ origin: chain.origin,
5950
+ identity: chain.identity,
5951
+ explicitHead: chain.explicitHead,
5952
+ cleared: chain.cleared,
5953
+ };
5954
+ this.#appendEntry(entry);
5955
+ return entry.id;
5956
+ }
5957
+
4392
5958
  /** Append a message as child of current leaf, then advance leaf. Returns entry id.
4393
5959
  * Does not allow writing CompactionSummaryMessage and BranchSummaryMessage directly.
4394
5960
  * Reason: we want these to be top-level entries in the session, not message session entries,
@@ -4583,6 +6149,22 @@ export class SessionManager {
4583
6149
  this.#replayMetadataRevision++;
4584
6150
  }
4585
6151
 
6152
+ /** Write mutated custom-message entries back into the canonical entry store by id. */
6153
+ applyCustomMessageEntryUpdates(entries: readonly CustomMessageEntry[]): void {
6154
+ for (const updated of entries) {
6155
+ const canonical = this.#byId.get(updated.id);
6156
+ if (canonical?.type !== "custom_message") continue;
6157
+ canonical.content = updated.content;
6158
+ canonical.details = updated.details;
6159
+ // Pruning replaces content permanently; retaining a cold-spill marker would
6160
+ // rehydrate the superseded payload on the next materialization.
6161
+ canonical.evictedContent = undefined;
6162
+ }
6163
+ this.#needsFullRewriteOnNextPersist = true;
6164
+ this.#bumpEntryRevision();
6165
+ this.#replayMetadataRevision++;
6166
+ }
6167
+
4586
6168
  /**
4587
6169
  * Rewrite the session file after in-place entry updates.
4588
6170
  * Use sparingly (e.g., pruning old tool outputs).
@@ -4638,18 +6220,29 @@ export class SessionManager {
4638
6220
  // TTSR (Time Traveling Stream Rules)
4639
6221
  // =========================================================================
4640
6222
 
4641
- /**
4642
- * Append an MCP tool selection entry recording the discovery-selected MCP tools.
4643
- * @param selectedToolNames MCP tool names selected for this branch
4644
- * @returns Entry id
4645
- */
4646
- appendMCPToolSelection(selectedToolNames: string[]): string {
6223
+ /** Append MCP discovery selection authority without altering discovered built-in authority. */
6224
+ appendMCPToolSelection(selectedToolNames: string[], mutationCorrelationId?: string): string {
4647
6225
  const entry: MCPToolSelectionEntry = {
4648
6226
  type: "mcp_tool_selection",
4649
6227
  id: generateId(this.#byId),
4650
6228
  parentId: this.#leafId,
4651
6229
  timestamp: new Date().toISOString(),
4652
6230
  selectedToolNames: [...selectedToolNames],
6231
+ mutationCorrelationId,
6232
+ };
6233
+ this.#appendEntry(entry);
6234
+ return entry.id;
6235
+ }
6236
+
6237
+ /** Append discovered built-in selection authority without altering MCP authority. */
6238
+ appendDiscoveredBuiltinToolSelection(selectedToolNames: string[], mutationCorrelationId?: string): string {
6239
+ const entry: DiscoveredBuiltinToolSelectionEntry = {
6240
+ type: "discovered_builtin_tool_selection",
6241
+ id: generateId(this.#byId),
6242
+ parentId: this.#leafId,
6243
+ timestamp: new Date().toISOString(),
6244
+ selectedToolNames: [...selectedToolNames],
6245
+ mutationCorrelationId,
4653
6246
  };
4654
6247
  this.#appendEntry(entry);
4655
6248
  return entry.id;
@@ -4702,7 +6295,9 @@ export class SessionManager {
4702
6295
  getLeafEntry(): SessionEntry | undefined {
4703
6296
  if (!this.#leafId) return undefined;
4704
6297
  const entry = this.#byId.get(this.#leafId);
4705
- return entry ? materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map()) : undefined;
6298
+ return entry
6299
+ ? cloneSessionEntry(materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map()))
6300
+ : undefined;
4706
6301
  }
4707
6302
 
4708
6303
  getResidentImageBytes(): number {
@@ -4887,10 +6482,12 @@ export class SessionManager {
4887
6482
  getEntryForFidelity(id: string): SessionEntry | undefined {
4888
6483
  const entry = this.#byId.get(id);
4889
6484
  return entry
4890
- ? rehydrateColdSpillEntry(
4891
- materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map()),
4892
- this.#blobStore,
4893
- this.#residentBlobStoresForColdRehydrate(),
6485
+ ? cloneSessionEntry(
6486
+ rehydrateColdSpillEntry(
6487
+ materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map()),
6488
+ this.#blobStore,
6489
+ this.#residentBlobStoresForColdRehydrate(),
6490
+ ),
4894
6491
  )
4895
6492
  : undefined;
4896
6493
  }
@@ -4904,10 +6501,12 @@ export class SessionManager {
4904
6501
  if (visited.has(current.id)) break;
4905
6502
  visited.add(current.id);
4906
6503
  path.push(
4907
- rehydrateColdSpillEntry(
4908
- materializeResidentEntryForReadSync(current, this.#residentBlobStores(), cache),
4909
- this.#blobStore,
4910
- this.#residentBlobStoresForColdRehydrate(),
6504
+ cloneSessionEntry(
6505
+ rehydrateColdSpillEntry(
6506
+ materializeResidentEntryForReadSync(current, this.#residentBlobStores(), cache),
6507
+ this.#blobStore,
6508
+ this.#residentBlobStoresForColdRehydrate(),
6509
+ ),
4911
6510
  ),
4912
6511
  );
4913
6512
  current = current.parentId ? this.#byId.get(current.parentId) : undefined;
@@ -4944,10 +6543,12 @@ export class SessionManager {
4944
6543
  return this.#fileEntries
4945
6544
  .filter((entry): entry is SessionEntry => entry.type !== "session")
4946
6545
  .map(entry =>
4947
- rehydrateColdSpillEntry(
4948
- materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), cache),
4949
- this.#blobStore,
4950
- this.#residentBlobStoresForColdRehydrate(),
6546
+ cloneSessionEntry(
6547
+ rehydrateColdSpillEntry(
6548
+ materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), cache),
6549
+ this.#blobStore,
6550
+ this.#residentBlobStoresForColdRehydrate(),
6551
+ ),
4951
6552
  ),
4952
6553
  );
4953
6554
  }
@@ -4956,7 +6557,9 @@ export class SessionManager {
4956
6557
  this.#publicMaterializerCallCount++;
4957
6558
  this.#getEntryMaterializerCallCount++;
4958
6559
  const entry = this.#byId.get(id);
4959
- return entry ? materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map()) : undefined;
6560
+ return entry
6561
+ ? cloneSessionEntry(materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map()))
6562
+ : undefined;
4960
6563
  }
4961
6564
 
4962
6565
  /**
@@ -4967,7 +6570,9 @@ export class SessionManager {
4967
6570
  const children: SessionEntry[] = [];
4968
6571
  for (const entry of this.#byId.values()) {
4969
6572
  if (entry.parentId === parentId) {
4970
- children.push(materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), cache));
6573
+ children.push(
6574
+ cloneSessionEntry(materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), cache)),
6575
+ );
4971
6576
  }
4972
6577
  }
4973
6578
  return children;
@@ -5022,7 +6627,7 @@ export class SessionManager {
5022
6627
  while (current) {
5023
6628
  if (visited.has(current.id)) break;
5024
6629
  visited.add(current.id);
5025
- path.push(materializeResidentEntryForReadSync(current, this.#residentBlobStores(), cache));
6630
+ path.push(cloneSessionEntry(materializeResidentEntryForReadSync(current, this.#residentBlobStores(), cache)));
5026
6631
  current = current.parentId ? this.#byId.get(current.parentId) : undefined;
5027
6632
  }
5028
6633
  path.reverse();
@@ -5033,15 +6638,26 @@ export class SessionManager {
5033
6638
  * Build the session context (what gets sent to the LLM).
5034
6639
  * Uses tree traversal from current leaf.
5035
6640
  */
6641
+ /**
6642
+ * Return a defensive context snapshot for public consumers.
6643
+ */
5036
6644
  buildSessionContext(): SessionContext {
5037
- const cached = this.#sessionContextCache?.deref();
6645
+ return cloneSessionContext(this.#getSessionContextForRead());
6646
+ }
6647
+
6648
+ /**
6649
+ * Return the revision-keyed context cache for internal read-only consumers.
6650
+ * The strong reference avoids GC-driven rebuilds during a session.
6651
+ */
6652
+ #getSessionContextForRead(): Readonly<SessionContext> {
6653
+ const cached = this.#sessionContextCache;
5038
6654
  if (
5039
6655
  cached &&
5040
6656
  this.#sessionContextEntryRevision === this.#entryRevision &&
5041
6657
  this.#sessionContextLeafRevision === this.#leafRevision &&
5042
6658
  this.#sessionContextReplayMetadataRevision === this.#replayMetadataRevision
5043
6659
  ) {
5044
- return cloneSessionContext(cached);
6660
+ return cached;
5045
6661
  }
5046
6662
  this.#pathOnlyContextBuildCount++;
5047
6663
  const context = buildSessionContext(
@@ -5050,11 +6666,12 @@ export class SessionManager {
5050
6666
  undefined,
5051
6667
  this.#sessionId,
5052
6668
  );
5053
- this.#sessionContextCache = new WeakRef(context);
6669
+ this.#sessionContextCache = freezeInternalReadSnapshot(context);
6670
+ transferSessionMessageIdentity(context.messages, this.#sessionContextCache.messages);
5054
6671
  this.#sessionContextEntryRevision = this.#entryRevision;
5055
6672
  this.#sessionContextLeafRevision = this.#leafRevision;
5056
6673
  this.#sessionContextReplayMetadataRevision = this.#replayMetadataRevision;
5057
- return cloneSessionContext(context);
6674
+ return this.#sessionContextCache;
5058
6675
  }
5059
6676
 
5060
6677
  #getActivePathEntriesForProviderContext(fromId?: string | null): SessionEntry[] {
@@ -5109,28 +6726,33 @@ export class SessionManager {
5109
6726
  const marker = entry.type === "message" || entry.type === "custom_message" ? entry.evictedContent : undefined;
5110
6727
  return marker ? Object.keys(marker.payloads ?? {}).length : 0;
5111
6728
  }
5112
- /** Strip stale OpenAI Responses assistant replay metadata from loaded in-memory entries. */
6729
+ /** Strip stale OpenAI Responses assistant replay metadata from loaded in-memory entries without persisting it. */
5113
6730
  sanitizeLoadedOpenAIResponsesReplayMetadata(): boolean {
5114
- let didSanitize = false;
5115
- for (const entry of this.#fileEntries) {
5116
- if (entry.type !== "message" || entry.message.role !== "assistant") {
5117
- continue;
5118
- }
6731
+ return this.#sanitizeLoadedOpenAIResponsesReplayMetadata().length > 0;
6732
+ }
5119
6733
 
5120
- const sanitizedMessage = sanitizeRehydratedOpenAIResponsesAssistantMessage(entry.message);
5121
- if (sanitizedMessage === entry.message) {
5122
- continue;
5123
- }
6734
+ async #sanitizeLoadedOpenAIResponsesReplayMetadataAndPersist(): Promise<boolean> {
6735
+ const patches = this.#sanitizeLoadedOpenAIResponsesReplayMetadata();
6736
+ for (const patch of patches) {
6737
+ await this.#persistPatch(patch);
6738
+ }
6739
+ return patches.length > 0;
6740
+ }
5124
6741
 
6742
+ #sanitizeLoadedOpenAIResponsesReplayMetadata(): EntryPatchRecord[] {
6743
+ const patches: EntryPatchRecord[] = [];
6744
+ for (const entry of this.#fileEntries) {
6745
+ if (entry.type !== "message" || entry.message.role !== "assistant") continue;
6746
+ const sanitizedMessage = sanitizeRehydratedOpenAIResponsesAssistantMessage(entry.message);
6747
+ if (sanitizedMessage === entry.message) continue;
5125
6748
  entry.message = sanitizedMessage;
5126
- didSanitize = true;
6749
+ patches.push({ type: "entry_patch", entryId: entry.id, patch: { message: sanitizedMessage } });
5127
6750
  }
5128
- if (didSanitize) {
6751
+ if (patches.length > 0) {
5129
6752
  this.#bumpEntryRevision();
5130
6753
  this.#replayMetadataRevision++;
5131
6754
  }
5132
-
5133
- return didSanitize;
6755
+ return patches;
5134
6756
  }
5135
6757
 
5136
6758
  /**
@@ -5142,9 +6764,7 @@ export class SessionManager {
5142
6764
  }
5143
6765
 
5144
6766
  /**
5145
- * Get all session entries (excludes header). Returns a shallow copy.
5146
- * The session is append-only: use appendXXX() to add entries, branch() to
5147
- * change the leaf pointer. Entries cannot be modified or deleted.
6767
+ * Internal materialized entry cache. Public getters clone this snapshot before returning it.
5148
6768
  */
5149
6769
  #getMaterializedEntriesInternal(): SessionEntry[] {
5150
6770
  if (this.#materializedEntriesRevision === this.#entryRevision && this.#materializedEntriesCache) {
@@ -5170,12 +6790,14 @@ export class SessionManager {
5170
6790
  }
5171
6791
 
5172
6792
  /**
5173
- * Get the session as a tree structure. Returns a shallow defensive copy of all entries.
6793
+ * Get the session as a tree structure. Returns defensive copies of all entries.
5174
6794
  * A well-formed session has exactly one root (first entry with parentId === null).
5175
6795
  * Orphaned entries (broken parent chain) are also returned as roots.
5176
6796
  */
5177
6797
  getTree(): SessionTreeNode[] {
5178
- const entries = this.getEntries();
6798
+ return this.#getTree(this.getEntries());
6799
+ }
6800
+ #getTree(entries: readonly SessionEntry[]): SessionTreeNode[] {
5179
6801
  const nodeMap = new Map<string, SessionTreeNode>();
5180
6802
  const roots: SessionTreeNode[] = [];
5181
6803
 
@@ -5247,7 +6869,9 @@ export class SessionManager {
5247
6869
 
5248
6870
  return roots;
5249
6871
  }
5250
-
6872
+ [sessionManagerReadCapability](): SessionManagerReadAccess {
6873
+ return this.#internalReadAccess;
6874
+ }
5251
6875
  // =========================================================================
5252
6876
  // Branching
5253
6877
  // =========================================================================
@@ -5365,7 +6989,14 @@ export class SessionManager {
5365
6989
  labelEntries.push(labelEntry);
5366
6990
  parentId = labelEntry.id;
5367
6991
  }
5368
- this.storage.writeTextSync(newSessionFile, `${lines.join("\n")}\n`);
6992
+ if (this.destination.kind === "managed") {
6993
+ this.#managedTranscriptStore(newSessionFile).publishNoReplaceSync(
6994
+ path.basename(newSessionFile),
6995
+ Buffer.from(`${lines.join("\n")}\n`, "utf8"),
6996
+ );
6997
+ } else {
6998
+ this.storage.writeTextSync(newSessionFile, `${lines.join("\n")}\n`);
6999
+ }
5369
7000
  this.#sessionId = newSessionId;
5370
7001
  this.#sessionFile = newSessionFile;
5371
7002
  this.#resetResidentTextBlobStore();
@@ -5422,18 +7053,121 @@ export class SessionManager {
5422
7053
  return computeDefaultSessionDir(cwd, storage, getSessionsDir(agentDir));
5423
7054
  }
5424
7055
 
7056
+ /** Resolve the default session directory without creating or migrating storage. */
7057
+ static getDefaultSessionDirReadOnly(cwd: string, agentDir?: string): string {
7058
+ const sessionsRoot = getSessionsDir(agentDir);
7059
+ const resolved = resolveManagedScope({
7060
+ cwd,
7061
+ agentDir: agentDir ?? path.resolve(sessionsRoot, ".."),
7062
+ sessionsRoot,
7063
+ });
7064
+ if (resolved.kind === "error") throw new Error(`Could not resolve managed session scope: ${resolved.message}`);
7065
+ return resolved.scope.directoryPath;
7066
+ }
7067
+
5425
7068
  /**
5426
7069
  * Create a new session.
5427
7070
  * @param cwd Working directory (stored in session header)
5428
7071
  * @param sessionDir Optional session directory. If omitted, uses default (~/.skc/agent/sessions/<encoded-cwd>/).
5429
7072
  */
5430
- static create(cwd: string, sessionDir?: string, storage: SessionStorage = new FileSessionStorage()): SessionManager {
5431
- const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
5432
- const manager = new SessionManager(cwd, dir, true, storage);
7073
+ static nestedManagedDestination(
7074
+ authority: ManagedDirectoryRoot | ManagedSessionDescendantStore,
7075
+ directory: string,
7076
+ ): SessionDestination {
7077
+ const rootAuthority =
7078
+ authority instanceof ManagedSessionDescendantStore ? authority.subtreeRootAuthority : authority;
7079
+ const retainedAuthority =
7080
+ authority instanceof ManagedSessionDescendantStore
7081
+ ? authority.retainAuthority()
7082
+ : retainManagedDirectoryAuthority(rootAuthority, directory);
7083
+ const securityContext = createManagedSessionSecurityContext({
7084
+ agentDir: rootAuthority.canonicalPath,
7085
+ sessionsRoot: rootAuthority.canonicalPath,
7086
+ sessionDir: directory,
7087
+ rootAuthority,
7088
+ retainedAuthority,
7089
+ });
7090
+ const destination = Object.freeze({ kind: "managed" as const, directory, securityContext });
7091
+ trustedSessionDestinations.add(destination);
7092
+ return destination;
7093
+ }
7094
+
7095
+ static managedDestination(
7096
+ cwd: string,
7097
+ agentDir?: string,
7098
+ storage: SessionStorage = new FileSessionStorage(),
7099
+ ): SessionDestination {
7100
+ return managedDestination(cwd, storage, agentDir);
7101
+ }
7102
+
7103
+ static explicitDestination(directory: string): SessionDestination {
7104
+ return explicitDestination(directory);
7105
+ }
7106
+
7107
+ static create(
7108
+ cwd: string,
7109
+ destinationInput?: SessionDestinationInput,
7110
+ storage: SessionStorage = new FileSessionStorage(),
7111
+ ): SessionManager {
7112
+ const destination = destinationFor(cwd, destinationInput, storage);
7113
+ const manager = new SessionManager(cwd, destination.directory, true, storage, destination);
5433
7114
  manager.#initNewSession();
5434
7115
  return manager;
5435
7116
  }
5436
7117
 
7118
+ /** Resolve a default-managed candidate through binding validation and copy-retain migration before mutation. */
7119
+ static async prepareManagedCandidateForWrite(
7120
+ filePath: string,
7121
+ migrationPolicy: SessionDirectoryMigrationPolicy,
7122
+ ): Promise<string> {
7123
+ const storage = new FileSessionStorage();
7124
+ const inspected = inspectResumeSessionFile(filePath, storage);
7125
+ if ("kind" in inspected) {
7126
+ if (inspected.reason === "missing") return filePath;
7127
+ throw new Error(`Could not inspect managed session: ${inspected.reason}`);
7128
+ }
7129
+ const header = inspected.entries.find(entry => entry.type === "session") as SessionHeader | undefined;
7130
+ if (!header?.cwd) return filePath;
7131
+ const sessionsRoot = getSessionsDir();
7132
+ const resolvedPath = path.resolve(filePath);
7133
+ const relativeToManagedRoot = path.relative(sessionsRoot, resolvedPath);
7134
+ const isManagedPath =
7135
+ relativeToManagedRoot === "" ||
7136
+ (!relativeToManagedRoot.startsWith("..") && !path.isAbsolute(relativeToManagedRoot));
7137
+
7138
+ if (!isManagedPath) return filePath;
7139
+ const resolved = resolveManagedScope({
7140
+ cwd: header.cwd,
7141
+ agentDir: path.resolve(sessionsRoot, ".."),
7142
+ sessionsRoot,
7143
+ });
7144
+ if (resolved.kind === "error") throw new Error(`Could not resolve managed session scope: ${resolved.message}`);
7145
+
7146
+ const listing = listManagedCandidates(resolved.scope);
7147
+ if (listing.kind !== "complete") throw new Error(`Managed session scan failed: ${listing.message}`);
7148
+
7149
+ const candidate = listing.owned.find(item => path.resolve(item.path) === resolvedPath);
7150
+ if (!candidate) throw new Error("Session is inside managed storage but is not an authorized managed candidate.");
7151
+ const revalidated = inspectResumeSessionFile(filePath, storage);
7152
+ if (
7153
+ "kind" in revalidated ||
7154
+ !sameResumeIdentity(inspected.identity, revalidated.identity) ||
7155
+ candidate.identity.dev !== inspected.identity.dev ||
7156
+ candidate.identity.ino !== inspected.identity.ino ||
7157
+ candidate.identity.size !== inspected.identity.size ||
7158
+ candidate.identity.mtimeNs !== inspected.identity.mtimeNs ||
7159
+ candidate.identity.mtimeMs !== inspected.identity.mtimeMs ||
7160
+ candidate.identity.sha256 !== inspected.identity.sha256
7161
+ )
7162
+ throw new Error("Managed session changed before migration authority was adopted.");
7163
+ const opened = await openManagedCandidateForWrite(resolved.scope, candidate, migrationPolicy);
7164
+ if (opened.kind === "error") {
7165
+ if (opened.code === "legacy_migration_disabled") throw new SessionMigrationPolicyError();
7166
+ throw new Error(`Could not open managed session: ${opened.message}`);
7167
+ }
7168
+ return opened.path;
7169
+ }
7170
+
5437
7171
  /**
5438
7172
  * Fork a session into the current project directory.
5439
7173
  * Copies history from another session file while creating a new session file in the current sessionDir.
@@ -5441,12 +7175,18 @@ export class SessionManager {
5441
7175
  static async forkFrom(
5442
7176
  sourcePath: string,
5443
7177
  cwd: string,
5444
- sessionDir?: string,
7178
+ destinationInput?: SessionDestinationInput,
5445
7179
  storage: SessionStorage = new FileSessionStorage(),
7180
+ migrationPolicy: SessionDirectoryMigrationPolicy = "copy-retain",
5446
7181
  ): Promise<SessionManager> {
5447
- const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
5448
- const manager = new SessionManager(cwd, dir, true, storage);
5449
- const forkEntries = structuredClone(await loadEntriesFromFile(sourcePath, storage)) as FileEntry[];
7182
+ const destination = destinationFor(cwd, destinationInput, storage);
7183
+ const managedSourcePath =
7184
+ destination.kind === "managed" && storage instanceof FileSessionStorage
7185
+ ? await SessionManager.prepareManagedCandidateForWrite(sourcePath, migrationPolicy)
7186
+ : sourcePath;
7187
+ const dir = destination.directory;
7188
+ const manager = new SessionManager(cwd, dir, true, storage, destination);
7189
+ const forkEntries = structuredClone(await loadEntriesFromFile(managedSourcePath, storage)) as FileEntry[];
5450
7190
  migrateToCurrentVersion(forkEntries);
5451
7191
  await resolveBlobRefsInEntries(forkEntries, manager.#blobStore);
5452
7192
  manager.#fileEntries = forkEntries;
@@ -5479,20 +7219,103 @@ export class SessionManager {
5479
7219
  */
5480
7220
  static async open(
5481
7221
  filePath: string,
5482
- sessionDir?: string,
7222
+ destinationInput?: SessionDestinationInput,
5483
7223
  storage: SessionStorage = new FileSessionStorage(),
7224
+ migrationPolicy: SessionDirectoryMigrationPolicy = "copy-retain",
5484
7225
  ): Promise<SessionManager> {
5485
- // Extract cwd from session header if possible, otherwise use getProjectDir()
5486
- const entries = await loadEntriesFromFile(filePath, storage);
5487
- const header = entries.find(e => e.type === "session") as SessionHeader | undefined;
5488
- const cwd = header?.cwd ?? getProjectDir();
5489
- // If no sessionDir provided, derive from file's parent directory
5490
- const dir = sessionDir ?? path.resolve(filePath, "..");
5491
- const manager = new SessionManager(cwd, dir, true, storage);
5492
- await manager.#initSessionFile(filePath);
5493
- return manager;
7226
+ const destination =
7227
+ destinationInput === undefined
7228
+ ? explicitDestination(path.dirname(filePath))
7229
+ : destinationFor(getProjectDir(), destinationInput, storage);
7230
+ // Canonicalize benign ancestor symlinks in the caller-supplied path (e.g.
7231
+ // macOS `/var -> /private/var`, a symlinked `$HOME`/project) so explicit
7232
+ // session files land under a symlink-free trusted root that the strict
7233
+ // owner-only and reparse guards accept.
7234
+ if (storage instanceof FileSessionStorage) filePath = canonicalizeTrustedPath(filePath);
7235
+ if (destination.kind === "explicit" || !(storage instanceof FileSessionStorage)) {
7236
+ const entries = await loadEntriesFromFile(filePath, storage);
7237
+ const header = entries.find(entry => entry.type === "session") as SessionHeader | undefined;
7238
+ const manager = new SessionManager(
7239
+ header?.cwd ?? getProjectDir(),
7240
+ destination.directory,
7241
+ true,
7242
+ storage,
7243
+ destination,
7244
+ );
7245
+ await manager.#initSessionFile(filePath);
7246
+ return manager;
7247
+ }
7248
+
7249
+ const managedPath = await SessionManager.prepareManagedCandidateForWrite(filePath, migrationPolicy);
7250
+ const inspected = inspectResumeSessionFile(managedPath, storage);
7251
+ if ("kind" in inspected) {
7252
+ if (inspected.reason === "missing" && path.resolve(managedPath) === path.resolve(filePath)) {
7253
+ const manager = new SessionManager(getProjectDir(), destination.directory, true, storage, destination);
7254
+ await manager.#initSessionFile(filePath);
7255
+ return manager;
7256
+ }
7257
+ throw new Error(`Could not open session: ${inspected.reason}`);
7258
+ }
7259
+ const opened = await SessionManager.openExistingStrict(inspected.identity, destination, storage, migrationPolicy);
7260
+ if (opened.kind === "error") throw new Error(`Could not open session: ${opened.reason}`);
7261
+ return opened.manager;
5494
7262
  }
5495
7263
 
7264
+ static async openNestedManaged(
7265
+ filePath: string,
7266
+ destination: SessionDestination,
7267
+ store: ManagedSessionDescendantStore,
7268
+ storage: SessionStorage = new FileSessionStorage(),
7269
+ ): Promise<SessionManager> {
7270
+ if (destination.kind !== "managed" || !trustedSessionDestinations.has(destination))
7271
+ throw new Error("Nested managed session authority is unavailable");
7272
+ const resolved = path.resolve(filePath);
7273
+ if (
7274
+ path.dirname(resolved) !== path.resolve(store.dir) ||
7275
+ path.dirname(resolved) !== path.resolve(destination.directory)
7276
+ )
7277
+ throw new Error("Nested managed session escaped retained authority");
7278
+ store.assertBound();
7279
+ const captured = store.readExpected(path.basename(resolved));
7280
+ const entries = captured ? parseSessionEntries(captured.bytes.toString("utf8")) : [];
7281
+ const header = entries.find(entry => entry.type === "session") as SessionHeader | undefined;
7282
+ const manager = new SessionManager(
7283
+ header?.cwd ?? getProjectDir(),
7284
+ destination.directory,
7285
+ true,
7286
+ storage,
7287
+ destination,
7288
+ );
7289
+ manager.#sessionFile = resolved;
7290
+ writeTerminalBreadcrumb(manager.cwd, resolved);
7291
+ manager.#fileEntries = entries;
7292
+ if (entries.length > 0) {
7293
+ manager.#sessionId = header?.id ?? createSessionId();
7294
+ manager.#sessionName = header?.title;
7295
+ manager.#titleSource = header?.titleSource;
7296
+ manager.#needsFullRewriteOnNextPersist = migrateToCurrentVersion(manager.#fileEntries);
7297
+ await resolveBlobRefsInEntries(manager.#fileEntries, manager.#blobStore);
7298
+ manager.#resetResidentTextBlobStore();
7299
+ manager.#fileEntries = manager.#fileEntries.map(entry =>
7300
+ prepareEntryForResidentSync(entry, manager.#residentBlobStores()),
7301
+ );
7302
+ manager.#buildIndex();
7303
+ manager.#bumpAllRevisions();
7304
+ manager.#flushed = true;
7305
+ manager.#ensuredOnDisk = true;
7306
+ await manager.#sanitizeLoadedOpenAIResponsesReplayMetadataAndPersist();
7307
+ } else {
7308
+ manager.#newSessionSync();
7309
+ manager.#sessionFile = resolved;
7310
+ manager.#resetResidentTextBlobStore();
7311
+ await manager.#rewriteFile();
7312
+ manager.#flushed = true;
7313
+ manager.#ensuredOnDisk = true;
7314
+ manager.#bumpAllRevisions();
7315
+ }
7316
+ store.assertBound();
7317
+ return manager;
7318
+ }
5496
7319
  /**
5497
7320
  * List sessions for the resume picker without recovery or other maintenance writes.
5498
7321
  */
@@ -5501,16 +7324,247 @@ export class SessionManager {
5501
7324
  sessionDir?: string,
5502
7325
  storage: SessionStorage = new FileSessionStorage(),
5503
7326
  ): Promise<SessionInfo[]> {
5504
- const dirs = sessionDir ? [sessionDir] : getReadOnlyDefaultSessionDirs(cwd);
7327
+ if (!sessionDir && !(storage instanceof FileSessionStorage)) return [];
7328
+ if (sessionDir && !resolveManagedSessionRoot(sessionDir, cwd)) {
7329
+ try {
7330
+ return await collectSessionsFromFiles(storage.listFilesSync(sessionDir, "*.jsonl"), storage);
7331
+ } catch {
7332
+ return [];
7333
+ }
7334
+ }
7335
+ const sessionsRoot = sessionDir
7336
+ ? (resolveManagedSessionRoot(sessionDir, cwd) ?? getSessionsDir())
7337
+ : getSessionsDir();
7338
+ const resolved = resolveManagedScope({ cwd, agentDir: path.resolve(sessionsRoot, ".."), sessionsRoot });
7339
+ if (resolved.kind === "error") return [];
7340
+ if (storage instanceof FileSessionStorage) {
7341
+ const listing = listManagedCandidates(resolved.scope);
7342
+ if (listing.kind === "error") return [];
7343
+ return await collectSessionsFromFiles(
7344
+ listing.owned.map(candidate => candidate.path),
7345
+ storage,
7346
+ );
7347
+ }
7348
+ const legacyDirectory = `--${resolved.scope.canonicalCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
7349
+ const files = new Set<string>();
5505
7350
  try {
5506
- const files = new Set<string>();
5507
- for (const dir of dirs) {
5508
- for (const filePath of storage.listFilesSync(dir, "*.jsonl")) files.add(filePath);
7351
+ for (const directory of [resolved.scope.directoryPath, path.join(sessionsRoot, legacyDirectory)]) {
7352
+ for (const file of storage.listFilesSync(directory, "*.jsonl")) files.add(file);
5509
7353
  }
5510
- return await collectSessionsFromFiles([...files], storage);
5511
7354
  } catch {
5512
7355
  return [];
5513
7356
  }
7357
+ return (await collectSessionsFromFiles([...files], storage)).filter(
7358
+ session => resolveEquivalentPath(session.cwd) === resolved.scope.canonicalCwd,
7359
+ );
7360
+ }
7361
+
7362
+ /** Tombstone and exact-delete a default-managed candidate. */
7363
+ static async deleteManagedCandidate(sessionPath: string): Promise<void> {
7364
+ const storage = new FileSessionStorage();
7365
+ const entries = await loadEntriesFromFile(sessionPath, storage);
7366
+ const header = entries.find(entry => entry.type === "session") as SessionHeader | undefined;
7367
+ if (!header?.cwd) throw new Error("Session has no valid managed workspace header.");
7368
+ const sessionsRoot = path.resolve(sessionPath, "../..");
7369
+ const resolved = resolveManagedScope({
7370
+ cwd: header.cwd,
7371
+ agentDir: path.resolve(sessionsRoot, ".."),
7372
+ sessionsRoot,
7373
+ });
7374
+ if (resolved.kind === "error") throw new Error(`Could not resolve managed session scope: ${resolved.message}`);
7375
+ const listing = listManagedCandidates(resolved.scope);
7376
+ if (listing.kind !== "complete") throw new Error("Managed session scan did not grant deletion authority.");
7377
+ const candidate = listing.owned.find(item => path.resolve(item.path) === path.resolve(sessionPath));
7378
+ if (!candidate) throw new Error("Session is not an authorized managed candidate.");
7379
+ const deleted = await deleteManagedSessionCandidate(resolved.scope, candidate);
7380
+ if (deleted.kind === "error") throw new Error(`Could not delete managed session: ${deleted.message}`);
7381
+ }
7382
+
7383
+ /** Capture exact source content for a strict fork without granting write ownership. */
7384
+ static captureTranscriptStrict(
7385
+ filePath: string,
7386
+ storage: SessionStorage = new FileSessionStorage(),
7387
+ ): StrictSessionCaptureResult {
7388
+ const inspected = inspectResumeSessionFile(filePath, storage);
7389
+ if ("kind" in inspected) return inspected;
7390
+ return {
7391
+ kind: "captured",
7392
+ snapshot: {
7393
+ sourcePath: path.resolve(filePath),
7394
+ identity: inspected.identity,
7395
+ content: Uint8Array.from(inspected.content),
7396
+ storage,
7397
+ },
7398
+ };
7399
+ }
7400
+
7401
+ /**
7402
+ * Fork strictly from captured source bytes. The source pathname is used only to
7403
+ * revalidate captured authority before destination initialization and transcript
7404
+ * persistence; destination history always comes from the captured bytes.
7405
+ */
7406
+ static async forkFromCaptured(
7407
+ snapshot: CapturedSessionTranscriptSnapshot,
7408
+ cwd: string,
7409
+ destinationInput?: SessionDestinationInput,
7410
+ _migrationPolicy: SessionDirectoryMigrationPolicy = "copy-retain",
7411
+ ): Promise<StrictSessionForkResult> {
7412
+ const destination = destinationFor(cwd, destinationInput, snapshot.storage);
7413
+ if (crypto.createHash("sha256").update(snapshot.content).digest("hex") !== snapshot.identity.sha256) {
7414
+ return { kind: "error", reason: "identity-mismatch" };
7415
+ }
7416
+
7417
+ let forkEntries: FileEntry[] = [];
7418
+ try {
7419
+ const content = new TextDecoder("utf-8", { fatal: true }).decode(snapshot.content);
7420
+ for (const line of content.split(/\r?\n/)) {
7421
+ if (line.length > 0) JSON.parse(line);
7422
+ }
7423
+ forkEntries = parseSessionEntries(content);
7424
+ const sourceHeader = forkEntries[0] as SessionHeader | undefined;
7425
+ if (sourceHeader?.type !== "session" || sourceHeader.id !== snapshot.identity.sessionId)
7426
+ return { kind: "error", reason: "identity-mismatch" };
7427
+ migrateToCurrentVersion(forkEntries);
7428
+ if (!hasStrictSessionSchema(forkEntries)) return { kind: "error", reason: "malformed" };
7429
+ } catch {
7430
+ return { kind: "error", reason: "malformed" };
7431
+ }
7432
+
7433
+ const revalidated = inspectResumeSessionFile(snapshot.sourcePath, snapshot.storage);
7434
+ if ("kind" in revalidated) return revalidated;
7435
+ if (!sameResumeIdentity(snapshot.identity, revalidated.identity)) {
7436
+ return { kind: "error", reason: "identity-mismatch" };
7437
+ }
7438
+
7439
+ const dir = destination.directory;
7440
+ const removeSessionDirOnFailure =
7441
+ destination.kind !== "managed" &&
7442
+ snapshot.storage instanceof FileSessionStorage &&
7443
+ !snapshot.storage.existsSync(dir);
7444
+ let managedForkStore: ManagedSessionDescendantStore | undefined;
7445
+ let managedForkTranscript: ManagedFileSnapshot | null = null;
7446
+ let manager: SessionManager | undefined;
7447
+ let authorityFailure: StrictSessionOpenFailure | undefined;
7448
+ try {
7449
+ manager = new SessionManager(cwd, dir, true, snapshot.storage, destination);
7450
+ await resolveBlobRefsInEntries(forkEntries, manager.#blobStore);
7451
+ manager.#fileEntries = forkEntries;
7452
+ const sourceHeader = manager.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
7453
+ const historyEntries = manager.#fileEntries.filter(entry => entry.type !== "session") as SessionEntry[];
7454
+ manager.#newSessionSync({ parentSession: sourceHeader?.id }, false);
7455
+ manager.#resetResidentTextBlobStore();
7456
+ const newHeader = manager.#fileEntries[0] as SessionHeader;
7457
+ newHeader.title = sourceHeader?.title;
7458
+ newHeader.titleSource = sourceHeader?.titleSource;
7459
+ const residentBlobStores = manager.#residentBlobStores();
7460
+ manager.#fileEntries = [
7461
+ newHeader,
7462
+ ...historyEntries.map(entry => prepareEntryForResidentSync(entry, residentBlobStores) as SessionEntry),
7463
+ ];
7464
+ manager.#sessionName = newHeader.title;
7465
+ manager.#titleSource = newHeader.titleSource;
7466
+ manager.sanitizeLoadedOpenAIResponsesReplayMetadata();
7467
+ manager.#buildIndex();
7468
+ manager.#bumpAllRevisions();
7469
+ const beforeWrite = inspectResumeSessionFile(snapshot.sourcePath, snapshot.storage);
7470
+ if ("kind" in beforeWrite) {
7471
+ authorityFailure = beforeWrite;
7472
+ throw new Error("Captured fork source authority changed before destination write.");
7473
+ }
7474
+ if (!sameResumeIdentity(snapshot.identity, beforeWrite.identity)) {
7475
+ authorityFailure = { kind: "error", reason: "identity-mismatch" };
7476
+ throw new Error("Captured fork source authority changed before destination write.");
7477
+ }
7478
+ await manager.#rewriteFile();
7479
+ if (destination.kind === "managed" && manager.#sessionFile) {
7480
+ managedForkStore = manager.#managedTranscriptStore();
7481
+ managedForkTranscript = managedForkStore.readExpected(path.basename(manager.#sessionFile));
7482
+ if (!managedForkTranscript) throw new Error("managed_fork_transcript_publish_missing");
7483
+ }
7484
+ const afterWrite = inspectResumeSessionFile(snapshot.sourcePath, snapshot.storage);
7485
+ if ("kind" in afterWrite) {
7486
+ authorityFailure = afterWrite;
7487
+ throw new Error("Captured fork source authority changed during destination write.");
7488
+ }
7489
+ if (!sameResumeIdentity(snapshot.identity, afterWrite.identity)) {
7490
+ authorityFailure = { kind: "error", reason: "identity-mismatch" };
7491
+ throw new Error("Captured fork source authority changed during destination write.");
7492
+ }
7493
+ if (managedForkStore && managedForkTranscript && manager.#sessionFile) {
7494
+ const terminalTranscript = managedForkStore.readExpected(path.basename(manager.#sessionFile));
7495
+ if (!managedFileSnapshotEquals(terminalTranscript, managedForkTranscript))
7496
+ throw new Error("managed_fork_transcript_changed");
7497
+ }
7498
+ if (manager.#sessionFile) writeTerminalBreadcrumb(manager.cwd, manager.#sessionFile);
7499
+ return { kind: "forked", manager };
7500
+ } catch (error) {
7501
+ if (manager) {
7502
+ const sessionFile = manager.#sessionFile;
7503
+ const cleanupErrors: Error[] = [];
7504
+ try {
7505
+ await manager.close();
7506
+ } catch (cleanupError) {
7507
+ cleanupErrors.push(toError(cleanupError));
7508
+ }
7509
+ if (sessionFile) {
7510
+ if (destination.kind === "managed") {
7511
+ try {
7512
+ if (managedForkStore && managedForkTranscript)
7513
+ managedForkStore.removeExpected(path.basename(sessionFile), managedForkTranscript);
7514
+ } catch (cleanupError) {
7515
+ cleanupErrors.push(toError(cleanupError));
7516
+ }
7517
+ } else {
7518
+ try {
7519
+ await manager.storage.unlink(sessionFile);
7520
+ } catch (cleanupError) {
7521
+ if (!isEnoent(cleanupError)) cleanupErrors.push(toError(cleanupError));
7522
+ }
7523
+ if (manager.storage instanceof FileSessionStorage) {
7524
+ try {
7525
+ await fs.promises.rm(sessionFile.slice(0, -6), { recursive: true, force: true });
7526
+ } catch (cleanupError) {
7527
+ cleanupErrors.push(toError(cleanupError));
7528
+ }
7529
+ try {
7530
+ const sessionFileName = path.basename(sessionFile);
7531
+ const transientPrefix = `.${sessionFileName}.`;
7532
+ const backupPrefix = `${sessionFileName}.`;
7533
+ for (const name of await fs.promises.readdir(path.dirname(sessionFile))) {
7534
+ if (
7535
+ (name.startsWith(transientPrefix) && name.endsWith(".tmp")) ||
7536
+ (name.startsWith(backupPrefix) && name.endsWith(".bak"))
7537
+ ) {
7538
+ await fs.promises.rm(path.join(path.dirname(sessionFile), name), {
7539
+ recursive: true,
7540
+ force: true,
7541
+ });
7542
+ }
7543
+ }
7544
+ } catch (cleanupError) {
7545
+ if (!isEnoent(cleanupError)) cleanupErrors.push(toError(cleanupError));
7546
+ }
7547
+ }
7548
+ }
7549
+ }
7550
+ if (removeSessionDirOnFailure) {
7551
+ try {
7552
+ await fs.promises.rmdir(dir);
7553
+ } catch (cleanupError) {
7554
+ const code = (cleanupError as NodeJS.ErrnoException).code;
7555
+ if (code !== "ENOENT" && code !== "ENOTEMPTY" && code !== "EEXIST")
7556
+ cleanupErrors.push(toError(cleanupError));
7557
+ }
7558
+ }
7559
+ if (cleanupErrors.length > 0) {
7560
+ throw new Error(`Failed to clean up fork destination: ${cleanupErrors[0]!.message}`, {
7561
+ cause: toError(error),
7562
+ });
7563
+ }
7564
+ if (authorityFailure) return authorityFailure;
7565
+ }
7566
+ throw error;
7567
+ }
5514
7568
  }
5515
7569
 
5516
7570
  /** Inspect a selected session without acquiring write-capable ownership. */
@@ -5525,6 +7579,67 @@ export class SessionManager {
5525
7579
  : { kind: "terminal", identity: inspected.identity };
5526
7580
  }
5527
7581
 
7582
+ /**
7583
+ * Hydrate an existing predecessor transcript for recovery without taking any
7584
+ * write-capable action. The caller must retain the returned context and use
7585
+ * the explicit promotion seam only after its ownership-ready fence and writer
7586
+ * lease are durable.
7587
+ */
7588
+ static async openExistingForRecoveryHydrationStrict(
7589
+ identity: ResumeSessionIdentity,
7590
+ destinationInput?: SessionDestinationInput,
7591
+ storage: SessionStorage = new FileSessionStorage(),
7592
+ ): Promise<RecoveryHydrationOpenResult> {
7593
+ const destination =
7594
+ destinationInput === undefined
7595
+ ? explicitDestination(path.dirname(identity.canonicalPath))
7596
+ : destinationFor(getProjectDir(), destinationInput, storage);
7597
+ const inspected = inspectResumeSessionFile(identity.canonicalPath, storage);
7598
+ if ("kind" in inspected) return inspected;
7599
+ if (!sameResumeIdentity(identity, inspected.identity)) return { kind: "error", reason: "identity-mismatch" };
7600
+ if (inspected.migrationApplied) return { kind: "error", reason: "migration-required" };
7601
+
7602
+ const header = inspected.entries[0] as SessionHeader;
7603
+ const manager = new SessionManager(
7604
+ header.cwd || getProjectDir(),
7605
+ destination.directory,
7606
+ true,
7607
+ storage,
7608
+ destination,
7609
+ );
7610
+ await manager.#hydrateExistingSession(identity.canonicalPath, inspected.entries, false);
7611
+ const revalidated = inspectResumeSessionFile(identity.canonicalPath, storage);
7612
+ if ("kind" in revalidated || !sameResumeIdentity(identity, revalidated.identity)) {
7613
+ await manager.close();
7614
+ return "kind" in revalidated ? revalidated : { kind: "error", reason: "identity-mismatch" };
7615
+ }
7616
+ const context: RecoveryHydrationContext = Object.freeze({
7617
+ identity: Object.freeze({ ...identity }),
7618
+ });
7619
+ manager.#recoveryHydrationContext = context;
7620
+ return { kind: "hydrated", manager, context };
7621
+ }
7622
+
7623
+ /**
7624
+ * Allows the normal post-open metadata sanitation only after the caller has
7625
+ * fsynced its external ownership-ready fence and acquired the writer lease.
7626
+ */
7627
+ async promoteRecoveryHydrationAfterOwnershipReadyFence(
7628
+ context: RecoveryHydrationContext,
7629
+ fence: RecoveryHydrationPromotionFence,
7630
+ ): Promise<void> {
7631
+ if (fence.ownershipReady !== true || this.#recoveryHydrationContext !== context) {
7632
+ throw new Error("Recovery hydration promotion requires the original ownership-ready context.");
7633
+ }
7634
+ const inspected = inspectResumeSessionFile(context.identity.canonicalPath, this.storage);
7635
+ if ("kind" in inspected || !sameResumeIdentity(context.identity, inspected.identity)) {
7636
+ throw new Error("Recovery transcript authority changed before promotion.");
7637
+ }
7638
+ await this.#sanitizeLoadedOpenAIResponsesReplayMetadataAndPersist();
7639
+ writeTerminalBreadcrumb(this.cwd, context.identity.canonicalPath);
7640
+ this.#recoveryHydrationContext = undefined;
7641
+ }
7642
+
5528
7643
  /**
5529
7644
  * Main startup code MUST retain the consented inspection identity and branch on
5530
7645
  * the returned discriminant; an error result never creates, rewrites, or adopts
@@ -5532,29 +7647,54 @@ export class SessionManager {
5532
7647
  */
5533
7648
  static async openExistingStrict(
5534
7649
  identity: ResumeSessionIdentity,
5535
- sessionDir?: string,
7650
+ destinationInput?: SessionDestinationInput,
5536
7651
  storage: SessionStorage = new FileSessionStorage(),
7652
+ migrationPolicy: SessionDirectoryMigrationPolicy = "copy-retain",
5537
7653
  ): Promise<StrictSessionOpenResult> {
5538
- const inspected = inspectResumeSessionFile(identity.canonicalPath, storage);
7654
+ const destination =
7655
+ destinationInput === undefined
7656
+ ? explicitDestination(path.dirname(identity.canonicalPath))
7657
+ : destinationFor(getProjectDir(), destinationInput, storage);
7658
+ let sessionPath = identity.canonicalPath;
7659
+ if (destination.kind === "managed" && storage instanceof FileSessionStorage) {
7660
+ try {
7661
+ sessionPath = await SessionManager.prepareManagedCandidateForWrite(sessionPath, migrationPolicy);
7662
+ } catch (error) {
7663
+ if (error instanceof SessionMigrationPolicyError)
7664
+ return { kind: "error", reason: "legacy_migration_disabled" };
7665
+ throw error;
7666
+ }
7667
+ }
7668
+ const inspected = inspectResumeSessionFile(sessionPath, storage);
5539
7669
  if ("kind" in inspected) return inspected;
5540
- if (!sameResumeIdentity(identity, inspected.identity)) {
7670
+ if (
7671
+ (sessionPath === identity.canonicalPath && !sameResumeIdentity(identity, inspected.identity)) ||
7672
+ (sessionPath !== identity.canonicalPath &&
7673
+ (identity.sessionId !== inspected.identity.sessionId || identity.sha256 !== inspected.identity.sha256))
7674
+ ) {
5541
7675
  return { kind: "error", reason: "identity-mismatch" };
5542
7676
  }
5543
- const entries = structuredClone(inspected.entries) as FileEntry[];
7677
+ const entries = inspected.entries;
5544
7678
  const header = entries[0] as SessionHeader;
5545
- const dir = sessionDir ?? path.resolve(identity.canonicalPath, "..");
5546
- const manager = new SessionManager(header.cwd || getProjectDir(), dir, true, storage);
5547
- await manager.#hydrateExistingSession(identity.canonicalPath, entries, inspected.migrationApplied);
5548
- const ownershipInspection = inspectResumeSessionFile(identity.canonicalPath, storage);
7679
+ const dir = destination.directory;
7680
+ const manager = new SessionManager(header.cwd || getProjectDir(), dir, true, storage, destination);
7681
+ await manager.#hydrateExistingSession(sessionPath, entries, inspected.migrationApplied);
7682
+ const ownershipInspection = inspectResumeSessionFile(sessionPath, storage);
5549
7683
  if ("kind" in ownershipInspection) {
5550
7684
  await manager.close();
5551
7685
  return ownershipInspection;
5552
7686
  }
5553
- if (!sameResumeIdentity(identity, ownershipInspection.identity)) {
7687
+ if (!sameResumeIdentity(inspected.identity, ownershipInspection.identity)) {
5554
7688
  await manager.close();
5555
7689
  return { kind: "error", reason: "identity-mismatch" };
5556
7690
  }
5557
- writeTerminalBreadcrumb(manager.cwd, identity.canonicalPath);
7691
+ try {
7692
+ await manager.#sanitizeLoadedOpenAIResponsesReplayMetadataAndPersist();
7693
+ writeTerminalBreadcrumb(manager.cwd, sessionPath);
7694
+ } catch (error) {
7695
+ await manager.close();
7696
+ throw error;
7697
+ }
5558
7698
  return { kind: "opened", manager };
5559
7699
  }
5560
7700
 
@@ -5565,28 +7705,48 @@ export class SessionManager {
5565
7705
  */
5566
7706
  static async continueRecent(
5567
7707
  cwd: string,
5568
- sessionDir?: string,
7708
+ destinationInput?: SessionDestinationInput,
5569
7709
  storage: SessionStorage = new FileSessionStorage(),
7710
+ migrationPolicy: SessionDirectoryMigrationPolicy = "copy-retain",
5570
7711
  ): Promise<SessionManager> {
5571
- const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
5572
- // Prefer terminal-scoped breadcrumb (handles concurrent sessions correctly)
7712
+ const destination = destinationFor(cwd, destinationInput, storage);
7713
+ const dir = destination.directory;
7714
+ const openSelectedStrictly = async (selectedPath: string): Promise<SessionManager | undefined> => {
7715
+ const inspected = await SessionManager.inspectSessionTailReadOnly(selectedPath, storage);
7716
+ if (inspected.kind === "error") return undefined;
7717
+ const opened = await SessionManager.openExistingStrict(
7718
+ inspected.identity,
7719
+ destination,
7720
+ storage,
7721
+ migrationPolicy,
7722
+ );
7723
+ if (opened.kind === "error") {
7724
+ if (opened.reason === "legacy_migration_disabled") throw new SessionMigrationPolicyError();
7725
+ return undefined;
7726
+ }
7727
+ return opened.manager;
7728
+ };
7729
+ // Legacy paths are candidates only: the managed scan validates each transcript
7730
+ // header cwd before it can affect default resume authority.
7731
+ const managedCandidates =
7732
+ destination.kind === "explicit"
7733
+ ? []
7734
+ : await SessionManager.listForResumePickerReadOnly(cwd, destination.directory, storage);
5573
7735
  const terminalSession = await readTerminalBreadcrumb(cwd);
5574
- const mostRecent = terminalSession ?? (await findMostRecentSession(dir, storage));
7736
+ if (terminalSession) {
7737
+ const opened = await openSelectedStrictly(terminalSession);
7738
+ if (opened) return opened;
7739
+ }
7740
+ for (const candidate of managedCandidates) {
7741
+ const opened = await openSelectedStrictly(candidate.path);
7742
+ if (opened) return opened;
7743
+ }
7744
+ const mostRecent = await findMostRecentSession(dir, storage);
5575
7745
  if (mostRecent) {
5576
- // Adopt the resumed session's recorded cwd and its own directory. A
5577
- // `--worktree` session lives in a linked worktree whose path differs from
5578
- // the invocation cwd; binding the manager (and HUD) to `cwd` would leave
5579
- // it on the main checkout instead of the worktree it was created in.
5580
- const header = (await loadEntriesFromFile(mostRecent, storage)).find(e => e.type === "session") as
5581
- | SessionHeader
5582
- | undefined;
5583
- const resumeCwd = header?.cwd || cwd;
5584
- const resumeDir = sessionDir ?? path.resolve(mostRecent, "..");
5585
- const manager = new SessionManager(resumeCwd, resumeDir, true, storage);
5586
- await manager.#initSessionFile(mostRecent);
5587
- return manager;
7746
+ const opened = await openSelectedStrictly(mostRecent);
7747
+ if (opened) return opened;
5588
7748
  }
5589
- const manager = new SessionManager(cwd, dir, true, storage);
7749
+ const manager = new SessionManager(cwd, dir, true, storage, destination);
5590
7750
  manager.#initNewSession();
5591
7751
  return manager;
5592
7752
  }
@@ -5611,11 +7771,10 @@ export class SessionManager {
5611
7771
  sessionDir?: string,
5612
7772
  storage: SessionStorage = new FileSessionStorage(),
5613
7773
  ): Promise<SessionInfo[]> {
5614
- const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
7774
+ if (!sessionDir) return await SessionManager.listForResumePickerReadOnly(cwd, undefined, storage);
5615
7775
  try {
5616
- await recoverOrphanedBackups(dir, storage);
5617
- const files = storage.listFilesSync(dir, "*.jsonl");
5618
- return await collectSessionsFromFiles(files, storage);
7776
+ await recoverOrphanedBackups(sessionDir, storage);
7777
+ return await collectSessionsFromFiles(storage.listFilesSync(sessionDir, "*.jsonl"), storage);
5619
7778
  } catch {
5620
7779
  return [];
5621
7780
  }
@@ -5625,12 +7784,39 @@ export class SessionManager {
5625
7784
  * List all sessions across all project directories.
5626
7785
  */
5627
7786
  static async listAll(storage: SessionStorage = new FileSessionStorage()): Promise<SessionInfo[]> {
7787
+ if (!(storage instanceof FileSessionStorage)) return [];
5628
7788
  const sessionsRoot = path.join(getDefaultAgentDir(), "sessions");
5629
7789
  try {
5630
- const files = await Array.fromAsync(new Bun.Glob("*/*.jsonl").scan(sessionsRoot), name =>
5631
- path.join(sessionsRoot, name),
5632
- );
5633
- return await collectSessionsFromFiles(files, storage);
7790
+ const directories = await fs.promises.readdir(sessionsRoot, { withFileTypes: true });
7791
+ const seedFiles = directories
7792
+ .filter(entry => entry.isDirectory())
7793
+ .flatMap(entry => storage.listFilesSync(path.join(sessionsRoot, entry.name), "*.jsonl"));
7794
+ const seedSessions = await collectSessionsFromFiles(seedFiles, storage);
7795
+ const logicalFiles = new Set<string>();
7796
+ for (const cwd of new Set(seedSessions.map(session => session.cwd).filter(Boolean))) {
7797
+ const scope = resolveManagedScope({
7798
+ cwd,
7799
+ agentDir: path.resolve(sessionsRoot, ".."),
7800
+ sessionsRoot,
7801
+ });
7802
+ if (scope.kind !== "resolved") {
7803
+ logger.warn("Skipped invalid managed session scope during global listing", { message: scope.message });
7804
+ continue;
7805
+ }
7806
+ const listing = listManagedCandidates(scope.scope);
7807
+ if (listing.kind !== "complete") {
7808
+ logger.warn("Skipped unreadable managed session scope during global listing", {
7809
+ message: listing.message,
7810
+ });
7811
+ continue;
7812
+ }
7813
+ if (listing.invalid.length > 0)
7814
+ logger.warn("Ignored invalid managed session candidates during global listing", {
7815
+ count: listing.invalid.length,
7816
+ });
7817
+ for (const candidate of listing.owned) logicalFiles.add(candidate.path);
7818
+ }
7819
+ return await collectSessionsFromFiles([...logicalFiles], storage);
5634
7820
  } catch {
5635
7821
  return [];
5636
7822
  }