chainlesschain 0.166.115 → 0.166.118

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 (207) hide show
  1. package/README.md +43 -5
  2. package/package.json +4 -4
  3. package/src/assets/linux-subreaper/linux-arm64/manifest.json +1 -1
  4. package/src/assets/linux-subreaper/linux-x64/manifest.json +1 -1
  5. package/src/assets/web-panel/.build-hash +1 -1
  6. package/src/assets/web-panel/assets/{AIOps-BsoSoDNb.js → AIOps-BGySwMIP.js} +1 -1
  7. package/src/assets/web-panel/assets/{ActionButton-Dcx7Gm5t.js → ActionButton-Chn7fBLr.js} +1 -1
  8. package/src/assets/web-panel/assets/{Analytics-S1hsBIz3.js → Analytics-ORH7UkGa.js} +3 -3
  9. package/src/assets/web-panel/assets/{AppLayout-B2j067qM.js → AppLayout-8xV4jWjP.js} +5 -5
  10. package/src/assets/web-panel/assets/{Artifacts-D5iGV9ic.js → Artifacts-26SBK-x-.js} +1 -1
  11. package/src/assets/web-panel/assets/{Audit-C8ChVvXu.js → Audit-CtXpqbXg.js} +1 -1
  12. package/src/assets/web-panel/assets/{BackgroundAgents-Cg97AAJS.js → BackgroundAgents-C5zLvQis.js} +1 -1
  13. package/src/assets/web-panel/assets/{Backup-CyxU9IUg.js → Backup-C93xY4vO.js} +1 -1
  14. package/src/assets/web-panel/assets/{BaseInput-CKlWjA8I.js → BaseInput-DpVCHfAn.js} +1 -1
  15. package/src/assets/web-panel/assets/{Chat-C-EqZza6.js → Chat-DM0LblR2.js} +5 -5
  16. package/src/assets/web-panel/assets/{ChatBubbleRenderer-BRFiXKPX.js → ChatBubbleRenderer-unlQUF1T.js} +1 -1
  17. package/src/assets/web-panel/assets/{Checkbox-kkI13tSJ.js → Checkbox-BsAt3cKR.js} +1 -1
  18. package/src/assets/web-panel/assets/{Codegen-NlqEDwHK.js → Codegen-E_kp4KFd.js} +1 -1
  19. package/src/assets/web-panel/assets/{Col-CfepEu79.js → Col-6msR3T6I.js} +1 -1
  20. package/src/assets/web-panel/assets/{Community-BRo0qLXM.js → Community-PyX9c5D-.js} +1 -1
  21. package/src/assets/web-panel/assets/{Compact-BMu8TP0E.js → Compact-7G1bVDSH.js} +1 -1
  22. package/src/assets/web-panel/assets/{Compliance-B7Q3Moef.js → Compliance-DcHN7GlD.js} +1 -1
  23. package/src/assets/web-panel/assets/{Cowork-BVwt7Xmc.js → Cowork-WUIHJYDx.js} +3 -3
  24. package/src/assets/web-panel/assets/{Cron-CgBv20Qy.js → Cron-CWgCbptg.js} +2 -2
  25. package/src/assets/web-panel/assets/{Crosschain-CwcSBJk2.js → Crosschain-BzKZ6roD.js} +1 -1
  26. package/src/assets/web-panel/assets/{DID-d20rCiIm.js → DID-r7zxeItn.js} +2 -2
  27. package/src/assets/web-panel/assets/{Dashboard-PpZaUYpS.js → Dashboard-D3pBiN2H.js} +2 -2
  28. package/src/assets/web-panel/assets/{Dropdown-DjsLdX_Z.js → Dropdown-C4otRJlZ.js} +1 -1
  29. package/src/assets/web-panel/assets/{EmailListRenderer-CaPkrtxn.js → EmailListRenderer-CzDTC8lm.js} +1 -1
  30. package/src/assets/web-panel/assets/{EvolutionSettings-DDY8cQnE.js → EvolutionSettings-DGkXiIs-.js} +1 -1
  31. package/src/assets/web-panel/assets/{FamilyGuardDashboard-VQavMjyj.js → FamilyGuardDashboard-BKdEyuiR.js} +1 -1
  32. package/src/assets/web-panel/assets/{Federation-D0GvKQgI.js → Federation-dsI-xcpf.js} +1 -1
  33. package/src/assets/web-panel/assets/{FormItemContext-Dl_TWHmb.js → FormItemContext-CEEb1fMR.js} +1 -1
  34. package/src/assets/web-panel/assets/{GenericCardRenderer-Bny2uagG.js → GenericCardRenderer-D_mfO3Ts.js} +1 -1
  35. package/src/assets/web-panel/assets/{Git-B01KXq47.js → Git-CBwr2ZI3.js} +2 -2
  36. package/src/assets/web-panel/assets/{Governance-F4c4Vwo1.js → Governance-D-Wy2Jjr.js} +1 -1
  37. package/src/assets/web-panel/assets/{Inference-_025Q-8K.js → Inference-B4Yk_IRx.js} +1 -1
  38. package/src/assets/web-panel/assets/{KnowledgeGraph-DwbTmQb1.js → KnowledgeGraph-DoI7bi6m.js} +1 -1
  39. package/src/assets/web-panel/assets/{Logs-CnNo0hCm.js → Logs-Ce1IiKAj.js} +2 -2
  40. package/src/assets/web-panel/assets/MarkdownRenderer-GND_8y0V.js +1 -0
  41. package/src/assets/web-panel/assets/{Marketplace-Dkz5bv7F.js → Marketplace-CsIEByzK.js} +1 -1
  42. package/src/assets/web-panel/assets/{McpTools-CBtRwDW_.js → McpTools-_DRzjcIn.js} +3 -3
  43. package/src/assets/web-panel/assets/{Memory-Y9jnyTb7.js → Memory-zvLpyi__.js} +2 -2
  44. package/src/assets/web-panel/assets/{MobileBridge-Br1urkOz.js → MobileBridge-DXd1ik2Z.js} +1 -1
  45. package/src/assets/web-panel/assets/{MobileProjects-DnsyuerX.js → MobileProjects-DKh2q-Y9.js} +1 -1
  46. package/src/assets/web-panel/assets/{Mtc-he3ZzieF.js → Mtc-Ct-fhtIx.js} +4 -4
  47. package/src/assets/web-panel/assets/{MtcAudit-BadphAuN.js → MtcAudit-J4zY3Oqk.js} +2 -2
  48. package/src/assets/web-panel/assets/{Multisig-3_pVLymK.js → Multisig-B2HJMCKe.js} +3 -3
  49. package/src/assets/web-panel/assets/{NLProgramming-DmtmHfsI.js → NLProgramming-CwcSmU4q.js} +1 -1
  50. package/src/assets/web-panel/assets/{Notes-CE4L2O55.js → Notes-Da53L6wb.js} +4 -4
  51. package/src/assets/web-panel/assets/{NotificationSettings-B7QmfJn9.js → NotificationSettings-BxhufdrA.js} +1 -1
  52. package/src/assets/web-panel/assets/{OrderTableRenderer-O0wRSyJ0.js → OrderTableRenderer-BoFwohAi.js} +1 -1
  53. package/src/assets/web-panel/assets/{Organization-BrlMSsyT.js → Organization-MIsMrUes.js} +4 -4
  54. package/src/assets/web-panel/assets/{Overflow-CY4ULPrZ.js → Overflow-Dy2P4v3q.js} +1 -1
  55. package/src/assets/web-panel/assets/{P2P-kIANZYKd.js → P2P-CD9Sxn2y.js} +2 -2
  56. package/src/assets/web-panel/assets/{PdhVaultBrowser-DEgB4rvF.js → PdhVaultBrowser-BvBWgfG0.js} +5 -5
  57. package/src/assets/web-panel/assets/{Permissions-CWxywRp5.js → Permissions-CwOlDRis.js} +4 -4
  58. package/src/assets/web-panel/assets/{PersonalDataHub-DAg7iCbr.js → PersonalDataHub-CZYaxEpv.js} +4 -4
  59. package/src/assets/web-panel/assets/{Pipeline-Brc6ONnR.js → Pipeline-kLU7PUbB.js} +1 -1
  60. package/src/assets/web-panel/assets/{Privacy-CcinYTL8.js → Privacy-BVRycSiI.js} +1 -1
  61. package/src/assets/web-panel/assets/{ProjectInit-Bz8XEsom.js → ProjectInit-DP3pr7uJ.js} +2 -2
  62. package/src/assets/web-panel/assets/{ProjectSettings-CsOMlSTZ.js → ProjectSettings-CACaDYjs.js} +2 -2
  63. package/src/assets/web-panel/assets/Projects-CB1k4zTC.js +1 -0
  64. package/src/assets/web-panel/assets/{Providers-CeunOQiH.js → Providers-D7Y9WHRE.js} +1 -1
  65. package/src/assets/web-panel/assets/{QrScannerModal-CN2a2_Fb.js → QrScannerModal-CeP8xzs1.js} +1 -1
  66. package/src/assets/web-panel/assets/{QuickAsk-CVeLif3e.js → QuickAsk-iJVFrhZ2.js} +1 -1
  67. package/src/assets/web-panel/assets/{Recommend-1k9WOEGM.js → Recommend-DmTpnagw.js} +1 -1
  68. package/src/assets/web-panel/assets/{RemoteSession-Q8UCqIMw.js → RemoteSession-C6ek8Uni.js} +2 -2
  69. package/src/assets/web-panel/assets/{Reputation-CkrCgXKM.js → Reputation-CPfwc9de.js} +1 -1
  70. package/src/assets/web-panel/assets/{Row-JFdvuTzi.js → Row-C1wwtSMJ.js} +1 -1
  71. package/src/assets/web-panel/assets/{RssFeed-C74JHCmk.js → RssFeed-BiDJsqaH.js} +3 -3
  72. package/src/assets/web-panel/assets/{Search-DrJiVO1T.js → Search-BF6Aon95.js} +1 -1
  73. package/src/assets/web-panel/assets/{Security-BJzNlSJq.js → Security-Bps0XnV4.js} +3 -3
  74. package/src/assets/web-panel/assets/{Services-D4TLMnJm.js → Services-uf92smGR.js} +2 -2
  75. package/src/assets/web-panel/assets/{Skeleton-cswTDqhh.js → Skeleton-DQd4eVWW.js} +1 -1
  76. package/src/assets/web-panel/assets/{Skills-Bv0rM1BS.js → Skills-DgamiYa5.js} +1 -1
  77. package/src/assets/web-panel/assets/{Sla-D9rE7hNh.js → Sla-CF1IVIrm.js} +1 -1
  78. package/src/assets/web-panel/assets/SpeechSettings-Bzxmjbpc.js +1 -0
  79. package/src/assets/web-panel/assets/{SyncSettings-BwnMG8kq.js → SyncSettings-B8zKauiq.js} +2 -2
  80. package/src/assets/web-panel/assets/{Tasks-wJvU24cB.js → Tasks-DPKV_xSt.js} +1 -1
  81. package/src/assets/web-panel/assets/{Templates-CZbRU3tN.js → Templates-BikeSZIF.js} +1 -1
  82. package/src/assets/web-panel/assets/{Tenant-_jbpFJPZ.js → Tenant-CEaUaDkJ.js} +1 -1
  83. package/src/assets/web-panel/assets/{Terminal-BzNm5WKZ.js → Terminal-ZPgOSQ_q.js} +3 -3
  84. package/src/assets/web-panel/assets/{TimelineRenderer-bmybWjuS.js → TimelineRenderer-DmyB4Udk.js} +1 -1
  85. package/src/assets/web-panel/assets/{Tokens-o1WDcqVQ.js → Tokens-g85Pm5TO.js} +1 -1
  86. package/src/assets/web-panel/assets/{Trigger-BB1D6sIB.js → Trigger-D202hx9a.js} +1 -1
  87. package/src/assets/web-panel/assets/{Trust-CLxdCfy2.js → Trust-DzvQYVFy.js} +1 -1
  88. package/src/assets/web-panel/assets/{UkeySign-B9LS6vS0.js → UkeySign-KRzwddVg.js} +1 -1
  89. package/src/assets/web-panel/assets/{VideoEditing-Bg0-wxjz.js → VideoEditing-IRBXLO2z.js} +1 -1
  90. package/src/assets/web-panel/assets/{Wallet-Bw0G2F6P.js → Wallet-DSf5KPgQ.js} +4 -4
  91. package/src/assets/web-panel/assets/{WebAuthn-irEiNY9b.js → WebAuthn-DNPxLTCc.js} +5 -5
  92. package/src/assets/web-panel/assets/{WorkflowEditor-Bh08FbJs.js → WorkflowEditor-C3wAtVcK.js} +1 -1
  93. package/src/assets/web-panel/assets/{addon-fit-CK6X9sAG.js → addon-fit-CbkjSUJl.js} +1 -1
  94. package/src/assets/web-panel/assets/{chat-De-FPjVT.js → chat-q3XXUtv2.js} +1 -1
  95. package/src/assets/web-panel/assets/{colors-ygdyvinU.js → colors-CF54SNei.js} +1 -1
  96. package/src/assets/web-panel/assets/{compact-item-BQ9jH9KV.js → compact-item-BaRkvZuT.js} +1 -1
  97. package/src/assets/web-panel/assets/{createContext-BFuVSyox.js → createContext-CDR9hsfs.js} +1 -1
  98. package/src/assets/web-panel/assets/devWarning-1LO-NplX.js +1 -0
  99. package/src/assets/web-panel/assets/{hasIn-i19GtR1P.js → hasIn-DlC--S8R.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DS1IwwGL.js → index-B0Ptc7yu.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-B1IsP_JO.js → index-B0V9rgdu.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-CWfgeaG4.js → index-BEiUlFKb.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DTY20pWu.js → index-BJO0n1LQ.js} +1 -1
  104. package/src/assets/web-panel/assets/index-BLaJsev4.js +1 -0
  105. package/src/assets/web-panel/assets/index-BLmviChG.js +1 -0
  106. package/src/assets/web-panel/assets/{index-IXkif6J_.js → index-BU5DJqk7.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BVhtukYX.js → index-BY-Tf3OM.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-BKlU5VZy.js → index-BZrE0Go8.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-Bw5pTpnI.js → index-BizdX2X5.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-BGMxK155.js → index-C87Yr3h1.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DVnFgasM.js → index-C9Wypz9e.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-C61w3omz.js → index-CAoG_E-q.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-l5-J0jHx.js → index-CAxSyMrF.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-28DjsABm.js → index-CC5HjNM6.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-BPUCvc8i.js → index-CNZQEZ9g.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CpdXhhMY.js → index-CPvIhha6.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-DAboGZTw.js → index-CR_wqMGy.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-DL7ewjD2.js → index-CSClXoB3.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-Bhf-MuUh.js → index-CZ8azVgB.js} +3 -3
  120. package/src/assets/web-panel/assets/{index-BaaydJcC.js → index-Ccc5z8eJ.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-B2LieObB.js → index-CeiKbM3a.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-We2nK3ju.js → index-Cl6av3X7.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-m8waSAyv.js → index-CtGfeD7E.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-cDJsyAkI.js → index-Ctid12Gq.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DlwxUZQL.js → index-D0H-N0uG.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-BBYiCI1d.js → index-D0vIgprF.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-_9tUBcLm.js → index-D4wUDFRg.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-4HoFz1MA.js → index-DJFipyK7.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-Cq9k4-Ws.js → index-DX2REKNp.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-DuuOX5u3.js → index-Dc3gD0cw.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-DgjLaB7k.js → index-DiQzer2R.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-CMNjaSir.js → index-DntKLOs9.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-vXtH3o6O.js → index-Dplhl6gh.js} +1 -1
  134. package/src/assets/web-panel/assets/{index-BLMtCKAY.js → index-Dq8q_PWB.js} +1 -1
  135. package/src/assets/web-panel/assets/{index-CXrk8OYO.js → index-IVQdOBNN.js} +1 -1
  136. package/src/assets/web-panel/assets/{index-CBVcGaxk.js → index-Kerduyao.js} +1 -1
  137. package/src/assets/web-panel/assets/{index-BKblSCL9.js → index-W6cLGjpW.js} +1 -1
  138. package/src/assets/web-panel/assets/{index-CgoSgmjK.js → index-qZDNFll9.js} +1 -1
  139. package/src/assets/web-panel/assets/{initDefaultProps-B_EZHeqN.js → initDefaultProps-Ccz7v71u.js} +1 -1
  140. package/src/assets/web-panel/assets/{markdown-Yo_c6Krv.js → markdown-Bq2x0XsR.js} +1 -1
  141. package/src/assets/web-panel/assets/markdown-DUKxbhE4.js +51 -0
  142. package/src/assets/web-panel/assets/{motion-BrVb_Gy1.js → motion-BlCLpVDp.js} +1 -1
  143. package/src/assets/web-panel/assets/{move-CKxVRL_J.js → move-B80G7fLU.js} +1 -1
  144. package/src/assets/web-panel/assets/{mtc-parser-C80rvQ1J.js → mtc-parser-Djw08NJh.js} +1 -1
  145. package/src/assets/web-panel/assets/{omit-De5vD7BR.js → omit-Bg_G1eUr.js} +1 -1
  146. package/src/assets/web-panel/assets/{pickAttrs-BTyEDjth.js → pickAttrs-Df9zYUg2.js} +1 -1
  147. package/src/assets/web-panel/assets/{placementArrow-BWR94AjR.js → placementArrow-DjCDLhJ4.js} +1 -1
  148. package/src/assets/web-panel/assets/{responsiveObserve-1g-Vnibk.js → responsiveObserve--Naz7NgY.js} +1 -1
  149. package/src/assets/web-panel/assets/{slide-Dmg6dYuF.js → slide-CHn3ald-.js} +1 -1
  150. package/src/assets/web-panel/assets/{statusUtils-Ddj6rSab.js → statusUtils-BiGwoIK_.js} +1 -1
  151. package/src/assets/web-panel/assets/{styleChecker-DyP1mPDf.js → styleChecker-Zf2dQx1j.js} +1 -1
  152. package/src/assets/web-panel/assets/{useFlexGapSupport-D3d_0YuY.js → useFlexGapSupport-sLES8nyD.js} +1 -1
  153. package/src/assets/web-panel/assets/{useFs-M3vq-gE2.js → useFs-DV1Hl4qu.js} +1 -1
  154. package/src/assets/web-panel/assets/{usePersonalDataHub-GtP2TgtK.js → usePersonalDataHub-CZXr2Qxg.js} +1 -1
  155. package/src/assets/web-panel/assets/{vnode-BurE2ATQ.js → vnode-e8Eo3Oyl.js} +1 -1
  156. package/src/assets/web-panel/assets/{xterm-BZcWGsqw.js → xterm-zbskxxGA.js} +1 -1
  157. package/src/assets/web-panel/assets/{zoom-Ch-ViiDe.js → zoom-B8A6xKMF.js} +1 -1
  158. package/src/assets/web-panel/index.html +1 -1
  159. package/src/commands/compact.js +7 -2
  160. package/src/data/changelog.json +27 -1
  161. package/src/harness/jsonl-session-store.js +80 -74
  162. package/src/harness/prompt-compressor.js +57 -3
  163. package/src/lib/artifact-store.js +129 -20
  164. package/src/lib/bounded-artifact-file-read.js +128 -46
  165. package/src/lib/context-memory-kernel/context-image-transport.js +248 -0
  166. package/src/lib/context-memory-kernel/jsonl-session-context-port.js +4 -1
  167. package/src/lib/context-memory-kernel/live-compaction.js +40 -10
  168. package/src/lib/context-memory-kernel/message-adapter.js +80 -10
  169. package/src/lib/context-memory-kernel/message-bundle-store.js +1140 -0
  170. package/src/lib/context-memory-kernel/provider-context.js +23 -8
  171. package/src/lib/evolution/agent-evolution-ingress.js +103 -19
  172. package/src/lib/evolution/agent-evolution-runtime-composition.js +65 -27
  173. package/src/lib/evolution/agent-model-projection.js +90 -18
  174. package/src/lib/evolution/evidence-json-text.js +341 -42
  175. package/src/lib/evolution/evolution-artifact-ports.js +1005 -83
  176. package/src/lib/evolution/evolution-evidence-artifact-adapter.js +59 -15
  177. package/src/lib/evolution/evolution-evidence-projector.js +153 -16
  178. package/src/lib/evolution/evolution-evidence-storage-layout.js +115 -0
  179. package/src/lib/evolution/evolution-file-witness.js +7 -3
  180. package/src/lib/evolution/evolution-ledger-file-manifest-catalog.js +6 -2
  181. package/src/lib/evolution/evolution-ledger-file-manifest-head-store.js +6 -2
  182. package/src/lib/evolution/evolution-ledger.js +226 -55
  183. package/src/lib/evolution/evolution-private-storage.js +20 -0
  184. package/src/lib/evolution/evolution-run-ledger-adapter.js +298 -11
  185. package/src/lib/evolution/evolution-windows-directory-durability.js +215 -32
  186. package/src/lib/evolution/local-ui-evolution-runtime.js +3 -38
  187. package/src/lib/evolution/local-ui-signing-authority.js +69 -0
  188. package/src/lib/image-token-estimate.js +59 -0
  189. package/src/lib/ollama-provider-fetch.js +33 -0
  190. package/src/lib/provider-stream.js +14 -13
  191. package/src/lib/read-file-loop-guard.js +7 -4
  192. package/src/lib/session-history-origins.js +94 -35
  193. package/src/lib/session-message-provenance.js +26 -2
  194. package/src/lib/session-runtime-retention.js +10 -1
  195. package/src/lib/session-transcript-history.js +5 -1
  196. package/src/lib/session-transcript-migration.js +67 -12
  197. package/src/lib/storage-failure-cause-codes.js +449 -0
  198. package/src/lib/tool-result-preview.js +276 -0
  199. package/src/runtime/agent-core.js +111 -92
  200. package/src/runtime/headless-stream.js +121 -27
  201. package/src/assets/web-panel/assets/MarkdownRenderer-D8_iZief.js +0 -1
  202. package/src/assets/web-panel/assets/Projects-BeSOM6wi.js +0 -1
  203. package/src/assets/web-panel/assets/SpeechSettings-Bfhvin8B.js +0 -1
  204. package/src/assets/web-panel/assets/devWarning-c2I2Rn4_.js +0 -1
  205. package/src/assets/web-panel/assets/index-CTs4dYcV.js +0 -1
  206. package/src/assets/web-panel/assets/index-DeuvaW15.js +0 -1
  207. package/src/assets/web-panel/assets/markdown-CsiA8-E5.js +0 -50
@@ -0,0 +1,1140 @@
1
+ import fs, {
2
+ constants as fsConstants,
3
+ closeSync,
4
+ existsSync,
5
+ fsyncSync,
6
+ lstatSync,
7
+ linkSync,
8
+ openSync,
9
+ opendirSync,
10
+ realpathSync,
11
+ readdirSync,
12
+ rmdirSync,
13
+ unlinkSync,
14
+ } from "node:fs";
15
+ import { createHash } from "node:crypto";
16
+ import {
17
+ basename,
18
+ dirname,
19
+ isAbsolute,
20
+ join,
21
+ relative,
22
+ resolve,
23
+ } from "node:path";
24
+ import { TextDecoder } from "node:util";
25
+ import { canonicalJson } from "@chainlesschain/context-memory-kernel";
26
+ import {
27
+ normalizeSessionGenerationAuthority,
28
+ SESSION_GENERATION_AUTHORITY_SCHEMA,
29
+ } from "../../harness/session-list-index.js";
30
+ import { getSessionStoreDir } from "../paths.js";
31
+ import {
32
+ ensurePrivateDirectory,
33
+ ensurePrivateFile,
34
+ PRIVATE_FILE_MODE,
35
+ } from "../secure-fs.js";
36
+ import { readBoundedArtifactFile } from "../bounded-artifact-file-read.js";
37
+ import { withFileLock } from "../with-file-lock.js";
38
+ import { AGENT_MODEL_IMAGE_LIMITS } from "../evolution/agent-model-projection.js";
39
+ import { inspectCanonicalImageDataUrl } from "../image-token-estimate.js";
40
+
41
+ export const SESSION_MESSAGE_BUNDLE_STORE = "cli-session-message-bundles-v1";
42
+ export const SESSION_MESSAGE_BUNDLE_MAX_INLINE_BYTES = 4 * 1024 * 1024;
43
+ export const SESSION_MESSAGE_BUNDLE_MAX_BLOB_BYTES = 56 * 1024 * 1024;
44
+
45
+ const STORE_DIRECTORY = ".context-message-bundles-v1";
46
+ const STORE_SCHEMA = "chainlesschain.cli-session-message-bundle-store/v1";
47
+ const PENDING_SCHEMA =
48
+ "chainlesschain.cli-session-message-bundle-delete-pending/v1";
49
+ const DELETED_SCHEMA = "chainlesschain.cli-session-message-bundle-deleted/v1";
50
+ const MESSAGE_BUNDLE_SCHEMA = "chainlesschain.cli-context-message-bundle/v1";
51
+ const MAX_DIRECTORY_SWEEP = 32;
52
+ const LOCK_OPTIONS = Object.freeze({
53
+ failIfUnavailable: true,
54
+ timeoutMs: 30_000,
55
+ retryMs: 1,
56
+ maxRetryMs: 8,
57
+ retryJitterMs: 4,
58
+ yieldAfterReleaseMs: 2,
59
+ });
60
+ const FATAL_UTF8 = new TextDecoder("utf-8", { fatal: true });
61
+ const MAX_IMAGE_DATA_URL_CHARS =
62
+ Math.ceil(AGENT_MODEL_IMAGE_LIMITS.maxImageBytes / 3) * 4 + 128;
63
+
64
+ function imageBundleError(
65
+ message,
66
+ code = "CC_SESSION_MESSAGE_BUNDLE_INVALID",
67
+ cause,
68
+ ) {
69
+ const error = new Error(message, cause ? { cause } : undefined);
70
+ error.code = code;
71
+ return error;
72
+ }
73
+
74
+ function canonicalPath(value) {
75
+ return process.platform === "win32" ? value.toLowerCase() : value;
76
+ }
77
+
78
+ function isContained(parent, candidate) {
79
+ const relation = relative(parent, candidate);
80
+ return (
81
+ relation === "" ||
82
+ (!relation.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) &&
83
+ relation !== ".." &&
84
+ !isAbsolute(relation))
85
+ );
86
+ }
87
+
88
+ function assertSessionId(sessionId) {
89
+ if (
90
+ typeof sessionId !== "string" ||
91
+ sessionId.length === 0 ||
92
+ sessionId.includes("/") ||
93
+ sessionId.includes("\\") ||
94
+ sessionId.includes("..") ||
95
+ Buffer.byteLength(sessionId, "utf8") > 200
96
+ ) {
97
+ throw imageBundleError("Session message bundle requires a safe session id");
98
+ }
99
+ return sessionId;
100
+ }
101
+
102
+ function sha256(bytes) {
103
+ return createHash("sha256").update(bytes).digest("hex");
104
+ }
105
+
106
+ function sessionHash(sessionId) {
107
+ return sha256(Buffer.from(sessionId, "utf8"));
108
+ }
109
+
110
+ function objectIdFor(sessionId, generationId, digest) {
111
+ return `bundle-${sha256(
112
+ Buffer.from(
113
+ `${sessionHash(sessionId)}\0${generationId}\0${digest}`,
114
+ "utf8",
115
+ ),
116
+ )}`;
117
+ }
118
+
119
+ function canonicalSessionRoot(create) {
120
+ const configured = resolve(getSessionStoreDir());
121
+ if (!create && !existsSync(configured)) return null;
122
+ ensurePrivateDirectory(configured, {
123
+ applyWindowsAcl: create,
124
+ failIfUnavailable: true,
125
+ });
126
+ const sessionRoot = realpathSync.native(configured);
127
+ ensureDirectory(sessionRoot, "session store root", false);
128
+ return sessionRoot;
129
+ }
130
+
131
+ function storePaths(create = false) {
132
+ const sessionRoot = canonicalSessionRoot(create);
133
+ if (!sessionRoot) return null;
134
+ const root = join(sessionRoot, STORE_DIRECTORY);
135
+ const paths = {
136
+ sessionRoot,
137
+ root,
138
+ sessions: join(root, "sessions"),
139
+ tombstones: join(root, "tombstones"),
140
+ locks: join(root, "locks"),
141
+ };
142
+ if (create) {
143
+ ensureDirectory(root, "message bundle store root", true);
144
+ ensureDirectory(paths.sessions, "message bundle sessions root", true);
145
+ ensureDirectory(paths.tombstones, "message bundle tombstones root", true);
146
+ ensureDirectory(paths.locks, "message bundle locks root", true);
147
+ } else if (!existsSync(root)) {
148
+ return null;
149
+ } else {
150
+ ensureDirectory(root, "message bundle store root", false);
151
+ ensureDirectory(paths.sessions, "message bundle sessions root", false);
152
+ ensureDirectory(paths.tombstones, "message bundle tombstones root", false);
153
+ ensureDirectory(paths.locks, "message bundle locks root", false);
154
+ }
155
+ return paths;
156
+ }
157
+
158
+ function ensureDirectory(target, label, create) {
159
+ const wasMissing = create && !existsSync(target);
160
+ if (create) {
161
+ ensurePrivateDirectory(target, {
162
+ applyWindowsAcl: true,
163
+ failIfUnavailable: true,
164
+ });
165
+ }
166
+ let stat;
167
+ try {
168
+ stat = lstatSync(target);
169
+ } catch (cause) {
170
+ throw imageBundleError(`${label} is unavailable`, undefined, cause);
171
+ }
172
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
173
+ throw imageBundleError(`${label} must be a real directory`);
174
+ }
175
+ const actual = realpathSync.native(target);
176
+ if (canonicalPath(actual) !== canonicalPath(resolve(target))) {
177
+ throw imageBundleError(`${label} is not physically canonical`);
178
+ }
179
+ if (wasMissing) fsyncDirectory(dirname(target));
180
+ return target;
181
+ }
182
+
183
+ function sessionObjectsDir(paths, sessionId) {
184
+ const hash = sessionHash(sessionId);
185
+ return join(paths.sessions, hash, "objects");
186
+ }
187
+
188
+ function ensureSessionObjectsDir(paths, sessionId) {
189
+ const sessionDir = join(paths.sessions, sessionHash(sessionId));
190
+ const objectsDir = join(sessionDir, "objects");
191
+ if (
192
+ !isContained(paths.sessions, sessionDir) ||
193
+ !isContained(paths.root, objectsDir)
194
+ ) {
195
+ throw imageBundleError("Message bundle path escaped its private store");
196
+ }
197
+ ensureDirectory(sessionDir, "message bundle session directory", true);
198
+ ensureDirectory(objectsDir, "message bundle objects directory", true);
199
+ return objectsDir;
200
+ }
201
+
202
+ function assertExistingSessionObjectsDir(paths, sessionId) {
203
+ const objectsDir = sessionObjectsDir(paths, sessionId);
204
+ if (
205
+ !isContained(paths.sessions, dirname(objectsDir)) ||
206
+ !isContained(paths.root, objectsDir)
207
+ ) {
208
+ throw imageBundleError("Message bundle path escaped its private store");
209
+ }
210
+ if (!existsSync(objectsDir)) return null;
211
+ ensureDirectory(
212
+ dirname(objectsDir),
213
+ "message bundle session directory",
214
+ false,
215
+ );
216
+ ensureDirectory(objectsDir, "message bundle objects directory", false);
217
+ return objectsDir;
218
+ }
219
+
220
+ function fsyncDirectory(directory) {
221
+ if (process.platform === "win32") return;
222
+ const descriptor = openSync(
223
+ directory,
224
+ fsConstants.O_RDONLY |
225
+ Number(fsConstants.O_DIRECTORY || 0) |
226
+ Number(fsConstants.O_NOFOLLOW || 0),
227
+ );
228
+ try {
229
+ if (!fs.fstatSync(descriptor).isDirectory()) {
230
+ throw imageBundleError(
231
+ "Message bundle durability parent is not a directory",
232
+ );
233
+ }
234
+ fsyncSync(descriptor);
235
+ } finally {
236
+ closeSync(descriptor);
237
+ }
238
+ }
239
+
240
+ function fsyncDirectoryAncestry(paths, objectsDir) {
241
+ for (const directory of [
242
+ objectsDir,
243
+ dirname(objectsDir),
244
+ paths.sessions,
245
+ paths.root,
246
+ paths.sessionRoot,
247
+ ]) {
248
+ fsyncDirectory(directory);
249
+ }
250
+ }
251
+
252
+ function writeExclusiveFile(target, bytes) {
253
+ const tempPath = join(dirname(target), `.${basename(target)}.tmp`);
254
+ recoverTemporaryLink(target, tempPath);
255
+ let descriptor = null;
256
+ let preservePublishedPair = false;
257
+ try {
258
+ descriptor = openSync(
259
+ tempPath,
260
+ fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY,
261
+ PRIVATE_FILE_MODE,
262
+ );
263
+ const payload = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes, "utf8");
264
+ let offset = 0;
265
+ while (offset < payload.length) {
266
+ const written = fs.writeSync(
267
+ descriptor,
268
+ payload,
269
+ offset,
270
+ payload.length - offset,
271
+ );
272
+ if (written <= 0) {
273
+ throw imageBundleError("Message bundle write made no forward progress");
274
+ }
275
+ offset += written;
276
+ }
277
+ fsyncSync(descriptor);
278
+ closeSync(descriptor);
279
+ descriptor = null;
280
+ ensurePrivateFile(tempPath);
281
+ try {
282
+ linkSync(tempPath, target);
283
+ preservePublishedPair = true;
284
+ } catch (cause) {
285
+ if (cause?.code !== "EEXIST") throw cause;
286
+ return false;
287
+ }
288
+ fsyncDirectory(dirname(target));
289
+ preservePublishedPair = false;
290
+ unlinkSync(tempPath);
291
+ fsyncDirectory(dirname(target));
292
+ return true;
293
+ } finally {
294
+ if (descriptor !== null) closeSync(descriptor);
295
+ if (existsSync(tempPath) && !preservePublishedPair) unlinkSync(tempPath);
296
+ }
297
+ }
298
+
299
+ function safeReadSmallFile(filePath, maximumBytes) {
300
+ const stat = lstatSync(filePath, { bigint: true });
301
+ if (
302
+ !stat.isFile() ||
303
+ stat.isSymbolicLink() ||
304
+ stat.nlink !== 1n ||
305
+ stat.size < 1n ||
306
+ stat.size > BigInt(maximumBytes)
307
+ ) {
308
+ throw imageBundleError("Message bundle store metadata is unsafe");
309
+ }
310
+ const bytes = readBoundedArtifactFile(
311
+ filePath,
312
+ Number(stat.size),
313
+ maximumBytes,
314
+ );
315
+ return FATAL_UTF8.decode(bytes);
316
+ }
317
+
318
+ function readJsonFile(filePath, maximumBytes) {
319
+ try {
320
+ return JSON.parse(safeReadSmallFile(filePath, maximumBytes));
321
+ } catch (cause) {
322
+ if (cause?.code === "ENOENT") throw cause;
323
+ if (cause?.code === "CC_SESSION_MESSAGE_BUNDLE_INVALID") throw cause;
324
+ throw imageBundleError(
325
+ "Message bundle store metadata could not be verified",
326
+ undefined,
327
+ cause,
328
+ );
329
+ }
330
+ }
331
+
332
+ function inspectExternalizableBundle(payload) {
333
+ if (
334
+ !payload ||
335
+ payload.schema !== MESSAGE_BUNDLE_SCHEMA ||
336
+ !Array.isArray(payload.messages) ||
337
+ !Array.isArray(payload.durableSystemKinds) ||
338
+ payload.durableSystemKinds.length !== payload.messages.length
339
+ ) {
340
+ return null;
341
+ }
342
+
343
+ let imageCount = 0;
344
+ let imageBytes = 0;
345
+ const reduced = {
346
+ ...payload,
347
+ messages: payload.messages.map((message) => {
348
+ if (message?.role !== "user" || !Array.isArray(message.content))
349
+ return message;
350
+ let messageImages = 0;
351
+ let messageBytes = 0;
352
+ return {
353
+ ...message,
354
+ content: message.content.map((block) => {
355
+ if (
356
+ !block ||
357
+ typeof block !== "object" ||
358
+ Array.isArray(block) ||
359
+ Object.keys(block).sort().join() !== "image_url,type" ||
360
+ block.type !== "image_url" ||
361
+ !block.image_url ||
362
+ typeof block.image_url !== "object" ||
363
+ Array.isArray(block.image_url) ||
364
+ Object.keys(block.image_url).some(
365
+ (key) => !["url", "detail"].includes(key),
366
+ ) ||
367
+ (block.image_url.detail !== undefined &&
368
+ !["auto", "low", "high"].includes(block.image_url.detail)) ||
369
+ typeof block.image_url.url !== "string" ||
370
+ block.image_url.url.length > MAX_IMAGE_DATA_URL_CHARS
371
+ ) {
372
+ return block;
373
+ }
374
+ const image = inspectCanonicalImageDataUrl(block.image_url.url);
375
+ if (!image) return block;
376
+ if (image.decodedBytes > AGENT_MODEL_IMAGE_LIMITS.maxImageBytes) {
377
+ throw imageBundleError(
378
+ "Image exceeds the existing per-image byte budget",
379
+ "CC_CONTEXT_IMAGE_TRANSPORT_INVALID",
380
+ );
381
+ }
382
+ messageImages += 1;
383
+ messageBytes += image.decodedBytes;
384
+ imageCount += 1;
385
+ imageBytes += image.decodedBytes;
386
+ if (
387
+ messageImages > AGENT_MODEL_IMAGE_LIMITS.maxMessageImages ||
388
+ messageBytes > AGENT_MODEL_IMAGE_LIMITS.maxMessageImageBytes
389
+ ) {
390
+ throw imageBundleError(
391
+ "Images exceed the existing per-message image budget",
392
+ "CC_CONTEXT_IMAGE_TRANSPORT_INVALID",
393
+ );
394
+ }
395
+ return {
396
+ ...block,
397
+ image_url: { ...block.image_url, url: "" },
398
+ };
399
+ }),
400
+ };
401
+ }),
402
+ };
403
+ if (imageCount === 0) return null;
404
+ return {
405
+ reducedContent: canonicalJson(reduced),
406
+ imageCount,
407
+ imageBytes,
408
+ };
409
+ }
410
+
411
+ function parseCanonicalBundle(content) {
412
+ if (typeof content !== "string") {
413
+ throw imageBundleError("Message bundle content must be a string");
414
+ }
415
+ const bytes = Buffer.from(content, "utf8");
416
+ if (bytes.length > SESSION_MESSAGE_BUNDLE_MAX_BLOB_BYTES) {
417
+ throw imageBundleError("Message bundle exceeds the 56 MiB blob limit");
418
+ }
419
+ let payload;
420
+ try {
421
+ payload = JSON.parse(content);
422
+ } catch (cause) {
423
+ throw imageBundleError(
424
+ "Message bundle is not valid JSON",
425
+ undefined,
426
+ cause,
427
+ );
428
+ }
429
+ if (canonicalJson(payload) !== content) {
430
+ throw imageBundleError("Message bundle is not canonical JSON");
431
+ }
432
+ const inspected = inspectExternalizableBundle(payload);
433
+ if (
434
+ bytes.length <= SESSION_MESSAGE_BUNDLE_MAX_INLINE_BYTES ||
435
+ !inspected ||
436
+ Buffer.byteLength(inspected.reducedContent, "utf8") >
437
+ SESSION_MESSAGE_BUNDLE_MAX_INLINE_BYTES
438
+ ) {
439
+ throw imageBundleError(
440
+ "Only large canonical user image bundles may use out-of-line storage",
441
+ );
442
+ }
443
+ return { bytes, payload, inspected };
444
+ }
445
+
446
+ function lockTargets(paths, sessionId) {
447
+ const hash = sessionHash(sessionId);
448
+ return {
449
+ global: join(paths.locks, "global-store"),
450
+ session: join(paths.locks, `session-${hash}`),
451
+ };
452
+ }
453
+
454
+ function tombstonePath(paths, hash, suffix) {
455
+ return join(paths.tombstones, `${hash}.${suffix}`);
456
+ }
457
+
458
+ function sessionTranscriptState(paths, sessionId) {
459
+ const transcript = join(paths.sessionRoot, `${sessionId}.jsonl`);
460
+ const metaPath = join(paths.sessionRoot, `${sessionId}.meta.json`);
461
+ const markerPath = join(paths.sessionRoot, `${sessionId}.tombstone`);
462
+ let transcriptExists = false;
463
+ try {
464
+ const stat = lstatSync(transcript);
465
+ if (!stat.isFile() || stat.isSymbolicLink()) {
466
+ throw imageBundleError("Session transcript path is unsafe");
467
+ }
468
+ transcriptExists = true;
469
+ } catch (cause) {
470
+ if (cause?.code !== "ENOENT") throw cause;
471
+ }
472
+ let meta = null;
473
+ let marker = null;
474
+ for (const [path, assign] of [
475
+ [metaPath, (value) => (meta = value)],
476
+ [markerPath, (value) => (marker = value)],
477
+ ]) {
478
+ try {
479
+ const value = readJsonFile(path, 256 * 1024);
480
+ assign(value);
481
+ } catch (cause) {
482
+ if (cause?.code !== "ENOENT") throw cause;
483
+ }
484
+ }
485
+ const metaMatches = meta?.schema === 2 && meta?.id === sessionId;
486
+ const markerMatches =
487
+ marker?.schema === "chainlesschain.session-tombstone/v1" &&
488
+ marker?.id === sessionId;
489
+ const metaGeneration = normalizeSessionGenerationAuthority(meta?.generation);
490
+ const markerGeneration = normalizeSessionGenerationAuthority(
491
+ marker?.generation,
492
+ );
493
+ const generationId =
494
+ (metaGeneration?.sessionId === sessionId && metaGeneration.generationId) ||
495
+ (markerGeneration?.sessionId === sessionId &&
496
+ markerGeneration.generationId) ||
497
+ null;
498
+ const hasNewerLiveGeneration =
499
+ metaMatches &&
500
+ meta.deleted !== true &&
501
+ metaGeneration?.sessionId === sessionId &&
502
+ metaGeneration.generationId !== markerGeneration?.generationId;
503
+ return {
504
+ transcriptExists,
505
+ metaMatches,
506
+ metaDeleted: metaMatches && meta.deleted === true,
507
+ deletedWitness:
508
+ (metaMatches && meta.deleted === true) ||
509
+ (markerMatches && !hasNewerLiveGeneration),
510
+ generationId,
511
+ liveGenerationId:
512
+ metaMatches &&
513
+ meta.deleted !== true &&
514
+ metaGeneration?.sessionId === sessionId
515
+ ? metaGeneration.generationId
516
+ : null,
517
+ };
518
+ }
519
+
520
+ function requireLiveSessionGeneration(paths, sessionId) {
521
+ const state = sessionTranscriptState(paths, sessionId);
522
+ if (state.deletedWitness || state.metaDeleted) {
523
+ throw imageBundleError(
524
+ `Session message bundle was deleted: ${sessionId}`,
525
+ "SESSION_DELETED",
526
+ );
527
+ }
528
+ if (
529
+ !state.transcriptExists ||
530
+ !state.metaMatches ||
531
+ !state.liveGenerationId
532
+ ) {
533
+ throw imageBundleError(
534
+ "Large session image bundles require a live JSONL session generation",
535
+ "CC_CONTEXT_IMAGE_TRANSPORT_INVALID",
536
+ );
537
+ }
538
+ return state.liveGenerationId;
539
+ }
540
+
541
+ function generationForDeletion(paths, sessionId) {
542
+ return sessionTranscriptState(paths, sessionId).generationId;
543
+ }
544
+
545
+ function verifyPendingRecord(paths, hash) {
546
+ const pendingPath = tombstonePath(paths, hash, "pending");
547
+ recoverTemporaryLink(
548
+ pendingPath,
549
+ join(paths.tombstones, `.${hash}.pending.tmp`),
550
+ );
551
+ if (!existsSync(pendingPath)) return null;
552
+ const value = readJsonFile(pendingPath, 4096);
553
+ const generationId = value?.generationId ?? null;
554
+ if (
555
+ value?.schema !== PENDING_SCHEMA ||
556
+ typeof value.sessionId !== "string" ||
557
+ sessionHash(assertSessionId(value.sessionId)) !== hash ||
558
+ (generationId !== null &&
559
+ (typeof generationId !== "string" ||
560
+ !/^generation-[0-9a-f-]{32,36}$/u.test(generationId)))
561
+ ) {
562
+ throw imageBundleError("Session image deletion marker is invalid");
563
+ }
564
+ return { sessionId: value.sessionId, generationId };
565
+ }
566
+
567
+ function assertStoreTombstone(paths, hash) {
568
+ const filePath = tombstonePath(paths, hash, "deleted");
569
+ recoverTemporaryLink(
570
+ filePath,
571
+ join(paths.tombstones, `.${hash}.deleted.tmp`),
572
+ );
573
+ if (!existsSync(filePath)) return false;
574
+ const value = readJsonFile(filePath, 4096);
575
+ const generationId = value?.generationId ?? null;
576
+ if (
577
+ value?.schema !== DELETED_SCHEMA ||
578
+ value?.sessionHash !== hash ||
579
+ (generationId !== null &&
580
+ (typeof generationId !== "string" ||
581
+ !/^generation-[0-9a-f-]{32,36}$/u.test(generationId)))
582
+ ) {
583
+ throw imageBundleError("Session image deletion tombstone is invalid");
584
+ }
585
+ return { generationId };
586
+ }
587
+
588
+ function assertOwnRegularFile(filePath, { allowLinked = false } = {}) {
589
+ const stat = lstatSync(filePath, { bigint: true });
590
+ if (
591
+ !stat.isFile() ||
592
+ stat.isSymbolicLink() ||
593
+ stat.nlink < 1n ||
594
+ (!allowLinked && stat.nlink !== 1n) ||
595
+ (allowLinked && stat.nlink > 2n)
596
+ ) {
597
+ throw imageBundleError("Message bundle file is not a private regular file");
598
+ }
599
+ return stat;
600
+ }
601
+
602
+ function recoverTemporaryLink(target, tempPath) {
603
+ if (!existsSync(tempPath)) return;
604
+ const tempStat = assertOwnRegularFile(tempPath, { allowLinked: true });
605
+ if (tempStat.nlink === 2n) {
606
+ if (!existsSync(target)) {
607
+ throw imageBundleError(
608
+ "Message bundle temporary link has no published object",
609
+ );
610
+ }
611
+ const targetStat = assertOwnRegularFile(target, { allowLinked: true });
612
+ if (
613
+ targetStat.dev !== tempStat.dev ||
614
+ targetStat.ino !== tempStat.ino ||
615
+ targetStat.nlink !== 2n
616
+ ) {
617
+ throw imageBundleError("Message bundle temporary link identity changed");
618
+ }
619
+ }
620
+ unlinkSync(tempPath);
621
+ fsyncDirectory(dirname(tempPath));
622
+ }
623
+
624
+ function sweepSessionTempsLocked(
625
+ paths,
626
+ hash,
627
+ maxEntries = MAX_DIRECTORY_SWEEP,
628
+ ) {
629
+ const sessionDir = join(paths.sessions, hash);
630
+ const objectsDir = join(sessionDir, "objects");
631
+ if (!existsSync(objectsDir)) return;
632
+ ensureDirectory(sessionDir, "message bundle session directory", false);
633
+ ensureDirectory(objectsDir, "message bundle objects directory", false);
634
+ const directory = opendirSync(objectsDir);
635
+ let processed = 0;
636
+ let scanned = 0;
637
+ let removed = false;
638
+ try {
639
+ for (;;) {
640
+ const entry = directory.readSync();
641
+ if (!entry || scanned >= maxEntries * 8 || processed >= maxEntries) break;
642
+ scanned += 1;
643
+ if (!/^\.bundle-[a-f0-9]{64}\.json\.tmp$/u.test(entry.name)) continue;
644
+ processed += 1;
645
+ const target = join(objectsDir, entry.name);
646
+ if (!isContained(objectsDir, target)) {
647
+ throw imageBundleError(
648
+ "Temporary message bundle path escaped its store",
649
+ );
650
+ }
651
+ const tempStat = assertOwnRegularFile(target, { allowLinked: true });
652
+ if (tempStat.nlink === 2n) {
653
+ const bundleName = entry.name.slice(1, -".tmp".length);
654
+ const finalPath = join(objectsDir, bundleName);
655
+ if (!existsSync(finalPath)) {
656
+ throw imageBundleError(
657
+ "Orphan message bundle hard link could not be paired",
658
+ );
659
+ }
660
+ const finalStat = assertOwnRegularFile(finalPath, {
661
+ allowLinked: true,
662
+ });
663
+ if (
664
+ finalStat.dev !== tempStat.dev ||
665
+ finalStat.ino !== tempStat.ino ||
666
+ finalStat.nlink !== 2n
667
+ ) {
668
+ throw imageBundleError("Message bundle hard link identity changed");
669
+ }
670
+ }
671
+ unlinkSync(target);
672
+ removed = true;
673
+ }
674
+ } finally {
675
+ directory.closeSync();
676
+ }
677
+ if (removed) fsyncDirectory(objectsDir);
678
+ }
679
+
680
+ function cleanupSessionObjectsLocked(paths, hash) {
681
+ const sessionDir = join(paths.sessions, hash);
682
+ const objectsDir = join(sessionDir, "objects");
683
+ if (!existsSync(sessionDir)) return;
684
+ ensureDirectory(sessionDir, "message bundle session directory", false);
685
+ if (!existsSync(objectsDir)) {
686
+ rmdirSync(sessionDir);
687
+ fsyncDirectory(paths.sessions);
688
+ return;
689
+ }
690
+ ensureDirectory(objectsDir, "message bundle objects directory", false);
691
+ const names = readdirSync(objectsDir);
692
+ const known = names.every(
693
+ (name) =>
694
+ /^bundle-[a-f0-9]{64}\.json$/u.test(name) ||
695
+ /^\.bundle-[a-f0-9]{64}\.json\.tmp$/u.test(name),
696
+ );
697
+ if (!known) {
698
+ throw imageBundleError(
699
+ "Session image cleanup found an unknown store entry",
700
+ );
701
+ }
702
+ for (const name of names) {
703
+ const target = join(objectsDir, name);
704
+ if (!isContained(objectsDir, target)) {
705
+ throw imageBundleError("Session image cleanup path escaped its store");
706
+ }
707
+ const stat = assertOwnRegularFile(target, { allowLinked: true });
708
+ if (name.startsWith(".")) {
709
+ const finalPath = join(objectsDir, name.slice(1, -".tmp".length));
710
+ if (stat.nlink === 2n) {
711
+ if (!names.includes(basename(finalPath))) {
712
+ throw imageBundleError(
713
+ "Session image cleanup found an external hard link",
714
+ );
715
+ }
716
+ const finalStat = assertOwnRegularFile(finalPath, {
717
+ allowLinked: true,
718
+ });
719
+ if (
720
+ finalStat.dev !== stat.dev ||
721
+ finalStat.ino !== stat.ino ||
722
+ finalStat.nlink !== 2n
723
+ ) {
724
+ throw imageBundleError(
725
+ "Session image cleanup hard link identity changed",
726
+ );
727
+ }
728
+ }
729
+ } else if (stat.nlink === 2n) {
730
+ const tempPath = join(objectsDir, `.${name}.tmp`);
731
+ if (!names.includes(basename(tempPath))) {
732
+ throw imageBundleError(
733
+ "Session image cleanup found an external hard link",
734
+ );
735
+ }
736
+ const tempStat = assertOwnRegularFile(tempPath, { allowLinked: true });
737
+ if (
738
+ tempStat.dev !== stat.dev ||
739
+ tempStat.ino !== stat.ino ||
740
+ tempStat.nlink !== 2n
741
+ ) {
742
+ throw imageBundleError(
743
+ "Session image cleanup hard link identity changed",
744
+ );
745
+ }
746
+ }
747
+ }
748
+ for (const name of names.filter((entry) => entry.startsWith("."))) {
749
+ unlinkSync(join(objectsDir, name));
750
+ }
751
+ for (const name of names.filter((entry) => !entry.startsWith("."))) {
752
+ const target = join(objectsDir, name);
753
+ if (assertOwnRegularFile(target).nlink !== 1n) {
754
+ throw imageBundleError(
755
+ "Session image cleanup left a hard link outside its store",
756
+ );
757
+ }
758
+ unlinkSync(target);
759
+ }
760
+ fsyncDirectory(objectsDir);
761
+ rmdirSync(objectsDir);
762
+ fsyncDirectory(sessionDir);
763
+ rmdirSync(sessionDir);
764
+ fsyncDirectory(paths.sessions);
765
+ }
766
+
767
+ function publishDeletedTombstoneLocked(paths, hash, generationId) {
768
+ const target = tombstonePath(paths, hash, "deleted");
769
+ const payload = canonicalJson({
770
+ schema: DELETED_SCHEMA,
771
+ sessionHash: hash,
772
+ generationId: generationId ?? null,
773
+ });
774
+ const created = writeExclusiveFile(target, payload);
775
+ if (!created) {
776
+ const existing = assertStoreTombstone(paths, hash);
777
+ if (existing.generationId !== (generationId ?? null)) {
778
+ throw imageBundleError(
779
+ "Session image deletion tombstone generation changed",
780
+ );
781
+ }
782
+ }
783
+ }
784
+
785
+ function removePendingTombstoneLocked(paths, hash) {
786
+ const target = tombstonePath(paths, hash, "pending");
787
+ if (!existsSync(target)) return;
788
+ assertOwnRegularFile(target);
789
+ unlinkSync(target);
790
+ fsyncDirectory(paths.tombstones);
791
+ }
792
+
793
+ function finishPendingDeletionLocked(paths, sessionId, generationId) {
794
+ const hash = sessionHash(sessionId);
795
+ publishDeletedTombstoneLocked(paths, hash, generationId);
796
+ cleanupSessionObjectsLocked(paths, hash);
797
+ removePendingTombstoneLocked(paths, hash);
798
+ }
799
+
800
+ function resolvePendingDeletionLocked(paths, hash, sessionId) {
801
+ const pending = verifyPendingRecord(paths, hash);
802
+ if (pending === null) return null;
803
+ if (pending.sessionId !== sessionId) {
804
+ throw imageBundleError(
805
+ "Session image deletion marker belongs to another session",
806
+ );
807
+ }
808
+ const state = sessionTranscriptState(paths, sessionId);
809
+ const supersededByNewGeneration =
810
+ state.transcriptExists &&
811
+ !state.deletedWitness &&
812
+ state.liveGenerationId !== null &&
813
+ state.liveGenerationId !== pending.generationId;
814
+ if (
815
+ !state.transcriptExists ||
816
+ state.deletedWitness ||
817
+ supersededByNewGeneration
818
+ ) {
819
+ finishPendingDeletionLocked(paths, sessionId, pending.generationId);
820
+ return { generationId: pending.generationId };
821
+ }
822
+ removePendingTombstoneLocked(paths, hash);
823
+ return null;
824
+ }
825
+
826
+ function resolveCurrentSessionStateLocked(paths, sessionId) {
827
+ const hash = sessionHash(sessionId);
828
+ const recovered = resolvePendingDeletionLocked(paths, hash, sessionId);
829
+ const state = sessionTranscriptState(paths, sessionId);
830
+ const tombstone = assertStoreTombstone(paths, hash);
831
+ if (tombstone) {
832
+ if (
833
+ state.liveGenerationId === null ||
834
+ tombstone.generationId === state.liveGenerationId
835
+ ) {
836
+ cleanupSessionObjectsLocked(paths, hash);
837
+ throw imageBundleError(
838
+ "Session message bundle was deleted: " + sessionId,
839
+ "SESSION_DELETED",
840
+ );
841
+ }
842
+ // The session id now names a newer JSONL generation. Retire only the old
843
+ // generation's bundle namespace before admitting the new generation.
844
+ retireStaleDeletedTombstoneLocked(paths, hash, state.liveGenerationId);
845
+ }
846
+ const generationId = requireLiveSessionGeneration(paths, sessionId);
847
+ if (recovered && recovered.generationId === generationId) {
848
+ throw imageBundleError(
849
+ "Session message bundle deletion was recovered: " + sessionId,
850
+ "SESSION_DELETED",
851
+ );
852
+ }
853
+ return generationId;
854
+ }
855
+
856
+ function retireStaleDeletedTombstoneLocked(paths, hash, liveGenerationId) {
857
+ if (liveGenerationId === null) return false;
858
+ const tombstone = assertStoreTombstone(paths, hash);
859
+ if (!tombstone || tombstone.generationId === liveGenerationId) return false;
860
+ cleanupSessionObjectsLocked(paths, hash);
861
+ const deletedPath = tombstonePath(paths, hash, "deleted");
862
+ assertOwnRegularFile(deletedPath);
863
+ unlinkSync(deletedPath);
864
+ fsyncDirectory(paths.tombstones);
865
+ return true;
866
+ }
867
+
868
+ function sweepOrphansLocked(paths, sessionId) {
869
+ const hash = sessionHash(sessionId);
870
+ sweepSessionTempsLocked(paths, hash);
871
+ return resolvePendingDeletionLocked(paths, hash, sessionId);
872
+ }
873
+ function withStoreLocks(paths, sessionId, callback) {
874
+ const targets = lockTargets(paths, sessionId);
875
+ return withFileLock(
876
+ targets.global,
877
+ () => {
878
+ return withFileLock(
879
+ targets.session,
880
+ () => {
881
+ sweepOrphansLocked(paths, sessionId);
882
+ const generationId = resolveCurrentSessionStateLocked(
883
+ paths,
884
+ sessionId,
885
+ );
886
+ return callback(generationId);
887
+ },
888
+ LOCK_OPTIONS,
889
+ );
890
+ },
891
+ LOCK_OPTIONS,
892
+ );
893
+ }
894
+
895
+ function publishContentLocked(paths, sessionId, generationId, bytes, digest) {
896
+ const objectsDir = ensureSessionObjectsDir(paths, sessionId);
897
+ const objectId = objectIdFor(sessionId, generationId, digest);
898
+ const target = join(objectsDir, `${objectId}.json`);
899
+ recoverTemporaryLink(target, join(objectsDir, `.${objectId}.json.tmp`));
900
+ if (!isContained(objectsDir, target)) {
901
+ throw imageBundleError("Message bundle object path escaped its store");
902
+ }
903
+ if (existsSync(target)) {
904
+ const existing = readBoundedArtifactFile(
905
+ target,
906
+ bytes.length,
907
+ SESSION_MESSAGE_BUNDLE_MAX_BLOB_BYTES,
908
+ );
909
+ if (sha256(existing) !== digest) {
910
+ throw imageBundleError(
911
+ "Existing session message bundle digest does not match",
912
+ );
913
+ }
914
+ } else {
915
+ const created = writeExclusiveFile(target, bytes);
916
+ if (!created) {
917
+ const existing = readBoundedArtifactFile(
918
+ target,
919
+ bytes.length,
920
+ SESSION_MESSAGE_BUNDLE_MAX_BLOB_BYTES,
921
+ );
922
+ if (sha256(existing) !== digest) {
923
+ throw imageBundleError(
924
+ "Existing session message bundle digest does not match",
925
+ );
926
+ }
927
+ }
928
+ }
929
+ const readback = readBoundedArtifactFile(
930
+ target,
931
+ bytes.length,
932
+ SESSION_MESSAGE_BUNDLE_MAX_BLOB_BYTES,
933
+ );
934
+ if (sha256(readback) !== digest) {
935
+ throw imageBundleError(
936
+ "Session message bundle publication failed verification",
937
+ );
938
+ }
939
+ assertOwnRegularFile(target);
940
+ // Re-settle the published object and every newly-created directory entry,
941
+ // including on EEXIST reuse after an earlier response was lost before a
942
+ // parent-directory barrier completed.
943
+ fsyncDirectoryAncestry(paths, objectsDir);
944
+ return {
945
+ store: SESSION_MESSAGE_BUNDLE_STORE,
946
+ objectId,
947
+ digest: `sha256:${digest}`,
948
+ byteLength: bytes.length,
949
+ mimeType: "application/json",
950
+ summary: "Canonical user image message bundle",
951
+ recoverable: true,
952
+ accessPolicy: "same-session",
953
+ };
954
+ }
955
+
956
+ export function storeSessionMessageBundle({
957
+ sessionId: sessionIdInput,
958
+ content,
959
+ }) {
960
+ const sessionId = assertSessionId(sessionIdInput);
961
+ const { bytes } = parseCanonicalBundle(content);
962
+ const digest = sha256(bytes);
963
+ const sessionRoot = canonicalSessionRoot(false);
964
+ if (!sessionRoot) {
965
+ throw imageBundleError(
966
+ "Large session image bundles require a live JSONL session generation",
967
+ "CC_CONTEXT_IMAGE_TRANSPORT_INVALID",
968
+ );
969
+ }
970
+ const expectedGenerationId = requireLiveSessionGeneration(
971
+ { sessionRoot },
972
+ sessionId,
973
+ );
974
+ const paths = storePaths(true);
975
+ return withStoreLocks(paths, sessionId, (generationId) => {
976
+ if (generationId !== expectedGenerationId) {
977
+ throw imageBundleError(
978
+ "Session generation changed while publishing an image bundle",
979
+ "SESSION_GENERATION_CHANGED",
980
+ );
981
+ }
982
+ return publishContentLocked(paths, sessionId, generationId, bytes, digest);
983
+ });
984
+ }
985
+
986
+ export function readSessionMessageBundle(contentRef, sessionIdInput) {
987
+ const sessionId = assertSessionId(sessionIdInput);
988
+ if (
989
+ !contentRef ||
990
+ contentRef.store !== SESSION_MESSAGE_BUNDLE_STORE ||
991
+ typeof contentRef.objectId !== "string" ||
992
+ !/^bundle-[a-f0-9]{64}$/u.test(contentRef.objectId) ||
993
+ typeof contentRef.digest !== "string" ||
994
+ !/^sha256-[a-f0-9]{64}$|^sha256:[a-f0-9]{64}$/u.test(contentRef.digest) ||
995
+ !Number.isSafeInteger(contentRef.byteLength) ||
996
+ contentRef.byteLength <= SESSION_MESSAGE_BUNDLE_MAX_INLINE_BYTES ||
997
+ contentRef.byteLength > SESSION_MESSAGE_BUNDLE_MAX_BLOB_BYTES ||
998
+ contentRef.mimeType !== "application/json" ||
999
+ contentRef.summary !== "Canonical user image message bundle" ||
1000
+ contentRef.recoverable !== true ||
1001
+ contentRef.accessPolicy !== "same-session"
1002
+ ) {
1003
+ throw imageBundleError(
1004
+ "ContextItem contentRef is not a session message bundle",
1005
+ );
1006
+ }
1007
+ const expectedDigest = contentRef.digest.replace(":", "-");
1008
+ const digestHex = expectedDigest.slice("sha256-".length);
1009
+ const paths = storePaths(false);
1010
+ if (!paths) throw imageBundleError("Session message bundle is missing");
1011
+ return withStoreLocks(paths, sessionId, (generationId) => {
1012
+ if (
1013
+ objectIdFor(sessionId, generationId, digestHex) !== contentRef.objectId
1014
+ ) {
1015
+ throw imageBundleError(
1016
+ "Session message bundle reference belongs to another session generation",
1017
+ );
1018
+ }
1019
+ const objectsDir = assertExistingSessionObjectsDir(paths, sessionId);
1020
+ if (!objectsDir)
1021
+ throw imageBundleError("Session message bundle is missing");
1022
+ const target = join(objectsDir, `${contentRef.objectId}.json`);
1023
+ if (!isContained(objectsDir, target)) {
1024
+ throw imageBundleError("Session message bundle path escaped its store");
1025
+ }
1026
+ recoverTemporaryLink(
1027
+ target,
1028
+ join(objectsDir, `.${contentRef.objectId}.json.tmp`),
1029
+ );
1030
+ const bytes = readBoundedArtifactFile(
1031
+ target,
1032
+ contentRef.byteLength,
1033
+ SESSION_MESSAGE_BUNDLE_MAX_BLOB_BYTES,
1034
+ );
1035
+ if (sha256(bytes) !== digestHex) {
1036
+ throw imageBundleError(
1037
+ "Session message bundle digest does not match its reference",
1038
+ );
1039
+ }
1040
+ let content;
1041
+ try {
1042
+ content = FATAL_UTF8.decode(bytes);
1043
+ } catch (cause) {
1044
+ throw imageBundleError(
1045
+ "Session message bundle is not valid UTF-8",
1046
+ undefined,
1047
+ cause,
1048
+ );
1049
+ }
1050
+ parseCanonicalBundle(content);
1051
+ return content;
1052
+ });
1053
+ }
1054
+
1055
+ /** Keep the session deletion lock order: JSONL writer lock, then store global, then store session. */
1056
+ export function withSessionMessageBundleDeletion(sessionIdInput, task) {
1057
+ const sessionId = assertSessionId(sessionIdInput);
1058
+ if (
1059
+ typeof task !== "function" ||
1060
+ task.constructor?.name === "AsyncFunction"
1061
+ ) {
1062
+ throw new TypeError(
1063
+ "Session message bundle deletion callback must be synchronous",
1064
+ );
1065
+ }
1066
+ const paths = storePaths(true);
1067
+ const targets = lockTargets(paths, sessionId);
1068
+ return withFileLock(
1069
+ targets.global,
1070
+ () => {
1071
+ return withFileLock(
1072
+ targets.session,
1073
+ () => {
1074
+ const hash = sessionHash(sessionId);
1075
+ sweepOrphansLocked(paths, sessionId);
1076
+ const stateBeforeDelete = sessionTranscriptState(paths, sessionId);
1077
+ const generationId = stateBeforeDelete.generationId;
1078
+ retireStaleDeletedTombstoneLocked(
1079
+ paths,
1080
+ hash,
1081
+ stateBeforeDelete.liveGenerationId,
1082
+ );
1083
+ const currentTombstone = assertStoreTombstone(paths, hash);
1084
+ const alreadyDeleted =
1085
+ currentTombstone !== null &&
1086
+ currentTombstone.generationId === generationId;
1087
+ if (!alreadyDeleted) {
1088
+ const pendingPath = tombstonePath(paths, hash, "pending");
1089
+ const pending = canonicalJson({
1090
+ schema: PENDING_SCHEMA,
1091
+ sessionId,
1092
+ generationId: generationId ?? null,
1093
+ });
1094
+ if (!writeExclusiveFile(pendingPath, pending)) {
1095
+ const existing = verifyPendingRecord(paths, hash);
1096
+ if (
1097
+ existing?.sessionId !== sessionId ||
1098
+ existing?.generationId !== (generationId ?? null)
1099
+ ) {
1100
+ throw imageBundleError(
1101
+ "Session image deletion marker belongs to another generation",
1102
+ );
1103
+ }
1104
+ }
1105
+ }
1106
+ let result;
1107
+ try {
1108
+ result = task();
1109
+ } catch (error) {
1110
+ if (!alreadyDeleted) {
1111
+ const state = sessionTranscriptState(paths, sessionId);
1112
+ if (state.transcriptExists && !state.deletedWitness) {
1113
+ removePendingTombstoneLocked(paths, hash);
1114
+ }
1115
+ }
1116
+ throw error;
1117
+ }
1118
+ try {
1119
+ finishPendingDeletionLocked(paths, sessionId, generationId);
1120
+ } catch (cause) {
1121
+ throw imageBundleError(
1122
+ `Session transcript was deleted, but image bundle cleanup failed; retry session deletion (${cause?.message || "unknown cleanup error"})`,
1123
+ "SESSION_IMAGE_BUNDLE_DELETE_FAILED",
1124
+ cause,
1125
+ );
1126
+ }
1127
+ return result;
1128
+ },
1129
+ LOCK_OPTIONS,
1130
+ );
1131
+ },
1132
+ LOCK_OPTIONS,
1133
+ );
1134
+ }
1135
+
1136
+ export const _messageBundleStore = Object.freeze({
1137
+ storePaths,
1138
+ inspectExternalizableBundle,
1139
+ sessionHash,
1140
+ });