chainlesschain 0.163.6 → 0.163.7

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 (218) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-DDIUofA8.js → AIOps-axqQfwv8.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-Bm-5xYRn.js → ActionButton-DV-Ro0By.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-D5UH4ljI.js → Analytics-Bt72WEVu.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-D_ifjOOP.js → AppLayout-BtepG55E.js} +5 -5
  6. package/src/assets/web-panel/assets/{Artifacts-BY1mXdC7.js → Artifacts-B7KaJ7S_.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-Bujr4Ml3.js → Audit-FNFaUg7A.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-sioKSTcC.js → BackgroundAgents-DFb6J2wQ.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-DDOAK8RK.js → Backup-CC7YBhyj.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-CtCsVVke.js → BaseInput-RkgTNVtj.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-DpNj0vnd.js → Chat-hHEmGZy-.js} +4 -4
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-CZta3Ftr.js → ChatBubbleRenderer-CAP9YhHv.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-DoHVNSXq.js → Checkbox-z5wXOQP1.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-DlVl7Qmv.js → Codegen-DVaDJnxy.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-GIqoPYty.js → Col-CJmaRDR5.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-CIAPEVji.js → Community-DF29_2bI.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-BN7JiI_h.js → Compact-DtkQkBzn.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-CqUWCM9B.js → Compliance-xoHDxp7I.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-A4iPp7A4.js → Cowork-BUm-R7ht.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-B2zeXXWb.js → Cron-Cgl14ZkB.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-BR2xmWr8.js → Crosschain-BNcbmoqh.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-BzFDjrIv.js → DID-Bwja0Zo9.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-DKtDo3yA.js → Dashboard-Bk5XjEdJ.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-RNx43_YG.js → Dropdown-Ci36Oitq.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-SwMt4Rcy.js → EmailListRenderer--U04ZN4q.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-CsYebZ4X.js → FamilyGuardDashboard-B2gNPR2s.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-BmRKvBTy.js → Federation-C4KghYn7.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-CVxGTA-0.js → FormItemContext-QL7x_Q_0.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-x2iXpyB7.js → GenericCardRenderer-F3Ryltwm.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-DfqbkiOP.js → Git-2HassK5k.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-RuHyyj5O.js → Governance-DoBIp7oe.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-Bn7uZ7rI.js → Inference--W01QhZ-.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-DlP7XefZ.js → KnowledgeGraph-DmvwqGpT.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-BYNdOiCd.js → Logs-BBJ4yAel.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-aMtgN-R6.js → MarkdownRenderer-BLCZMzhJ.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-JQYmDqM8.js → Marketplace-D5lNngSI.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-Cyx4yZ-9.js → McpTools-CkEVOMKH.js} +4 -4
  38. package/src/assets/web-panel/assets/{Memory-VDuGZTN2.js → Memory-B1Ow1xVE.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-_yKo97gi.js → MobileBridge-SgYbUfb6.js} +1 -1
  40. package/src/assets/web-panel/assets/{MobileProjects-CXRXW3q7.js → MobileProjects-BWdOvQAj.js} +1 -1
  41. package/src/assets/web-panel/assets/{Mtc-acTGtfLH.js → Mtc-DcTXSSma.js} +5 -5
  42. package/src/assets/web-panel/assets/{MtcAudit-ClpjXfkY.js → MtcAudit-Dt3WG4oj.js} +4 -4
  43. package/src/assets/web-panel/assets/{Multisig-_6Tn3o96.js → Multisig-B9B38UZx.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-CK7pbZzP.js → NLProgramming-B0S4GGvG.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-Czy6V_P8.js → Notes-DZ5U58Ng.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-Cqx-UVv3.js → NotificationSettings-PjL5ZW34.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-C69WDxGy.js → OrderTableRenderer-DrAX2ESt.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-DFnA9vLQ.js → Organization-Cv1GYs-c.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-ReZkk2yT.js → Overflow-Bbkp_PL3.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-DzjSI0aI.js → P2P-BD8QUgoZ.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-DKTf5xIP.js → PdhVaultBrowser-BO-2PeaB.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-DhQ5klhR.js → Permissions-D_OPVa02.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub-CUL1iHrW.js → PersonalDataHub-mh-pEmaB.js} +4 -4
  54. package/src/assets/web-panel/assets/{Pipeline-DU88iu4N.js → Pipeline-6JC2EDdb.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-Bw-sYfyK.js → Privacy-CUshzUAG.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-Bq3Q5y0z.js → ProjectInit-Cs2Aalri.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-C8spzpho.js → ProjectSettings-1ak49tcj.js} +2 -2
  58. package/src/assets/web-panel/assets/Projects-BV4oApsm.js +1 -0
  59. package/src/assets/web-panel/assets/{Providers-Bf4eTP9W.js → Providers-CXXxvafH.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-Dme9BDU1.js → QrScannerModal-PTcDsF58.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-CIXfv1rE.js → QuickAsk-CDwucrJw.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-CIxvdeZ-.js → Recommend-CJlLVRXR.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-C6k7MExI.js → RemoteSession-DlN-ETsO.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-CISDz2bl.js → Reputation-DI23LR6m.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-CdnDxXyf.js → Row-Cl7UQMrM.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-C-QpRHsl.js → RssFeed-CexHNfh8.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-DBjaGzSn.js → Search-BneBWvRN.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-CQKukOeE.js → Security-DX7Phgrp.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-DZz86jWb.js → Services-BWXO8Mte.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-jb8RMK0d.js → Skeleton-DCyOjKPJ.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-CdY76fZG.js → Skills-CXqERsTB.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-C_CSiZX9.js → Sla-bhbrbz8E.js} +1 -1
  73. package/src/assets/web-panel/assets/SpeechSettings-3IPU0HBZ.js +1 -0
  74. package/src/assets/web-panel/assets/{SyncSettings-qvimGO0O.js → SyncSettings-DBNTQLbM.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-Dng5LPJC.js → Tasks-quP0bkN-.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-Bwqq5Wll.js → Templates-BouWmgho.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-DNWuwP7Z.js → Tenant-qv0D9s7z.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-B-6Othpp.js → Terminal-BxvbE8BB.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-DKpKZfzx.js → TimelineRenderer-D-IeLnJ2.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-DzfGMd2J.js → Tokens-CFMKKyVG.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-BXwUi74J.js → Trigger-L8as3UuJ.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-BNzUq4ui.js → Trust-MxmA4VZG.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-BYugxww4.js → UkeySign-C0VvJYRi.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-DdauhtSJ.js → VideoEditing-Bf5QZj8N.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-CSC9bCe_.js → Wallet-BIJ9riRz.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-Q8AN3bTO.js → WebAuthn-B8p9Mhkw.js} +5 -5
  87. package/src/assets/web-panel/assets/{WorkflowEditor-d-LBGCuA.js → WorkflowEditor-BVsuaPen.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-BLVDCgsZ.js → chat-BfkjoKpV.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-BZlPSEHl.js → colors-DeAlsWK8.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-IWseP6R6.js → compact-item-Cs97bz5j.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-DnVRV3Ao.js → createContext-BqbG60LH.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-DPty0mGN.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-DLQZA_Wn.js → hasIn-i_RY0b1X.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-B_N4-Gq9.js → index-0-agYlWL.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-DB0MJgF_.js → index-79V_zv1s.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BOZJBTWZ.js → index-B-3tEpTc.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-DUq0c7gq.js → index-B-JVTrpr.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-Dy0R5ZED.js → index-B1CbOH1i.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-Bxp7uDD8.js → index-B5UkyA3c.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DpVrK6fr.js → index-BESLFt4q.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CT9vVN-X.js → index-BOVkf1K3.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-DOoQpuQY.js → index-BYUVuwKt.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-CINJQEj6.js → index-BcUXAB2s.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CarMTIyQ.js → index-BkBi5TmC.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-C5dgnilC.js → index-Bm7lLR4c.js} +1 -1
  106. package/src/assets/web-panel/assets/index-Bt7g0Vq_.js +1 -0
  107. package/src/assets/web-panel/assets/{index-CbQNL0ue.js → index-BuLQobzW.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-CpBaJ3TD.js → index-CE2VtTlp.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-jWb70ZgZ.js → index-CED2PL_y.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-BEu2lMBy.js → index-CG3PRyAL.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DocEhinm.js → index-CIG8fQ2_.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-D_LQodeJ.js → index-COdGA-8n.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-DZpYeqrh.js → index-CeWoNHau.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-gt8bNefW.js → index-Cf3WCLmy.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-N_GoWZX4.js → index-CgsqdnRV.js} +1 -1
  116. package/src/assets/web-panel/assets/index-ChVT0p9u.js +1 -0
  117. package/src/assets/web-panel/assets/{index-CZDekj7T.js → index-D2B4VPyP.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-YSw_gxlZ.js → index-D9evtvg2.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DTbxp4UH.js → index-DUUrv141.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-Bbj2NS_I.js → index-DiSkx4UB.js} +3 -3
  121. package/src/assets/web-panel/assets/{index-BZs5bBnk.js → index-Dk2qouAz.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-DalA0TKC.js → index-Dk7-v4hP.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-1jeAudHd.js → index-DmbfhJUP.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-C1ifZtXo.js → index-DqBgBpgp.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-BhA3QEni.js → index-PT_5sBMA.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-BSjilZ9L.js → index-U4f5izHz.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-C2FqfMvU.js → index-YXgLng0K.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-COev0W2g.js → index-ZxO_-E8a.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-M7wxfAbm.js → index-dIHr4B_u.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-DjWnONdy.js → index-gyFpvL7l.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-Dkwsi1Kn.js → index-s1opAL-M.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-hwVDeoC0.js → index-yObj1O4n.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-Co67Kqmb.js → initDefaultProps-C9h9yKL3.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-BMOxQSF_.js → motion-BvmLsPdu.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-C5fWDw-N.js → move-CoL4ekU1.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-BJevpL0e.js → mtc-parser-C4pMy6xV.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-BCzNU8s6.js → omit-DPuJfNxx.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-CybWvDPk.js → pickAttrs-D0KEeaK2.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-BMWS9HnW.js → placementArrow-BpkyINxt.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-BoL2UnGY.js → responsiveObserve-DQI_wOq8.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-DWNfXraU.js → slide-DYfdDiRg.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-BquTM3tX.js → statusUtils-B5dtvb0D.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-BEkCleim.js → styleChecker-CF310mSj.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-Oecnq7EA.js → useFlexGapSupport-D1Qp4BLp.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-CkDffJ4F.js → useFs-C7_iyvJK.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-qbvKXesB.js → usePersonalDataHub-DZUt_fIm.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-C6v8n6mA.js → vnode-D3dsi0lZ.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-4CTeFILV.js → zoom-WdbOlvdQ.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/command-help-index.json +2 -2
  151. package/src/commands/agent.js +90 -1
  152. package/src/commands/chat.js +11 -2
  153. package/src/commands/compact.js +106 -23
  154. package/src/commands/loop.js +28 -10
  155. package/src/commands/routine.js +1 -1
  156. package/src/commands/scheduler-daemon.js +359 -2
  157. package/src/commands/session-observability.js +305 -0
  158. package/src/commands/session.js +38 -0
  159. package/src/data/changelog.json +14 -1
  160. package/src/gateways/ws/ws-agent-handler.js +439 -5
  161. package/src/harness/jsonl-session-store.js +463 -125
  162. package/src/harness/prompt-compressor.js +86 -13
  163. package/src/harness/provider-backed-compaction.js +100 -27
  164. package/src/lib/advisor-runtime.js +3 -1
  165. package/src/lib/agent-schedule-store.js +414 -4
  166. package/src/lib/agent-team/team-adjudication.js +40 -1
  167. package/src/lib/automation-engine.js +494 -0
  168. package/src/lib/background-agent-supervisor.js +793 -215
  169. package/src/lib/background-command-argv.js +24 -0
  170. package/src/lib/background-interaction-journal.js +361 -52
  171. package/src/lib/background-interaction-resolver.js +32 -0
  172. package/src/lib/background-launch-profile.js +2 -0
  173. package/src/lib/causal-observability.js +2661 -0
  174. package/src/lib/chat-session-admission.js +76 -0
  175. package/src/lib/cost-budget.js +5 -7
  176. package/src/lib/cowork/ab-comparator-cli.js +11 -0
  177. package/src/lib/cowork/debate-review-cli.js +12 -0
  178. package/src/lib/cowork-adapter.js +159 -38
  179. package/src/lib/cowork-cron.js +357 -1
  180. package/src/lib/cowork-task-runner.js +112 -17
  181. package/src/lib/delivery-coordinator.js +142 -0
  182. package/src/lib/direct-model-usage.js +121 -0
  183. package/src/lib/file-lines.js +183 -47
  184. package/src/lib/llm-pricing.js +12 -17
  185. package/src/lib/loop.js +26 -2
  186. package/src/lib/routine-store.js +380 -21
  187. package/src/lib/runtime-usage-ledger.js +182 -0
  188. package/src/lib/scheduler-kernel/agenda-adapter.js +308 -3
  189. package/src/lib/scheduler-kernel/automation-adapter.js +410 -8
  190. package/src/lib/scheduler-kernel/automation-event-adapter.js +36 -0
  191. package/src/lib/scheduler-kernel/cowork-cron-adapter.js +285 -3
  192. package/src/lib/scheduler-kernel/loop-adapter.js +541 -6
  193. package/src/lib/scheduler-kernel/migration-admin.js +346 -0
  194. package/src/lib/scheduler-kernel/migration-rollback-admin.js +932 -0
  195. package/src/lib/scheduler-kernel/routine-adapter.js +394 -20
  196. package/src/lib/scheduler-kernel/runtime.js +63 -1
  197. package/src/lib/scheduler-kernel/source-locator-path.js +142 -0
  198. package/src/lib/scheduler-kernel/store.js +2586 -85
  199. package/src/lib/session-host-lease.js +171 -2
  200. package/src/lib/session-host-snapshot.js +14 -3
  201. package/src/lib/session-persistence-failure.js +1 -0
  202. package/src/lib/session-tail.js +179 -34
  203. package/src/lib/session-transcript-structure.js +96 -0
  204. package/src/lib/sub-agent-context.js +328 -22
  205. package/src/lib/ws-agent-handler.js +4 -1
  206. package/src/repl/agent-repl.js +732 -91
  207. package/src/repl/chat-repl.js +5 -1
  208. package/src/runtime/agent-core.js +1272 -207
  209. package/src/runtime/fallback-model.js +5 -0
  210. package/src/runtime/headless-runner.js +324 -82
  211. package/src/runtime/headless-stream.js +232 -26
  212. package/src/runtime/policies/agent-policy.js +4 -0
  213. package/src/workers/background-agent-worker.js +403 -96
  214. package/src/assets/web-panel/assets/Projects-Dd-xuPIc.js +0 -1
  215. package/src/assets/web-panel/assets/SpeechSettings-C7tT9iad.js +0 -1
  216. package/src/assets/web-panel/assets/devWarning-Jzu62CLC.js +0 -1
  217. package/src/assets/web-panel/assets/index-Zb2MeDSy.js +0 -1
  218. package/src/assets/web-panel/assets/index-fikqml4k.js +0 -1
@@ -125,6 +125,54 @@ function normalizeActiveLease(value, fallbackRevocationEpoch = null) {
125
125
  });
126
126
  }
127
127
 
128
+ function normalizeWriteDelegation(value, expectedSessionId = null) {
129
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
130
+ throw new TypeError("session host write delegation must be an object");
131
+ }
132
+ const sessionId = canonicalSessionId(value.sessionId);
133
+ if (expectedSessionId !== null && sessionId !== expectedSessionId) {
134
+ throw leaseError(
135
+ SESSION_HOST_LEASE_FENCED_CODE,
136
+ `Session host write delegation belongs to a different session`,
137
+ { sessionId: expectedSessionId, delegatedSessionId: sessionId },
138
+ );
139
+ }
140
+ if (
141
+ typeof value.leaseId !== "string" ||
142
+ !LEASE_ID_PATTERN.test(value.leaseId) ||
143
+ !Number.isSafeInteger(value.fencingToken) ||
144
+ value.fencingToken < 1 ||
145
+ !Number.isSafeInteger(value.revocationEpoch) ||
146
+ value.revocationEpoch < 0 ||
147
+ !Number.isSafeInteger(value.ownerPid) ||
148
+ value.ownerPid < 1 ||
149
+ typeof value.hostKind !== "string" ||
150
+ !HOST_KIND_PATTERN.test(value.hostKind)
151
+ ) {
152
+ throw new TypeError("session host write delegation is invalid");
153
+ }
154
+ return Object.freeze({
155
+ sessionId,
156
+ leaseId: value.leaseId,
157
+ fencingToken: value.fencingToken,
158
+ revocationEpoch: value.revocationEpoch,
159
+ ownerPid: value.ownerPid,
160
+ hostKind: value.hostKind,
161
+ });
162
+ }
163
+
164
+ function delegationMatchesLease(delegation, lease) {
165
+ return Boolean(
166
+ delegation &&
167
+ lease &&
168
+ delegation.leaseId === lease.leaseId &&
169
+ delegation.fencingToken === lease.fencingToken &&
170
+ delegation.revocationEpoch === lease.revocationEpoch &&
171
+ delegation.ownerPid === lease.ownerPid &&
172
+ delegation.hostKind === lease.hostKind,
173
+ );
174
+ }
175
+
128
176
  function normalizeRevocation(value, expectedEpoch) {
129
177
  if (!value || typeof value !== "object" || Array.isArray(value)) {
130
178
  throw new TypeError("session host revocation must be an object");
@@ -268,6 +316,7 @@ export class SessionHostLeaseAuthority {
268
316
  this._lock = lock;
269
317
  this._local = new Map();
270
318
  this._fenced = new Map();
319
+ this._delegatedWrites = [];
271
320
  }
272
321
 
273
322
  pathFor(sessionId) {
@@ -568,6 +617,8 @@ export class SessionHostLeaseAuthority {
568
617
  leaseId: record.active.leaseId,
569
618
  fencingToken: record.active.fencingToken,
570
619
  revocationEpoch: record.active.revocationEpoch,
620
+ ownerPid: record.active.ownerPid,
621
+ hostKind: record.active.hostKind,
571
622
  signal: controller.signal,
572
623
  renew,
573
624
  assert: () => this.withWriteAuthority(id, (authority) => authority),
@@ -756,8 +807,16 @@ export class SessionHostLeaseAuthority {
756
807
  const priorFence = this._fenced.get(key);
757
808
  if (priorFence) throw priorFence;
758
809
  const local = this._local.get(key);
810
+ const delegation = this._delegatedWrites.at(-1) || null;
759
811
  if (!fs.existsSync(filePath)) {
760
- if (!local) return invokeTask(null);
812
+ if (!delegation && !local) return invokeTask(null);
813
+ if (delegation) {
814
+ throw leaseError(
815
+ SESSION_HOST_LEASE_FENCED_CODE,
816
+ `Delegated session write lost its active host lease: ${id}`,
817
+ { sessionId: id, delegatedLeaseId: delegation.leaseId },
818
+ );
819
+ }
761
820
  const error = leaseError(
762
821
  SESSION_HOST_LEASE_FENCED_CODE,
763
822
  `Session host authority disappeared while locally held: ${id}`,
@@ -777,6 +836,43 @@ export class SessionHostLeaseAuthority {
777
836
  const snapshot = this._read(filePath, id);
778
837
  const record = this._local.get(key);
779
838
  const now = Math.trunc(this._now());
839
+ // A delegated scope is always constrained by the delegated lease.
840
+ // Do this before considering a local record: an authority instance
841
+ // may have acquired a successor/recovery lease after the delegation
842
+ // was minted, and that local authority must never upgrade the stale
843
+ // capability into a write under the successor.
844
+ if (
845
+ delegation &&
846
+ (!snapshot.active ||
847
+ !delegationMatchesLease(delegation, snapshot.active) ||
848
+ delegation.sessionId !== id ||
849
+ snapshot.active.expiresAtMs <= now)
850
+ ) {
851
+ throw leaseError(
852
+ SESSION_HOST_LEASE_FENCED_CODE,
853
+ `Delegated session write is fenced by a revocation, successor, or expired host lease: ${id}`,
854
+ {
855
+ sessionId: id,
856
+ delegatedLeaseId: delegation.leaseId,
857
+ delegatedFencingToken: delegation.fencingToken,
858
+ activeLeaseId: snapshot.active?.leaseId ?? null,
859
+ activeFencingToken: snapshot.active?.fencingToken ?? null,
860
+ activeRevocationEpoch: snapshot.revocationEpoch,
861
+ },
862
+ );
863
+ }
864
+ if (delegation && snapshot.active) {
865
+ return invokeTask(
866
+ Object.freeze({
867
+ leaseId: snapshot.active.leaseId,
868
+ fencingToken: snapshot.active.fencingToken,
869
+ revocationEpoch: snapshot.active.revocationEpoch,
870
+ ownerPid: snapshot.active.ownerPid,
871
+ hostKind: snapshot.active.hostKind,
872
+ delegated: true,
873
+ }),
874
+ );
875
+ }
780
876
  if (
781
877
  record &&
782
878
  (record.released ||
@@ -806,7 +902,7 @@ export class SessionHostLeaseAuthority {
806
902
  `Session write is fenced by another host lease: ${id}`,
807
903
  {
808
904
  sessionId: id,
809
- activeFencingToken: snapshot.active.fencingToken,
905
+ activeFencingToken: snapshot.active?.fencingToken ?? null,
810
906
  localFencingToken: null,
811
907
  activeRevocationEpoch: snapshot.revocationEpoch,
812
908
  },
@@ -837,6 +933,41 @@ export class SessionHostLeaseAuthority {
837
933
  throw this._unavailable(id, filePath, cause);
838
934
  }
839
935
  }
936
+
937
+ withDelegatedWriteAuthority(sessionId, delegation, task, options = {}) {
938
+ if (typeof task !== "function") {
939
+ throw new TypeError("delegated session host write callback is required");
940
+ }
941
+ const id = canonicalSessionId(sessionId);
942
+ const normalized = normalizeWriteDelegation(delegation, id);
943
+ if (
944
+ options.expectedOwnerPid !== undefined &&
945
+ normalized.ownerPid !==
946
+ positiveInteger(options.expectedOwnerPid, "delegated owner pid")
947
+ ) {
948
+ throw leaseError(
949
+ SESSION_HOST_LEASE_FENCED_CODE,
950
+ `Session host write delegation owner does not match the turn child (delegated ${normalized.ownerPid}, expected ${Number(options.expectedOwnerPid)})`,
951
+ {
952
+ sessionId: id,
953
+ delegatedOwnerPid: normalized.ownerPid,
954
+ expectedOwnerPid: Number(options.expectedOwnerPid),
955
+ },
956
+ );
957
+ }
958
+ this._delegatedWrites.push(normalized);
959
+ try {
960
+ const result = task();
961
+ if (result && typeof result.then === "function") {
962
+ throw new TypeError(
963
+ "delegated session host write callback must be synchronous",
964
+ );
965
+ }
966
+ return result;
967
+ } finally {
968
+ this._delegatedWrites.pop();
969
+ }
970
+ }
840
971
  }
841
972
 
842
973
  const defaultAuthority = new SessionHostLeaseAuthority();
@@ -849,6 +980,44 @@ export function withSessionHostWriteAuthority(sessionId, task) {
849
980
  return defaultAuthority.withWriteAuthority(sessionId, task);
850
981
  }
851
982
 
983
+ export function createSessionHostWriteDelegation(lease) {
984
+ return normalizeWriteDelegation(lease);
985
+ }
986
+
987
+ export function withSessionHostDelegatedWriteAuthority(
988
+ sessionId,
989
+ delegation,
990
+ task,
991
+ options,
992
+ ) {
993
+ return defaultAuthority.withDelegatedWriteAuthority(
994
+ sessionId,
995
+ delegation,
996
+ task,
997
+ options,
998
+ );
999
+ }
1000
+
1001
+ export function withSessionHostRecoveryLease(
1002
+ sessionId,
1003
+ task,
1004
+ { hostKind = "background-recovery" } = {},
1005
+ ) {
1006
+ if (typeof task !== "function") {
1007
+ throw new TypeError("session host recovery callback is required");
1008
+ }
1009
+ const lease = acquireSessionHostLease(sessionId, { hostKind });
1010
+ try {
1011
+ const result = task(lease);
1012
+ if (result && typeof result.then === "function") {
1013
+ throw new TypeError("session host recovery callback must be synchronous");
1014
+ }
1015
+ return result;
1016
+ } finally {
1017
+ lease.release();
1018
+ }
1019
+ }
1020
+
852
1021
  export function assertSessionHostWriteAuthority(sessionId) {
853
1022
  return defaultAuthority.withWriteAuthority(
854
1023
  sessionId,
@@ -26,6 +26,7 @@ import {
26
26
  sanitizePersistedMessage,
27
27
  sanitizePersistedMessages,
28
28
  } from "./session-message-provenance.js";
29
+ import { createSessionTranscriptStructureProjection } from "./session-transcript-structure.js";
29
30
 
30
31
  export const SESSION_HOST_SNAPSHOT_SCHEMA =
31
32
  "chainlesschain.session-host-snapshot/v1";
@@ -122,12 +123,17 @@ function titleFromEvents(events) {
122
123
  return title;
123
124
  }
124
125
 
125
- function validatePlainSessionEvents(events) {
126
+ function validatePlainSessionEvents(events, sessionId) {
126
127
  // Validate the complete nested graph without invoking accessors/Proxy traps,
127
128
  // then retain the original in-process objects so an already-established
128
129
  // runtime WeakMap provenance capability is not lost on the trusted legacy
129
130
  // adapter seam.
130
131
  sanitizePersistedMessages(events, { strict: true });
132
+ const structure = createSessionTranscriptStructureProjection(sessionId, {
133
+ failFast: true,
134
+ });
135
+ for (const event of events) structure.accept(event);
136
+ structure.finish();
131
137
  return events;
132
138
  }
133
139
 
@@ -250,7 +256,7 @@ export function projectSessionHostObservation({
250
256
  messages,
251
257
  recovery,
252
258
  }) {
253
- const safeEvents = validatePlainSessionEvents(events);
259
+ const safeEvents = validatePlainSessionEvents(events, sessionId);
254
260
  if (safeEvents.length === 0) {
255
261
  throw new TypeError("Verified session events must be a non-empty array");
256
262
  }
@@ -271,7 +277,7 @@ export function projectSessionHostObservation({
271
277
 
272
278
  /** Build a content-free snapshot from one already-verified, stable event set. */
273
279
  export function projectVerifiedSessionHostSnapshot(sessionId, events) {
274
- const safeEvents = validatePlainSessionEvents(events);
280
+ const safeEvents = validatePlainSessionEvents(events, sessionId);
275
281
  const messages = replayMessagesFromVerifiedEvents(safeEvents);
276
282
  const recovery = reduceMcpLedgerEvents(safeEvents, {
277
283
  sessionId,
@@ -292,6 +298,9 @@ export function projectVerifiedSessionHostSnapshot(sessionId, events) {
292
298
  function createStreamingSessionHostProjection(sessionId) {
293
299
  let title = "Untitled";
294
300
  let lastEventType = null;
301
+ const structure = createSessionTranscriptStructureProjection(sessionId, {
302
+ failFast: true,
303
+ });
295
304
  const mcpReducer = createMcpLedgerEventReducer({
296
305
  sessionId,
297
306
  verified: true,
@@ -303,6 +312,7 @@ function createStreamingSessionHostProjection(sessionId) {
303
312
  if (!safeEvent || typeof safeEvent !== "object") {
304
313
  throw new TypeError("Verified session event must be plain JSON data");
305
314
  }
315
+ structure.accept(safeEvent);
306
316
  if (safeEvent.type === "session_start" && safeEvent.data?.title) {
307
317
  title = String(safeEvent.data.title);
308
318
  } else if (safeEvent.type === "session_rename" && safeEvent.data?.title) {
@@ -313,6 +323,7 @@ function createStreamingSessionHostProjection(sessionId) {
313
323
  mcpReducer.accept(safeEvent);
314
324
  },
315
325
  finish({ headHash, eventCount, readMessages }) {
326
+ structure.finish();
316
327
  if (typeof readMessages !== "function") {
317
328
  throw new TypeError("Verified resume message reader is missing");
318
329
  }
@@ -10,6 +10,7 @@ const COMMIT_STATES = new Set(["not-committed", "unknown", "committed"]);
10
10
  const OPERATIONS = new Set([
11
11
  "append-event",
12
12
  "append-authority-event",
13
+ "verified-projection-authority-append",
13
14
  "transcript-append",
14
15
  "transcript-settlement",
15
16
  "session-start",
@@ -11,11 +11,19 @@
11
11
  import { promises as fsp } from "node:fs";
12
12
  import { existsSync, statSync } from "node:fs";
13
13
  import {
14
+ assertCanonicalJsonlRecordByteLength,
15
+ assertCanonicalJsonlRecordSize,
16
+ CANONICAL_JSONL_RECORD_MAX_BYTES,
17
+ CANONICAL_JSONL_RECORD_TOO_LARGE_CODE,
14
18
  getSessionPresence,
19
+ parseCanonicalJsonlRecord,
20
+ resolveCanonicalJsonlRecordMaxBytes,
15
21
  SESSION_PRESENCE,
16
22
  sessionPath,
17
23
  } from "../harness/jsonl-session-store.js";
18
24
 
25
+ const DEFAULT_LIVE_READ_CHUNK_BYTES = 64 * 1024;
26
+
19
27
  export function readLiveTranscriptPresence(
20
28
  sessionId,
21
29
  readPresence = getSessionPresence,
@@ -59,35 +67,149 @@ function assertLiveTranscriptReadable(sessionId) {
59
67
  throw error;
60
68
  }
61
69
 
62
- function parseLine(line) {
70
+ function positiveSafeInteger(value, label) {
71
+ const parsed = Number(value);
72
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
73
+ throw new TypeError(`${label} must be a positive safe integer`);
74
+ }
75
+ return parsed;
76
+ }
77
+
78
+ function nonNegativeSafeInteger(value, label) {
79
+ const parsed = Number(value);
80
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
81
+ throw new TypeError(`${label} must be a non-negative safe integer`);
82
+ }
83
+ return parsed;
84
+ }
85
+
86
+ function rethrowCanonicalRecordLimit(error) {
87
+ if (error?.code === CANONICAL_JSONL_RECORD_TOO_LARGE_CODE) throw error;
88
+ }
89
+
90
+ function parseLine(line, maxRecordBytes = CANONICAL_JSONL_RECORD_MAX_BYTES) {
91
+ // Bound attacker-controlled text before trim() can copy it and again inside
92
+ // the canonical parser before JSON.parse.
93
+ assertCanonicalJsonlRecordSize(line, maxRecordBytes);
63
94
  const trimmed = line.trim();
64
95
  if (!trimmed) return null;
65
96
  try {
66
- return JSON.parse(trimmed);
67
- } catch {
97
+ return parseCanonicalJsonlRecord(trimmed, maxRecordBytes);
98
+ } catch (error) {
99
+ rethrowCanonicalRecordLimit(error);
68
100
  return null;
69
101
  }
70
102
  }
71
103
 
104
+ function assertUtf8RangeSize(text, start, end, maxRecordBytes) {
105
+ let byteLength = 0;
106
+ for (let index = start; index < end; index += 1) {
107
+ const codeUnit = text.charCodeAt(index);
108
+ if (codeUnit <= 0x7f) {
109
+ byteLength += 1;
110
+ } else if (codeUnit <= 0x7ff) {
111
+ byteLength += 2;
112
+ } else if (
113
+ codeUnit >= 0xd800 &&
114
+ codeUnit <= 0xdbff &&
115
+ index + 1 < end &&
116
+ text.charCodeAt(index + 1) >= 0xdc00 &&
117
+ text.charCodeAt(index + 1) <= 0xdfff
118
+ ) {
119
+ byteLength += 4;
120
+ index += 1;
121
+ } else {
122
+ // Lone UTF-16 surrogates are encoded as the three-byte replacement
123
+ // character by Buffer.byteLength()/UTF-8 decoders.
124
+ byteLength += 3;
125
+ }
126
+ }
127
+ assertCanonicalJsonlRecordByteLength(byteLength, maxRecordBytes);
128
+ }
129
+
72
130
  /**
73
131
  * Parse a contiguous chunk of buffered text into events. Returns {events, rest}
74
132
  * where `rest` is the unterminated trailing partial line to keep for the next
75
133
  * read.
76
134
  */
77
- export function parseChunk(buffer) {
135
+ export function parseChunk(
136
+ buffer,
137
+ { maxRecordBytes = CANONICAL_JSONL_RECORD_MAX_BYTES } = {},
138
+ ) {
139
+ const limit = resolveCanonicalJsonlRecordMaxBytes(maxRecordBytes);
78
140
  const events = [];
79
- let rest = buffer;
80
- let nl = rest.indexOf("\n");
141
+ let start = 0;
142
+ let nl = buffer.indexOf("\n", start);
81
143
  while (nl !== -1) {
82
- const line = rest.slice(0, nl);
83
- rest = rest.slice(nl + 1);
84
- const evt = parseLine(line);
144
+ // Count the source range before slice() can retain/copy an attacker-sized
145
+ // terminated line.
146
+ assertUtf8RangeSize(buffer, start, nl, limit);
147
+ const line = buffer.slice(start, nl);
148
+ const evt = parseLine(line, limit);
85
149
  if (evt) events.push(evt);
86
- nl = rest.indexOf("\n");
150
+ start = nl + 1;
151
+ nl = buffer.indexOf("\n", start);
87
152
  }
153
+ assertUtf8RangeSize(buffer, start, buffer.length, limit);
154
+ const rest = buffer.slice(start);
88
155
  return { events, rest };
89
156
  }
90
157
 
158
+ function offsetBoundaryError(offset) {
159
+ const error = new Error(
160
+ `Session tail offset ${offset} is not at a JSONL record boundary`,
161
+ );
162
+ error.code = "SESSION_TAIL_OFFSET_NOT_RECORD_BOUNDARY";
163
+ error.offset = offset;
164
+ return error;
165
+ }
166
+
167
+ async function assertOffsetAtRecordBoundary(fd, offset, fileSize) {
168
+ if (offset === 0) return;
169
+ if (offset > fileSize) throw offsetBoundaryError(offset);
170
+ const previousByte = Buffer.allocUnsafe(1);
171
+ const read = await fd.read(previousByte, 0, 1, offset - 1);
172
+ if (read.bytesRead !== 1 || previousByte[0] !== 0x0a) {
173
+ throw offsetBoundaryError(offset);
174
+ }
175
+ }
176
+
177
+ function joinedRecordBuffer(parts, byteLength) {
178
+ if (parts.length === 0) return Buffer.alloc(0);
179
+ if (parts.length === 1) return parts[0];
180
+ return Buffer.concat(parts, byteLength);
181
+ }
182
+
183
+ function consumeLiveJsonlBytes(state, bytes, maxRecordBytes) {
184
+ const events = [];
185
+ let start = 0;
186
+ let newline = bytes.indexOf(0x0a, start);
187
+ while (newline >= 0) {
188
+ const segment = bytes.subarray(start, newline);
189
+ const recordBytes = state.byteLength + segment.length;
190
+ assertCanonicalJsonlRecordByteLength(recordBytes, maxRecordBytes);
191
+ const record = joinedRecordBuffer(
192
+ segment.length > 0 ? [...state.parts, segment] : state.parts,
193
+ recordBytes,
194
+ );
195
+ // Decode only after the byte limit passes. The parser repeats the same
196
+ // canonical check immediately before JSON.parse.
197
+ const event = parseLine(record.toString("utf8"), maxRecordBytes);
198
+ if (event) events.push(event);
199
+ state.parts = [];
200
+ state.byteLength = 0;
201
+ start = newline + 1;
202
+ newline = bytes.indexOf(0x0a, start);
203
+ }
204
+
205
+ const trailing = bytes.subarray(start);
206
+ const pendingBytes = state.byteLength + trailing.length;
207
+ assertCanonicalJsonlRecordByteLength(pendingBytes, maxRecordBytes);
208
+ if (trailing.length > 0) state.parts.push(Buffer.from(trailing));
209
+ state.byteLength = pendingBytes;
210
+ return events;
211
+ }
212
+
91
213
  function matchesFilter(event, { types, sinceMs }) {
92
214
  if (types && types.length > 0 && !types.includes(event.type)) return false;
93
215
  if (sinceMs && event.timestamp && event.timestamp < sinceMs) return false;
@@ -119,6 +241,8 @@ export function initialOffset(sessionId, { fromStart = false } = {}) {
119
241
  * - types string[] of event.type to include (null = all)
120
242
  * - sinceMs only yield events with timestamp >= sinceMs
121
243
  * - once if true, stop once file is drained (no polling)
244
+ * - maxRecordBytes canonical JSONL record cap (test seam; default 16 MiB)
245
+ * - readChunkBytes bounded file read size (test seam; default 64 KiB)
122
246
  */
123
247
  export async function* followSession(sessionId, options = {}) {
124
248
  const {
@@ -129,18 +253,23 @@ export async function* followSession(sessionId, options = {}) {
129
253
  types = null,
130
254
  sinceMs = null,
131
255
  once = false,
256
+ maxRecordBytes = CANONICAL_JSONL_RECORD_MAX_BYTES,
257
+ readChunkBytes = DEFAULT_LIVE_READ_CHUNK_BYTES,
132
258
  } = options;
133
259
 
260
+ const recordLimit = resolveCanonicalJsonlRecordMaxBytes(maxRecordBytes);
261
+ const readLimit = Math.min(
262
+ positiveSafeInteger(readChunkBytes, "readChunkBytes"),
263
+ recordLimit,
264
+ );
265
+
134
266
  const filePath = sessionPath(sessionId);
135
- let offset =
136
- typeof fromOffset === "number"
137
- ? fromOffset
138
- : initialOffset(sessionId, { fromStart });
139
- let buffer = "";
140
- // Streaming decoder persisted across polls: when the file grows by a partial
141
- // multi-byte UTF-8 character between reads (the rest arrives next poll), the
142
- // held bytes are reassembled instead of corrupted into U+FFFD.
143
- let decoder = new TextDecoder("utf-8");
267
+ const hasExplicitOffset = fromOffset !== undefined;
268
+ let offset = hasExplicitOffset
269
+ ? nonNegativeSafeInteger(fromOffset, "fromOffset")
270
+ : initialOffset(sessionId, { fromStart });
271
+ const pending = { parts: [], byteLength: 0 };
272
+ let offsetBoundaryValidated = offset === 0;
144
273
 
145
274
  while (true) {
146
275
  if (signal?.aborted) return;
@@ -156,27 +285,43 @@ export async function* followSession(sessionId, options = {}) {
156
285
  if (stat.size < offset) {
157
286
  // File was truncated / rotated — restart from beginning
158
287
  offset = 0;
159
- buffer = "";
160
- decoder = new TextDecoder("utf-8"); // drop any held partial-char bytes
288
+ pending.parts = [];
289
+ pending.byteLength = 0;
290
+ offsetBoundaryValidated = true;
161
291
  }
162
- if (stat.size > offset) {
292
+ if (!offsetBoundaryValidated || stat.size > offset) {
163
293
  const fd = await fsp.open(filePath, "r");
164
294
  try {
165
- const length = stat.size - offset;
166
- const buf = Buffer.alloc(length);
167
- await fd.read(buf, 0, length, offset);
168
- offset = stat.size;
169
- buffer += decoder.decode(buf, { stream: true });
295
+ const targetOffset = stat.size;
296
+ if (!offsetBoundaryValidated) {
297
+ await assertOffsetAtRecordBoundary(fd, offset, targetOffset);
298
+ offsetBoundaryValidated = true;
299
+ }
300
+ while (offset < targetOffset) {
301
+ const length = Math.min(readLimit, targetOffset - offset);
302
+ const buf = Buffer.allocUnsafe(length);
303
+ const read = await fd.read(buf, 0, length, offset);
304
+ if (
305
+ !Number.isSafeInteger(read.bytesRead) ||
306
+ read.bytesRead <= 0
307
+ ) {
308
+ break;
309
+ }
310
+ offset += read.bytesRead;
311
+ const events = consumeLiveJsonlBytes(
312
+ pending,
313
+ buf.subarray(0, read.bytesRead),
314
+ recordLimit,
315
+ );
316
+ for (const evt of events) {
317
+ if (matchesFilter(evt, { types, sinceMs })) {
318
+ yield { event: evt, offset };
319
+ }
320
+ }
321
+ }
170
322
  } finally {
171
323
  await fd.close();
172
324
  }
173
- const { events, rest } = parseChunk(buffer);
174
- buffer = rest;
175
- for (const evt of events) {
176
- if (matchesFilter(evt, { types, sinceMs })) {
177
- yield { event: evt, offset };
178
- }
179
- }
180
325
  }
181
326
  } catch (error) {
182
327
  if (error?.code !== "ENOENT") throw error;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Streaming structural authority for canonical session transcripts.
3
+ *
4
+ * Hash-chain verification proves that bytes were not changed after they were
5
+ * anchored. It does not prove that the event protocol has one genesis marker.
6
+ * Every usable session must therefore contain exactly one `session_start`, and
7
+ * that marker must be the first event.
8
+ */
9
+
10
+ export const SESSION_TRANSCRIPT_STRUCTURE_ERROR_CODE =
11
+ "CC_SESSION_TRANSCRIPT_STRUCTURE_INVALID";
12
+
13
+ function structureReason({ eventCount, sessionStartCount, sessionStartIndex }) {
14
+ if (eventCount > 0 && sessionStartIndex !== 0) {
15
+ return "session_start must be the first event";
16
+ }
17
+ if (sessionStartCount !== 1) {
18
+ return "session must contain exactly one session_start event";
19
+ }
20
+ return null;
21
+ }
22
+
23
+ function structureError(sessionId, reason, projection) {
24
+ const label =
25
+ typeof sessionId === "string" && sessionId.length > 0
26
+ ? sessionId.slice(0, 256)
27
+ : "unknown";
28
+ const error = new Error(`session ${label} ${reason}`);
29
+ error.code = SESSION_TRANSCRIPT_STRUCTURE_ERROR_CODE;
30
+ error.sessionId = sessionId ?? null;
31
+ error.structure = projection;
32
+ return error;
33
+ }
34
+
35
+ /**
36
+ * Build a constant-space structure projection for a stream of session events.
37
+ *
38
+ * `failFast` rejects violations that are already irreversible while events are
39
+ * accepted. `finish({ assertValid: true })` additionally rejects a missing
40
+ * marker once the complete stream is known.
41
+ */
42
+ export function createSessionTranscriptStructureProjection(
43
+ sessionId,
44
+ { failFast = false } = {},
45
+ ) {
46
+ let eventCount = 0;
47
+ let sessionStartCount = 0;
48
+ let sessionStartIndex = null;
49
+
50
+ const snapshot = () => {
51
+ const projection = {
52
+ eventCount,
53
+ sessionStartCount,
54
+ sessionStartIndex,
55
+ hasStartEvent: sessionStartCount > 0,
56
+ };
57
+ const reason = structureReason(projection);
58
+ return Object.freeze({
59
+ ...projection,
60
+ sessionStartIsFirst: sessionStartIndex === 0,
61
+ valid: reason === null,
62
+ reason,
63
+ });
64
+ };
65
+
66
+ const assertProjection = (projection) => {
67
+ if (!projection.valid) {
68
+ throw structureError(sessionId, projection.reason, projection);
69
+ }
70
+ return projection;
71
+ };
72
+
73
+ return Object.freeze({
74
+ accept(event) {
75
+ const index = eventCount;
76
+ eventCount += 1;
77
+ if (event?.type === "session_start") {
78
+ sessionStartCount += 1;
79
+ if (sessionStartIndex === null) sessionStartIndex = index;
80
+ }
81
+
82
+ const projection = snapshot();
83
+ const irreversibleViolation =
84
+ (index === 0 && event?.type !== "session_start") ||
85
+ sessionStartCount > 1;
86
+ if (failFast && irreversibleViolation) {
87
+ assertProjection(projection);
88
+ }
89
+ return projection;
90
+ },
91
+ finish({ assertValid = failFast } = {}) {
92
+ const projection = snapshot();
93
+ return assertValid ? assertProjection(projection) : projection;
94
+ },
95
+ });
96
+ }