chainlesschain 0.162.198 → 0.162.200

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 (195) hide show
  1. package/bin/chainlesschain.js +14 -6
  2. package/package.json +4 -1
  3. package/scripts/command-lifecycle-report.mjs +94 -0
  4. package/src/assets/web-panel/assets/{AIOps-CSN-D2xb.js → AIOps-DcQ0bWKy.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-CBSMlGzv.js → ActionButton-GecwfFPt.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-3yr3Nt2t.js → Analytics-CD6iSt9v.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-1b-o3lMH.js → AppLayout-CDEtuEYf.js} +5 -5
  8. package/src/assets/web-panel/assets/{Artifacts-Dr8rJi17.js → Artifacts-DS3jAjeW.js} +1 -1
  9. package/src/assets/web-panel/assets/{Audit-2V689HC2.js → Audit-CcSJA048.js} +1 -1
  10. package/src/assets/web-panel/assets/{BackgroundAgents-DIxkNrD_.js → BackgroundAgents-DRo9Morb.js} +1 -1
  11. package/src/assets/web-panel/assets/{Backup-Pg6BcwrE.js → Backup-Vw5MrWft.js} +1 -1
  12. package/src/assets/web-panel/assets/{BaseInput-CLUpqyfH.js → BaseInput-DNHIu823.js} +1 -1
  13. package/src/assets/web-panel/assets/{Chat-qVS8rD4G.js → Chat-DvAsO8-b.js} +5 -5
  14. package/src/assets/web-panel/assets/{ChatBubbleRenderer-ti49X12Q.js → ChatBubbleRenderer-9aDuObay.js} +1 -1
  15. package/src/assets/web-panel/assets/{Checkbox-Bs9AaEay.js → Checkbox-CKCa7tvn.js} +1 -1
  16. package/src/assets/web-panel/assets/{Codegen-Dvc1mq8q.js → Codegen-CIFvZdaT.js} +1 -1
  17. package/src/assets/web-panel/assets/{Col-C4_biO7A.js → Col-C79yz7bK.js} +1 -1
  18. package/src/assets/web-panel/assets/{Community-al25XQUV.js → Community-DVvZskLJ.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compact-D-ZDB8M1.js → Compact-oaew61hV.js} +1 -1
  20. package/src/assets/web-panel/assets/{Compliance-6lIfDz1W.js → Compliance-DqA7wmRx.js} +1 -1
  21. package/src/assets/web-panel/assets/{Cowork-DdI48XMJ.js → Cowork-B-qGadxa.js} +2 -2
  22. package/src/assets/web-panel/assets/{Cron-DF5HZd2g.js → Cron-CUlCl4sS.js} +2 -2
  23. package/src/assets/web-panel/assets/{Crosschain-DuxeSvt3.js → Crosschain-CNOlr9DU.js} +1 -1
  24. package/src/assets/web-panel/assets/{DID-B9yHkUTd.js → DID-DJqFCT_3.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dashboard-Z5EuI3IM.js → Dashboard-CznGpphf.js} +2 -2
  26. package/src/assets/web-panel/assets/{Dropdown-vuRfO3dY.js → Dropdown-_cJw218E.js} +1 -1
  27. package/src/assets/web-panel/assets/{EmailListRenderer-QFLCc1uL.js → EmailListRenderer-Dr-1BZ9C.js} +1 -1
  28. package/src/assets/web-panel/assets/{FamilyGuardDashboard-BZ7EXkFJ.js → FamilyGuardDashboard-Dxf4xtSk.js} +1 -1
  29. package/src/assets/web-panel/assets/{Federation-DycIEg2s.js → Federation-DOiG3i_1.js} +1 -1
  30. package/src/assets/web-panel/assets/{FormItemContext-Ba0H_7yG.js → FormItemContext-BzbMcFB9.js} +1 -1
  31. package/src/assets/web-panel/assets/{GenericCardRenderer--JgSTK-C.js → GenericCardRenderer-VbBrYttP.js} +1 -1
  32. package/src/assets/web-panel/assets/{Git-DEvTbldE.js → Git-CLG8E6ED.js} +2 -2
  33. package/src/assets/web-panel/assets/{Governance-BJIljy18.js → Governance-LGJQfk_q.js} +1 -1
  34. package/src/assets/web-panel/assets/{Inference-CVyW3G3q.js → Inference-DlQKx9lF.js} +1 -1
  35. package/src/assets/web-panel/assets/{KnowledgeGraph-v8-borge.js → KnowledgeGraph-DU8vEouV.js} +1 -1
  36. package/src/assets/web-panel/assets/{Logs-BCdS7gGo.js → Logs-FZIQOdln.js} +2 -2
  37. package/src/assets/web-panel/assets/MarkdownRenderer-BJNWIrbw.js +1 -0
  38. package/src/assets/web-panel/assets/{Marketplace-AN6tGA2B.js → Marketplace-C_xd-zZI.js} +1 -1
  39. package/src/assets/web-panel/assets/{McpTools-DGViXASN.js → McpTools-q-YktphS.js} +5 -5
  40. package/src/assets/web-panel/assets/{Memory-Bpvmx9X4.js → Memory-Cq9OhDin.js} +2 -2
  41. package/src/assets/web-panel/assets/{MobileBridge-CTP-DOTC.js → MobileBridge-B03_R98T.js} +1 -1
  42. package/src/assets/web-panel/assets/{MobileProjects-C8qfktxn.js → MobileProjects-DZrk-qmF.js} +1 -1
  43. package/src/assets/web-panel/assets/{Mtc-ndYr0M1s.js → Mtc-BidCKW28.js} +2 -2
  44. package/src/assets/web-panel/assets/{MtcAudit-Ci0-YK7W.js → MtcAudit-r-iaMVM7.js} +2 -2
  45. package/src/assets/web-panel/assets/{Multisig-BehSTFW7.js → Multisig-CgTUFvff.js} +3 -3
  46. package/src/assets/web-panel/assets/{NLProgramming-B91_YBLv.js → NLProgramming-B7NvzfAV.js} +1 -1
  47. package/src/assets/web-panel/assets/{Notes-BQlJ0-gq.js → Notes-80CWNqJI.js} +3 -3
  48. package/src/assets/web-panel/assets/{NotificationSettings-CDgYMjYK.js → NotificationSettings-0H6xdiY3.js} +1 -1
  49. package/src/assets/web-panel/assets/{OrderTableRenderer-BJkSlKeH.js → OrderTableRenderer-CKP4AFWy.js} +1 -1
  50. package/src/assets/web-panel/assets/{Organization-B-b3cWCp.js → Organization-B8sxCV_h.js} +4 -4
  51. package/src/assets/web-panel/assets/{Overflow-CILUL4nL.js → Overflow-CW9JBBhQ.js} +1 -1
  52. package/src/assets/web-panel/assets/{P2P-BTCFBkF0.js → P2P-bdSQV7oZ.js} +2 -2
  53. package/src/assets/web-panel/assets/{PdhVaultBrowser-BAR6AUx5.js → PdhVaultBrowser-DQad42nW.js} +3 -3
  54. package/src/assets/web-panel/assets/{Permissions-RRf6HbWJ.js → Permissions-CueexxTR.js} +4 -4
  55. package/src/assets/web-panel/assets/{PersonalDataHub-BGTMiICa.js → PersonalDataHub-fqxflhR8.js} +6 -6
  56. package/src/assets/web-panel/assets/{Pipeline-945qY9YF.js → Pipeline-KFwwnIMC.js} +1 -1
  57. package/src/assets/web-panel/assets/{Privacy-DPZwI6lJ.js → Privacy-ClnD9buY.js} +1 -1
  58. package/src/assets/web-panel/assets/{ProjectInit-BPH2NRKc.js → ProjectInit-CZEz6sm3.js} +2 -2
  59. package/src/assets/web-panel/assets/{ProjectSettings-BZCRNCvb.js → ProjectSettings-B6NzpmOI.js} +2 -2
  60. package/src/assets/web-panel/assets/{Projects-BTI__tBt.js → Projects-DYyuwPCs.js} +1 -1
  61. package/src/assets/web-panel/assets/Providers-C3tz_jdh.js +1 -0
  62. package/src/assets/web-panel/assets/{QrScannerModal-N6rbqt1r.js → QrScannerModal-BzuOBbqB.js} +1 -1
  63. package/src/assets/web-panel/assets/{QuickAsk-DrwjPS7M.js → QuickAsk-CmNO9qSj.js} +1 -1
  64. package/src/assets/web-panel/assets/{Recommend-Cog9Wd99.js → Recommend-VJNj3_NF.js} +1 -1
  65. package/src/assets/web-panel/assets/{RemoteSession-DC9pynjM.js → RemoteSession-CrbdbSBF.js} +2 -2
  66. package/src/assets/web-panel/assets/{Reputation-D4th4gKS.js → Reputation-DTyMd45j.js} +1 -1
  67. package/src/assets/web-panel/assets/{Row-ogiWR6v9.js → Row-DHrQ-Icz.js} +1 -1
  68. package/src/assets/web-panel/assets/{RssFeed-uYlk-2Oj.js → RssFeed-DqWxY9c5.js} +3 -3
  69. package/src/assets/web-panel/assets/{Search-DYzzwvtM.js → Search-ctF1Ks3J.js} +1 -1
  70. package/src/assets/web-panel/assets/{Security-Da7YOyX6.js → Security-BrQz8uo5.js} +4 -4
  71. package/src/assets/web-panel/assets/{Services-CLI1VM1X.js → Services-D4scVFar.js} +2 -2
  72. package/src/assets/web-panel/assets/{Skeleton-DK77dzDC.js → Skeleton-oUScaOjv.js} +1 -1
  73. package/src/assets/web-panel/assets/{Skills-C_z7Lmhl.js → Skills-CL-y0eLt.js} +1 -1
  74. package/src/assets/web-panel/assets/{Sla-D6413Q8L.js → Sla-DQExhwXj.js} +1 -1
  75. package/src/assets/web-panel/assets/{SpeechSettings-C07ydbj2.js → SpeechSettings-iNp_I7kN.js} +1 -1
  76. package/src/assets/web-panel/assets/{SyncSettings-DGOm2W22.js → SyncSettings-CTDlMo0A.js} +2 -2
  77. package/src/assets/web-panel/assets/{Tasks-Cr_PVyLM.js → Tasks-4kb8l3sc.js} +1 -1
  78. package/src/assets/web-panel/assets/{Templates-BhL2mZ0t.js → Templates-BXcL9oXW.js} +1 -1
  79. package/src/assets/web-panel/assets/{Tenant-CPk5oyUr.js → Tenant-DLE8eS0D.js} +1 -1
  80. package/src/assets/web-panel/assets/{Terminal-ClhqUsqV.js → Terminal-DGpUZPPK.js} +2 -2
  81. package/src/assets/web-panel/assets/{TimelineRenderer-BCC7MV5a.js → TimelineRenderer-C1pWXGPC.js} +1 -1
  82. package/src/assets/web-panel/assets/{Tokens-DD1Jt4oO.js → Tokens-BYfbfqhZ.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trigger-BCydFvN2.js → Trigger-K9YfLOCw.js} +1 -1
  84. package/src/assets/web-panel/assets/{Trust-DDHT1noa.js → Trust-Ctkq77Hy.js} +1 -1
  85. package/src/assets/web-panel/assets/{UkeySign-CWPZC7oI.js → UkeySign-C6_CSe9J.js} +1 -1
  86. package/src/assets/web-panel/assets/{VideoEditing-9Ea0nJR0.js → VideoEditing-Be3rpOWs.js} +1 -1
  87. package/src/assets/web-panel/assets/{Wallet-BjxhLxTf.js → Wallet-D0UIplxk.js} +4 -4
  88. package/src/assets/web-panel/assets/{WebAuthn-C6uCye67.js → WebAuthn-CF9bM9I3.js} +3 -3
  89. package/src/assets/web-panel/assets/{WorkflowEditor-d2g4reYo.js → WorkflowEditor-D0H1XVPY.js} +1 -1
  90. package/src/assets/web-panel/assets/{chat-frTc6ScX.js → chat-Cgzzbe0o.js} +1 -1
  91. package/src/assets/web-panel/assets/{colors-B5K6lG3v.js → colors-BwNxEVS0.js} +1 -1
  92. package/src/assets/web-panel/assets/{compact-item-Cdn46r8D.js → compact-item-D60Z4Hhs.js} +1 -1
  93. package/src/assets/web-panel/assets/{createContext-DU1jqVQi.js → createContext-MFk58XB0.js} +1 -1
  94. package/src/assets/web-panel/assets/devWarning-BJiw369i.js +1 -0
  95. package/src/assets/web-panel/assets/{hasIn-DdNH-aW_.js → hasIn-CRj2SvGO.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-DM5FmPZU.js → index--s45_1cb.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-D4l82Me1.js → index--tquQ2ZG.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-CQorP1Ng.js → index-1T1rhokn.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-CHmkkdfT.js → index-B-M82_rx.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CGiwfwB9.js → index-BE_el1Zs.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BwlIxAmX.js → index-BhRkR7IO.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Cj6P9Nst.js → index-Bo0y96-K.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-BoFak7le.js → index-CJgu5JUK.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-Dca_i7H8.js → index-CLWwdebO.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BVLL-mq_.js → index-CNAK5QG_.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-LaivuXyF.js → index-CQjJjPRD.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-t_OBzIP4.js → index-CTDbAfae.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-BcGwGIaR.js → index-CVI_9sRK.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-D23xGYjM.js → index-CY14oMQK.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-VDM2KKo9.js → index-CbiTJkZf.js} +1 -1
  111. package/src/assets/web-panel/assets/index-CiMouosn.js +1 -0
  112. package/src/assets/web-panel/assets/{index-32ajXmTb.js → index-CqWpxMqx.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-PjAS0sIZ.js → index-CvB8lYUR.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-7ehytj1K.js → index-D51XwtCY.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-DtBCCkC4.js → index-D6HvK_1O.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-Be4OtL-N.js → index-D9jVIwLJ.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CUoNAmE8.js → index-DA4tE98u.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-cYaWV4Mq.js → index-DBwOtXw0.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-MM4Zz2OE.js → index-DKdNzTIb.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-DIVyTmZ3.js → index-DRySdmvN.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CtRPi4YC.js → index-DWzyTev7.js} +1 -1
  122. package/src/assets/web-panel/assets/index-DXcKlwy7.js +1 -0
  123. package/src/assets/web-panel/assets/{index-kVO_MDm-.js → index-DYjs7jAP.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-D4miNkWv.js → index-DbROzMBp.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-yZRuZpwK.js → index-DhFtk5PR.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-BXKhhx9V.js → index-Di2AbMLg.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BC4eGkar.js → index-DvmocQsM.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-NmEXVjzt.js → index-Dw-o0zEF.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-DRzBBdQ0.js → index-DysPoP38.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-HjCvXHf5.js → index-LX9pJxm_.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-gg3QPC3i.js → index-OI_fXWx0.js} +3 -3
  132. package/src/assets/web-panel/assets/{index-mgU3Q-lN.js → index-eDTOX8Xj.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-D1qIC4GR.js → index-gWKDsTzF.js} +1 -1
  134. package/src/assets/web-panel/assets/{index-DftZuyA7.js → index-q_GDUVAA.js} +1 -1
  135. package/src/assets/web-panel/assets/{initDefaultProps-DwLTRNdo.js → initDefaultProps-C61kZqEd.js} +1 -1
  136. package/src/assets/web-panel/assets/{motion-BchZDXnh.js → motion-DZnT5U1L.js} +1 -1
  137. package/src/assets/web-panel/assets/{move-e8M-zZFl.js → move-L8wzwbel.js} +1 -1
  138. package/src/assets/web-panel/assets/{mtc-parser-Ci40fQb3.js → mtc-parser-DLdYSejc.js} +1 -1
  139. package/src/assets/web-panel/assets/{omit-C7110_f2.js → omit-D53cbS59.js} +1 -1
  140. package/src/assets/web-panel/assets/{pickAttrs-skteMgnM.js → pickAttrs-Bbc5HPtu.js} +1 -1
  141. package/src/assets/web-panel/assets/{placementArrow-MGZ-lh2B.js → placementArrow-BkvmO860.js} +1 -1
  142. package/src/assets/web-panel/assets/{responsiveObserve-N4Sd7uy-.js → responsiveObserve-u2y1ryS1.js} +1 -1
  143. package/src/assets/web-panel/assets/{slide-C7HDGRaa.js → slide-BBqj7svU.js} +1 -1
  144. package/src/assets/web-panel/assets/{statusUtils-BhRexlgy.js → statusUtils-GN0mqu2d.js} +1 -1
  145. package/src/assets/web-panel/assets/{styleChecker-ChEoQkN2.js → styleChecker-BvwPKoBT.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFlexGapSupport-CDbG1s2Y.js → useFlexGapSupport-CCt8tnfY.js} +1 -1
  147. package/src/assets/web-panel/assets/{useFs-CUIx0tcJ.js → useFs-DXAxl2SV.js} +1 -1
  148. package/src/assets/web-panel/assets/{usePersonalDataHub-bkO7L7ec.js → usePersonalDataHub-DihlR2dj.js} +1 -1
  149. package/src/assets/web-panel/assets/{vnode-B_ZvABIG.js → vnode-D7BOX2gi.js} +1 -1
  150. package/src/assets/web-panel/assets/{zoom-D5shjpS8.js → zoom-CVbO-nL7.js} +1 -1
  151. package/src/assets/web-panel/index.html +1 -1
  152. package/src/command-help-index.json +1 -1
  153. package/src/commands/mcp.js +119 -0
  154. package/src/commands/session-show.js +8 -10
  155. package/src/commands/session.js +60 -52
  156. package/src/data/changelog.json +27 -1
  157. package/src/gateways/ws/session-protocol.js +14 -4
  158. package/src/gateways/ws/ws-agent-handler.js +31 -0
  159. package/src/harness/jsonl-session-store.js +1147 -130
  160. package/src/harness/mcp-client.js +44 -11
  161. package/src/harness/session-index.js +80 -80
  162. package/src/harness/session-list-index.js +147 -2
  163. package/src/lib/background-interaction-journal.js +2 -2
  164. package/src/lib/command-lifecycle-report.js +516 -0
  165. package/src/lib/mcp-stdio-environment.js +48 -0
  166. package/src/lib/mcp-stdio-executable-identity.js +552 -17
  167. package/src/lib/mcp-stdio-package-materialization.js +1424 -0
  168. package/src/lib/packer/native-update-state.js +979 -1
  169. package/src/lib/packer/pack-update-applier.js +1050 -125
  170. package/src/lib/packer/pkg-config-generator.js +1 -0
  171. package/src/lib/paths.js +53 -0
  172. package/src/lib/process-execution-broker/index.js +180 -37
  173. package/src/lib/process-execution-broker/platform-sandbox.js +291 -24
  174. package/src/lib/recent-session.js +17 -17
  175. package/src/lib/secure-file-identity.js +47 -0
  176. package/src/lib/secure-fs.js +21 -2
  177. package/src/lib/session-anti-rollback-anchor.js +766 -0
  178. package/src/lib/session-host-lease.js +492 -0
  179. package/src/lib/session-persistence-failure.js +109 -0
  180. package/src/lib/session-picker.js +26 -3
  181. package/src/lib/session-search.js +25 -11
  182. package/src/lib/side-effect-ledger-store.js +2 -2
  183. package/src/lib/skill-execution-authority.js +13 -9
  184. package/src/lib/turn-binding-store.js +2 -2
  185. package/src/repl/agent-repl.js +309 -39
  186. package/src/runtime/agent-core.js +88 -5
  187. package/src/runtime/cleanup-deadline.js +143 -0
  188. package/src/runtime/headless-runner.js +357 -139
  189. package/src/runtime/headless-stream.js +207 -28
  190. package/src/runtime/output-backpressure.js +53 -0
  191. package/src/assets/web-panel/assets/MarkdownRenderer-CU-JB2Ns.js +0 -1
  192. package/src/assets/web-panel/assets/Providers-DDCf_nIw.js +0 -1
  193. package/src/assets/web-panel/assets/devWarning-CP-jEUyw.js +0 -1
  194. package/src/assets/web-panel/assets/index-CzrCHrFy.js +0 -1
  195. package/src/assets/web-panel/assets/index-OJsGskZw.js +0 -1
@@ -2,7 +2,7 @@
2
2
  * JSONL Session Store — append-only session persistence.
3
3
  */
4
4
 
5
- import {
5
+ import fs, {
6
6
  constants as fsConstants,
7
7
  existsSync,
8
8
  appendFileSync,
@@ -16,6 +16,7 @@ import {
16
16
  fstatSync,
17
17
  lstatSync,
18
18
  readSync,
19
+ writeSync,
19
20
  ftruncateSync,
20
21
  } from "node:fs";
21
22
  import { join, basename, resolve } from "node:path";
@@ -23,9 +24,16 @@ import { createHash, randomUUID } from "node:crypto";
23
24
  import { TextDecoder } from "node:util";
24
25
  import { getHomeDir } from "../lib/paths.js";
25
26
  import { ensurePrivateDirectory, ensurePrivateFile } from "../lib/secure-fs.js";
27
+ import {
28
+ isAffectedWindowsZeroDeviceStatRuntime,
29
+ samePathHandleStableFileIdentity,
30
+ withTrustedFileParentSync,
31
+ } from "../lib/secure-file-identity.js";
26
32
  import {
27
33
  computeEventHash,
34
+ latestChainHash,
28
35
  TRANSCRIPT_CHAIN_STATUS,
36
+ verifyTranscriptText,
29
37
  } from "./transcript-integrity.js";
30
38
  import { withFileLock } from "../lib/with-file-lock.js";
31
39
  import {
@@ -48,12 +56,24 @@ import {
48
56
  publicSessionMeta,
49
57
  readLatestSessionActivity,
50
58
  readSessionMeta,
59
+ readSessionTombstoneMarker,
51
60
  recordSessionDeleted,
52
61
  recordSessionEvent,
53
62
  recordSessionActivity,
54
63
  replaceSessionMeta,
64
+ normalizeSessionGenerationAuthority,
65
+ SESSION_GENERATION_AUTHORITY_FIELD,
66
+ SESSION_GENERATION_AUTHORITY_SCHEMA,
55
67
  SESSION_TOMBSTONE_MARKER_SUFFIX,
56
68
  } from "./session-list-index.js";
69
+ import { createSessionPersistenceFailure } from "../lib/session-persistence-failure.js";
70
+ import { withSessionHostWriteAuthority } from "../lib/session-host-lease.js";
71
+ import {
72
+ listSessionAntiRollbackIds,
73
+ publishSessionAntiRollbackAnchor,
74
+ readSessionAntiRollbackAnchor,
75
+ sessionAntiRollbackPredecessorWitness,
76
+ } from "../lib/session-anti-rollback-anchor.js";
57
77
 
58
78
  let securedSessionsDir = null;
59
79
  let securedSessionsDirIdentity = null;
@@ -62,6 +82,7 @@ let securedSessionsDirIdentity = null;
62
82
  // gate. The hooks are inert unless the dedicated child process opts in; this
63
83
  // keeps production behavior and ordinary tests on the exact same append path.
64
84
  export const _sessionScaleFaultHooks = Object.seal({
85
+ beforeTranscriptAppend: null,
65
86
  afterTranscriptAppend: null,
66
87
  afterForkCopy: null,
67
88
  afterForkLineage: null,
@@ -79,6 +100,11 @@ const WS_TURN_INPUT_DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
79
100
  const WS_TURN_CLAIM_ID_PATTERN = /^claim-[0-9a-f-]{36}$/;
80
101
  const WS_TURN_FAILURE_CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
81
102
 
103
+ export {
104
+ SESSION_GENERATION_AUTHORITY_FIELD,
105
+ SESSION_GENERATION_AUTHORITY_SCHEMA,
106
+ };
107
+
82
108
  function wsTurnError(message, code) {
83
109
  const error = new Error(message);
84
110
  error.code = code;
@@ -511,6 +537,100 @@ export function sessionPath(sessionId) {
511
537
  return join(getSessionsDir(), `${sessionId}.jsonl`);
512
538
  }
513
539
 
540
+ function physicalTranscriptStateFromStats(stats) {
541
+ const exact = (value) =>
542
+ typeof value === "bigint" ? String(value) : undefined;
543
+ const legacyNumber = (value) => Number(value);
544
+ const timeMs = (name) => {
545
+ const nanoseconds = stats[`${name}Ns`];
546
+ return typeof nanoseconds === "bigint"
547
+ ? Number(nanoseconds / 1_000_000n) +
548
+ Number(nanoseconds % 1_000_000n) / 1_000_000
549
+ : Number(stats[`${name}Ms`]);
550
+ };
551
+ return Object.freeze({
552
+ // Keep the rounded legacy fields so metadata produced by older releases
553
+ // remains readable. BigIntStats supplies the exact witness used by new
554
+ // writers; this avoids Node 22.12 Windows fstat/lstat precision drift.
555
+ dev: String(legacyNumber(stats.dev)),
556
+ ino: String(legacyNumber(stats.ino)),
557
+ size: legacyNumber(stats.size),
558
+ mtimeMs: timeMs("mtime"),
559
+ ctimeMs: timeMs("ctime"),
560
+ devExact: exact(stats.dev),
561
+ inoExact: exact(stats.ino),
562
+ sizeExact: exact(stats.size),
563
+ mtimeNs: exact(stats.mtimeNs),
564
+ ctimeNs: exact(stats.ctimeNs),
565
+ });
566
+ }
567
+
568
+ function readPhysicalTranscriptStats(filePath) {
569
+ const stats = lstatSync(filePath, { bigint: true });
570
+ if (stats.isSymbolicLink() || !stats.isFile()) {
571
+ const error = new Error("Session transcript path is not a regular file");
572
+ error.code = "SESSION_TRANSCRIPT_IDENTITY_CHANGED";
573
+ error.commitState = "not-committed";
574
+ throw error;
575
+ }
576
+ return stats;
577
+ }
578
+
579
+ function readPhysicalTranscriptState(filePath) {
580
+ return physicalTranscriptStateFromStats(
581
+ readPhysicalTranscriptStats(filePath),
582
+ );
583
+ }
584
+
585
+ function samePhysicalTranscriptState(left, right, { content = true } = {}) {
586
+ if (!left || !right) return left === right;
587
+ const sameField = (exactField, legacyField, { numeric = false } = {}) => {
588
+ if (left[exactField] != null && right[exactField] != null) {
589
+ return String(left[exactField]) === String(right[exactField]);
590
+ }
591
+ return numeric
592
+ ? Number(left[legacyField]) === Number(right[legacyField])
593
+ : String(left[legacyField]) === String(right[legacyField]);
594
+ };
595
+ return (
596
+ sameField("devExact", "dev") &&
597
+ sameField("inoExact", "ino") &&
598
+ (!content ||
599
+ (sameField("sizeExact", "size", { numeric: true }) &&
600
+ sameField("mtimeNs", "mtimeMs", { numeric: true }) &&
601
+ sameField("ctimeNs", "ctimeMs", { numeric: true })))
602
+ );
603
+ }
604
+
605
+ function samePhysicalTranscriptContentState(left, right) {
606
+ if (!left || !right) return left === right;
607
+ const sameField = (exactField, legacyField) => {
608
+ if (left[exactField] != null && right[exactField] != null) {
609
+ return String(left[exactField]) === String(right[exactField]);
610
+ }
611
+ return Number(left[legacyField]) === Number(right[legacyField]);
612
+ };
613
+ return (
614
+ sameField("sizeExact", "size") &&
615
+ sameField("mtimeNs", "mtimeMs") &&
616
+ sameField("ctimeNs", "ctimeMs")
617
+ );
618
+ }
619
+
620
+ function withTranscriptFileParent(filePath, task) {
621
+ if (!isAffectedWindowsZeroDeviceStatRuntime()) {
622
+ return task({ canonicalPath: filePath, parentDevice: null });
623
+ }
624
+ return withTrustedFileParentSync(fs, filePath, task);
625
+ }
626
+
627
+ function transcriptIdentityError(commitState, reason) {
628
+ const error = new Error(`Session transcript identity changed: ${reason}`);
629
+ error.code = "SESSION_TRANSCRIPT_IDENTITY_CHANGED";
630
+ error.commitState = commitState;
631
+ return error;
632
+ }
633
+
514
634
  export function appendTokenUsage(sessionId, usage) {
515
635
  appendEvent(sessionId, "token_usage", usage || {});
516
636
  }
@@ -661,19 +781,26 @@ function appendVerifiedWsAuthorityEventLocked(
661
781
  const core = { type, timestamp: Date.now(), data };
662
782
  const hash = computeEventHash(previousHeadHash, core);
663
783
  const event = { ...core, prevHash: previousHeadHash, hash };
664
- appendFileSync(filePath, `${JSON.stringify(event)}\n`, {
665
- encoding: "utf8",
666
- mode: 0o600,
667
- });
668
784
  ensurePrivateFile(filePath);
669
- runSessionScaleFaultHook("afterTranscriptAppend", {
785
+ const expectedState = readPhysicalTranscriptState(filePath);
786
+ const transcriptState = appendTranscriptEvent(
670
787
  sessionId,
671
788
  type,
672
789
  event,
673
790
  filePath,
674
- });
791
+ expectedState,
792
+ );
675
793
  try {
676
- recordSessionEvent(getSessionsDir(), sessionId, event, hash);
794
+ const nextMeta = recordSessionEvent(
795
+ getSessionsDir(),
796
+ sessionId,
797
+ event,
798
+ hash,
799
+ {
800
+ transcriptState,
801
+ },
802
+ );
803
+ publishSessionAntiRollbackWitness(sessionId, nextMeta, filePath, "live");
677
804
  } catch (cause) {
678
805
  const error = new Error(
679
806
  `Session authority anchor could not be persisted: ${sessionId}`,
@@ -711,9 +838,17 @@ function appendVerifiedWsAuthorityEventLocked(
711
838
  return Object.freeze({ hash, event });
712
839
  }
713
840
 
841
+ function withSessionHostWriterLock(sessionId, filePath, task, options) {
842
+ return withFileLock(
843
+ filePath,
844
+ () => withSessionHostWriteAuthority(sessionId, task),
845
+ options,
846
+ );
847
+ }
848
+
714
849
  function withVerifiedWsTurnLock(sessionId, task) {
715
850
  const filePath = sessionPath(sessionId);
716
- return withFileLock(filePath, () => task(filePath), {
851
+ return withSessionHostWriterLock(sessionId, filePath, () => task(filePath), {
717
852
  failIfUnavailable: true,
718
853
  timeoutMs: 30_000,
719
854
  retryMs: 1,
@@ -740,6 +875,153 @@ function encodeEventMessageProvenance(type, data) {
740
875
  return data;
741
876
  }
742
877
 
878
+ function appendTranscriptEvent(
879
+ sessionId,
880
+ type,
881
+ event,
882
+ filePath,
883
+ expectedState,
884
+ ) {
885
+ const payload = { sessionId, type, event, filePath };
886
+ try {
887
+ runSessionScaleFaultHook("beforeTranscriptAppend", payload);
888
+ } catch (cause) {
889
+ throw createSessionPersistenceFailure(cause, {
890
+ sessionId,
891
+ operation: "transcript-append",
892
+ commitState: "not-committed",
893
+ });
894
+ }
895
+ try {
896
+ return withTranscriptFileParent(
897
+ filePath,
898
+ ({ canonicalPath, parentDevice }) =>
899
+ appendTranscriptEventAtPath({
900
+ sessionId,
901
+ event,
902
+ payload,
903
+ filePath: canonicalPath,
904
+ expectedState,
905
+ parentDevice,
906
+ }),
907
+ );
908
+ } catch (cause) {
909
+ if (
910
+ cause?.code === "SESSION_TRANSCRIPT_IDENTITY_CHANGED" ||
911
+ cause?.operation === "transcript-append" ||
912
+ cause?.operation === "transcript-settlement"
913
+ ) {
914
+ throw cause;
915
+ }
916
+ throw createSessionPersistenceFailure(cause, {
917
+ sessionId,
918
+ operation: "transcript-settlement",
919
+ commitState: "unknown",
920
+ });
921
+ }
922
+ }
923
+
924
+ function appendTranscriptEventAtPath({
925
+ sessionId,
926
+ event,
927
+ payload,
928
+ filePath,
929
+ expectedState,
930
+ parentDevice,
931
+ }) {
932
+ let fd = null;
933
+ let wroteBytes = false;
934
+ let appendCompleted = false;
935
+ try {
936
+ let flags = fsConstants.O_WRONLY | fsConstants.O_APPEND;
937
+ if (expectedState === null) {
938
+ flags |= fsConstants.O_CREAT | fsConstants.O_EXCL;
939
+ }
940
+ if (typeof fsConstants.O_NOFOLLOW === "number") {
941
+ flags |= fsConstants.O_NOFOLLOW;
942
+ }
943
+ fd = openSync(filePath, flags, 0o600);
944
+ const openedStats = fstatSync(fd, { bigint: true });
945
+ const openedState = physicalTranscriptStateFromStats(openedStats);
946
+ if (expectedState !== null) {
947
+ if (parentDevice === null) {
948
+ if (!samePhysicalTranscriptState(expectedState, openedState)) {
949
+ throw transcriptIdentityError(
950
+ "not-committed",
951
+ "the append descriptor does not match the verified path",
952
+ );
953
+ }
954
+ } else {
955
+ const publishedBeforeStats = readPhysicalTranscriptStats(filePath);
956
+ const publishedBeforeState =
957
+ physicalTranscriptStateFromStats(publishedBeforeStats);
958
+ if (
959
+ !samePhysicalTranscriptState(expectedState, publishedBeforeState) ||
960
+ !samePathHandleStableFileIdentity(
961
+ publishedBeforeStats,
962
+ openedStats,
963
+ parentDevice,
964
+ )
965
+ ) {
966
+ throw transcriptIdentityError(
967
+ "not-committed",
968
+ "the append descriptor does not match the verified path",
969
+ );
970
+ }
971
+ }
972
+ }
973
+ const bytes = Buffer.from(`${JSON.stringify(event)}\n`, "utf8");
974
+ let offset = 0;
975
+ while (offset < bytes.length) {
976
+ const written = writeSync(fd, bytes, offset, bytes.length - offset);
977
+ if (!Number.isSafeInteger(written) || written <= 0) {
978
+ throw new Error("Session transcript append made no forward progress");
979
+ }
980
+ offset += written;
981
+ wroteBytes = true;
982
+ }
983
+ appendCompleted = true;
984
+ runSessionScaleFaultHook("afterTranscriptAppend", payload);
985
+ const descriptorStats = fstatSync(fd, { bigint: true });
986
+ const descriptorState = physicalTranscriptStateFromStats(descriptorStats);
987
+ const pathStats = readPhysicalTranscriptStats(filePath);
988
+ const pathState = physicalTranscriptStateFromStats(pathStats);
989
+ const publishedIdentityMatches =
990
+ parentDevice === null
991
+ ? samePhysicalTranscriptState(descriptorState, pathState, {
992
+ content: false,
993
+ })
994
+ : samePathHandleStableFileIdentity(
995
+ pathStats,
996
+ descriptorStats,
997
+ parentDevice,
998
+ );
999
+ if (!publishedIdentityMatches) {
1000
+ throw transcriptIdentityError(
1001
+ "unknown",
1002
+ "the published path no longer names the appended descriptor",
1003
+ );
1004
+ }
1005
+ return pathState;
1006
+ } catch (cause) {
1007
+ if (cause?.code === "SESSION_TRANSCRIPT_IDENTITY_CHANGED") throw cause;
1008
+ throw createSessionPersistenceFailure(cause, {
1009
+ sessionId,
1010
+ operation: appendCompleted
1011
+ ? "transcript-settlement"
1012
+ : "transcript-append",
1013
+ // ENOSPC can follow a short write. EROFS fails before the append can
1014
+ // mutate the target.
1015
+ commitState:
1016
+ !appendCompleted && (cause?.code === "EROFS" || !wroteBytes)
1017
+ ? "not-committed"
1018
+ : "unknown",
1019
+ });
1020
+ } finally {
1021
+ if (fd !== null) closeSync(fd);
1022
+ }
1023
+ }
1024
+
743
1025
  const SESSION_AUTHORITY_RECOVERY_TEXT_LIMITS = Object.freeze({
744
1026
  safetyId: 512,
745
1027
  safetyIdentity: 512,
@@ -774,6 +1056,174 @@ function sanitizeSessionAuthorityRecoveryEvidence(value) {
774
1056
  return Object.keys(evidence).length > 0 ? Object.freeze(evidence) : null;
775
1057
  }
776
1058
 
1059
+ function sessionDeletedError(sessionId) {
1060
+ const error = new Error(`Session was deleted: ${sessionId}`);
1061
+ error.code = "SESSION_DELETED";
1062
+ return error;
1063
+ }
1064
+
1065
+ function createSessionGenerationAuthority(
1066
+ sessionId,
1067
+ predecessorWitness = null,
1068
+ generationId = `generation-${randomUUID()}`,
1069
+ ) {
1070
+ const previous = normalizeSessionGenerationAuthority(
1071
+ predecessorWitness?.generation,
1072
+ );
1073
+ const isReplacement = predecessorWitness !== null;
1074
+ return Object.freeze({
1075
+ schema: SESSION_GENERATION_AUTHORITY_SCHEMA,
1076
+ sessionId,
1077
+ generationId,
1078
+ ordinal:
1079
+ previous?.sessionId === sessionId
1080
+ ? previous.ordinal + 1
1081
+ : isReplacement
1082
+ ? 2
1083
+ : 1,
1084
+ predecessor: isReplacement
1085
+ ? Object.freeze({
1086
+ kind:
1087
+ previous?.sessionId === sessionId
1088
+ ? "tombstone"
1089
+ : "legacy-tombstone",
1090
+ generationId:
1091
+ previous?.sessionId === sessionId ? previous.generationId : null,
1092
+ headHash:
1093
+ typeof predecessorWitness?.last_hash === "string" &&
1094
+ /^[0-9a-f]{64}$/.test(predecessorWitness.last_hash)
1095
+ ? predecessorWitness.last_hash
1096
+ : null,
1097
+ eventCount: Math.max(0, Number(predecessorWitness?.event_count) || 0),
1098
+ tombstonedAtMs: Number.isSafeInteger(
1099
+ Number(predecessorWitness?.deleted_at_ms),
1100
+ )
1101
+ ? Math.max(0, Number(predecessorWitness.deleted_at_ms))
1102
+ : null,
1103
+ })
1104
+ : null,
1105
+ });
1106
+ }
1107
+
1108
+ function encodeSessionGenerationData(data, authority = null) {
1109
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
1110
+ return authority
1111
+ ? { [SESSION_GENERATION_AUTHORITY_FIELD]: authority }
1112
+ : data;
1113
+ }
1114
+ const clean = { ...data };
1115
+ // The namespace generation is store-owned authority. Callers may not forge
1116
+ // it on arbitrary events or choose a replacement predecessor.
1117
+ delete clean[SESSION_GENERATION_AUTHORITY_FIELD];
1118
+ if (authority) clean[SESSION_GENERATION_AUTHORITY_FIELD] = authority;
1119
+ return clean;
1120
+ }
1121
+
1122
+ function sessionAntiRollbackCandidateFromWitness(witness, status = "live") {
1123
+ if (!witness || typeof witness !== "object") return null;
1124
+ const eventCount = Math.max(0, Number(witness.event_count) || 0);
1125
+ const headHash =
1126
+ typeof witness.last_hash === "string" &&
1127
+ /^[0-9a-f]{64}$/.test(witness.last_hash)
1128
+ ? witness.last_hash
1129
+ : null;
1130
+ const generation = normalizeSessionGenerationAuthority(witness.generation);
1131
+ const legacyDeletion =
1132
+ status === "deleted" && generation === null && headHash === null;
1133
+ if (!legacyDeletion && (eventCount === 0) !== (headHash === null))
1134
+ return null;
1135
+ return Object.freeze({
1136
+ status,
1137
+ generation,
1138
+ headHash,
1139
+ eventCount,
1140
+ deletedAtMs:
1141
+ status === "deleted"
1142
+ ? Math.max(
1143
+ 0,
1144
+ Number(witness.deleted_at_ms || witness.updated_at_ms) || 0,
1145
+ )
1146
+ : null,
1147
+ });
1148
+ }
1149
+
1150
+ function antiRollbackAnchorMatchesCandidate(anchor, candidate) {
1151
+ return (
1152
+ anchor !== null &&
1153
+ candidate !== null &&
1154
+ anchor.status === candidate.status &&
1155
+ JSON.stringify(anchor.generation) ===
1156
+ JSON.stringify(candidate.generation) &&
1157
+ anchor.headHash === candidate.headHash &&
1158
+ anchor.eventCount === candidate.eventCount &&
1159
+ anchor.deletedAtMs === candidate.deletedAtMs
1160
+ );
1161
+ }
1162
+
1163
+ function transcriptHashAtEventCount(filePath, eventCount) {
1164
+ if (eventCount === 0) return null;
1165
+ let count = 0;
1166
+ for (const { line } of iterateFileLinesSync(filePath)) {
1167
+ let event;
1168
+ try {
1169
+ event = JSON.parse(line);
1170
+ } catch {
1171
+ continue;
1172
+ }
1173
+ if (typeof event?.hash !== "string") continue;
1174
+ count += 1;
1175
+ if (count === eventCount) return event.hash;
1176
+ }
1177
+ return null;
1178
+ }
1179
+
1180
+ function publishSessionAntiRollbackWitness(
1181
+ sessionId,
1182
+ witness,
1183
+ filePath,
1184
+ status = "live",
1185
+ ) {
1186
+ const candidate = sessionAntiRollbackCandidateFromWitness(witness, status);
1187
+ if (candidate === null) {
1188
+ const error = new Error(
1189
+ `Session anti-rollback candidate is incomplete: ${sessionId}`,
1190
+ );
1191
+ error.code = "CC_SESSION_ANTI_ROLLBACK_UNAVAILABLE";
1192
+ throw error;
1193
+ }
1194
+ return publishSessionAntiRollbackAnchor(sessionId, candidate, {
1195
+ provePrefix: (current) =>
1196
+ current.status === "live" &&
1197
+ existsSync(filePath) &&
1198
+ transcriptHashAtEventCount(filePath, current.eventCount) ===
1199
+ current.headHash,
1200
+ });
1201
+ }
1202
+
1203
+ function assertSessionAntiRollbackBeforeWrite(
1204
+ sessionId,
1205
+ witness,
1206
+ filePath,
1207
+ status = "live",
1208
+ ) {
1209
+ const current = readSessionAntiRollbackAnchor(sessionId);
1210
+ if (current === null && witness === null) return null;
1211
+ const candidate = sessionAntiRollbackCandidateFromWitness(witness, status);
1212
+ if (candidate === null) return current;
1213
+ if (antiRollbackAnchorMatchesCandidate(current, candidate)) return current;
1214
+
1215
+ if (status === "live" && existsSync(filePath)) {
1216
+ const verification = verifyTranscriptFile(filePath);
1217
+ assertLocalVerifiedTranscriptAnchor(sessionId, verification);
1218
+ }
1219
+ return publishSessionAntiRollbackWitness(
1220
+ sessionId,
1221
+ witness,
1222
+ filePath,
1223
+ status,
1224
+ );
1225
+ }
1226
+
777
1227
  function appendEventLocked(
778
1228
  sessionId,
779
1229
  type,
@@ -781,35 +1231,69 @@ function appendEventLocked(
781
1231
  { expectedHeadHash, compareHead = false, requireIndexAnchor = false } = {},
782
1232
  ) {
783
1233
  const filePath = sessionPath(sessionId);
784
- return withFileLock(
1234
+ return withSessionHostWriterLock(
1235
+ sessionId,
785
1236
  filePath,
786
1237
  () => {
787
- const existingMeta = readSessionMeta(getSessionsDir(), sessionId);
788
- const persistedWitness = existingMeta;
1238
+ let existingMeta = readSessionMeta(getSessionsDir(), sessionId);
1239
+ const marker = readSessionTombstoneMarker(getSessionsDir(), sessionId);
1240
+ const externalAnchor = readSessionAntiRollbackAnchor(sessionId);
1241
+ const presence = getSessionPresence(sessionId);
789
1242
  const transcriptExists = existsSync(filePath);
790
1243
  const startsNewGeneration =
791
- !transcriptExists &&
792
- persistedWitness?.deleted === true &&
793
- type === "session_start";
1244
+ presence === SESSION_PRESENCE.TOMBSTONED && type === "session_start";
1245
+ if (presence === SESSION_PRESENCE.CONFLICT) {
1246
+ if (marker !== null || existingMeta?.deleted === true) {
1247
+ throw sessionDeletedError(sessionId);
1248
+ }
1249
+ throw unverifiedTranscriptError(
1250
+ sessionId,
1251
+ transcriptAnchorMismatchVerification(),
1252
+ );
1253
+ }
794
1254
  if (
795
- persistedWitness?.deleted === true &&
796
- (transcriptExists || type !== "session_start")
1255
+ presence === SESSION_PRESENCE.TOMBSTONED &&
1256
+ type !== "session_start"
797
1257
  ) {
798
- const error = new Error(`Session was deleted: ${sessionId}`);
799
- error.code = "SESSION_DELETED";
800
- throw error;
1258
+ throw sessionDeletedError(sessionId);
801
1259
  }
802
1260
  if (
803
- !transcriptExists &&
804
- persistedWitness !== null &&
805
- persistedWitness?.deleted !== true
1261
+ presence === SESSION_PRESENCE.MISSING_TRANSCRIPT ||
1262
+ (!transcriptExists &&
1263
+ existingMeta !== null &&
1264
+ existingMeta?.deleted !== true)
806
1265
  ) {
807
1266
  throw unverifiedTranscriptError(
808
1267
  sessionId,
809
1268
  missingTranscriptVerification(),
810
1269
  );
811
1270
  }
1271
+ const duplicateLiveStart =
1272
+ presence === SESSION_PRESENCE.PRESENT && type === "session_start";
812
1273
  const { prevHash, recovery } = _resolveChainTail(filePath);
1274
+ let expectedTranscriptState = transcriptExists
1275
+ ? readPhysicalTranscriptState(filePath)
1276
+ : null;
1277
+ if (
1278
+ transcriptExists &&
1279
+ existingMeta?.transcript &&
1280
+ (!samePhysicalTranscriptState(
1281
+ existingMeta.transcript,
1282
+ expectedTranscriptState,
1283
+ { content: recovery === null },
1284
+ ) ||
1285
+ Number(existingMeta.transcript.size) !==
1286
+ Number(expectedTranscriptState.size))
1287
+ ) {
1288
+ throw transcriptIdentityError(
1289
+ "not-committed",
1290
+ "the transcript no longer matches its persisted physical witness",
1291
+ );
1292
+ }
1293
+ if (transcriptExists) {
1294
+ ensurePrivateFile(filePath);
1295
+ expectedTranscriptState = readPhysicalTranscriptState(filePath);
1296
+ }
813
1297
  if (
814
1298
  transcriptExists &&
815
1299
  existingMeta !== null &&
@@ -834,6 +1318,29 @@ function appendEventLocked(
834
1318
  // publish a count of one for a multi-event transcript. Restore the
835
1319
  // projection under the same writer lock before advancing the chain.
836
1320
  rebuildSessionMetaUnlocked(getSessionsDir(), sessionId, filePath);
1321
+ existingMeta = readSessionMeta(getSessionsDir(), sessionId);
1322
+ }
1323
+ if (transcriptExists) {
1324
+ assertSessionAntiRollbackBeforeWrite(
1325
+ sessionId,
1326
+ existingMeta,
1327
+ filePath,
1328
+ "live",
1329
+ );
1330
+ } else if (presence === SESSION_PRESENCE.TOMBSTONED) {
1331
+ assertSessionAntiRollbackBeforeWrite(
1332
+ sessionId,
1333
+ existingMeta ||
1334
+ marker ||
1335
+ sessionAntiRollbackPredecessorWitness(externalAnchor),
1336
+ filePath,
1337
+ "deleted",
1338
+ );
1339
+ }
1340
+ if (duplicateLiveStart) {
1341
+ const error = new Error(`Session already exists: ${sessionId}`);
1342
+ error.code = "SESSION_ALREADY_EXISTS";
1343
+ throw error;
837
1344
  }
838
1345
  if (compareHead && prevHash !== (expectedHeadHash || null)) {
839
1346
  const error = new Error(
@@ -844,35 +1351,60 @@ function appendEventLocked(
844
1351
  error.actualHeadHash = prevHash;
845
1352
  throw error;
846
1353
  }
847
- const persistedData = encodeEventMessageProvenance(type, data);
1354
+ const generationAuthority =
1355
+ type === "session_start" &&
1356
+ [SESSION_PRESENCE.ABSENT, SESSION_PRESENCE.TOMBSTONED].includes(
1357
+ presence,
1358
+ )
1359
+ ? createSessionGenerationAuthority(
1360
+ sessionId,
1361
+ startsNewGeneration
1362
+ ? existingMeta ||
1363
+ marker ||
1364
+ sessionAntiRollbackPredecessorWitness(externalAnchor)
1365
+ : null,
1366
+ )
1367
+ : null;
1368
+ const persistedData = encodeSessionGenerationData(
1369
+ encodeEventMessageProvenance(type, data),
1370
+ generationAuthority,
1371
+ );
848
1372
  const core = { type, timestamp: Date.now(), data: persistedData };
849
1373
  const hash = computeEventHash(prevHash, core);
850
1374
  const event = { ...core, prevHash, hash };
851
- const line = JSON.stringify(event) + "\n";
852
- appendFileSync(filePath, line, { encoding: "utf8", mode: 0o600 });
853
- ensurePrivateFile(filePath);
854
- runSessionScaleFaultHook("afterTranscriptAppend", {
1375
+ const transcriptState = appendTranscriptEvent(
855
1376
  sessionId,
856
1377
  type,
857
1378
  event,
858
1379
  filePath,
859
- });
1380
+ expectedTranscriptState,
1381
+ );
860
1382
  try {
861
- recordSessionEvent(getSessionsDir(), sessionId, event, hash, {
862
- resetGeneration: startsNewGeneration,
863
- });
1383
+ const nextMeta = recordSessionEvent(
1384
+ getSessionsDir(),
1385
+ sessionId,
1386
+ event,
1387
+ hash,
1388
+ {
1389
+ resetGeneration: startsNewGeneration,
1390
+ transcriptState,
1391
+ },
1392
+ );
1393
+ publishSessionAntiRollbackWitness(
1394
+ sessionId,
1395
+ nextMeta,
1396
+ filePath,
1397
+ "live",
1398
+ );
864
1399
  } catch (cause) {
865
- if (requireIndexAnchor) {
866
- const error = new Error(
867
- `Session authority anchor could not be persisted: ${sessionId}`,
868
- { cause },
869
- );
870
- error.code = "SESSION_INDEX_ANCHOR_FAILED";
871
- error.sessionId = sessionId;
872
- throw error;
873
- }
874
- // The metadata index is explicitly rebuildable. Never turn a committed
875
- // transcript event into an apparent failure that a caller may retry.
1400
+ const error = new Error(
1401
+ `Session authority anchor could not be persisted: ${sessionId}`,
1402
+ { cause },
1403
+ );
1404
+ error.code = "SESSION_INDEX_ANCHOR_FAILED";
1405
+ error.sessionId = sessionId;
1406
+ error.commitState = "unknown";
1407
+ throw error;
876
1408
  }
877
1409
  if (requireIndexAnchor) {
878
1410
  const verification = verifyTranscriptFile(filePath);
@@ -891,10 +1423,11 @@ function appendEventLocked(
891
1423
  error.code = "SESSION_INDEX_ANCHOR_FAILED";
892
1424
  error.sessionId = sessionId;
893
1425
  error.verification = verification;
1426
+ error.commitState = "unknown";
894
1427
  throw error;
895
1428
  }
896
1429
  }
897
- return { hash, recovery };
1430
+ return { hash, recovery, commitState: "committed" };
898
1431
  },
899
1432
  {
900
1433
  failIfUnavailable: true,
@@ -907,8 +1440,24 @@ function appendEventLocked(
907
1440
  );
908
1441
  }
909
1442
 
1443
+ function runClassifiedAppend(sessionId, operation, append) {
1444
+ try {
1445
+ return append();
1446
+ } catch (cause) {
1447
+ throw createSessionPersistenceFailure(cause, {
1448
+ sessionId,
1449
+ operation,
1450
+ // Preserve an inner append/settlement classification. Without one,
1451
+ // EROFS defaults to not-committed and ENOSPC remains unknown.
1452
+ commitState: cause?.commitState,
1453
+ });
1454
+ }
1455
+ }
1456
+
910
1457
  export function appendEvent(sessionId, type, data) {
911
- return appendEventLocked(sessionId, type, data);
1458
+ return runClassifiedAppend(sessionId, "append-event", () =>
1459
+ appendEventLocked(sessionId, type, data),
1460
+ );
912
1461
  }
913
1462
 
914
1463
  /**
@@ -916,9 +1465,11 @@ export function appendEvent(sessionId, type, data) {
916
1465
  * crash/tail-truncation anchor are durably updated and still agree.
917
1466
  */
918
1467
  export function appendAuthorityEvent(sessionId, type, data) {
919
- return appendEventLocked(sessionId, type, data, {
920
- requireIndexAnchor: true,
921
- });
1468
+ return runClassifiedAppend(sessionId, "append-authority-event", () =>
1469
+ appendEventLocked(sessionId, type, data, {
1470
+ requireIndexAnchor: true,
1471
+ }),
1472
+ );
922
1473
  }
923
1474
 
924
1475
  /**
@@ -932,10 +1483,12 @@ export function appendEventIfHead(
932
1483
  data,
933
1484
  expectedHeadHash = null,
934
1485
  ) {
935
- return appendEventLocked(sessionId, type, data, {
936
- expectedHeadHash,
937
- compareHead: true,
938
- });
1486
+ return runClassifiedAppend(sessionId, "compare-and-append-event", () =>
1487
+ appendEventLocked(sessionId, type, data, {
1488
+ expectedHeadHash,
1489
+ compareHead: true,
1490
+ }),
1491
+ );
939
1492
  }
940
1493
 
941
1494
  /** Authority-bearing compare-and-append with a mandatory index anchor. */
@@ -945,11 +1498,16 @@ export function appendAuthorityEventIfHead(
945
1498
  data,
946
1499
  expectedHeadHash = null,
947
1500
  ) {
948
- return appendEventLocked(sessionId, type, data, {
949
- expectedHeadHash,
950
- compareHead: true,
951
- requireIndexAnchor: true,
952
- });
1501
+ return runClassifiedAppend(
1502
+ sessionId,
1503
+ "compare-and-append-authority-event",
1504
+ () =>
1505
+ appendEventLocked(sessionId, type, data, {
1506
+ expectedHeadHash,
1507
+ compareHead: true,
1508
+ requireIndexAnchor: true,
1509
+ }),
1510
+ );
953
1511
  }
954
1512
 
955
1513
  /**
@@ -984,7 +1542,8 @@ export function appendAuthorityEventWithVerifiedProjection(
984
1542
  );
985
1543
  }
986
1544
  const filePath = sessionPath(sessionId);
987
- return withFileLock(
1545
+ return withSessionHostWriterLock(
1546
+ sessionId,
988
1547
  filePath,
989
1548
  () => {
990
1549
  const projection = createProjection();
@@ -1093,7 +1652,8 @@ export function withSessionAuthorityTransaction(
1093
1652
  }
1094
1653
 
1095
1654
  const filePath = sessionPath(sessionId);
1096
- return withFileLock(
1655
+ return withSessionHostWriterLock(
1656
+ sessionId,
1097
1657
  filePath,
1098
1658
  () => {
1099
1659
  const existingMeta = readSessionMeta(getSessionsDir(), sessionId);
@@ -1560,20 +2120,77 @@ export function listSessionIds() {
1560
2120
  .map((file) => basename(file, ".jsonl"));
1561
2121
  }
1562
2122
 
1563
- /** Resolve an exact id or one unambiguous prefix without reading transcripts. */
1564
- export function resolveSessionId(input) {
2123
+ /**
2124
+ * Enumerate every durable id occupying the canonical JSONL namespace.
2125
+ *
2126
+ * A transcript is not the only authority witness: a live/missing sidecar and
2127
+ * a tombstone marker must continue to fence legacy stores even when the JSONL
2128
+ * path itself is absent. Keeping this enumeration content-free also lets
2129
+ * prefix resolution detect ambiguity without opening transcript bodies.
2130
+ */
2131
+ export function listSessionEvidenceIds() {
2132
+ const dir = getSessionsDir();
2133
+ const ids = new Set(listSessionAntiRollbackIds());
2134
+ if (!existsSync(dir)) return [...ids].sort();
2135
+ for (const file of readdirSync(dir)) {
2136
+ let id = null;
2137
+ if (file.endsWith(".jsonl")) {
2138
+ id = basename(file, ".jsonl");
2139
+ } else if (file.endsWith(".meta.json")) {
2140
+ id = file.slice(0, -".meta.json".length);
2141
+ } else if (file.endsWith(SESSION_TOMBSTONE_MARKER_SUFFIX)) {
2142
+ id = file.slice(0, -SESSION_TOMBSTONE_MARKER_SUFFIX.length);
2143
+ }
2144
+ if (id && !isUnsafeSessionId(id)) ids.add(id);
2145
+ }
2146
+ return [...ids].sort();
2147
+ }
2148
+
2149
+ /**
2150
+ * Resolve an exact id or unambiguous prefix across the complete persisted
2151
+ * namespace, including damaged and tombstoned generations.
2152
+ *
2153
+ * Returning the presence separately from the id prevents callers from using
2154
+ * "not readable" as "not canonical" and then silently consulting a legacy
2155
+ * store. Ambiguity is fail-closed even when only one candidate is currently
2156
+ * readable.
2157
+ */
2158
+ export function resolveSessionAuthority(input) {
1565
2159
  if (isUnsafeSessionId(input)) return null;
1566
- if (sessionExists(input)) return input;
1567
- const matches = listSessionIds().filter(
1568
- (id) => id.startsWith(input) && sessionExists(id),
1569
- );
2160
+ const exactPresence = getSessionPresence(input);
2161
+ if (exactPresence !== SESSION_PRESENCE.ABSENT) {
2162
+ return Object.freeze({
2163
+ id: input,
2164
+ match: "exact",
2165
+ presence: exactPresence,
2166
+ readable: exactPresence === SESSION_PRESENCE.PRESENT,
2167
+ });
2168
+ }
2169
+ const matches = listSessionEvidenceIds()
2170
+ .filter((id) => id.startsWith(input))
2171
+ .map((id) => ({ id, presence: getSessionPresence(id) }))
2172
+ .filter(({ presence }) => presence !== SESSION_PRESENCE.ABSENT);
1570
2173
  if (matches.length > 1) {
1571
2174
  const error = new Error(`Ambiguous session id prefix: ${input}`);
1572
2175
  error.code = "AMBIGUOUS_SESSION_ID";
1573
- error.matches = matches.slice(0, 20);
2176
+ error.matches = matches.slice(0, 20).map(({ id }) => id);
2177
+ error.authorities = matches.slice(0, 20).map((item) => ({ ...item }));
1574
2178
  throw error;
1575
2179
  }
1576
- return matches[0] || null;
2180
+ if (matches.length === 0) return null;
2181
+ const [{ id, presence }] = matches;
2182
+ return Object.freeze({
2183
+ id,
2184
+ match: "prefix",
2185
+ presence,
2186
+ readable: presence === SESSION_PRESENCE.PRESENT,
2187
+ });
2188
+ }
2189
+
2190
+ /** Resolve an exact id or one unambiguous prefix without reading transcripts. */
2191
+ export function resolveSessionId(input) {
2192
+ const authority = resolveSessionAuthority(input);
2193
+ return authority?.readable ? authority.id : null;
1577
2194
  }
1578
2195
 
1579
2196
  export { TRANSCRIPT_CHAIN_STATUS };
@@ -1593,11 +2210,17 @@ export function startSession(sessionId, meta = {}) {
1593
2210
  }
1594
2211
 
1595
2212
  export function appendUserMessage(sessionId, content) {
1596
- appendEvent(sessionId, "user_message", { role: "user", content });
2213
+ return appendEvent(sessionId, "user_message", {
2214
+ role: "user",
2215
+ content,
2216
+ });
1597
2217
  }
1598
2218
 
1599
2219
  export function appendAssistantMessage(sessionId, content) {
1600
- appendEvent(sessionId, "assistant_message", { role: "assistant", content });
2220
+ return appendEvent(sessionId, "assistant_message", {
2221
+ role: "assistant",
2222
+ content,
2223
+ });
1601
2224
  }
1602
2225
 
1603
2226
  function wsTurnLifecycleResult(snapshot, overrides = {}) {
@@ -2019,25 +2642,82 @@ function readSessionPersistenceWitness(sessionId) {
2019
2642
 
2020
2643
  function hasLiveSessionWitness(sessionId) {
2021
2644
  const witness = readSessionPersistenceWitness(sessionId);
2022
- return witness !== null && witness?.deleted !== true;
2645
+ if (witness !== null && witness?.deleted !== true) return true;
2646
+ return readSessionAntiRollbackAnchor(sessionId)?.status === "live";
2023
2647
  }
2024
2648
 
2025
- function assertVerifiedTranscriptAnchor(sessionId, verification) {
2649
+ function assertLocalVerifiedTranscriptAnchor(
2650
+ sessionId,
2651
+ verification,
2652
+ physicalState = null,
2653
+ ) {
2026
2654
  const meta = readSessionMeta(getSessionsDir(), sessionId);
2655
+ let physicalAnchorMatches = true;
2656
+ let currentPhysicalState = physicalState;
2657
+ if (meta?.transcript) {
2658
+ try {
2659
+ currentPhysicalState ||= readPhysicalTranscriptState(
2660
+ sessionPath(sessionId),
2661
+ );
2662
+ physicalAnchorMatches = samePhysicalTranscriptState(
2663
+ meta.transcript,
2664
+ currentPhysicalState,
2665
+ );
2666
+ } catch {
2667
+ physicalAnchorMatches = false;
2668
+ }
2669
+ }
2027
2670
  const trustedStatus =
2028
2671
  verification.status === TRANSCRIPT_CHAIN_STATUS.VERIFIED;
2672
+ const headMatches = meta?.last_hash === verification.lastHash;
2673
+ const eventCountMatches =
2674
+ Number(meta?.event_count) === verification.chainedEvents;
2029
2675
  const anchoredHead =
2030
2676
  meta?.deleted !== true &&
2031
- meta?.last_hash === verification.lastHash &&
2032
- Number(meta?.event_count) === verification.chainedEvents;
2677
+ headMatches &&
2678
+ eventCountMatches &&
2679
+ physicalAnchorMatches;
2033
2680
  if (
2034
2681
  !trustedStatus ||
2035
2682
  !anchoredHead ||
2036
2683
  verification.malformedLines > 0 ||
2037
2684
  verification.truncatedTail
2038
2685
  ) {
2039
- throw unverifiedTranscriptError(sessionId, verification);
2686
+ const error = unverifiedTranscriptError(sessionId, verification);
2687
+ error.anchor = Object.freeze({
2688
+ metadataPresent: meta !== null,
2689
+ metadataDeleted: meta?.deleted === true,
2690
+ headMatches,
2691
+ eventCountMatches,
2692
+ physicalAnchorMatches,
2693
+ metadataHeadHash: meta?.last_hash ?? null,
2694
+ metadataEventCount: Number.isSafeInteger(Number(meta?.event_count))
2695
+ ? Number(meta.event_count)
2696
+ : null,
2697
+ metadataTranscript: meta?.transcript ?? null,
2698
+ currentTranscript: currentPhysicalState ?? null,
2699
+ });
2700
+ throw error;
2040
2701
  }
2702
+ return meta;
2703
+ }
2704
+
2705
+ function assertVerifiedTranscriptAnchor(
2706
+ sessionId,
2707
+ verification,
2708
+ physicalState = null,
2709
+ ) {
2710
+ const meta = assertLocalVerifiedTranscriptAnchor(
2711
+ sessionId,
2712
+ verification,
2713
+ physicalState,
2714
+ );
2715
+ publishSessionAntiRollbackWitness(
2716
+ sessionId,
2717
+ meta,
2718
+ sessionPath(sessionId),
2719
+ "live",
2720
+ );
2041
2721
  }
2042
2722
 
2043
2723
  /**
@@ -2052,8 +2732,8 @@ function assertVerifiedTranscriptAnchor(sessionId, verification) {
2052
2732
  *
2053
2733
  * This removes the mandatory all-event array from resume projections. It does
2054
2734
  * not make verification IO sublinear: authenticating a plain chained JSONL
2055
- * transcript remains O(N), and the local sidecar is not an anti-rollback
2056
- * anchor or a cross-process lease.
2735
+ * transcript remains O(N). The local sidecar is checked against an independent
2736
+ * machine-local anti-rollback witness; it is not itself that witness.
2057
2737
  */
2058
2738
  export function readVerifiedProjection(
2059
2739
  sessionId,
@@ -2087,13 +2767,11 @@ export function readVerifiedProjection(
2087
2767
  );
2088
2768
  }
2089
2769
  if (!existsSync(filePath)) {
2090
- const witness = readSessionPersistenceWitness(sessionId);
2091
- if (witness?.deleted === true) {
2092
- const error = new Error(`Session was deleted: ${sessionId}`);
2093
- error.code = "SESSION_DELETED";
2094
- throw error;
2770
+ const presence = getSessionPresence(sessionId);
2771
+ if (presence === SESSION_PRESENCE.TOMBSTONED) {
2772
+ throw sessionDeletedError(sessionId);
2095
2773
  }
2096
- if (witness !== null) {
2774
+ if (presence === SESSION_PRESENCE.MISSING_TRANSCRIPT) {
2097
2775
  throw unverifiedTranscriptError(
2098
2776
  sessionId,
2099
2777
  missingTranscriptVerification(),
@@ -2153,6 +2831,112 @@ export function readVerifiedEvents(sessionId) {
2153
2831
  });
2154
2832
  }
2155
2833
 
2834
+ /**
2835
+ * Return the exact JSONL bytes only after the transcript and its persisted
2836
+ * anchor verify under the canonical writer lock. This is the export/mirror
2837
+ * boundary; derived consumers must never copy a merely parseable raw path.
2838
+ */
2839
+ export function readVerifiedTranscriptBytes(sessionId) {
2840
+ if (isUnsafeSessionId(sessionId)) {
2841
+ throw unverifiedTranscriptError(
2842
+ sessionId,
2843
+ transcriptAnchorMismatchVerification(),
2844
+ );
2845
+ }
2846
+ const filePath = sessionPath(sessionId);
2847
+ return withFileLock(
2848
+ filePath,
2849
+ () => {
2850
+ const presence = getSessionPresence(sessionId);
2851
+ if (presence === SESSION_PRESENCE.MISSING_TRANSCRIPT) {
2852
+ throw unverifiedTranscriptError(
2853
+ sessionId,
2854
+ missingTranscriptVerification(),
2855
+ );
2856
+ }
2857
+ if (presence !== SESSION_PRESENCE.PRESENT) {
2858
+ throw unverifiedTranscriptError(
2859
+ sessionId,
2860
+ transcriptAnchorMismatchVerification(),
2861
+ );
2862
+ }
2863
+ return withTranscriptFileParent(
2864
+ filePath,
2865
+ ({ canonicalPath, parentDevice }) =>
2866
+ readVerifiedTranscriptBytesAtPath(
2867
+ sessionId,
2868
+ canonicalPath,
2869
+ parentDevice,
2870
+ ),
2871
+ );
2872
+ },
2873
+ {
2874
+ failIfUnavailable: true,
2875
+ timeoutMs: 30_000,
2876
+ retryMs: 1,
2877
+ maxRetryMs: 8,
2878
+ retryJitterMs: 4,
2879
+ yieldAfterReleaseMs: 2,
2880
+ },
2881
+ );
2882
+ }
2883
+
2884
+ function readVerifiedTranscriptBytesAtPath(sessionId, filePath, parentDevice) {
2885
+ let flags = fsConstants.O_RDONLY;
2886
+ if (typeof fsConstants.O_NOFOLLOW === "number") {
2887
+ flags |= fsConstants.O_NOFOLLOW;
2888
+ }
2889
+ const fd = openSync(filePath, flags);
2890
+ try {
2891
+ const beforeStats = fstatSync(fd, { bigint: true });
2892
+ const before = physicalTranscriptStateFromStats(beforeStats);
2893
+ const publishedBeforeStats = readPhysicalTranscriptStats(filePath);
2894
+ const publishedBefore =
2895
+ physicalTranscriptStateFromStats(publishedBeforeStats);
2896
+ const descriptorMatchesPublished =
2897
+ parentDevice === null
2898
+ ? samePhysicalTranscriptState(before, publishedBefore)
2899
+ : samePathHandleStableFileIdentity(
2900
+ publishedBeforeStats,
2901
+ beforeStats,
2902
+ parentDevice,
2903
+ ) && samePhysicalTranscriptContentState(before, publishedBefore);
2904
+ if (!descriptorMatchesPublished) {
2905
+ throw transcriptIdentityError(
2906
+ "not-committed",
2907
+ "the read descriptor does not match the canonical path",
2908
+ );
2909
+ }
2910
+ const text = readFileSync(fd, "utf8");
2911
+ const afterStats = fstatSync(fd, { bigint: true });
2912
+ const after = physicalTranscriptStateFromStats(afterStats);
2913
+ const publishedStats = readPhysicalTranscriptStats(filePath);
2914
+ const published = physicalTranscriptStateFromStats(publishedStats);
2915
+ const publishedStillMatches =
2916
+ parentDevice === null
2917
+ ? samePhysicalTranscriptState(after, published)
2918
+ : samePathHandleStableFileIdentity(
2919
+ publishedStats,
2920
+ afterStats,
2921
+ parentDevice,
2922
+ ) && samePhysicalTranscriptContentState(after, published);
2923
+ if (!samePhysicalTranscriptState(before, after) || !publishedStillMatches) {
2924
+ throw transcriptIdentityError(
2925
+ "unknown",
2926
+ "the transcript changed while verified bytes were read",
2927
+ );
2928
+ }
2929
+ const verification = {
2930
+ ...verifyTranscriptText(text),
2931
+ lastHash: latestChainHash(text),
2932
+ };
2933
+ assertVerifiedTranscriptAnchor(sessionId, verification, published);
2934
+ return text;
2935
+ } finally {
2936
+ closeSync(fd);
2937
+ }
2938
+ }
2939
+
2156
2940
  /**
2157
2941
  * Read the active context only after transcript and sidecar-anchor validation.
2158
2942
  * Unlike the compatibility `rebuildMessages()` reader, this is allowed to
@@ -2208,30 +2992,29 @@ export function readVerifiedWsTurnState(sessionId, requestId, options = {}) {
2208
2992
  }
2209
2993
 
2210
2994
  /**
2211
- * Return the newest event matching a type/predicate using bounded reverse IO.
2212
- * This is the recovery path for snapshots and ledgers: callers never need to
2213
- * materialize a multi-gigabyte transcript merely to inspect its latest state.
2995
+ * Return the newest event matching a type/predicate after authenticating the
2996
+ * complete transcript and sidecar anchor. Heap remains bounded to one event;
2997
+ * the forward integrity pass is intentionally O(N).
2214
2998
  */
2215
2999
  export function findLatestEvent(sessionId, type, predicate = null) {
2216
3000
  if (isUnsafeSessionId(sessionId)) return null;
2217
- const filePath = sessionPath(sessionId);
2218
- if (!existsSync(filePath)) return null;
2219
3001
  const wanted = Array.isArray(type) ? new Set(type) : null;
2220
- for (const { line } of iterateFileLinesReverseSync(filePath)) {
2221
- let event;
2222
- try {
2223
- event = JSON.parse(line);
2224
- } catch {
2225
- continue;
2226
- }
2227
- const typeMatches = wanted
2228
- ? wanted.has(event?.type)
2229
- : type == null || event?.type === type;
2230
- if (!typeMatches) continue;
2231
- if (typeof predicate === "function" && !predicate(event)) continue;
2232
- return event;
2233
- }
2234
- return null;
3002
+ return readVerifiedProjection(sessionId, () => {
3003
+ let latest = null;
3004
+ return {
3005
+ accept(event) {
3006
+ const typeMatches = wanted
3007
+ ? wanted.has(event?.type)
3008
+ : type == null || event?.type === type;
3009
+ if (!typeMatches) return;
3010
+ if (typeof predicate === "function" && !predicate(event)) return;
3011
+ latest = event;
3012
+ },
3013
+ finish() {
3014
+ return latest;
3015
+ },
3016
+ };
3017
+ });
2235
3018
  }
2236
3019
 
2237
3020
  /**
@@ -2343,6 +3126,7 @@ function rebuildSessionMetaUnlocked(dir, sessionId, filePath) {
2343
3126
  // a best-effort projection over all intact events.
2344
3127
  }
2345
3128
  }
3129
+ meta.transcript = readPhysicalTranscriptState(filePath);
2346
3130
  return replaceSessionMeta(dir, meta);
2347
3131
  }
2348
3132
 
@@ -2352,7 +3136,30 @@ function rebuildSessionMeta(sessionId) {
2352
3136
  if (!existsSync(filePath)) return null;
2353
3137
  return withFileLock(
2354
3138
  filePath,
2355
- () => rebuildSessionMetaUnlocked(dir, sessionId, filePath),
3139
+ () => {
3140
+ const verification = verifyTranscriptFile(filePath);
3141
+ const externalAnchor = readSessionAntiRollbackAnchor(sessionId);
3142
+ if (
3143
+ externalAnchor === null &&
3144
+ [
3145
+ TRANSCRIPT_CHAIN_STATUS.LEGACY,
3146
+ TRANSCRIPT_CHAIN_STATUS.PARTIAL,
3147
+ ].includes(verification.status) &&
3148
+ !verification.truncatedTail
3149
+ ) {
3150
+ return rebuildSessionMetaUnlocked(dir, sessionId, filePath);
3151
+ }
3152
+ if (
3153
+ verification.status !== TRANSCRIPT_CHAIN_STATUS.VERIFIED ||
3154
+ verification.malformedLines > 0 ||
3155
+ verification.truncatedTail
3156
+ ) {
3157
+ throw unverifiedTranscriptError(sessionId, verification);
3158
+ }
3159
+ const meta = rebuildSessionMetaUnlocked(dir, sessionId, filePath);
3160
+ assertVerifiedTranscriptAnchor(sessionId, verification);
3161
+ return meta;
3162
+ },
2356
3163
  {
2357
3164
  failIfUnavailable: true,
2358
3165
  timeoutMs: 30_000,
@@ -2425,6 +3232,58 @@ export function listJsonlSessions(options = {}) {
2425
3232
  return indexed;
2426
3233
  }
2427
3234
 
3235
+ /**
3236
+ * List canonical namespace occupants for recent-session and picker consumers.
3237
+ * Damaged generations are deliberately represented as blocked rows so a
3238
+ * newer missing/conflicting canonical session cannot disappear and cause an
3239
+ * older legacy conversation to be resumed instead. Tombstones fence duplicate
3240
+ * legacy ids but are not resumable choices unless explicitly requested.
3241
+ */
3242
+ export function listSessionAuthoritySummaries(options = {}) {
3243
+ const limit = Math.max(1, Number(options.limit) || 20);
3244
+ const evidenceIds = listSessionEvidenceIds();
3245
+ const readable = new Map(
3246
+ listJsonlSessions({ limit: Math.max(limit, evidenceIds.length) }).map(
3247
+ (item) => [item.id, item],
3248
+ ),
3249
+ );
3250
+ const rows = [];
3251
+ for (const id of evidenceIds) {
3252
+ const presence = getSessionPresence(id);
3253
+ if (
3254
+ presence === SESSION_PRESENCE.ABSENT ||
3255
+ (presence === SESSION_PRESENCE.TOMBSTONED &&
3256
+ options.includeTombstoned !== true)
3257
+ ) {
3258
+ continue;
3259
+ }
3260
+ const item = readable.get(id);
3261
+ const meta = item ? null : readSessionMeta(getSessionsDir(), id);
3262
+ rows.push({
3263
+ ...(item ||
3264
+ publicSessionMeta({
3265
+ ...emptySessionMeta(id),
3266
+ ...(meta || {}),
3267
+ id,
3268
+ })),
3269
+ id,
3270
+ _store: "jsonl",
3271
+ _presence: presence,
3272
+ _blocked: presence !== SESSION_PRESENCE.PRESENT,
3273
+ });
3274
+ }
3275
+ return rows
3276
+ .sort((a, b) => {
3277
+ const delta =
3278
+ (Date.parse(b.updated_at || "") || 0) -
3279
+ (Date.parse(a.updated_at || "") || 0);
3280
+ if (delta !== 0) return delta;
3281
+ if (a._blocked !== b._blocked) return a._blocked ? -1 : 1;
3282
+ return String(b.id).localeCompare(String(a.id));
3283
+ })
3284
+ .slice(0, limit);
3285
+ }
3286
+
2428
3287
  /**
2429
3288
  * Rename a session (gap-analysis 2026-07-11 P1 "命名会话"): appends a
2430
3289
  * `session_rename` event so the hash chain stays intact (no rewrite). The
@@ -2447,25 +3306,83 @@ export function deleteJsonlSession(sessionId) {
2447
3306
  if (isUnsafeSessionId(sessionId)) return false;
2448
3307
  const filePath = sessionPath(sessionId);
2449
3308
  if (!existsSync(filePath) && !hasLiveSessionWitness(sessionId)) return false;
2450
- return withFileLock(
3309
+ return withSessionHostWriterLock(
3310
+ sessionId,
2451
3311
  filePath,
2452
3312
  () => {
2453
3313
  const transcriptExists = existsSync(filePath);
2454
- const existingMeta = readSessionPersistenceWitness(sessionId);
3314
+ let existingMeta = readSessionPersistenceWitness(sessionId);
3315
+ const marker = readSessionTombstoneMarker(getSessionsDir(), sessionId);
3316
+ const externalAnchor = readSessionAntiRollbackAnchor(sessionId);
2455
3317
  if (
2456
3318
  !transcriptExists &&
2457
3319
  (existingMeta === null || existingMeta?.deleted === true)
2458
3320
  ) {
2459
3321
  return false;
2460
3322
  }
3323
+ const deletionWitness =
3324
+ existingMeta?.deleted === true
3325
+ ? existingMeta
3326
+ : sessionAntiRollbackPredecessorWitness(externalAnchor) || marker;
3327
+ if (
3328
+ transcriptExists &&
3329
+ existingMeta === null &&
3330
+ deletionWitness === null
3331
+ ) {
3332
+ const verification = verifyTranscriptFile(filePath);
3333
+ if (
3334
+ verification.status !== TRANSCRIPT_CHAIN_STATUS.VERIFIED ||
3335
+ verification.malformedLines > 0 ||
3336
+ verification.truncatedTail
3337
+ ) {
3338
+ throw unverifiedTranscriptError(sessionId, verification);
3339
+ }
3340
+ rebuildSessionMetaUnlocked(getSessionsDir(), sessionId, filePath);
3341
+ existingMeta = readSessionPersistenceWitness(sessionId);
3342
+ }
3343
+ if (deletionWitness === null) {
3344
+ assertSessionAntiRollbackBeforeWrite(
3345
+ sessionId,
3346
+ existingMeta,
3347
+ filePath,
3348
+ "live",
3349
+ );
3350
+ } else if (existingMeta?.deleted !== true) {
3351
+ existingMeta = replaceSessionMeta(getSessionsDir(), {
3352
+ ...emptySessionMeta(sessionId),
3353
+ id: sessionId,
3354
+ generation: deletionWitness.generation || null,
3355
+ last_hash: deletionWitness.last_hash || null,
3356
+ event_count: Math.max(0, Number(deletionWitness.event_count) || 0),
3357
+ updated_at_ms: Math.max(
3358
+ 0,
3359
+ Number(deletionWitness.deleted_at_ms) || Date.now(),
3360
+ ),
3361
+ deleted_at_ms: Math.max(
3362
+ 0,
3363
+ Number(deletionWitness.deleted_at_ms) || Date.now(),
3364
+ ),
3365
+ deleted: true,
3366
+ transcript: null,
3367
+ });
3368
+ }
2461
3369
  if (transcriptExists) rmSync(filePath, { force: true });
2462
3370
  try {
2463
- recordSessionDeleted(getSessionsDir(), sessionId);
3371
+ const tombstone = recordSessionDeleted(getSessionsDir(), sessionId);
3372
+ publishSessionAntiRollbackWitness(
3373
+ sessionId,
3374
+ tombstone,
3375
+ filePath,
3376
+ "deleted",
3377
+ );
2464
3378
  } catch (error) {
2465
3379
  // writeMetaAtomic precedes the activity-journal append. If only the
2466
3380
  // rebuildable journal lock/release failed, the durable sidecar still
2467
3381
  // prevents a stale writer from resurrecting this deleted session.
2468
- if (readSessionMeta(getSessionsDir(), sessionId)?.deleted !== true) {
3382
+ if (
3383
+ readSessionMeta(getSessionsDir(), sessionId)?.deleted !== true ||
3384
+ readSessionAntiRollbackAnchor(sessionId)?.status !== "deleted"
3385
+ ) {
2469
3386
  throw error;
2470
3387
  }
2471
3388
  }
@@ -2768,12 +3685,22 @@ export function forkSession(sourceId, options = {}) {
2768
3685
  sourceHeadHash: sourceVerification.lastHash,
2769
3686
  sourceEventCount: sourceVerification.chainedEvents,
2770
3687
  };
3688
+ const forkGenerationId = `generation-${createHash("sha256")
3689
+ .update(`fork-generation\0${newId}`, "utf8")
3690
+ .digest("hex")
3691
+ .slice(0, 32)}`;
2771
3692
  const core = {
2772
3693
  type: "system",
2773
3694
  timestamp: Date.now(),
2774
3695
  data: {
2775
3696
  ...persistedData,
2776
3697
  [SESSION_FORK_AUTHORITY_FIELD]: forkAuthority,
3698
+ [SESSION_GENERATION_AUTHORITY_FIELD]:
3699
+ createSessionGenerationAuthority(
3700
+ newId,
3701
+ null,
3702
+ forkGenerationId,
3703
+ ),
2777
3704
  },
2778
3705
  };
2779
3706
  const hash = computeEventHash(prevHash, core);
@@ -2803,6 +3730,17 @@ export function forkSession(sourceId, options = {}) {
2803
3730
  sourceHeadHash: authority?.sourceHeadHash,
2804
3731
  sourceEventCount: authorityEventCount,
2805
3732
  };
3733
+ const generationAuthority = normalizeSessionGenerationAuthority(
3734
+ target.lineageEvent?.data?.[SESSION_GENERATION_AUTHORITY_FIELD],
3735
+ );
3736
+ const hasGenerationAuthority =
3737
+ target.lineageEvent?.data?.[SESSION_GENERATION_AUTHORITY_FIELD] !==
3738
+ undefined;
3739
+ const generationMatches =
3740
+ !hasGenerationAuthority ||
3741
+ (generationAuthority?.sessionId === newId &&
3742
+ generationAuthority.ordinal === 1 &&
3743
+ generationAuthority.predecessor === null);
2806
3744
  const lineageMatches =
2807
3745
  target.matchingLineageEvents === 1 &&
2808
3746
  target.lineageEvent?.type === "system" &&
@@ -2811,10 +3749,16 @@ export function forkSession(sourceId, options = {}) {
2811
3749
  typeof authority?.sourceHeadHash === "string" &&
2812
3750
  target.lineageEventIndex === creationEventCount &&
2813
3751
  target.lineageEvent?.prevHash === authority.sourceHeadHash &&
3752
+ generationMatches &&
2814
3753
  JSON.stringify(target.lineageEvent?.data) ===
2815
3754
  JSON.stringify({
2816
3755
  ...persistedData,
2817
3756
  [SESSION_FORK_AUTHORITY_FIELD]: expectedAuthority,
3757
+ ...(hasGenerationAuthority
3758
+ ? {
3759
+ [SESSION_GENERATION_AUTHORITY_FIELD]: generationAuthority,
3760
+ }
3761
+ : {}),
2818
3762
  });
2819
3763
  if (
2820
3764
  target.verification.status !== TRANSCRIPT_CHAIN_STATUS.VERIFIED ||
@@ -2976,7 +3920,7 @@ export function createBranchSession({
2976
3920
  provider: typeof cleanMeta.provider === "string" ? cleanMeta.provider : "",
2977
3921
  model: typeof cleanMeta.model === "string" ? cleanMeta.model : "",
2978
3922
  };
2979
- const plannedEvents = [
3923
+ const digestEvents = [
2980
3924
  { type: "session_start", data: startData },
2981
3925
  {
2982
3926
  type: "session_branch",
@@ -2989,19 +3933,19 @@ export function createBranchSession({
2989
3933
  let count = 0;
2990
3934
  for (const m of canonicalMessages) {
2991
3935
  if (m.role === "user") {
2992
- plannedEvents.push({
3936
+ digestEvents.push({
2993
3937
  type: "user_message",
2994
3938
  data: { role: "user", content: m.content },
2995
3939
  });
2996
3940
  count += 1;
2997
3941
  } else if (m.role === "assistant") {
2998
- plannedEvents.push({
3942
+ digestEvents.push({
2999
3943
  type: "assistant_message",
3000
3944
  data: { role: "assistant", content: m.content },
3001
3945
  });
3002
3946
  count += 1;
3003
3947
  } else if (m.role === "system") {
3004
- plannedEvents.push({
3948
+ digestEvents.push({
3005
3949
  type: "system",
3006
3950
  data: encodePersistedMessage(m),
3007
3951
  });
@@ -3011,8 +3955,27 @@ export function createBranchSession({
3011
3955
  // canonical projection above retains only explicitly durable systems.
3012
3956
  }
3013
3957
  const inputDigest = `sha256:${createHash("sha256")
3014
- .update(JSON.stringify(plannedEvents), "utf8")
3958
+ .update(JSON.stringify(digestEvents), "utf8")
3015
3959
  .digest("hex")}`;
3960
+ const branchGenerationId = `generation-${createHash("sha256")
3961
+ .update(`branch-generation\0${branchSessionId}`, "utf8")
3962
+ .digest("hex")
3963
+ .slice(0, 32)}`;
3964
+ const plannedEvents = digestEvents.map((event, index) =>
3965
+ index === 0
3966
+ ? {
3967
+ ...event,
3968
+ data: encodeSessionGenerationData(
3969
+ event.data,
3970
+ createSessionGenerationAuthority(
3971
+ branchSessionId,
3972
+ null,
3973
+ branchGenerationId,
3974
+ ),
3975
+ ),
3976
+ }
3977
+ : event,
3978
+ );
3016
3979
  plannedEvents.push({
3017
3980
  type: "session_branch_complete",
3018
3981
  data: {
@@ -3027,6 +3990,23 @@ export function createBranchSession({
3027
3990
  return withFileLock(
3028
3991
  filePath,
3029
3992
  () => {
3993
+ const presence = getSessionPresence(branchSessionId);
3994
+ if (
3995
+ presence === SESSION_PRESENCE.TOMBSTONED ||
3996
+ (presence === SESSION_PRESENCE.CONFLICT &&
3997
+ readSessionTombstoneMarker(sessionsDir, branchSessionId) !== null)
3998
+ ) {
3999
+ throw sessionDeletedError(branchSessionId);
4000
+ }
4001
+ if (
4002
+ presence === SESSION_PRESENCE.MISSING_TRANSCRIPT ||
4003
+ presence === SESSION_PRESENCE.CONFLICT
4004
+ ) {
4005
+ throw unverifiedTranscriptError(
4006
+ branchSessionId,
4007
+ transcriptAnchorMismatchVerification(),
4008
+ );
4009
+ }
3030
4010
  if (existsSync(filePath)) inspectPhysicalTail(filePath);
3031
4011
  let verification = existsSync(filePath)
3032
4012
  ? verifyTranscriptFile(filePath)
@@ -3048,12 +4028,22 @@ export function createBranchSession({
3048
4028
  : [];
3049
4029
  const prefixMatches = existingEvents
3050
4030
  .slice(0, Math.min(existingEvents.length, plannedEvents.length))
3051
- .every(
3052
- (event, index) =>
4031
+ .every((event, index) => {
4032
+ let plannedData = plannedEvents[index].data;
4033
+ // Existing v1 branches predate generation authority. They remain
4034
+ // idempotently readable; only newly created branches publish it.
4035
+ if (
4036
+ index === 0 &&
4037
+ event?.data?.[SESSION_GENERATION_AUTHORITY_FIELD] === undefined
4038
+ ) {
4039
+ plannedData = { ...plannedData };
4040
+ delete plannedData[SESSION_GENERATION_AUTHORITY_FIELD];
4041
+ }
4042
+ return (
3053
4043
  event.type === plannedEvents[index].type &&
3054
- JSON.stringify(event.data) ===
3055
- JSON.stringify(plannedEvents[index].data),
3056
- );
4044
+ JSON.stringify(event.data) === JSON.stringify(plannedData)
4045
+ );
4046
+ });
3057
4047
  if (!prefixMatches) {
3058
4048
  const error = new Error(
3059
4049
  `Branch session conflicts with its deterministic input: ${branchSessionId}`,
@@ -3108,6 +4098,9 @@ export function createBranchSession({
3108
4098
  }
3109
4099
 
3110
4100
  let prevHash = verification.lastHash;
4101
+ let expectedTranscriptState = existsSync(filePath)
4102
+ ? readPhysicalTranscriptState(filePath)
4103
+ : null;
3111
4104
  for (
3112
4105
  let index = existingEvents.length;
3113
4106
  index < plannedEvents.length;
@@ -3121,17 +4114,13 @@ export function createBranchSession({
3121
4114
  };
3122
4115
  const hash = computeEventHash(prevHash, core);
3123
4116
  const event = { ...core, prevHash, hash };
3124
- appendFileSync(filePath, `${JSON.stringify(event)}\n`, {
3125
- encoding: "utf8",
3126
- mode: 0o600,
3127
- });
3128
- ensurePrivateFile(filePath);
3129
- runSessionScaleFaultHook("afterTranscriptAppend", {
3130
- sessionId: branchSessionId,
3131
- type: planned.type,
4117
+ expectedTranscriptState = appendTranscriptEvent(
4118
+ branchSessionId,
4119
+ planned.type,
3132
4120
  event,
3133
4121
  filePath,
3134
- });
4122
+ expectedTranscriptState,
4123
+ );
3135
4124
  prevHash = hash;
3136
4125
  }
3137
4126
 
@@ -3170,14 +4159,42 @@ export const SESSION_PRESENCE = Object.freeze({
3170
4159
 
3171
4160
  export function getSessionPresence(sessionId) {
3172
4161
  if (isUnsafeSessionId(sessionId)) return SESSION_PRESENCE.ABSENT;
4162
+ const dir = getSessionsDir();
3173
4163
  const transcriptExists = existsSync(sessionPath(sessionId));
4164
+ const tombstoneMarkerExists = existsSync(
4165
+ join(dir, `${sessionId}${SESSION_TOMBSTONE_MARKER_SUFFIX}`),
4166
+ );
3174
4167
  const witness = readSessionPersistenceWitness(sessionId);
3175
- if (transcriptExists && witness?.deleted === true) {
4168
+ const externalAnchor = readSessionAntiRollbackAnchor(sessionId);
4169
+ if (
4170
+ transcriptExists &&
4171
+ (witness?.deleted === true ||
4172
+ tombstoneMarkerExists ||
4173
+ externalAnchor?.status === "deleted")
4174
+ ) {
3176
4175
  return SESSION_PRESENCE.CONFLICT;
3177
4176
  }
3178
- if (transcriptExists) return SESSION_PRESENCE.PRESENT;
3179
- if (witness?.deleted === true) return SESSION_PRESENCE.TOMBSTONED;
3180
- if (witness !== null) return SESSION_PRESENCE.MISSING_TRANSCRIPT;
4177
+ if (transcriptExists) {
4178
+ if (witness !== null && witness?.deleted !== true) {
4179
+ assertSessionAntiRollbackBeforeWrite(
4180
+ sessionId,
4181
+ witness,
4182
+ sessionPath(sessionId),
4183
+ "live",
4184
+ );
4185
+ }
4186
+ return SESSION_PRESENCE.PRESENT;
4187
+ }
4188
+ if (
4189
+ witness?.deleted === true ||
4190
+ tombstoneMarkerExists ||
4191
+ externalAnchor?.status === "deleted"
4192
+ ) {
4193
+ return SESSION_PRESENCE.TOMBSTONED;
4194
+ }
4195
+ if (witness !== null || externalAnchor?.status === "live") {
4196
+ return SESSION_PRESENCE.MISSING_TRANSCRIPT;
4197
+ }
3181
4198
  return SESSION_PRESENCE.ABSENT;
3182
4199
  }
3183
4200