@oh-my-pi/pi-coding-agent 17.0.9 → 17.1.1

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 (349) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +1 -1
  3. package/dist/cli.js +4685 -4614
  4. package/dist/types/async/job-manager.d.ts +37 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/bench-cli.d.ts +41 -0
  7. package/dist/types/commands/bench.d.ts +15 -3
  8. package/dist/types/commands/launch.d.ts +4 -0
  9. package/dist/types/config/model-registry.d.ts +3 -1
  10. package/dist/types/config/models-config-schema.d.ts +18 -6
  11. package/dist/types/config/models-config.d.ts +14 -5
  12. package/dist/types/config/provider-globals.d.ts +2 -2
  13. package/dist/types/config/service-tier.d.ts +5 -1
  14. package/dist/types/config/settings-schema.d.ts +374 -47
  15. package/dist/types/config/settings.d.ts +7 -0
  16. package/dist/types/dap/client.d.ts +20 -0
  17. package/dist/types/exec/bash-executor.d.ts +38 -0
  18. package/dist/types/exec/direnv.d.ts +33 -0
  19. package/dist/types/export/html/args.d.ts +15 -0
  20. package/dist/types/export/html/index.d.ts +11 -26
  21. package/dist/types/export/html/web-palette.d.ts +55 -116
  22. package/dist/types/extensibility/extensions/loader.d.ts +3 -0
  23. package/dist/types/extensibility/extensions/types.d.ts +17 -2
  24. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
  25. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
  26. package/dist/types/live/attestation.d.ts +2 -0
  27. package/dist/types/live/controller.d.ts +48 -0
  28. package/dist/types/live/protocol.d.ts +85 -0
  29. package/dist/types/live/protocol.test.d.ts +1 -0
  30. package/dist/types/live/transport.d.ts +35 -0
  31. package/dist/types/live/visualizer.d.ts +30 -0
  32. package/dist/types/lsp/client.d.ts +16 -1
  33. package/dist/types/mcp/config-writer.test.d.ts +1 -0
  34. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  35. package/dist/types/mcp/smithery-auth.d.ts +1 -1
  36. package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
  37. package/dist/types/memory-backend/tool-names.d.ts +2 -0
  38. package/dist/types/modes/components/assistant-message.d.ts +1 -0
  39. package/dist/types/modes/components/custom-message.d.ts +1 -1
  40. package/dist/types/modes/components/login-dialog.d.ts +2 -2
  41. package/dist/types/modes/components/message-frame.d.ts +8 -4
  42. package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
  43. package/dist/types/modes/components/session-account-selector.d.ts +11 -0
  44. package/dist/types/modes/components/settings-defs.d.ts +7 -1
  45. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
  46. package/dist/types/modes/components/tool-execution.d.ts +9 -1
  47. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  48. package/dist/types/modes/controllers/event-controller.d.ts +6 -1
  49. package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
  50. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  51. package/dist/types/modes/interactive-mode.d.ts +6 -0
  52. package/dist/types/modes/theme/theme.d.ts +1 -1
  53. package/dist/types/modes/types.d.ts +4 -1
  54. package/dist/types/modes/utils/context-usage.d.ts +12 -2
  55. package/dist/types/plan-mode/plan-files.d.ts +10 -0
  56. package/dist/types/registry/agent-lifecycle.d.ts +21 -9
  57. package/dist/types/registry/agent-registry.d.ts +11 -4
  58. package/dist/types/sdk.d.ts +15 -1
  59. package/dist/types/secrets/index.d.ts +15 -2
  60. package/dist/types/secrets/obfuscator.d.ts +113 -15
  61. package/dist/types/session/acp-permission-gate.d.ts +19 -0
  62. package/dist/types/session/agent-session-events.d.ts +84 -0
  63. package/dist/types/session/agent-session-types.d.ts +321 -0
  64. package/dist/types/session/agent-session.d.ts +135 -753
  65. package/dist/types/session/async-job-delivery.d.ts +28 -0
  66. package/dist/types/session/auth-storage.d.ts +1 -1
  67. package/dist/types/session/bash-runner.d.ts +75 -0
  68. package/dist/types/session/blob-store.d.ts +11 -1
  69. package/dist/types/session/blob-store.test.d.ts +1 -0
  70. package/dist/types/session/checkpoint-entries.d.ts +28 -0
  71. package/dist/types/session/eval-runner.d.ts +51 -0
  72. package/dist/types/session/irc-bridge.d.ts +46 -0
  73. package/dist/types/session/messages.d.ts +24 -0
  74. package/dist/types/session/model-controls.d.ts +180 -0
  75. package/dist/types/session/prewalk.d.ts +46 -0
  76. package/dist/types/session/queued-messages.d.ts +22 -0
  77. package/dist/types/session/redis-session-storage.d.ts +1 -0
  78. package/dist/types/session/retry-fallback-chains.d.ts +74 -0
  79. package/dist/types/session/role-models.d.ts +15 -0
  80. package/dist/types/session/session-advisors.d.ts +228 -0
  81. package/dist/types/session/session-entries.d.ts +8 -0
  82. package/dist/types/session/session-handoff.d.ts +70 -0
  83. package/dist/types/session/session-history-format.d.ts +6 -1
  84. package/dist/types/session/session-maintenance.d.ts +246 -0
  85. package/dist/types/session/session-manager.d.ts +41 -0
  86. package/dist/types/session/session-memory.d.ts +56 -0
  87. package/dist/types/session/session-provider-boundary.d.ts +62 -0
  88. package/dist/types/session/session-stats.d.ts +48 -0
  89. package/dist/types/session/session-tools.d.ts +158 -0
  90. package/dist/types/session/session-workspace.d.ts +32 -0
  91. package/dist/types/session/stream-guards.d.ts +44 -0
  92. package/dist/types/session/streaming-output.d.ts +8 -0
  93. package/dist/types/session/todo-tracker.d.ts +56 -0
  94. package/dist/types/session/ttsr-coordinator.d.ts +51 -0
  95. package/dist/types/session/turn-recovery.d.ts +225 -0
  96. package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
  97. package/dist/types/stt/index.d.ts +0 -2
  98. package/dist/types/stt/stt-controller.d.ts +7 -0
  99. package/dist/types/system-prompt.d.ts +2 -0
  100. package/dist/types/task/executor.d.ts +4 -1
  101. package/dist/types/task/output-manager.d.ts +3 -1
  102. package/dist/types/tiny/title-client.d.ts +10 -0
  103. package/dist/types/tools/approval.d.ts +1 -0
  104. package/dist/types/tools/browser/launch.d.ts +5 -0
  105. package/dist/types/tools/builtin-names.d.ts +1 -1
  106. package/dist/types/tools/computer/protocol.d.ts +43 -0
  107. package/dist/types/tools/computer/supervisor.d.ts +32 -0
  108. package/dist/types/tools/computer/worker-entry.d.ts +1 -0
  109. package/dist/types/tools/computer/worker.d.ts +15 -0
  110. package/dist/types/tools/computer-renderer.d.ts +22 -0
  111. package/dist/types/tools/computer.d.ts +71 -0
  112. package/dist/types/tools/context.d.ts +2 -0
  113. package/dist/types/tools/default-renderer.d.ts +21 -0
  114. package/dist/types/tools/image-gen.d.ts +4 -4
  115. package/dist/types/tools/image-providers.d.ts +38 -0
  116. package/dist/types/tools/index.d.ts +7 -0
  117. package/dist/types/tools/path-utils.d.ts +8 -0
  118. package/dist/types/tools/todo.d.ts +10 -4
  119. package/dist/types/tools/xdev.d.ts +5 -1
  120. package/dist/types/tts/streaming-player.d.ts +10 -29
  121. package/dist/types/tts/vocalizer.d.ts +5 -0
  122. package/dist/types/utils/jj.d.ts +29 -0
  123. package/dist/types/utils/lang-from-path.d.ts +1 -0
  124. package/dist/types/utils/title-generator.d.ts +30 -0
  125. package/dist/types/utils/tools-manager.d.ts +1 -2
  126. package/dist/types/vibe/runtime.d.ts +36 -8
  127. package/dist/types/web/search/index.d.ts +1 -1
  128. package/dist/types/web/search/provider.d.ts +15 -5
  129. package/dist/types/web/search/types.d.ts +94 -0
  130. package/package.json +12 -12
  131. package/scripts/generate-share-viewer.ts +4 -6
  132. package/src/advisor/__tests__/advisor.test.ts +643 -0
  133. package/src/advisor/runtime.ts +144 -43
  134. package/src/async/job-manager.ts +90 -2
  135. package/src/cli/args.ts +3 -0
  136. package/src/cli/auth-gateway-cli.ts +18 -3
  137. package/src/cli/bench-cli.ts +395 -29
  138. package/src/cli/flag-tables.ts +3 -0
  139. package/src/cli/gc-cli.ts +2 -2
  140. package/src/cli/setup-cli.ts +2 -14
  141. package/src/cli/usage-cli.ts +10 -1
  142. package/src/cli.ts +8 -0
  143. package/src/collab/host.ts +3 -2
  144. package/src/commands/bench.ts +18 -4
  145. package/src/commands/launch.ts +4 -0
  146. package/src/config/model-registry.ts +16 -2
  147. package/src/config/models-config-schema.ts +15 -4
  148. package/src/config/provider-globals.ts +9 -9
  149. package/src/config/service-tier.ts +26 -1
  150. package/src/config/settings-schema.ts +270 -51
  151. package/src/config/settings.ts +81 -2
  152. package/src/dap/client.ts +37 -4
  153. package/src/edit/index.ts +49 -12
  154. package/src/eval/executor-base.ts +1 -0
  155. package/src/eval/js/executor.ts +2 -0
  156. package/src/eval/py/executor.ts +103 -33
  157. package/src/exec/bash-executor.ts +90 -6
  158. package/src/exec/direnv.ts +145 -0
  159. package/src/export/html/args.ts +20 -0
  160. package/src/export/html/index.ts +45 -47
  161. package/src/export/html/template.css +19 -1
  162. package/src/export/html/template.html +6 -0
  163. package/src/export/html/template.js +21 -0
  164. package/src/export/html/tool-views.generated.js +31 -31
  165. package/src/export/html/web-palette.ts +116 -132
  166. package/src/export/share.ts +249 -49
  167. package/src/extensibility/extensions/loader.ts +29 -1
  168. package/src/extensibility/extensions/runner.ts +6 -0
  169. package/src/extensibility/extensions/types.ts +33 -2
  170. package/src/extensibility/extensions/wrapper.ts +68 -12
  171. package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
  172. package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
  173. package/src/hindsight/state.ts +64 -6
  174. package/src/internal-urls/local-protocol.ts +2 -1
  175. package/src/internal-urls/memory-protocol.ts +2 -2
  176. package/src/internal-urls/skill-protocol.ts +2 -2
  177. package/src/internal-urls/ssh-protocol.ts +2 -1
  178. package/src/internal-urls/vault-protocol.ts +2 -1
  179. package/src/live/attestation.ts +91 -0
  180. package/src/live/controller.ts +517 -0
  181. package/src/live/prompts/agent-final-message.md +3 -0
  182. package/src/live/prompts/live-instructions.md +23 -0
  183. package/src/live/protocol.test.ts +140 -0
  184. package/src/live/protocol.ts +233 -0
  185. package/src/live/transport.ts +422 -0
  186. package/src/live/visualizer.ts +214 -0
  187. package/src/lsp/client.ts +29 -9
  188. package/src/lsp/index.ts +19 -4
  189. package/src/main.ts +8 -1
  190. package/src/mcp/config-writer.test.ts +43 -0
  191. package/src/mcp/config-writer.ts +109 -82
  192. package/src/mcp/oauth-discovery.ts +10 -2
  193. package/src/mcp/smithery-auth.test.ts +29 -0
  194. package/src/mcp/smithery-auth.ts +3 -2
  195. package/src/memories/index.ts +40 -20
  196. package/src/memory-backend/tool-names.ts +2 -0
  197. package/src/mnemopi/backend.ts +24 -7
  198. package/src/modes/acp/acp-agent.ts +56 -10
  199. package/src/modes/acp/acp-event-mapper.ts +8 -1
  200. package/src/modes/components/agent-hub.ts +1 -1
  201. package/src/modes/components/assistant-message.ts +7 -2
  202. package/src/modes/components/custom-message.ts +4 -1
  203. package/src/modes/components/login-dialog.ts +14 -4
  204. package/src/modes/components/message-frame.ts +14 -8
  205. package/src/modes/components/plan-review-overlay.ts +350 -35
  206. package/src/modes/components/session-account-selector.ts +62 -0
  207. package/src/modes/components/settings-defs.ts +23 -1
  208. package/src/modes/components/settings-selector.ts +169 -1
  209. package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
  210. package/src/modes/components/status-line/component.ts +123 -2
  211. package/src/modes/components/tool-execution.ts +28 -112
  212. package/src/modes/controllers/command-controller.ts +56 -15
  213. package/src/modes/controllers/event-controller.ts +111 -16
  214. package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
  215. package/src/modes/controllers/extension-ui-controller.ts +20 -4
  216. package/src/modes/controllers/input-controller.ts +47 -39
  217. package/src/modes/controllers/live-command-controller.ts +255 -0
  218. package/src/modes/controllers/mcp-command-controller.ts +10 -1
  219. package/src/modes/controllers/omfg-controller.ts +44 -40
  220. package/src/modes/controllers/selector-controller.ts +80 -10
  221. package/src/modes/controllers/session-focus-controller.ts +6 -1
  222. package/src/modes/interactive-mode.ts +176 -38
  223. package/src/modes/runtime-init.ts +2 -0
  224. package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
  225. package/src/modes/theme/theme.ts +1 -1
  226. package/src/modes/types.ts +4 -1
  227. package/src/modes/utils/context-usage.ts +23 -7
  228. package/src/modes/utils/ui-helpers.ts +10 -5
  229. package/src/plan-mode/plan-files.ts +40 -0
  230. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  231. package/src/prompts/bench/cache-prefix.md +3 -0
  232. package/src/prompts/bench/cache-suffix.md +1 -0
  233. package/src/prompts/system/computer-safety.md +14 -0
  234. package/src/prompts/system/custom-system-prompt.md +1 -1
  235. package/src/prompts/system/project-prompt.md +8 -1
  236. package/src/prompts/system/subagent-async-pending.md +6 -0
  237. package/src/prompts/system/system-prompt.md +1 -1
  238. package/src/prompts/system/workflow-notice.md +48 -40
  239. package/src/prompts/system/xdev-mount-notice.md +4 -0
  240. package/src/prompts/tools/computer.md +26 -0
  241. package/src/prompts/tools/todo.md +3 -1
  242. package/src/registry/agent-lifecycle.ts +116 -46
  243. package/src/registry/agent-registry.ts +35 -8
  244. package/src/registry/persisted-agents.ts +26 -2
  245. package/src/sdk.ts +215 -136
  246. package/src/secrets/index.ts +127 -4
  247. package/src/secrets/obfuscator.ts +2328 -105
  248. package/src/session/acp-permission-gate.ts +165 -0
  249. package/src/session/agent-session-error-log.test.ts +1 -1
  250. package/src/session/agent-session-events.ts +66 -0
  251. package/src/session/agent-session-types.ts +343 -0
  252. package/src/session/agent-session.ts +1940 -12218
  253. package/src/session/artifacts.ts +6 -5
  254. package/src/session/async-job-delivery.ts +74 -0
  255. package/src/session/auth-storage.ts +1 -0
  256. package/src/session/bash-runner.ts +326 -0
  257. package/src/session/blob-store.test.ts +56 -0
  258. package/src/session/blob-store.ts +18 -2
  259. package/src/session/checkpoint-entries.ts +81 -0
  260. package/src/session/eval-runner.ts +217 -0
  261. package/src/session/indexed-session-storage.ts +9 -2
  262. package/src/session/irc-bridge.ts +203 -0
  263. package/src/session/messages.ts +266 -1
  264. package/src/session/model-controls.ts +714 -0
  265. package/src/session/prewalk.ts +252 -0
  266. package/src/session/queued-messages.ts +93 -0
  267. package/src/session/redis-session-storage.ts +51 -11
  268. package/src/session/retry-fallback-chains.ts +455 -0
  269. package/src/session/role-models.ts +85 -0
  270. package/src/session/session-advisors.ts +1679 -0
  271. package/src/session/session-context.ts +6 -3
  272. package/src/session/session-entries.ts +8 -0
  273. package/src/session/session-handoff.ts +308 -0
  274. package/src/session/session-history-format.ts +20 -9
  275. package/src/session/session-loader.ts +0 -46
  276. package/src/session/session-maintenance.ts +2983 -0
  277. package/src/session/session-manager.ts +390 -30
  278. package/src/session/session-memory.ts +222 -0
  279. package/src/session/session-provider-boundary.ts +307 -0
  280. package/src/session/session-stats.ts +293 -0
  281. package/src/session/session-tools.ts +980 -0
  282. package/src/session/session-workspace.ts +53 -0
  283. package/src/session/stream-guards.ts +417 -0
  284. package/src/session/streaming-output.ts +52 -5
  285. package/src/session/todo-tracker.ts +380 -0
  286. package/src/session/ttsr-coordinator.ts +496 -0
  287. package/src/session/turn-recovery.ts +1629 -0
  288. package/src/slash-commands/builtin-registry.ts +257 -20
  289. package/src/slash-commands/helpers/session-pin.ts +44 -0
  290. package/src/slash-commands/helpers/usage-report.ts +22 -3
  291. package/src/stt/downloader.ts +0 -2
  292. package/src/stt/index.ts +0 -2
  293. package/src/stt/stt-controller.ts +57 -146
  294. package/src/system-prompt.ts +45 -17
  295. package/src/task/executor.ts +285 -70
  296. package/src/task/index.ts +98 -105
  297. package/src/task/output-manager.ts +25 -3
  298. package/src/task/persisted-revive.ts +4 -3
  299. package/src/task/structured-subagent.ts +1 -0
  300. package/src/tiny/title-client.ts +22 -0
  301. package/src/tools/approval.ts +55 -10
  302. package/src/tools/bash-interactive.ts +116 -86
  303. package/src/tools/bash-skill-urls.ts +28 -2
  304. package/src/tools/bash.ts +339 -123
  305. package/src/tools/browser/launch.ts +9 -1
  306. package/src/tools/browser/tab-supervisor.ts +8 -5
  307. package/src/tools/browser.ts +94 -47
  308. package/src/tools/builtin-names.ts +1 -0
  309. package/src/tools/computer/protocol.ts +28 -0
  310. package/src/tools/computer/supervisor.ts +258 -0
  311. package/src/tools/computer/worker-entry.ts +25 -0
  312. package/src/tools/computer/worker.ts +135 -0
  313. package/src/tools/computer-renderer.ts +108 -0
  314. package/src/tools/computer.ts +433 -0
  315. package/src/tools/context.ts +2 -0
  316. package/src/tools/default-renderer.ts +139 -0
  317. package/src/tools/essential-tools.ts +1 -0
  318. package/src/tools/image-gen.ts +20 -28
  319. package/src/tools/image-providers.ts +50 -0
  320. package/src/tools/index.ts +14 -0
  321. package/src/tools/path-utils.ts +49 -2
  322. package/src/tools/read.ts +156 -88
  323. package/src/tools/renderers.ts +9 -1
  324. package/src/tools/todo.ts +101 -11
  325. package/src/tools/vibe.ts +1 -2
  326. package/src/tools/write.ts +51 -1
  327. package/src/tools/xdev.ts +121 -29
  328. package/src/tts/streaming-player.ts +82 -283
  329. package/src/tts/vocalizer.ts +18 -1
  330. package/src/utils/clipboard.ts +1 -30
  331. package/src/utils/jj.ts +125 -4
  332. package/src/utils/lang-from-path.ts +7 -0
  333. package/src/utils/mac-file-urls.applescript +37 -0
  334. package/src/utils/title-generator.ts +137 -1
  335. package/src/utils/tool-choice.ts +14 -0
  336. package/src/utils/tools-manager.ts +1 -19
  337. package/src/vibe/runtime.ts +852 -72
  338. package/src/web/search/index.ts +3 -6
  339. package/src/web/search/provider.ts +31 -16
  340. package/src/web/search/providers/xai.ts +7 -1
  341. package/src/web/search/types.ts +3 -0
  342. package/dist/types/stt/recorder.d.ts +0 -30
  343. package/dist/types/stt/transcriber.d.ts +0 -14
  344. package/dist/types/stt/wav.d.ts +0 -29
  345. package/dist/types/tts/player.d.ts +0 -32
  346. package/src/stt/recorder.ts +0 -551
  347. package/src/stt/transcriber.ts +0 -60
  348. package/src/stt/wav.ts +0 -173
  349. package/src/tts/player.ts +0 -137
@@ -73,11 +73,14 @@ import {
73
73
  type SessionStorageWriter,
74
74
  } from "./session-storage";
75
75
  import { type SessionTitleUpdate, serializeTitleSlot } from "./session-title-slot";
76
+ import {
77
+ additionalWorkspaceDirectories,
78
+ normalizeSessionWorkspace,
79
+ normalizeWorkspaceDirectory,
80
+ } from "./session-workspace";
76
81
 
77
82
  const JSONL_SUFFIX_LENGTH = ".jsonl".length;
78
83
  const DRAFT_ONLY_SESSION_MARKER = ".draft-only-session";
79
- const SUPERSEDED_COMPACTION_SUMMARY = "[Superseded compaction summary elided after a newer compaction]";
80
- const SUPERSEDED_COMPACTION_SHORT_SUMMARY = "Superseded compaction elided";
81
84
 
82
85
  function mintSessionId(): string {
83
86
  return Bun.randomUUIDv7();
@@ -365,6 +368,34 @@ interface DiskQueueOptions {
365
368
  epoch?: number;
366
369
  }
367
370
 
371
+ interface AtomicEntryBatch {
372
+ collecting: boolean;
373
+ entryIds: Set<string>;
374
+ deferredNotifications: SessionEntry[];
375
+ preBatchLeafId: string | null;
376
+ externalLeafChanged: boolean;
377
+ externalLeafId: string | null;
378
+ }
379
+
380
+ /**
381
+ * The storage may have published a write that rejected, and an authoritative
382
+ * repair could not be proven durable. Callers must fail closed until recovery.
383
+ */
384
+ export class SessionPersistenceIndeterminateError extends AggregateError {
385
+ readonly operationError: Error;
386
+ readonly recoveryErrors: readonly Error[];
387
+
388
+ constructor(operationError: Error, recoveryErrors: readonly Error[]) {
389
+ super(
390
+ [operationError, ...recoveryErrors],
391
+ `Session persistence is indeterminate after "${operationError.message}" and authoritative repair failed.`,
392
+ );
393
+ this.name = "SessionPersistenceIndeterminateError";
394
+ this.operationError = operationError;
395
+ this.recoveryErrors = [...recoveryErrors];
396
+ }
397
+ }
398
+
368
399
  /**
369
400
  * Stores and navigates an append-only conversation journal.
370
401
  *
@@ -380,6 +411,8 @@ interface DiskQueueOptions {
380
411
  */
381
412
  export class SessionManager {
382
413
  #cwd: string;
414
+ /** Additional workspace directories beyond cwd (multi-root). Normalized absolute, deduped, excludes cwd. */
415
+ #additionalDirectories: string[] = [];
383
416
  #sessionDir: string;
384
417
  readonly #persist: boolean;
385
418
  readonly #storage: SessionStorage;
@@ -425,6 +458,10 @@ export class SessionManager {
425
458
  #diskTail: Promise<void> = Promise.resolve();
426
459
  #diskFailure: Error | undefined;
427
460
  #diskFailureLogged = false;
461
+ /** FIFO reservation for atomic batches and authoritative recovery. */
462
+ #atomicPersistenceTail: Promise<void> = Promise.resolve();
463
+ /** Observer notifications withheld until their entries are proven durable. */
464
+ #pendingDurabilityNotifications: SessionEntry[] = [];
428
465
  /** Bumped on every sync rewrite / chain reset so stale queued tasks become no-ops. */
429
466
  #diskEpoch = 0;
430
467
  /**
@@ -438,6 +475,8 @@ export class SessionManager {
438
475
  #atomicRewriteFenceEpoch: number | null = null;
439
476
  /** Set by synchronous appends that land while an atomic replacement is active. */
440
477
  #atomicRewriteDirty = false;
478
+ /** Atomic entry batch currently staged for a full-file commit. */
479
+ #atomicEntryBatch: AtomicEntryBatch | undefined;
441
480
 
442
481
  #artifactManager: ArtifactManager | null = null;
443
482
  #artifactManagerSessionFile: string | null = null;
@@ -517,6 +556,18 @@ export class SessionManager {
517
556
  return reported;
518
557
  }
519
558
 
559
+ async #withAtomicPersistenceLock<T>(operation: () => Promise<T>): Promise<T> {
560
+ const predecessor = this.#atomicPersistenceTail;
561
+ const turn = Promise.withResolvers<void>();
562
+ this.#atomicPersistenceTail = predecessor.catch(() => undefined).then(() => turn.promise);
563
+ await predecessor.catch(() => undefined);
564
+ try {
565
+ return await operation();
566
+ } finally {
567
+ turn.resolve();
568
+ }
569
+ }
570
+
520
571
  async #drainAndCloseWriter(): Promise<void> {
521
572
  try {
522
573
  await this.#scheduleDiskWork(
@@ -544,6 +595,117 @@ export class SessionManager {
544
595
  await writer.close();
545
596
  }
546
597
 
598
+ #latchIndeterminate(operationError: Error, recoveryErrors: readonly Error[]): SessionPersistenceIndeterminateError {
599
+ const error = new SessionPersistenceIndeterminateError(operationError, recoveryErrors);
600
+ this.#diskFailure = error;
601
+ if (!this.#diskFailureLogged) {
602
+ this.#diskFailureLogged = true;
603
+ logger.error("Session persistence became indeterminate.", {
604
+ sessionFile: this.#sessionFile,
605
+ error: error.message,
606
+ });
607
+ }
608
+ return error;
609
+ }
610
+
611
+ #notifyDurableEntries(entries: readonly SessionEntry[] = []): void {
612
+ const notifications = [...this.#pendingDurabilityNotifications, ...entries];
613
+ this.#pendingDurabilityNotifications = [];
614
+ const seen = new Set<string>();
615
+ for (const entry of notifications) {
616
+ if (seen.has(entry.id)) continue;
617
+ seen.add(entry.id);
618
+ this.#notifyEntryAppended(entry);
619
+ }
620
+ }
621
+
622
+ async #authoritativelyRewriteCurrentStateLocked(operationError: Error): Promise<void> {
623
+ if (!this.#persist || !this.#sessionFile) return;
624
+ const previousDiskTail = this.#diskTail;
625
+ const writer = this.#writer;
626
+ this.#diskEpoch++;
627
+ const epoch = this.#diskEpoch;
628
+ this.#writer = undefined;
629
+ this.#diskTail = Promise.resolve();
630
+ this.#forceFileCreation = true;
631
+ this.#fileIsCurrent = false;
632
+ this.#rewriteRequired = true;
633
+ this.#atomicRewriteFenceEpoch = epoch;
634
+ if (!this.#diskFailure) this.#diskFailure = operationError;
635
+ try {
636
+ await previousDiskTail.catch(() => undefined);
637
+ let closeError: Error | undefined;
638
+ if (writer) {
639
+ try {
640
+ await writer.close();
641
+ } catch (error) {
642
+ closeError = toError(error);
643
+ }
644
+ }
645
+ let drainError: Error | undefined;
646
+ try {
647
+ await this.#storage.drain();
648
+ } catch (error) {
649
+ drainError = toError(error);
650
+ }
651
+ if (writer?.isOpen()) {
652
+ throw this.#latchIndeterminate(operationError, [
653
+ closeError ?? new Error("Failed to close session writer before authoritative repair."),
654
+ ...(drainError ? [drainError] : []),
655
+ ]);
656
+ }
657
+
658
+ do {
659
+ this.#atomicRewriteDirty = false;
660
+ const sessionFile = this.#sessionFile;
661
+ if (!sessionFile) {
662
+ throw this.#latchIndeterminate(operationError, [
663
+ new Error("Session file disappeared during authoritative repair."),
664
+ ]);
665
+ }
666
+ const body = this.#fileBody();
667
+ try {
668
+ await this.#storage.writeTextAtomic(sessionFile, body, {
669
+ commitGuard: () => this.#diskEpoch === epoch,
670
+ });
671
+ } catch (error) {
672
+ const recoveryErrors = [toError(error)];
673
+ try {
674
+ await this.#storage.drain();
675
+ } catch (drainFailure) {
676
+ recoveryErrors.push(toError(drainFailure));
677
+ }
678
+ let actual: string;
679
+ try {
680
+ actual = await this.#storage.readText(sessionFile);
681
+ } catch (readFailure) {
682
+ recoveryErrors.push(toError(readFailure));
683
+ throw this.#latchIndeterminate(operationError, recoveryErrors);
684
+ }
685
+ if (actual !== body) {
686
+ recoveryErrors.push(new Error("Authoritative session repair did not match durable storage."));
687
+ throw this.#latchIndeterminate(operationError, recoveryErrors);
688
+ }
689
+ }
690
+ if (this.#diskEpoch !== epoch) {
691
+ throw this.#latchIndeterminate(operationError, [
692
+ new Error("Authoritative session repair was superseded before verification."),
693
+ ]);
694
+ }
695
+ } while (this.#atomicRewriteDirty);
696
+
697
+ this.#fileIsCurrent = true;
698
+ this.#rewriteRequired = false;
699
+ this.#hasTitleSlot = true;
700
+ this.#clearDiskError();
701
+ } catch (error) {
702
+ if (error instanceof SessionPersistenceIndeterminateError) throw error;
703
+ throw this.#latchIndeterminate(operationError, [toError(error)]);
704
+ } finally {
705
+ if (this.#atomicRewriteFenceEpoch === epoch) this.#atomicRewriteFenceEpoch = null;
706
+ }
707
+ }
708
+
547
709
  #appendWriter(): SessionStorageWriter {
548
710
  if (!this.#sessionFile) throw new Error("Cannot open a session writer before a session file exists");
549
711
 
@@ -583,26 +745,6 @@ export class SessionManager {
583
745
  return this.#forceFileCreation || this.#fileIsCurrent || this.#historyContainsAssistantMessage();
584
746
  }
585
747
 
586
- #elideSupersededCompactionsOnBranch(leafId: string | null): boolean {
587
- if (!leafId) return false;
588
- let changed = false;
589
- for (const entry of this.#index.pathTo(leafId)) {
590
- if (entry.type !== "compaction") continue;
591
- if (
592
- entry.summary === SUPERSEDED_COMPACTION_SUMMARY &&
593
- entry.shortSummary === SUPERSEDED_COMPACTION_SHORT_SUMMARY &&
594
- entry.preserveData === undefined
595
- ) {
596
- continue;
597
- }
598
- entry.summary = SUPERSEDED_COMPACTION_SUMMARY;
599
- entry.shortSummary = SUPERSEDED_COMPACTION_SHORT_SUMMARY;
600
- entry.preserveData = undefined;
601
- changed = true;
602
- }
603
- return changed;
604
- }
605
-
606
748
  /**
607
749
  * Synchronously rewrite the whole file (header + entries) and keep no open
608
750
  * writer; the next append re-opens one. `writeTextSync` returns with the
@@ -690,6 +832,12 @@ export class SessionManager {
690
832
 
691
833
  #appendToSessionFile(entry: SessionEntry): void {
692
834
  if (!this.#persist || !this.#sessionFile) return;
835
+ if (this.#atomicEntryBatch) {
836
+ this.#fileIsCurrent = false;
837
+ this.#rewriteRequired = true;
838
+ this.#atomicRewriteDirty = true;
839
+ return;
840
+ }
693
841
  if (this.#diskFailure) throw this.#diskFailure;
694
842
 
695
843
  // Lazy gate: a brand-new session is not written until it has an assistant
@@ -805,6 +953,14 @@ export class SessionManager {
805
953
  parentSession: options?.parentSession,
806
954
  providerPromptCacheKey: options?.providerPromptCacheKey,
807
955
  };
956
+ const workspace = normalizeSessionWorkspace({
957
+ cwd: this.#cwd,
958
+ directories: options?.additionalDirectories ?? [],
959
+ });
960
+ this.#additionalDirectories = additionalWorkspaceDirectories(workspace);
961
+ if (this.#additionalDirectories.length > 0) {
962
+ this.#header.additionalDirectories = [...this.#additionalDirectories];
963
+ }
808
964
  this.#titleUpdatedAt = timestamp;
809
965
 
810
966
  this.#entries = [];
@@ -853,11 +1009,44 @@ export class SessionManager {
853
1009
  };
854
1010
  }
855
1011
 
1012
+ #setLeaf(id: string | null): void {
1013
+ this.#index.setLeaf(id);
1014
+ const batch = this.#atomicEntryBatch;
1015
+ if (batch && !batch.collecting) {
1016
+ batch.externalLeafChanged = true;
1017
+ batch.externalLeafId = id;
1018
+ }
1019
+ }
1020
+
856
1021
  #recordEntry(entry: SessionEntry): void {
857
1022
  this.#entries.push(entry);
858
1023
  this.#index.insert(entry);
1024
+ const batch = this.#atomicEntryBatch;
1025
+ if (batch?.collecting) batch.entryIds.add(entry.id);
1026
+ if (batch && !batch.collecting) {
1027
+ batch.externalLeafChanged = true;
1028
+ batch.externalLeafId = entry.id;
1029
+ }
859
1030
  this.#appendToSessionFile(entry);
860
- this.#notifyEntryAppended(entry);
1031
+ if (batch) batch.deferredNotifications.push(entry);
1032
+ else this.#notifyEntryAppended(entry);
1033
+ }
1034
+
1035
+ #rollbackAtomicEntryBatch(batch: AtomicEntryBatch): void {
1036
+ const retainedAncestor = (id: string | null): string | null => {
1037
+ const seen = new Set<string>();
1038
+ while (id && batch.entryIds.has(id) && !seen.has(id)) {
1039
+ seen.add(id);
1040
+ id = this.#index.get(id)?.parentId ?? null;
1041
+ }
1042
+ return id;
1043
+ };
1044
+ const retained = this.#entries.filter(entry => !batch.entryIds.has(entry.id));
1045
+ for (const entry of retained) entry.parentId = retainedAncestor(entry.parentId);
1046
+ const restoredLeaf = retainedAncestor(batch.externalLeafChanged ? batch.externalLeafId : batch.preBatchLeafId);
1047
+ this.#entries = retained;
1048
+ this.#index.rebuild(retained);
1049
+ this.#index.setLeaf(restoredLeaf && this.#index.has(restoredLeaf) ? restoredLeaf : null);
861
1050
  }
862
1051
 
863
1052
  #draftPath(): string | null {
@@ -988,6 +1177,7 @@ export class SessionManager {
988
1177
  this.#forceFileCreation = snapshot.onDisk;
989
1178
  this.#draftOnlySessionCleanupArmed = snapshot.draftOnlySessionCleanupArmed;
990
1179
  this.#applyEntries(snapshot.header, [...snapshot.entries]);
1180
+ this.#additionalDirectories = snapshot.header.additionalDirectories ?? [];
991
1181
  this.#sessionName = snapshot.sessionName;
992
1182
  this.#titleSource = snapshot.titleSource;
993
1183
  this.#titleUpdatedAt = snapshot.titleUpdatedAt;
@@ -1040,6 +1230,7 @@ export class SessionManager {
1040
1230
  }
1041
1231
 
1042
1232
  this.#applyEntries(header, fileEntries.slice(1) as SessionEntry[]);
1233
+ this.#additionalDirectories = header.additionalDirectories ?? [];
1043
1234
  this.#titleUpdatedAt = titleSlot?.updatedAt ?? header.timestamp;
1044
1235
  this.#hasTitleSlot = titleSlot !== undefined;
1045
1236
  this.#fileIsCurrent = true;
@@ -1090,6 +1281,7 @@ export class SessionManager {
1090
1281
  titleSource: this.#header.titleSource ?? this.#titleSource,
1091
1282
  timestamp,
1092
1283
  cwd: this.#cwd,
1284
+ additionalDirectories: this.#additionalDirectories.length > 0 ? [...this.#additionalDirectories] : undefined,
1093
1285
  parentSession: parentSessionId,
1094
1286
  providerPromptCacheKey: this.#header.providerPromptCacheKey ?? parentSessionId,
1095
1287
  };
@@ -1197,6 +1389,14 @@ export class SessionManager {
1197
1389
  this.#cwd = resolvedCwd;
1198
1390
  this.#sessionDir = nextSessionDir;
1199
1391
  this.#header.cwd = resolvedCwd;
1392
+ // Re-filter additional roots: the new cwd may have been an additional root,
1393
+ // or it may now contain/subsume one. Re-normalize to keep the invariant
1394
+ // that cwd is never also listed as an additional directory.
1395
+ if (this.#additionalDirectories.length > 0) {
1396
+ this.#additionalDirectories = this.#additionalDirectories.filter(d => d !== resolvedCwd);
1397
+ this.#header.additionalDirectories =
1398
+ this.#additionalDirectories.length > 0 ? this.#additionalDirectories : undefined;
1399
+ }
1200
1400
 
1201
1401
  // Rewrite at the new location when the file already existed (update cwd) or
1202
1402
  // there is in-memory output worth materializing; otherwise stay lazy.
@@ -1220,6 +1420,92 @@ export class SessionManager {
1220
1420
  await this.#rewriteAtomically();
1221
1421
  }
1222
1422
 
1423
+ /**
1424
+ * Stage a synchronous group of entry appends and publish the resulting full
1425
+ * journal with one atomic replace. A failed publish removes only the staged
1426
+ * entries, preserves/reparents entries appended concurrently, restores the
1427
+ * prior durable file view, and clears the failed writer latch for retry.
1428
+ *
1429
+ * The callback MUST be synchronous.
1430
+ */
1431
+ appendEntriesAtomically<T>(append: () => T): Promise<T> {
1432
+ return this.#withAtomicPersistenceLock(() => this.#appendEntriesAtomicallyLocked(append));
1433
+ }
1434
+
1435
+ async #appendEntriesAtomicallyLocked<T>(append: () => T): Promise<T> {
1436
+ if (!this.#persist || !this.#sessionFile) return append();
1437
+ if (this.#atomicEntryBatch) throw new Error("Atomic persistence lock ownership was violated.");
1438
+ try {
1439
+ await this.ensureOnDisk();
1440
+ await this.flush();
1441
+ } catch (error) {
1442
+ const operationError = toError(error);
1443
+ await this.#authoritativelyRewriteCurrentStateLocked(operationError);
1444
+ this.#notifyDurableEntries();
1445
+ throw error;
1446
+ }
1447
+
1448
+ const batch: AtomicEntryBatch = {
1449
+ collecting: true,
1450
+ entryIds: new Set(),
1451
+ deferredNotifications: [],
1452
+ preBatchLeafId: this.#index.leafId(),
1453
+ externalLeafChanged: false,
1454
+ externalLeafId: null,
1455
+ };
1456
+ this.#atomicEntryBatch = batch;
1457
+ let result!: T;
1458
+ try {
1459
+ try {
1460
+ result = append();
1461
+ } finally {
1462
+ batch.collecting = false;
1463
+ }
1464
+ await this.#rewriteAtomically();
1465
+ if (!this.#fileIsCurrent || this.#rewriteRequired) {
1466
+ throw new Error("Atomic session batch was superseded before commit.");
1467
+ }
1468
+ this.#atomicEntryBatch = undefined;
1469
+ this.#notifyDurableEntries(batch.deferredNotifications);
1470
+ return result;
1471
+ } catch (error) {
1472
+ batch.collecting = false;
1473
+ const operationError = toError(error);
1474
+ this.#rollbackAtomicEntryBatch(batch);
1475
+ try {
1476
+ await this.#authoritativelyRewriteCurrentStateLocked(operationError);
1477
+ } catch (repairError) {
1478
+ const retainedNotifications = batch.deferredNotifications.filter(entry => !batch.entryIds.has(entry.id));
1479
+ this.#pendingDurabilityNotifications.push(...retainedNotifications);
1480
+ this.#atomicEntryBatch = undefined;
1481
+ this.#fileIsCurrent = false;
1482
+ this.#rewriteRequired = true;
1483
+ if (repairError instanceof SessionPersistenceIndeterminateError) throw repairError;
1484
+ throw this.#latchIndeterminate(operationError, [toError(repairError)]);
1485
+ }
1486
+ const retainedNotifications = batch.deferredNotifications.filter(entry => !batch.entryIds.has(entry.id));
1487
+ this.#atomicEntryBatch = undefined;
1488
+ this.#notifyDurableEntries(retainedNotifications);
1489
+ throw error;
1490
+ }
1491
+ }
1492
+
1493
+ /**
1494
+ * Replace an uncertain append tail with the authoritative in-memory journal.
1495
+ * Callers must only use this for monotonic recovery where every retained
1496
+ * entry remains intended (for example, an explicit terminal tombstone).
1497
+ */
1498
+ recoverPersistenceFromCurrentState(): Promise<void> {
1499
+ return this.#withAtomicPersistenceLock(async () => {
1500
+ if (!this.#persist || !this.#sessionFile) return;
1501
+ if (this.#atomicEntryBatch) throw new Error("Atomic persistence lock ownership was violated.");
1502
+ const operationError =
1503
+ this.#diskFailure ?? new Error("Authoritative session persistence recovery was requested.");
1504
+ await this.#authoritativelyRewriteCurrentStateLocked(operationError);
1505
+ this.#notifyDurableEntries();
1506
+ });
1507
+ }
1508
+
1223
1509
  /** Flush pending writes. Call before switching sessions or on shutdown. */
1224
1510
  async flush(): Promise<void> {
1225
1511
  if (!this.#persist || !this.#sessionFile) return;
@@ -1240,6 +1526,7 @@ export class SessionManager {
1240
1526
  */
1241
1527
  flushSync(): void {
1242
1528
  if (!this.#persist || !this.#sessionFile) return;
1529
+ if (this.#atomicEntryBatch) throw new Error("Cannot synchronously flush during an atomic session batch.");
1243
1530
  if (this.#diskFailure) throw this.#diskFailure;
1244
1531
  if (this.#fileIsCurrent && !this.#rewriteRequired) {
1245
1532
  const writerError = this.#writer?.getError();
@@ -1303,6 +1590,79 @@ export class SessionManager {
1303
1590
  return this.#cwd;
1304
1591
  }
1305
1592
 
1593
+ /** Additional workspace directories beyond cwd (multi-root), absolute and normalized. */
1594
+ getAdditionalDirectories(): string[] {
1595
+ return [...this.#additionalDirectories];
1596
+ }
1597
+
1598
+ /**
1599
+ * Persist a workspace-directory change to the session header. Respects the
1600
+ * lazy-persistence gate: a session with no durable output yet keeps the
1601
+ * change in memory (the header lands with the first real write), so seeding
1602
+ * roots at launch never materializes an empty resumable session file.
1603
+ */
1604
+ async #persistWorkspaceDirectoriesChange(): Promise<void> {
1605
+ if (!this.#persist || !this.#sessionFile || !this.#shouldHaveSessionFile()) return;
1606
+ this.#rewriteRequired = true;
1607
+ await this.#rewriteAtomically();
1608
+ }
1609
+
1610
+ /**
1611
+ * Add a workspace directory. Normalizes (relative to cwd), dedupes, rejects
1612
+ * the cwd itself, persists to the session header, and triggers an atomic
1613
+ * rewrite so the change survives a crash. Returns the resolved absolute
1614
+ * path or `null` when the directory was already present (no-op).
1615
+ */
1616
+ async addWorkspaceDirectory(directory: string): Promise<string | null> {
1617
+ const resolved = normalizeWorkspaceDirectory(directory, this.#cwd);
1618
+ if (resolved === path.resolve(this.#cwd)) {
1619
+ throw new Error("The current working directory is already the primary workspace root.");
1620
+ }
1621
+ if (this.#additionalDirectories.includes(resolved)) return null;
1622
+ this.#additionalDirectories = [...this.#additionalDirectories, resolved];
1623
+ this.#header.additionalDirectories = this.#additionalDirectories;
1624
+ await this.#persistWorkspaceDirectoriesChange();
1625
+ return resolved;
1626
+ }
1627
+
1628
+ /**
1629
+ * Remove a workspace directory by absolute or cwd-relative path. Persists
1630
+ * the trimmed header. Returns the resolved path that was removed, or
1631
+ * `null` when the directory was not an additional root (no-op).
1632
+ */
1633
+ async removeWorkspaceDirectory(directory: string): Promise<string | null> {
1634
+ const resolved = normalizeWorkspaceDirectory(directory, this.#cwd);
1635
+ const idx = this.#additionalDirectories.findIndex(p => path.resolve(p) === resolved);
1636
+ if (idx === -1) return null;
1637
+ this.#additionalDirectories = this.#additionalDirectories.filter((_, i) => i !== idx);
1638
+ if (this.#additionalDirectories.length === 0) {
1639
+ this.#header.additionalDirectories = undefined;
1640
+ } else {
1641
+ this.#header.additionalDirectories = this.#additionalDirectories;
1642
+ }
1643
+ await this.#persistWorkspaceDirectoriesChange();
1644
+ return resolved;
1645
+ }
1646
+
1647
+ /** Seed additional directories from settings or a passed list. Also called on resumed sessions with --add-dir; persists the updated header when the session file is already durable. No-op when the normalized list is unchanged (avoids rewriting large session files on every startup). */
1648
+ async setAdditionalDirectories(directories: string[]): Promise<void> {
1649
+ const workspace = normalizeSessionWorkspace({ cwd: this.#cwd, directories });
1650
+ const next = additionalWorkspaceDirectories(workspace);
1651
+ if (
1652
+ next.length === this.#additionalDirectories.length &&
1653
+ next.every((d, i) => d === this.#additionalDirectories[i])
1654
+ ) {
1655
+ return;
1656
+ }
1657
+ this.#additionalDirectories = next;
1658
+ if (this.#additionalDirectories.length > 0) {
1659
+ this.#header.additionalDirectories = this.#additionalDirectories;
1660
+ } else {
1661
+ this.#header.additionalDirectories = undefined;
1662
+ }
1663
+ await this.#persistWorkspaceDirectoriesChange();
1664
+ }
1665
+
1306
1666
  getUsageStatistics(): UsageStatistics {
1307
1667
  return this.#index.usageSnapshot();
1308
1668
  }
@@ -1597,7 +1957,6 @@ export class SessionManager {
1597
1957
  fromExtension?: boolean,
1598
1958
  preserveData?: Record<string, unknown>,
1599
1959
  ): string {
1600
- const elidedSupersededCompactions = this.#elideSupersededCompactionsOnBranch(this.#index.leafId());
1601
1960
  const entry: CompactionEntry<T> = {
1602
1961
  type: "compaction",
1603
1962
  ...this.#freshEntryFields(),
@@ -1610,9 +1969,6 @@ export class SessionManager {
1610
1969
  preserveData,
1611
1970
  };
1612
1971
  this.#recordEntry(entry);
1613
- if (elidedSupersededCompactions) {
1614
- void this.#rewriteAtomically().catch(err => this.#noteDiskFailure(err));
1615
- }
1616
1972
  return entry.id;
1617
1973
  }
1618
1974
 
@@ -1782,19 +2138,19 @@ export class SessionManager {
1782
2138
  */
1783
2139
  branch(branchFromId: string): void {
1784
2140
  if (!this.#index.has(branchFromId)) throw new Error(`Entry ${branchFromId} not found`);
1785
- this.#index.setLeaf(branchFromId);
2141
+ this.#setLeaf(branchFromId);
1786
2142
  }
1787
2143
 
1788
2144
  /** Reset the leaf to null so the next append creates a new root entry. */
1789
2145
  resetLeaf(): void {
1790
- this.#index.setLeaf(null);
2146
+ this.#setLeaf(null);
1791
2147
  }
1792
2148
 
1793
2149
  /** Like branch(), but also records a branch_summary of the abandoned path. */
1794
2150
  branchWithSummary(branchFromId: string | null, summary: string, details?: unknown, fromExtension?: boolean): string {
1795
2151
  if (branchFromId !== null && !this.#index.has(branchFromId)) throw new Error(`Entry ${branchFromId} not found`);
1796
2152
 
1797
- this.#index.setLeaf(branchFromId);
2153
+ this.#setLeaf(branchFromId);
1798
2154
  const entry: BranchSummaryEntry = {
1799
2155
  type: "branch_summary",
1800
2156
  id: generateId(this.#index),
@@ -1836,6 +2192,7 @@ export class SessionManager {
1836
2192
  timestamp,
1837
2193
  cwd: this.#cwd,
1838
2194
  parentSession: this.#persist ? sourceSessionFile : undefined,
2195
+ additionalDirectories: this.#additionalDirectories.length > 0 ? [...this.#additionalDirectories] : undefined,
1839
2196
  };
1840
2197
 
1841
2198
  const labels: LabelEntry[] = [];
@@ -1956,6 +2313,9 @@ export class SessionManager {
1956
2313
  );
1957
2314
  manager.#header.title = sourceHeader?.title;
1958
2315
  manager.#header.titleSource = sourceHeader?.titleSource;
2316
+ manager.#additionalDirectories = (sourceHeader?.additionalDirectories ?? []).filter(d => d !== path.resolve(cwd));
2317
+ manager.#header.additionalDirectories =
2318
+ manager.#additionalDirectories.length > 0 ? manager.#additionalDirectories : undefined;
1959
2319
  manager.#sessionName = manager.#header.title;
1960
2320
  manager.#titleSource = manager.#header.titleSource;
1961
2321
  manager.#titleUpdatedAt = nowIso();