open-multi-agent-kit 0.90.8 → 0.91.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 (350) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +21 -2
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +4 -0
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/commands/doctor-provider-cli.d.ts +33 -0
  7. package/dist/commands/doctor-provider-cli.d.ts.map +1 -0
  8. package/dist/commands/doctor-provider-cli.js +215 -0
  9. package/dist/commands/doctor-provider-cli.js.map +1 -0
  10. package/dist/commands/doctor-provider-transport.d.ts +35 -0
  11. package/dist/commands/doctor-provider-transport.d.ts.map +1 -0
  12. package/dist/commands/doctor-provider-transport.js +185 -0
  13. package/dist/commands/doctor-provider-transport.js.map +1 -0
  14. package/dist/commands/doctor-provider.d.ts +121 -7
  15. package/dist/commands/doctor-provider.d.ts.map +1 -1
  16. package/dist/commands/doctor-provider.js +1295 -102
  17. package/dist/commands/doctor-provider.js.map +1 -1
  18. package/dist/commands/session-doctor-cli.d.ts +43 -0
  19. package/dist/commands/session-doctor-cli.d.ts.map +1 -0
  20. package/dist/commands/session-doctor-cli.js +494 -0
  21. package/dist/commands/session-doctor-cli.js.map +1 -0
  22. package/dist/commands/verify-ci.d.ts +21 -0
  23. package/dist/commands/verify-ci.d.ts.map +1 -0
  24. package/dist/commands/verify-ci.js +66 -0
  25. package/dist/commands/verify-ci.js.map +1 -0
  26. package/dist/core/agent-session-services.d.ts +3 -0
  27. package/dist/core/agent-session-services.d.ts.map +1 -1
  28. package/dist/core/agent-session-services.js +3 -0
  29. package/dist/core/agent-session-services.js.map +1 -1
  30. package/dist/core/agent-session.d.ts +140 -2
  31. package/dist/core/agent-session.d.ts.map +1 -1
  32. package/dist/core/agent-session.js +1179 -95
  33. package/dist/core/agent-session.js.map +1 -1
  34. package/dist/core/agent-tool-settings.d.ts +35 -0
  35. package/dist/core/agent-tool-settings.d.ts.map +1 -0
  36. package/dist/core/agent-tool-settings.js +155 -0
  37. package/dist/core/agent-tool-settings.js.map +1 -0
  38. package/dist/core/atomic-session-file.d.ts +14 -0
  39. package/dist/core/atomic-session-file.d.ts.map +1 -0
  40. package/dist/core/atomic-session-file.js +104 -0
  41. package/dist/core/atomic-session-file.js.map +1 -0
  42. package/dist/core/command-safety.d.ts.map +1 -1
  43. package/dist/core/command-safety.js +0 -8
  44. package/dist/core/command-safety.js.map +1 -1
  45. package/dist/core/compaction/compaction.d.ts +6 -0
  46. package/dist/core/compaction/compaction.d.ts.map +1 -1
  47. package/dist/core/compaction/compaction.js +12 -1
  48. package/dist/core/compaction/compaction.js.map +1 -1
  49. package/dist/core/compaction/hysteresis.d.ts +27 -0
  50. package/dist/core/compaction/hysteresis.d.ts.map +1 -0
  51. package/dist/core/compaction/hysteresis.js +84 -0
  52. package/dist/core/compaction/hysteresis.js.map +1 -0
  53. package/dist/core/compaction/index.d.ts +2 -0
  54. package/dist/core/compaction/index.d.ts.map +1 -1
  55. package/dist/core/compaction/index.js +2 -0
  56. package/dist/core/compaction/index.js.map +1 -1
  57. package/dist/core/compaction/transaction.d.ts +172 -0
  58. package/dist/core/compaction/transaction.d.ts.map +1 -0
  59. package/dist/core/compaction/transaction.js +824 -0
  60. package/dist/core/compaction/transaction.js.map +1 -0
  61. package/dist/core/context-budget-governor-v2.d.ts +2 -1
  62. package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
  63. package/dist/core/context-budget-governor-v2.js +2 -1
  64. package/dist/core/context-budget-governor-v2.js.map +1 -1
  65. package/dist/core/context-budget-reserved-tokens.d.ts +110 -0
  66. package/dist/core/context-budget-reserved-tokens.d.ts.map +1 -0
  67. package/dist/core/context-budget-reserved-tokens.js +188 -0
  68. package/dist/core/context-budget-reserved-tokens.js.map +1 -0
  69. package/dist/core/context-budget-v2-cache-invalidation.d.ts +133 -0
  70. package/dist/core/context-budget-v2-cache-invalidation.d.ts.map +1 -0
  71. package/dist/core/context-budget-v2-cache-invalidation.js +452 -0
  72. package/dist/core/context-budget-v2-cache-invalidation.js.map +1 -0
  73. package/dist/core/context-budget-v2-cache-keys.d.ts +3 -0
  74. package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
  75. package/dist/core/context-budget-v2-cache-keys.js +6 -0
  76. package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
  77. package/dist/core/context-budget-v2-cache-provider.d.ts +4 -0
  78. package/dist/core/context-budget-v2-cache-provider.d.ts.map +1 -1
  79. package/dist/core/context-budget-v2-cache-provider.js +19 -0
  80. package/dist/core/context-budget-v2-cache-provider.js.map +1 -1
  81. package/dist/core/context-budget-v2-plan-cache-keys.d.ts.map +1 -1
  82. package/dist/core/context-budget-v2-plan-cache-keys.js +1 -0
  83. package/dist/core/context-budget-v2-plan-cache-keys.js.map +1 -1
  84. package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
  85. package/dist/core/context-budget-v2-planner.js +1 -0
  86. package/dist/core/context-budget-v2-planner.js.map +1 -1
  87. package/dist/core/context-budget-v2-types.d.ts +4 -0
  88. package/dist/core/context-budget-v2-types.d.ts.map +1 -1
  89. package/dist/core/context-budget-v2-types.js.map +1 -1
  90. package/dist/core/durable-file-directory.d.ts +4 -0
  91. package/dist/core/durable-file-directory.d.ts.map +1 -0
  92. package/dist/core/durable-file-directory.js +76 -0
  93. package/dist/core/durable-file-directory.js.map +1 -0
  94. package/dist/core/durable-file-identity.d.ts +26 -0
  95. package/dist/core/durable-file-identity.d.ts.map +1 -0
  96. package/dist/core/durable-file-identity.js +118 -0
  97. package/dist/core/durable-file-identity.js.map +1 -0
  98. package/dist/core/durable-file-io.d.ts +29 -0
  99. package/dist/core/durable-file-io.d.ts.map +1 -0
  100. package/dist/core/durable-file-io.js +179 -0
  101. package/dist/core/durable-file-io.js.map +1 -0
  102. package/dist/core/durable-file-lock-cleanup.d.ts +12 -0
  103. package/dist/core/durable-file-lock-cleanup.d.ts.map +1 -0
  104. package/dist/core/durable-file-lock-cleanup.js +39 -0
  105. package/dist/core/durable-file-lock-cleanup.js.map +1 -0
  106. package/dist/core/durable-file-lock-owner.d.ts +18 -0
  107. package/dist/core/durable-file-lock-owner.d.ts.map +1 -0
  108. package/dist/core/durable-file-lock-owner.js +78 -0
  109. package/dist/core/durable-file-lock-owner.js.map +1 -0
  110. package/dist/core/durable-file-lock-storage.d.ts +35 -0
  111. package/dist/core/durable-file-lock-storage.d.ts.map +1 -0
  112. package/dist/core/durable-file-lock-storage.js +263 -0
  113. package/dist/core/durable-file-lock-storage.js.map +1 -0
  114. package/dist/core/durable-file-mode.d.ts +8 -0
  115. package/dist/core/durable-file-mode.d.ts.map +1 -0
  116. package/dist/core/durable-file-mode.js +41 -0
  117. package/dist/core/durable-file-mode.js.map +1 -0
  118. package/dist/core/extensions/loader.d.ts +12 -0
  119. package/dist/core/extensions/loader.d.ts.map +1 -1
  120. package/dist/core/extensions/loader.js +46 -37
  121. package/dist/core/extensions/loader.js.map +1 -1
  122. package/dist/core/footer-data-provider.d.ts +7 -1
  123. package/dist/core/footer-data-provider.d.ts.map +1 -1
  124. package/dist/core/footer-data-provider.js +12 -0
  125. package/dist/core/footer-data-provider.js.map +1 -1
  126. package/dist/core/index.d.ts +7 -2
  127. package/dist/core/index.d.ts.map +1 -1
  128. package/dist/core/index.js +6 -0
  129. package/dist/core/index.js.map +1 -1
  130. package/dist/core/metrics-sampler.d.ts +20 -0
  131. package/dist/core/metrics-sampler.d.ts.map +1 -1
  132. package/dist/core/metrics-sampler.js +52 -0
  133. package/dist/core/metrics-sampler.js.map +1 -1
  134. package/dist/core/model-registry.d.ts.map +1 -1
  135. package/dist/core/model-registry.js +4 -1
  136. package/dist/core/model-registry.js.map +1 -1
  137. package/dist/core/model-resolver.d.ts.map +1 -1
  138. package/dist/core/model-resolver.js +16 -7
  139. package/dist/core/model-resolver.js.map +1 -1
  140. package/dist/core/provider-resilience.d.ts +51 -0
  141. package/dist/core/provider-resilience.d.ts.map +1 -0
  142. package/dist/core/provider-resilience.js +86 -0
  143. package/dist/core/provider-resilience.js.map +1 -0
  144. package/dist/core/redaction.d.ts +8 -0
  145. package/dist/core/redaction.d.ts.map +1 -0
  146. package/dist/core/redaction.js +31 -0
  147. package/dist/core/redaction.js.map +1 -0
  148. package/dist/core/resource-loader.d.ts.map +1 -1
  149. package/dist/core/resource-loader.js +36 -0
  150. package/dist/core/resource-loader.js.map +1 -1
  151. package/dist/core/run-journal-store.d.ts +53 -0
  152. package/dist/core/run-journal-store.d.ts.map +1 -0
  153. package/dist/core/run-journal-store.js +278 -0
  154. package/dist/core/run-journal-store.js.map +1 -0
  155. package/dist/core/run-journal.d.ts +172 -0
  156. package/dist/core/run-journal.d.ts.map +1 -0
  157. package/dist/core/run-journal.js +1045 -0
  158. package/dist/core/run-journal.js.map +1 -0
  159. package/dist/core/sdk.d.ts +7 -0
  160. package/dist/core/sdk.d.ts.map +1 -1
  161. package/dist/core/sdk.js +94 -3
  162. package/dist/core/sdk.js.map +1 -1
  163. package/dist/core/session-doctor-plan.d.ts +128 -0
  164. package/dist/core/session-doctor-plan.d.ts.map +1 -0
  165. package/dist/core/session-doctor-plan.js +833 -0
  166. package/dist/core/session-doctor-plan.js.map +1 -0
  167. package/dist/core/session-file-compaction.d.ts +6 -0
  168. package/dist/core/session-file-compaction.d.ts.map +1 -0
  169. package/dist/core/session-file-compaction.js +169 -0
  170. package/dist/core/session-file-compaction.js.map +1 -0
  171. package/dist/core/session-file-head.d.ts +10 -0
  172. package/dist/core/session-file-head.d.ts.map +1 -0
  173. package/dist/core/session-file-head.js +78 -0
  174. package/dist/core/session-file-head.js.map +1 -0
  175. package/dist/core/session-file-persistence.d.ts +30 -0
  176. package/dist/core/session-file-persistence.d.ts.map +1 -0
  177. package/dist/core/session-file-persistence.js +106 -0
  178. package/dist/core/session-file-persistence.js.map +1 -0
  179. package/dist/core/session-integrity.d.ts +43 -0
  180. package/dist/core/session-integrity.d.ts.map +1 -0
  181. package/dist/core/session-integrity.js +420 -0
  182. package/dist/core/session-integrity.js.map +1 -0
  183. package/dist/core/session-manager.d.ts +24 -4
  184. package/dist/core/session-manager.d.ts.map +1 -1
  185. package/dist/core/session-manager.js +242 -125
  186. package/dist/core/session-manager.js.map +1 -1
  187. package/dist/core/session-owner-lease.d.ts +22 -0
  188. package/dist/core/session-owner-lease.d.ts.map +1 -0
  189. package/dist/core/session-owner-lease.js +176 -0
  190. package/dist/core/session-owner-lease.js.map +1 -0
  191. package/dist/core/session-path-policy.d.ts +132 -0
  192. package/dist/core/session-path-policy.d.ts.map +1 -0
  193. package/dist/core/session-path-policy.js +780 -0
  194. package/dist/core/session-path-policy.js.map +1 -0
  195. package/dist/core/session-repair-plan.d.ts +49 -0
  196. package/dist/core/session-repair-plan.d.ts.map +1 -0
  197. package/dist/core/session-repair-plan.js +202 -0
  198. package/dist/core/session-repair-plan.js.map +1 -0
  199. package/dist/core/session-termination.d.ts +106 -0
  200. package/dist/core/session-termination.d.ts.map +1 -0
  201. package/dist/core/session-termination.js +445 -0
  202. package/dist/core/session-termination.js.map +1 -0
  203. package/dist/core/settings-manager.d.ts +52 -0
  204. package/dist/core/settings-manager.d.ts.map +1 -1
  205. package/dist/core/settings-manager.js +54 -5
  206. package/dist/core/settings-manager.js.map +1 -1
  207. package/dist/core/system-prompt.d.ts.map +1 -1
  208. package/dist/core/system-prompt.js +1 -1
  209. package/dist/core/system-prompt.js.map +1 -1
  210. package/dist/core/tools/grep.d.ts.map +1 -1
  211. package/dist/core/tools/grep.js +47 -0
  212. package/dist/core/tools/grep.js.map +1 -1
  213. package/dist/core/tools/omp-pure-seams.d.ts +41 -0
  214. package/dist/core/tools/omp-pure-seams.d.ts.map +1 -0
  215. package/dist/core/tools/omp-pure-seams.js +142 -0
  216. package/dist/core/tools/omp-pure-seams.js.map +1 -0
  217. package/dist/core/tools/omp-seam-runtime.d.ts +100 -0
  218. package/dist/core/tools/omp-seam-runtime.d.ts.map +1 -0
  219. package/dist/core/tools/omp-seam-runtime.js +40 -0
  220. package/dist/core/tools/omp-seam-runtime.js.map +1 -0
  221. package/dist/core/tools/read.d.ts.map +1 -1
  222. package/dist/core/tools/read.js +34 -1
  223. package/dist/core/tools/read.js.map +1 -1
  224. package/dist/core/verified-bash-adapter.d.ts +36 -0
  225. package/dist/core/verified-bash-adapter.d.ts.map +1 -0
  226. package/dist/core/verified-bash-adapter.js +90 -0
  227. package/dist/core/verified-bash-adapter.js.map +1 -0
  228. package/dist/guardrails/command-redaction.d.ts +32 -0
  229. package/dist/guardrails/command-redaction.d.ts.map +1 -0
  230. package/dist/guardrails/command-redaction.js +776 -0
  231. package/dist/guardrails/command-redaction.js.map +1 -0
  232. package/dist/guardrails/evidence-attestation.d.ts +31 -0
  233. package/dist/guardrails/evidence-attestation.d.ts.map +1 -0
  234. package/dist/guardrails/evidence-attestation.js +178 -0
  235. package/dist/guardrails/evidence-attestation.js.map +1 -0
  236. package/dist/guardrails/evidence-receipt-store.d.ts +33 -0
  237. package/dist/guardrails/evidence-receipt-store.d.ts.map +1 -0
  238. package/dist/guardrails/evidence-receipt-store.js +446 -0
  239. package/dist/guardrails/evidence-receipt-store.js.map +1 -0
  240. package/dist/guardrails/evidence-receipt.d.ts +58 -0
  241. package/dist/guardrails/evidence-receipt.d.ts.map +1 -0
  242. package/dist/guardrails/evidence-receipt.js +389 -0
  243. package/dist/guardrails/evidence-receipt.js.map +1 -0
  244. package/dist/guardrails/evidence-system.d.ts +39 -15
  245. package/dist/guardrails/evidence-system.d.ts.map +1 -1
  246. package/dist/guardrails/evidence-system.js +255 -97
  247. package/dist/guardrails/evidence-system.js.map +1 -1
  248. package/dist/guardrails/replay-ledger-io.d.ts +6 -0
  249. package/dist/guardrails/replay-ledger-io.d.ts.map +1 -0
  250. package/dist/guardrails/replay-ledger-io.js +42 -0
  251. package/dist/guardrails/replay-ledger-io.js.map +1 -0
  252. package/dist/guardrails/replay-ledger-lock-owner.d.ts +32 -0
  253. package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -0
  254. package/dist/guardrails/replay-ledger-lock-owner.js +138 -0
  255. package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -0
  256. package/dist/guardrails/replay-ledger-lock.d.ts +24 -0
  257. package/dist/guardrails/replay-ledger-lock.d.ts.map +1 -0
  258. package/dist/guardrails/replay-ledger-lock.js +135 -0
  259. package/dist/guardrails/replay-ledger-lock.js.map +1 -0
  260. package/dist/guardrails/replay-ledger-mutation-gate.d.ts +24 -0
  261. package/dist/guardrails/replay-ledger-mutation-gate.d.ts.map +1 -0
  262. package/dist/guardrails/replay-ledger-mutation-gate.js +205 -0
  263. package/dist/guardrails/replay-ledger-mutation-gate.js.map +1 -0
  264. package/dist/guardrails/replay-ledger-quarantine.d.ts +2 -0
  265. package/dist/guardrails/replay-ledger-quarantine.d.ts.map +1 -0
  266. package/dist/guardrails/replay-ledger-quarantine.js +19 -0
  267. package/dist/guardrails/replay-ledger-quarantine.js.map +1 -0
  268. package/dist/guardrails/replay-ledger-store.d.ts +32 -0
  269. package/dist/guardrails/replay-ledger-store.d.ts.map +1 -0
  270. package/dist/guardrails/replay-ledger-store.js +211 -0
  271. package/dist/guardrails/replay-ledger-store.js.map +1 -0
  272. package/dist/guardrails/shell-command-name-glob.d.ts +3 -0
  273. package/dist/guardrails/shell-command-name-glob.d.ts.map +1 -0
  274. package/dist/guardrails/shell-command-name-glob.js +233 -0
  275. package/dist/guardrails/shell-command-name-glob.js.map +1 -0
  276. package/dist/guardrails/shell-command-option.d.ts +7 -0
  277. package/dist/guardrails/shell-command-option.d.ts.map +1 -0
  278. package/dist/guardrails/shell-command-option.js +24 -0
  279. package/dist/guardrails/shell-command-option.js.map +1 -0
  280. package/dist/guardrails/shell-command-tokenizer.d.ts +29 -0
  281. package/dist/guardrails/shell-command-tokenizer.d.ts.map +1 -0
  282. package/dist/guardrails/shell-command-tokenizer.js +145 -0
  283. package/dist/guardrails/shell-command-tokenizer.js.map +1 -0
  284. package/dist/guardrails/shell-command-wrapper.d.ts +18 -0
  285. package/dist/guardrails/shell-command-wrapper.d.ts.map +1 -0
  286. package/dist/guardrails/shell-command-wrapper.js +42 -0
  287. package/dist/guardrails/shell-command-wrapper.js.map +1 -0
  288. package/dist/guardrails/verified-executor.d.ts +65 -0
  289. package/dist/guardrails/verified-executor.d.ts.map +1 -0
  290. package/dist/guardrails/verified-executor.js +171 -0
  291. package/dist/guardrails/verified-executor.js.map +1 -0
  292. package/dist/guardrails/workspace-fingerprint.d.ts +23 -0
  293. package/dist/guardrails/workspace-fingerprint.d.ts.map +1 -0
  294. package/dist/guardrails/workspace-fingerprint.js +634 -0
  295. package/dist/guardrails/workspace-fingerprint.js.map +1 -0
  296. package/dist/index.d.ts +15 -3
  297. package/dist/index.d.ts.map +1 -1
  298. package/dist/index.js +13 -1
  299. package/dist/index.js.map +1 -1
  300. package/dist/main.d.ts.map +1 -1
  301. package/dist/main.js +12 -0
  302. package/dist/main.js.map +1 -1
  303. package/dist/modes/interactive/components/footer.d.ts +1 -1
  304. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  305. package/dist/modes/interactive/components/footer.js +11 -8
  306. package/dist/modes/interactive/components/footer.js.map +1 -1
  307. package/dist/modes/interactive/interactive-mode.d.ts +3 -0
  308. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  309. package/dist/modes/interactive/interactive-mode.js +59 -9
  310. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  311. package/dist/modes/interactive/theme/omk-aurora-dark.json +80 -0
  312. package/dist/modes/interactive/theme/omk-aurora-light.json +80 -0
  313. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  314. package/dist/modes/interactive/theme/theme.js +5 -0
  315. package/dist/modes/interactive/theme/theme.js.map +1 -1
  316. package/dist/modes/print-mode.d.ts.map +1 -1
  317. package/dist/modes/print-mode.js +43 -2
  318. package/dist/modes/print-mode.js.map +1 -1
  319. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  320. package/dist/modes/rpc/rpc-mode.js +23 -5
  321. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  322. package/dist/modes/rpc/rpc-types.d.ts +3 -0
  323. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  324. package/dist/modes/rpc/rpc-types.js.map +1 -1
  325. package/dist/types/evidence.d.ts +211 -1
  326. package/dist/types/evidence.d.ts.map +1 -1
  327. package/dist/types/evidence.js.map +1 -1
  328. package/dist/verify-ci.d.ts +3 -0
  329. package/dist/verify-ci.d.ts.map +1 -0
  330. package/dist/verify-ci.js +30 -0
  331. package/dist/verify-ci.js.map +1 -0
  332. package/docs/adaptorch-preview.md +1 -1
  333. package/docs/compaction.md +38 -4
  334. package/docs/extensions.md +2 -0
  335. package/docs/provider-resilience.md +87 -0
  336. package/docs/sdk.md +238 -1
  337. package/docs/settings.md +80 -2
  338. package/docs/themes.md +2 -0
  339. package/docs/usage.md +38 -0
  340. package/examples/extensions/custom-provider-anthropic/package-lock.json +5 -5
  341. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  342. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  343. package/examples/extensions/gondolin/package-lock.json +2 -2
  344. package/examples/extensions/gondolin/package.json +1 -1
  345. package/examples/extensions/sandbox/package-lock.json +2 -2
  346. package/examples/extensions/sandbox/package.json +1 -1
  347. package/examples/extensions/with-deps/package-lock.json +4 -4
  348. package/examples/extensions/with-deps/package.json +1 -1
  349. package/npm-shrinkwrap.json +22 -13
  350. package/package.json +6 -5
@@ -1,5 +1,5 @@
1
- import { randomUUID } from "crypto";
2
- import { appendFileSync, closeSync, createReadStream, existsSync, mkdirSync, openSync, readdirSync, readSync, renameSync, statSync, writeFileSync, } from "fs";
1
+ import { createHash, randomUUID } from "crypto";
2
+ import { closeSync, createReadStream, existsSync, openSync, readdirSync, readSync, renameSync, statSync } from "fs";
3
3
  import { readdir, stat } from "fs/promises";
4
4
  import { uuidv7 } from "omk-agent-core";
5
5
  import { join, resolve } from "path";
@@ -7,7 +7,16 @@ import { createInterface } from "readline";
7
7
  import { StringDecoder } from "string_decoder";
8
8
  import { getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.js";
9
9
  import { normalizePath, resolvePath } from "../utils/paths.js";
10
+ import { atomicRewriteFileSync } from "./atomic-session-file.js";
11
+ import { createSessionRevisionToken, } from "./compaction/transaction.js";
12
+ import { acquireDurableFileMutationLockSync } from "./durable-file-identity.js";
13
+ import { appendFileDurablySync, ensureDurableDirectorySync } from "./durable-file-io.js";
14
+ import { enforcePrivateFileModeSync } from "./durable-file-mode.js";
10
15
  import { createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "./messages.js";
16
+ import { rebindBranchedCompactionEnvelopes, validatePersistedCompactionEnvelopes } from "./session-file-compaction.js";
17
+ import { quarantineSessionTrailingFragment, SessionManagerStaleWriteError, sameDurableSessionHead, sessionDurableHeadFromFile, sessionDurableHeadFromSnapshot, } from "./session-file-persistence.js";
18
+ import { assertSessionOwnerRecoveryAllowedUnlockedSync } from "./session-owner-lease.js";
19
+ export { SessionManagerStaleWriteError };
11
20
  export const CURRENT_SESSION_VERSION = 3;
12
21
  function createSessionId() {
13
22
  return uuidv7();
@@ -98,8 +107,9 @@ export function parseSessionEntries(content) {
98
107
  const entry = JSON.parse(line);
99
108
  entries.push(entry);
100
109
  }
101
- catch {
102
- // Skip malformed lines
110
+ catch (error) {
111
+ if (!(error instanceof SyntaxError))
112
+ throw error;
103
113
  }
104
114
  }
105
115
  return entries;
@@ -225,9 +235,8 @@ function getDefaultSessionDirPath(cwd, agentDir = getDefaultAgentDir()) {
225
235
  }
226
236
  export function getDefaultSessionDir(cwd, agentDir = getDefaultAgentDir()) {
227
237
  const sessionDir = getDefaultSessionDirPath(cwd, agentDir);
228
- if (!existsSync(sessionDir)) {
229
- mkdirSync(sessionDir, { recursive: true });
230
- }
238
+ if (!existsSync(sessionDir))
239
+ ensureDurableDirectorySync(sessionDir);
231
240
  return sessionDir;
232
241
  }
233
242
  const SESSION_READ_BUFFER_SIZE = 1024 * 1024;
@@ -269,10 +278,9 @@ export function loadEntriesFromFile(filePath) {
269
278
  }
270
279
  pending = pending.slice(lineStart);
271
280
  }
272
- pending += decoder.end();
273
- const finalEntry = parseSessionEntryLine(pending);
274
- if (finalEntry)
275
- entries.push(finalEntry);
281
+ // Complete-prefix semantics: bytes after the final newline are never
282
+ // parsed here. SessionManager.open() quarantines them before loading.
283
+ decoder.end();
276
284
  }
277
285
  finally {
278
286
  closeSync(fd);
@@ -286,6 +294,9 @@ export function loadEntriesFromFile(filePath) {
286
294
  }
287
295
  return entries;
288
296
  }
297
+ function serializeFileEntries(entries) {
298
+ return `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`;
299
+ }
289
300
  /**
290
301
  * Preserve a corrupt (non-empty, headerless) session file by renaming it to
291
302
  * `<file>.corrupt-<timestamp>` so recovery never destroys user data.
@@ -312,25 +323,35 @@ function backupCorruptSessionFile(filePath) {
312
323
  function readSessionHeader(filePath) {
313
324
  try {
314
325
  const fd = openSync(filePath, "r");
315
- const buffer = Buffer.alloc(512);
316
- const bytesRead = readSync(fd, buffer, 0, 512, 0);
317
- closeSync(fd);
326
+ const buffer = Buffer.alloc(64 * 1024);
327
+ let bytesRead;
328
+ try {
329
+ bytesRead = readSync(fd, buffer, 0, buffer.byteLength, 0);
330
+ }
331
+ finally {
332
+ closeSync(fd);
333
+ }
318
334
  const firstLine = buffer.toString("utf8", 0, bytesRead).split("\n")[0];
319
335
  if (!firstLine)
320
336
  return null;
321
337
  const header = JSON.parse(firstLine);
322
- if (header.type !== "session" || typeof header.id !== "string") {
338
+ if (header.type !== "session" || typeof header.id !== "string")
323
339
  return null;
324
- }
325
- return header;
340
+ return {
341
+ type: "session",
342
+ id: header.id,
343
+ timestamp: typeof header.timestamp === "string" ? header.timestamp : "",
344
+ cwd: typeof header.cwd === "string" ? header.cwd : "",
345
+ ...(typeof header.version === "number" ? { version: header.version } : {}),
346
+ ...(typeof header.parentSession === "string" ? { parentSession: header.parentSession } : {}),
347
+ };
326
348
  }
327
349
  catch {
328
350
  return null;
329
351
  }
330
352
  }
331
353
  function getSessionHeaderCwd(header) {
332
- const cwd = header.cwd;
333
- return typeof cwd === "string" ? cwd : undefined;
354
+ return header.cwd;
334
355
  }
335
356
  function sessionCwdMatches(cwd, resolvedCwd) {
336
357
  return cwd !== undefined && cwd !== "" && resolvePath(cwd) === resolvedCwd;
@@ -508,8 +529,9 @@ async function listSessionsFromDir(dir, onProgress, progressOffset = 0, progress
508
529
  }
509
530
  }
510
531
  }
511
- catch {
512
- // Return empty list on error
532
+ catch (error) {
533
+ if (!(error instanceof Error))
534
+ throw error;
513
535
  }
514
536
  return sessions;
515
537
  }
@@ -536,13 +558,16 @@ export class SessionManager {
536
558
  labelsById = new Map();
537
559
  labelTimestampsById = new Map();
538
560
  leafId = null;
561
+ sessionFileLockDepth = 0;
562
+ acceptedDurableHead = null;
563
+ quarantineReport = null;
564
+ ownerLease;
539
565
  constructor(cwd, sessionDir, sessionFile, persist, newSessionOptions) {
540
566
  this.cwd = resolvePath(cwd);
541
567
  this.sessionDir = normalizePath(sessionDir);
542
568
  this.persist = persist;
543
- if (persist && this.sessionDir && !existsSync(this.sessionDir)) {
544
- mkdirSync(this.sessionDir, { recursive: true });
545
- }
569
+ if (persist && this.sessionDir && !existsSync(this.sessionDir))
570
+ ensureDurableDirectorySync(this.sessionDir);
546
571
  if (sessionFile) {
547
572
  this.setSessionFile(sessionFile);
548
573
  }
@@ -550,41 +575,76 @@ export class SessionManager {
550
575
  this.newSession(newSessionOptions);
551
576
  }
552
577
  }
553
- /** Switch to a different session file (used for resume and branching) */
578
+ /** Switch to a different session file (used for resume and branching). */
554
579
  setSessionFile(sessionFile) {
555
- this.sessionFile = resolvePath(sessionFile);
556
- if (existsSync(this.sessionFile)) {
557
- this.fileEntries = loadEntriesFromFile(this.sessionFile);
558
- // If file was empty or corrupted (no valid header), start fresh to avoid
559
- // appending messages without a session header (which breaks the session).
560
- // Never destroy existing data: if the file has content, preserve the
561
- // original as a .corrupt-* backup before rewriting.
562
- if (this.fileEntries.length === 0) {
563
- const explicitPath = this.sessionFile;
564
- if (this.persist) {
565
- backupCorruptSessionFile(explicitPath);
580
+ const explicitPath = resolvePath(sessionFile);
581
+ if (!existsSync(explicitPath)) {
582
+ this.newSession();
583
+ this.sessionFile = explicitPath;
584
+ return;
585
+ }
586
+ const loaded = this._withSessionPathLock(explicitPath, () => {
587
+ enforcePrivateFileModeSync(explicitPath);
588
+ const quarantine = quarantineSessionTrailingFragment(explicitPath);
589
+ let entries = loadEntriesFromFile(explicitPath);
590
+ if (entries.length === 0) {
591
+ if (statSync(explicitPath).nlink > 1) {
592
+ throw new Error("Session recovery refused for a target with more than one hard link");
566
593
  }
567
- this.newSession();
568
- this.sessionFile = explicitPath;
569
- this._rewriteFile();
570
- this.flushed = true;
571
- return;
594
+ if (this.persist)
595
+ backupCorruptSessionFile(explicitPath);
596
+ const timestamp = new Date().toISOString();
597
+ const sessionId = createSessionId();
598
+ entries = [{ type: "session", version: CURRENT_SESSION_VERSION, id: sessionId, timestamp, cwd: this.cwd }];
599
+ atomicRewriteFileSync(explicitPath, serializeFileEntries(entries));
600
+ return {
601
+ entries,
602
+ sessionId,
603
+ quarantineReport: quarantine.report,
604
+ acceptedDurableHead: sessionDurableHeadFromFile(explicitPath, null, entries),
605
+ };
572
606
  }
573
- const header = this.fileEntries.find((e) => e.type === "session");
574
- this.sessionId = header?.id ?? createSessionId();
575
- if (migrateToCurrentVersion(this.fileEntries)) {
576
- this._rewriteFile();
607
+ const header = entries[0];
608
+ if (header.type !== "session")
609
+ throw new Error("Session file has no valid header");
610
+ const sessionId = header.id;
611
+ const lastLoaded = entries.at(-1);
612
+ const loadedLeafId = lastLoaded && lastLoaded.type !== "session" && typeof lastLoaded.id === "string" ? lastLoaded.id : null;
613
+ const needsMigration = (header.version ?? 1) < CURRENT_SESSION_VERSION;
614
+ const acceptedBeforeMigration = needsMigration
615
+ ? quarantine.snapshot
616
+ ? sessionDurableHeadFromSnapshot(quarantine.snapshot, loadedLeafId, entries)
617
+ : sessionDurableHeadFromFile(explicitPath, loadedLeafId, entries)
618
+ : null;
619
+ validatePersistedCompactionEnvelopes(explicitPath, entries, sessionId);
620
+ const migrated = migrateToCurrentVersion(entries);
621
+ if (migrated) {
622
+ const current = sessionDurableHeadFromFile(explicitPath, loadedLeafId, loadEntriesFromFile(explicitPath));
623
+ if (!sameDurableSessionHead(current, acceptedBeforeMigration))
624
+ throw new SessionManagerStaleWriteError();
625
+ atomicRewriteFileSync(explicitPath, serializeFileEntries(entries));
577
626
  }
578
- this._buildIndex();
579
- this.flushed = true;
580
- }
581
- else {
582
- const explicitPath = this.sessionFile;
583
- this.newSession();
584
- this.sessionFile = explicitPath; // preserve explicit path from --session flag
585
- }
627
+ const lastEntry = entries.at(-1);
628
+ const leafId = lastEntry && lastEntry.type !== "session" ? lastEntry.id : null;
629
+ return {
630
+ entries,
631
+ sessionId,
632
+ quarantineReport: quarantine.report,
633
+ acceptedDurableHead: !migrated && quarantine.snapshot
634
+ ? sessionDurableHeadFromSnapshot(quarantine.snapshot, leafId, entries)
635
+ : sessionDurableHeadFromFile(explicitPath, leafId, entries),
636
+ };
637
+ });
638
+ this.fileEntries = loaded.entries;
639
+ this.sessionId = loaded.sessionId;
640
+ this.quarantineReport = loaded.quarantineReport;
641
+ this._buildIndex();
642
+ this.flushed = true;
643
+ this.acceptedDurableHead = loaded.acceptedDurableHead;
644
+ this.sessionFile = explicitPath;
586
645
  }
587
646
  newSession(options) {
647
+ this.quarantineReport = null;
588
648
  if (options?.id !== undefined) {
589
649
  assertValidSessionId(options.id);
590
650
  }
@@ -601,8 +661,10 @@ export class SessionManager {
601
661
  this.fileEntries = [header];
602
662
  this.byId.clear();
603
663
  this.labelsById.clear();
664
+ this.labelTimestampsById.clear();
604
665
  this.leafId = null;
605
666
  this.flushed = false;
667
+ this.acceptedDurableHead = null;
606
668
  if (this.persist) {
607
669
  const fileTimestamp = timestamp.replace(/[:.]/g, "-");
608
670
  this.sessionFile = join(this.getSessionDir(), `${fileTimestamp}_${this.sessionId}.jsonl`);
@@ -631,19 +693,59 @@ export class SessionManager {
631
693
  }
632
694
  }
633
695
  }
634
- _rewriteFile() {
635
- if (!this.persist || !this.sessionFile)
636
- return;
637
- const fd = openSync(this.sessionFile, "w");
696
+ _withSessionPathLock(sessionFile, fn) {
697
+ if (!this.persist || this.sessionFileLockDepth > 0)
698
+ return fn();
699
+ const lock = acquireDurableFileMutationLockSync(sessionFile);
700
+ this.sessionFileLockDepth += 1;
638
701
  try {
639
- for (const entry of this.fileEntries) {
640
- writeFileSync(fd, `${JSON.stringify(entry)}\n`);
641
- }
702
+ assertSessionOwnerRecoveryAllowedUnlockedSync(sessionFile, this.ownerLease);
703
+ return fn();
642
704
  }
643
705
  finally {
644
- closeSync(fd);
706
+ this.sessionFileLockDepth -= 1;
707
+ lock.release();
708
+ }
709
+ }
710
+ _withSessionFileLock(fn) {
711
+ return this.sessionFile ? this._withSessionPathLock(this.sessionFile, fn) : fn();
712
+ }
713
+ _assertAcceptedDurableHead() {
714
+ if (!this.sessionFile)
715
+ return;
716
+ const current = sessionDurableHeadFromFile(this.sessionFile, this.leafId, loadEntriesFromFile(this.sessionFile));
717
+ if ((current === null && existsSync(this.sessionFile)) ||
718
+ !sameDurableSessionHead(current, this.acceptedDurableHead)) {
719
+ throw new SessionManagerStaleWriteError();
645
720
  }
646
721
  }
722
+ /** Reread the interprocess-visible session head while holding the shared file lock. */
723
+ getDurableHeadToken() {
724
+ return this._withSessionFileLock(() => {
725
+ if (this.sessionFile) {
726
+ const durable = sessionDurableHeadFromFile(this.sessionFile, this.leafId, loadEntriesFromFile(this.sessionFile));
727
+ if (durable)
728
+ return durable.revision;
729
+ if (existsSync(this.sessionFile)) {
730
+ throw new SessionManagerStaleWriteError("Session file no longer has an accepted durable identity");
731
+ }
732
+ }
733
+ const bytes = new TextEncoder().encode(serializeFileEntries(this.fileEntries));
734
+ const lastEntry = this.fileEntries.at(-1);
735
+ return createSessionRevisionToken({
736
+ sessionId: this.sessionId,
737
+ completeBytes: bytes.byteLength,
738
+ recordCount: this.fileEntries.length,
739
+ leafId: this.leafId,
740
+ lastEntryId: lastEntry && lastEntry.type !== "session" ? lastEntry.id : null,
741
+ completePrefixSha256: createHash("sha256").update(bytes).digest("hex"),
742
+ });
743
+ });
744
+ }
745
+ /** Serialize compaction compare-and-append with every SessionManager writer for this file. */
746
+ withCompactionCommitLock(fn) {
747
+ return this._withSessionFileLock(fn);
748
+ }
647
749
  isPersisted() {
648
750
  return this.persist;
649
751
  }
@@ -662,41 +764,46 @@ export class SessionManager {
662
764
  getSessionFile() {
663
765
  return this.sessionFile;
664
766
  }
665
- _persist(entry) {
666
- if (!this.persist || !this.sessionFile)
667
- return;
668
- const hasAssistant = this.fileEntries.some((e) => e.type === "message" && e.message.role === "assistant");
669
- if (!hasAssistant) {
670
- if (this.flushed) {
671
- appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
672
- }
673
- else {
674
- // Mark as not flushed so when assistant arrives, all entries get written
675
- this.flushed = false;
676
- }
767
+ getQuarantineReport() {
768
+ return this.quarantineReport;
769
+ }
770
+ setOwnerLease(lease) {
771
+ if (lease && this.sessionFile && !lease.owns(this.sessionFile)) {
772
+ throw new TypeError("Session owner lease does not identify this session file");
773
+ }
774
+ this.ownerLease = lease;
775
+ }
776
+ _appendEntry(entry) {
777
+ if (!this.persist || !this.sessionFile) {
778
+ this.fileEntries.push(entry);
779
+ this.byId.set(entry.id, entry);
780
+ this.leafId = entry.id;
677
781
  return;
678
782
  }
679
- if (!this.flushed) {
680
- const fd = openSync(this.sessionFile, "wx");
681
- try {
682
- for (const e of this.fileEntries) {
683
- writeFileSync(fd, `${JSON.stringify(e)}\n`);
684
- }
783
+ const sessionFile = this.sessionFile;
784
+ this._withSessionFileLock(() => {
785
+ this._assertAcceptedDurableHead();
786
+ const candidateEntries = [...this.fileEntries, entry];
787
+ const hasAssistant = candidateEntries.some((candidate) => candidate.type === "message" && candidate.message.role === "assistant");
788
+ let nextDurableHead = this.acceptedDurableHead;
789
+ let flushed = this.flushed;
790
+ if (flushed) {
791
+ appendFileDurablySync(sessionFile, Buffer.from(`${JSON.stringify(entry)}\n`, "utf8"));
792
+ nextDurableHead = sessionDurableHeadFromFile(sessionFile, entry.id, candidateEntries);
685
793
  }
686
- finally {
687
- closeSync(fd);
794
+ else if (hasAssistant) {
795
+ atomicRewriteFileSync(sessionFile, serializeFileEntries(candidateEntries));
796
+ this.ownerLease?.refresh(true);
797
+ nextDurableHead = sessionDurableHeadFromFile(sessionFile, entry.id, candidateEntries);
798
+ flushed = true;
688
799
  }
689
- this.flushed = true;
690
- }
691
- else {
692
- appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
693
- }
694
- }
695
- _appendEntry(entry) {
696
- this.fileEntries.push(entry);
697
- this.byId.set(entry.id, entry);
698
- this.leafId = entry.id;
699
- this._persist(entry);
800
+ // Accept memory only after every required persistence step succeeds.
801
+ this.fileEntries.push(entry);
802
+ this.byId.set(entry.id, entry);
803
+ this.leafId = entry.id;
804
+ this.acceptedDurableHead = nextDurableHead;
805
+ this.flushed = flushed;
806
+ });
700
807
  }
701
808
  /** Append a message as child of current leaf, then advance leaf. Returns entry id.
702
809
  * Does not allow writing CompactionSummaryMessage and BranchSummaryMessage directly.
@@ -929,6 +1036,8 @@ export class SessionManager {
929
1036
  // Build tree
930
1037
  for (const entry of entries) {
931
1038
  const node = nodeMap.get(entry.id);
1039
+ if (!node)
1040
+ continue;
932
1041
  if (entry.parentId === null || entry.parentId === entry.id) {
933
1042
  roots.push(node);
934
1043
  }
@@ -948,6 +1057,8 @@ export class SessionManager {
948
1057
  const stack = [...roots];
949
1058
  while (stack.length > 0) {
950
1059
  const node = stack.pop();
1060
+ if (!node)
1061
+ continue;
951
1062
  node.children.sort((a, b) => new Date(a.entry.timestamp).getTime() - new Date(b.entry.timestamp).getTime());
952
1063
  stack.push(...node.children);
953
1064
  }
@@ -985,7 +1096,6 @@ export class SessionManager {
985
1096
  if (branchFromId !== null && !this.byId.has(branchFromId)) {
986
1097
  throw new Error(`Entry ${branchFromId} not found`);
987
1098
  }
988
- this.leafId = branchFromId;
989
1099
  const entry = {
990
1100
  type: "branch_summary",
991
1101
  id: generateId(this.byId),
@@ -1005,13 +1115,10 @@ export class SessionManager {
1005
1115
  * Returns the new session file path, or undefined if not persisting.
1006
1116
  */
1007
1117
  createBranchedSession(leafId) {
1008
- const previousSessionFile = this.sessionFile;
1009
1118
  const path = this.getBranch(leafId);
1010
1119
  if (path.length === 0) {
1011
1120
  throw new Error(`Entry ${leafId} not found`);
1012
1121
  }
1013
- // Filter out LabelEntry from path - we'll recreate them from the resolved map
1014
- const pathWithoutLabels = path.filter((e) => e.type !== "label");
1015
1122
  const newSessionId = createSessionId();
1016
1123
  const timestamp = new Date().toISOString();
1017
1124
  const fileTimestamp = timestamp.replace(/[:.]/g, "-");
@@ -1022,14 +1129,19 @@ export class SessionManager {
1022
1129
  id: newSessionId,
1023
1130
  timestamp,
1024
1131
  cwd: this.cwd,
1025
- parentSession: this.persist ? previousSessionFile : undefined,
1132
+ parentSession: this.persist ? this.sessionFile : undefined,
1026
1133
  };
1134
+ const pathWithoutLabels = rebindBranchedCompactionEnvelopes(header, path);
1027
1135
  // Collect labels for entries in the path
1028
1136
  const pathEntryIds = new Set(pathWithoutLabels.map((e) => e.id));
1137
+ const preservedLabels = new Map(pathWithoutLabels
1138
+ .filter((entry) => entry.type === "label")
1139
+ .map((entry) => [entry.targetId, entry.label]));
1029
1140
  const labelsToWrite = [];
1030
1141
  for (const [targetId, label] of this.labelsById) {
1031
- if (pathEntryIds.has(targetId)) {
1032
- labelsToWrite.push({ targetId, label, timestamp: this.labelTimestampsById.get(targetId) });
1142
+ const labelTimestamp = this.labelTimestampsById.get(targetId);
1143
+ if (pathEntryIds.has(targetId) && labelTimestamp && preservedLabels.get(targetId) !== label) {
1144
+ labelsToWrite.push({ targetId, label, timestamp: labelTimestamp });
1033
1145
  }
1034
1146
  }
1035
1147
  if (this.persist) {
@@ -1050,23 +1162,28 @@ export class SessionManager {
1050
1162
  labelEntries.push(labelEntry);
1051
1163
  parentId = labelEntry.id;
1052
1164
  }
1053
- this.fileEntries = [header, ...pathWithoutLabels, ...labelEntries];
1165
+ const candidateEntries = [header, ...pathWithoutLabels, ...labelEntries];
1166
+ const hasAssistant = candidateEntries.some((entry) => entry.type === "message" && entry.message.role === "assistant");
1167
+ let acceptedDurableHead = null;
1168
+ if (hasAssistant) {
1169
+ const lock = acquireDurableFileMutationLockSync(newSessionFile);
1170
+ try {
1171
+ if (existsSync(newSessionFile))
1172
+ throw new SessionManagerStaleWriteError("Branched session path already exists");
1173
+ atomicRewriteFileSync(newSessionFile, serializeFileEntries(candidateEntries));
1174
+ acceptedDurableHead = sessionDurableHeadFromFile(newSessionFile, labelEntries.at(-1)?.id ?? lastEntryId, candidateEntries);
1175
+ }
1176
+ finally {
1177
+ lock.release();
1178
+ }
1179
+ }
1180
+ // Switch accepted memory only after the optional rewrite succeeds.
1181
+ this.fileEntries = candidateEntries;
1054
1182
  this.sessionId = newSessionId;
1055
1183
  this.sessionFile = newSessionFile;
1056
1184
  this._buildIndex();
1057
- // Only write the file now if it contains an assistant message.
1058
- // Otherwise defer to _persist(), which creates the file on the
1059
- // first assistant response, matching the newSession() contract
1060
- // and avoiding the duplicate-header bug when _persist()'s
1061
- // no-assistant guard later resets flushed to false.
1062
- const hasAssistant = this.fileEntries.some((e) => e.type === "message" && e.message.role === "assistant");
1063
- if (hasAssistant) {
1064
- this._rewriteFile();
1065
- this.flushed = true;
1066
- }
1067
- else {
1068
- this.flushed = false;
1069
- }
1185
+ this.acceptedDurableHead = acceptedDurableHead;
1186
+ this.flushed = hasAssistant;
1070
1187
  return newSessionFile;
1071
1188
  }
1072
1189
  // In-memory mode: replace current session with the path + labels
@@ -1106,9 +1223,7 @@ export class SessionManager {
1106
1223
  */
1107
1224
  static open(path, sessionDir, cwdOverride) {
1108
1225
  const resolvedPath = resolvePath(path);
1109
- // Extract cwd from session header if possible, otherwise use process.cwd()
1110
- const entries = loadEntriesFromFile(resolvedPath);
1111
- const header = entries.find((e) => e.type === "session");
1226
+ const header = readSessionHeader(resolvedPath);
1112
1227
  const cwd = cwdOverride ?? header?.cwd ?? process.cwd();
1113
1228
  // If no sessionDir provided, derive from file's parent directory
1114
1229
  const dir = sessionDir ? normalizePath(sessionDir) : resolve(resolvedPath, "..");
@@ -1151,9 +1266,8 @@ export class SessionManager {
1151
1266
  throw new Error(`Cannot fork: source session has no header: ${resolvedSourcePath}`);
1152
1267
  }
1153
1268
  const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(resolvedTargetCwd);
1154
- if (!existsSync(dir)) {
1155
- mkdirSync(dir, { recursive: true });
1156
- }
1269
+ if (!existsSync(dir))
1270
+ ensureDurableDirectorySync(dir);
1157
1271
  // Create new session file with new ID but forked content
1158
1272
  if (options?.id !== undefined) {
1159
1273
  assertValidSessionId(options.id);
@@ -1171,12 +1285,15 @@ export class SessionManager {
1171
1285
  cwd: resolvedTargetCwd,
1172
1286
  parentSession: resolvedSourcePath,
1173
1287
  };
1174
- writeFileSync(newSessionFile, `${JSON.stringify(newHeader)}\n`, { flag: "wx" });
1175
- // Copy all non-header entries from source
1176
- for (const entry of sourceEntries) {
1177
- if (entry.type !== "session") {
1178
- appendFileSync(newSessionFile, `${JSON.stringify(entry)}\n`);
1179
- }
1288
+ const forkEntries = [newHeader, ...sourceEntries.filter((entry) => entry.type !== "session")];
1289
+ const lock = acquireDurableFileMutationLockSync(newSessionFile);
1290
+ try {
1291
+ if (existsSync(newSessionFile))
1292
+ throw new SessionManagerStaleWriteError("Fork session path already exists");
1293
+ atomicRewriteFileSync(newSessionFile, serializeFileEntries(forkEntries));
1294
+ }
1295
+ finally {
1296
+ lock.release();
1180
1297
  }
1181
1298
  return new SessionManager(resolvedTargetCwd, dir, newSessionFile, true);
1182
1299
  }