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.
- package/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-DDIUofA8.js → AIOps-axqQfwv8.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-Bm-5xYRn.js → ActionButton-DV-Ro0By.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-D5UH4ljI.js → Analytics-Bt72WEVu.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-D_ifjOOP.js → AppLayout-BtepG55E.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-BY1mXdC7.js → Artifacts-B7KaJ7S_.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-Bujr4Ml3.js → Audit-FNFaUg7A.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-sioKSTcC.js → BackgroundAgents-DFb6J2wQ.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DDOAK8RK.js → Backup-CC7YBhyj.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CtCsVVke.js → BaseInput-RkgTNVtj.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DpNj0vnd.js → Chat-hHEmGZy-.js} +4 -4
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CZta3Ftr.js → ChatBubbleRenderer-CAP9YhHv.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-DoHVNSXq.js → Checkbox-z5wXOQP1.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-DlVl7Qmv.js → Codegen-DVaDJnxy.js} +1 -1
- package/src/assets/web-panel/assets/{Col-GIqoPYty.js → Col-CJmaRDR5.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CIAPEVji.js → Community-DF29_2bI.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BN7JiI_h.js → Compact-DtkQkBzn.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CqUWCM9B.js → Compliance-xoHDxp7I.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-A4iPp7A4.js → Cowork-BUm-R7ht.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-B2zeXXWb.js → Cron-Cgl14ZkB.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-BR2xmWr8.js → Crosschain-BNcbmoqh.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BzFDjrIv.js → DID-Bwja0Zo9.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-DKtDo3yA.js → Dashboard-Bk5XjEdJ.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-RNx43_YG.js → Dropdown-Ci36Oitq.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-SwMt4Rcy.js → EmailListRenderer--U04ZN4q.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-CsYebZ4X.js → FamilyGuardDashboard-B2gNPR2s.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-BmRKvBTy.js → Federation-C4KghYn7.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-CVxGTA-0.js → FormItemContext-QL7x_Q_0.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-x2iXpyB7.js → GenericCardRenderer-F3Ryltwm.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DfqbkiOP.js → Git-2HassK5k.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-RuHyyj5O.js → Governance-DoBIp7oe.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-Bn7uZ7rI.js → Inference--W01QhZ-.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DlP7XefZ.js → KnowledgeGraph-DmvwqGpT.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BYNdOiCd.js → Logs-BBJ4yAel.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-aMtgN-R6.js → MarkdownRenderer-BLCZMzhJ.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-JQYmDqM8.js → Marketplace-D5lNngSI.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-Cyx4yZ-9.js → McpTools-CkEVOMKH.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-VDuGZTN2.js → Memory-B1Ow1xVE.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-_yKo97gi.js → MobileBridge-SgYbUfb6.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-CXRXW3q7.js → MobileProjects-BWdOvQAj.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-acTGtfLH.js → Mtc-DcTXSSma.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-ClpjXfkY.js → MtcAudit-Dt3WG4oj.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-_6Tn3o96.js → Multisig-B9B38UZx.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CK7pbZzP.js → NLProgramming-B0S4GGvG.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-Czy6V_P8.js → Notes-DZ5U58Ng.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-Cqx-UVv3.js → NotificationSettings-PjL5ZW34.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-C69WDxGy.js → OrderTableRenderer-DrAX2ESt.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-DFnA9vLQ.js → Organization-Cv1GYs-c.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-ReZkk2yT.js → Overflow-Bbkp_PL3.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-DzjSI0aI.js → P2P-BD8QUgoZ.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-DKTf5xIP.js → PdhVaultBrowser-BO-2PeaB.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-DhQ5klhR.js → Permissions-D_OPVa02.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-CUL1iHrW.js → PersonalDataHub-mh-pEmaB.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-DU88iu4N.js → Pipeline-6JC2EDdb.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-Bw-sYfyK.js → Privacy-CUshzUAG.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-Bq3Q5y0z.js → ProjectInit-Cs2Aalri.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-C8spzpho.js → ProjectSettings-1ak49tcj.js} +2 -2
- package/src/assets/web-panel/assets/Projects-BV4oApsm.js +1 -0
- package/src/assets/web-panel/assets/{Providers-Bf4eTP9W.js → Providers-CXXxvafH.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-Dme9BDU1.js → QrScannerModal-PTcDsF58.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CIXfv1rE.js → QuickAsk-CDwucrJw.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CIxvdeZ-.js → Recommend-CJlLVRXR.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-C6k7MExI.js → RemoteSession-DlN-ETsO.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-CISDz2bl.js → Reputation-DI23LR6m.js} +1 -1
- package/src/assets/web-panel/assets/{Row-CdnDxXyf.js → Row-Cl7UQMrM.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-C-QpRHsl.js → RssFeed-CexHNfh8.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DBjaGzSn.js → Search-BneBWvRN.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CQKukOeE.js → Security-DX7Phgrp.js} +4 -4
- package/src/assets/web-panel/assets/{Services-DZz86jWb.js → Services-BWXO8Mte.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-jb8RMK0d.js → Skeleton-DCyOjKPJ.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CdY76fZG.js → Skills-CXqERsTB.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-C_CSiZX9.js → Sla-bhbrbz8E.js} +1 -1
- package/src/assets/web-panel/assets/SpeechSettings-3IPU0HBZ.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-qvimGO0O.js → SyncSettings-DBNTQLbM.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-Dng5LPJC.js → Tasks-quP0bkN-.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-Bwqq5Wll.js → Templates-BouWmgho.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-DNWuwP7Z.js → Tenant-qv0D9s7z.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-B-6Othpp.js → Terminal-BxvbE8BB.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-DKpKZfzx.js → TimelineRenderer-D-IeLnJ2.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-DzfGMd2J.js → Tokens-CFMKKyVG.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BXwUi74J.js → Trigger-L8as3UuJ.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BNzUq4ui.js → Trust-MxmA4VZG.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-BYugxww4.js → UkeySign-C0VvJYRi.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DdauhtSJ.js → VideoEditing-Bf5QZj8N.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CSC9bCe_.js → Wallet-BIJ9riRz.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-Q8AN3bTO.js → WebAuthn-B8p9Mhkw.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-d-LBGCuA.js → WorkflowEditor-BVsuaPen.js} +1 -1
- package/src/assets/web-panel/assets/{chat-BLVDCgsZ.js → chat-BfkjoKpV.js} +1 -1
- package/src/assets/web-panel/assets/{colors-BZlPSEHl.js → colors-DeAlsWK8.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-IWseP6R6.js → compact-item-Cs97bz5j.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-DnVRV3Ao.js → createContext-BqbG60LH.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DPty0mGN.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DLQZA_Wn.js → hasIn-i_RY0b1X.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_N4-Gq9.js → index-0-agYlWL.js} +1 -1
- package/src/assets/web-panel/assets/{index-DB0MJgF_.js → index-79V_zv1s.js} +1 -1
- package/src/assets/web-panel/assets/{index-BOZJBTWZ.js → index-B-3tEpTc.js} +1 -1
- package/src/assets/web-panel/assets/{index-DUq0c7gq.js → index-B-JVTrpr.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dy0R5ZED.js → index-B1CbOH1i.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bxp7uDD8.js → index-B5UkyA3c.js} +1 -1
- package/src/assets/web-panel/assets/{index-DpVrK6fr.js → index-BESLFt4q.js} +1 -1
- package/src/assets/web-panel/assets/{index-CT9vVN-X.js → index-BOVkf1K3.js} +1 -1
- package/src/assets/web-panel/assets/{index-DOoQpuQY.js → index-BYUVuwKt.js} +1 -1
- package/src/assets/web-panel/assets/{index-CINJQEj6.js → index-BcUXAB2s.js} +1 -1
- package/src/assets/web-panel/assets/{index-CarMTIyQ.js → index-BkBi5TmC.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5dgnilC.js → index-Bm7lLR4c.js} +1 -1
- package/src/assets/web-panel/assets/index-Bt7g0Vq_.js +1 -0
- package/src/assets/web-panel/assets/{index-CbQNL0ue.js → index-BuLQobzW.js} +1 -1
- package/src/assets/web-panel/assets/{index-CpBaJ3TD.js → index-CE2VtTlp.js} +1 -1
- package/src/assets/web-panel/assets/{index-jWb70ZgZ.js → index-CED2PL_y.js} +1 -1
- package/src/assets/web-panel/assets/{index-BEu2lMBy.js → index-CG3PRyAL.js} +1 -1
- package/src/assets/web-panel/assets/{index-DocEhinm.js → index-CIG8fQ2_.js} +1 -1
- package/src/assets/web-panel/assets/{index-D_LQodeJ.js → index-COdGA-8n.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZpYeqrh.js → index-CeWoNHau.js} +1 -1
- package/src/assets/web-panel/assets/{index-gt8bNefW.js → index-Cf3WCLmy.js} +1 -1
- package/src/assets/web-panel/assets/{index-N_GoWZX4.js → index-CgsqdnRV.js} +1 -1
- package/src/assets/web-panel/assets/index-ChVT0p9u.js +1 -0
- package/src/assets/web-panel/assets/{index-CZDekj7T.js → index-D2B4VPyP.js} +1 -1
- package/src/assets/web-panel/assets/{index-YSw_gxlZ.js → index-D9evtvg2.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTbxp4UH.js → index-DUUrv141.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bbj2NS_I.js → index-DiSkx4UB.js} +3 -3
- package/src/assets/web-panel/assets/{index-BZs5bBnk.js → index-Dk2qouAz.js} +1 -1
- package/src/assets/web-panel/assets/{index-DalA0TKC.js → index-Dk7-v4hP.js} +1 -1
- package/src/assets/web-panel/assets/{index-1jeAudHd.js → index-DmbfhJUP.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1ifZtXo.js → index-DqBgBpgp.js} +1 -1
- package/src/assets/web-panel/assets/{index-BhA3QEni.js → index-PT_5sBMA.js} +1 -1
- package/src/assets/web-panel/assets/{index-BSjilZ9L.js → index-U4f5izHz.js} +1 -1
- package/src/assets/web-panel/assets/{index-C2FqfMvU.js → index-YXgLng0K.js} +1 -1
- package/src/assets/web-panel/assets/{index-COev0W2g.js → index-ZxO_-E8a.js} +1 -1
- package/src/assets/web-panel/assets/{index-M7wxfAbm.js → index-dIHr4B_u.js} +1 -1
- package/src/assets/web-panel/assets/{index-DjWnONdy.js → index-gyFpvL7l.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dkwsi1Kn.js → index-s1opAL-M.js} +1 -1
- package/src/assets/web-panel/assets/{index-hwVDeoC0.js → index-yObj1O4n.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-Co67Kqmb.js → initDefaultProps-C9h9yKL3.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BMOxQSF_.js → motion-BvmLsPdu.js} +1 -1
- package/src/assets/web-panel/assets/{move-C5fWDw-N.js → move-CoL4ekU1.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BJevpL0e.js → mtc-parser-C4pMy6xV.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BCzNU8s6.js → omit-DPuJfNxx.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-CybWvDPk.js → pickAttrs-D0KEeaK2.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BMWS9HnW.js → placementArrow-BpkyINxt.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-BoL2UnGY.js → responsiveObserve-DQI_wOq8.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DWNfXraU.js → slide-DYfdDiRg.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BquTM3tX.js → statusUtils-B5dtvb0D.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BEkCleim.js → styleChecker-CF310mSj.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-Oecnq7EA.js → useFlexGapSupport-D1Qp4BLp.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CkDffJ4F.js → useFs-C7_iyvJK.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-qbvKXesB.js → usePersonalDataHub-DZUt_fIm.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-C6v8n6mA.js → vnode-D3dsi0lZ.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-4CTeFILV.js → zoom-WdbOlvdQ.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +2 -2
- package/src/commands/agent.js +90 -1
- package/src/commands/chat.js +11 -2
- package/src/commands/compact.js +106 -23
- package/src/commands/loop.js +28 -10
- package/src/commands/routine.js +1 -1
- package/src/commands/scheduler-daemon.js +359 -2
- package/src/commands/session-observability.js +305 -0
- package/src/commands/session.js +38 -0
- package/src/data/changelog.json +14 -1
- package/src/gateways/ws/ws-agent-handler.js +439 -5
- package/src/harness/jsonl-session-store.js +463 -125
- package/src/harness/prompt-compressor.js +86 -13
- package/src/harness/provider-backed-compaction.js +100 -27
- package/src/lib/advisor-runtime.js +3 -1
- package/src/lib/agent-schedule-store.js +414 -4
- package/src/lib/agent-team/team-adjudication.js +40 -1
- package/src/lib/automation-engine.js +494 -0
- package/src/lib/background-agent-supervisor.js +793 -215
- package/src/lib/background-command-argv.js +24 -0
- package/src/lib/background-interaction-journal.js +361 -52
- package/src/lib/background-interaction-resolver.js +32 -0
- package/src/lib/background-launch-profile.js +2 -0
- package/src/lib/causal-observability.js +2661 -0
- package/src/lib/chat-session-admission.js +76 -0
- package/src/lib/cost-budget.js +5 -7
- package/src/lib/cowork/ab-comparator-cli.js +11 -0
- package/src/lib/cowork/debate-review-cli.js +12 -0
- package/src/lib/cowork-adapter.js +159 -38
- package/src/lib/cowork-cron.js +357 -1
- package/src/lib/cowork-task-runner.js +112 -17
- package/src/lib/delivery-coordinator.js +142 -0
- package/src/lib/direct-model-usage.js +121 -0
- package/src/lib/file-lines.js +183 -47
- package/src/lib/llm-pricing.js +12 -17
- package/src/lib/loop.js +26 -2
- package/src/lib/routine-store.js +380 -21
- package/src/lib/runtime-usage-ledger.js +182 -0
- package/src/lib/scheduler-kernel/agenda-adapter.js +308 -3
- package/src/lib/scheduler-kernel/automation-adapter.js +410 -8
- package/src/lib/scheduler-kernel/automation-event-adapter.js +36 -0
- package/src/lib/scheduler-kernel/cowork-cron-adapter.js +285 -3
- package/src/lib/scheduler-kernel/loop-adapter.js +541 -6
- package/src/lib/scheduler-kernel/migration-admin.js +346 -0
- package/src/lib/scheduler-kernel/migration-rollback-admin.js +932 -0
- package/src/lib/scheduler-kernel/routine-adapter.js +394 -20
- package/src/lib/scheduler-kernel/runtime.js +63 -1
- package/src/lib/scheduler-kernel/source-locator-path.js +142 -0
- package/src/lib/scheduler-kernel/store.js +2586 -85
- package/src/lib/session-host-lease.js +171 -2
- package/src/lib/session-host-snapshot.js +14 -3
- package/src/lib/session-persistence-failure.js +1 -0
- package/src/lib/session-tail.js +179 -34
- package/src/lib/session-transcript-structure.js +96 -0
- package/src/lib/sub-agent-context.js +328 -22
- package/src/lib/ws-agent-handler.js +4 -1
- package/src/repl/agent-repl.js +732 -91
- package/src/repl/chat-repl.js +5 -1
- package/src/runtime/agent-core.js +1272 -207
- package/src/runtime/fallback-model.js +5 -0
- package/src/runtime/headless-runner.js +324 -82
- package/src/runtime/headless-stream.js +232 -26
- package/src/runtime/policies/agent-policy.js +4 -0
- package/src/workers/background-agent-worker.js +403 -96
- package/src/assets/web-panel/assets/Projects-Dd-xuPIc.js +0 -1
- package/src/assets/web-panel/assets/SpeechSettings-C7tT9iad.js +0 -1
- package/src/assets/web-panel/assets/devWarning-Jzu62CLC.js +0 -1
- package/src/assets/web-panel/assets/index-Zb2MeDSy.js +0 -1
- 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
|
|
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",
|
package/src/lib/session-tail.js
CHANGED
|
@@ -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
|
|
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
|
|
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(
|
|
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
|
|
80
|
-
let nl =
|
|
141
|
+
let start = 0;
|
|
142
|
+
let nl = buffer.indexOf("\n", start);
|
|
81
143
|
while (nl !== -1) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
+
}
|