chainlesschain 0.162.188 → 0.162.193
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/README.md +61 -11
- package/bin/chainlesschain.js +7 -52
- package/completions/_cc +209 -0
- package/completions/cc.bash +18 -0
- package/completions/cc.fish +13 -0
- package/completions/cc.ps1 +226 -0
- package/package.json +10 -2
- package/src/assets/web-panel/assets/{AIOps-Brr1LVcE.js → AIOps-DBLkaTje.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DQU74DKG.js → ActionButton-BAy-Qebo.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CYp9178y.js → Analytics-Dfb1Mlau.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DW4SCYsW.js → AppLayout-BbFkN8m0.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-CYlD_ed1.js → Artifacts-kM4-cEEH.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CWl1rZOi.js → Audit-BSQfYWf3.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-BOR-LPsa.js → BackgroundAgents-CWDbI_fG.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DLpG7j1X.js → Backup-BxLPbq_Q.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-B35K9wdr.js → BaseInput-DuIbdrp6.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CwUJb9gB.js → Chat-NEmjGahU.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-1v6aMS1m.js → ChatBubbleRenderer-DfJEo-5_.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-pi3-tPNW.js → Checkbox-CaoeG3Ns.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-FyNF35eL.js → Codegen-BFf_g3xY.js} +1 -1
- package/src/assets/web-panel/assets/{Col-tGt_JDEr.js → Col-CNOd8mdQ.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CC7LonQX.js → Community-BhpDqz-i.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BGXrHHjE.js → Compact-CQ42T7Z6.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DSW0TBZ8.js → Compliance-DI5PcLMN.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Bhk7rbvE.js → Cowork-wYYZe0wR.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DxyiQ2NZ.js → Cron-Dlq03DD_.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-B2ZkxHok.js → Crosschain-BzqS8PP3.js} +1 -1
- package/src/assets/web-panel/assets/{DID-yOJV99eJ.js → DID-It55Yem7.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CKsttz7D.js → Dashboard-9HkH6oxn.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CVFldXSL.js → Dropdown-00e2YWRB.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Bufv638J.js → EmailListRenderer-By-ugRDL.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-4ncjoIjL.js → FamilyGuardDashboard-C396yu5m.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DsknEDoX.js → Federation-BME5sbZY.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Cqe5jWXA.js → FormItemContext-B4TEZCr-.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-NAJaFKxc.js → GenericCardRenderer-_9CX9ieY.js} +1 -1
- package/src/assets/web-panel/assets/{Git-CszKQpnl.js → Git-BrS6WVSr.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BXGodXi6.js → Governance-BMdCWnKF.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CkoYuG2c.js → Inference-Bcp3vzcA.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DFlEr_T0.js → KnowledgeGraph-DlLuqGY5.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CVaENddA.js → Logs-D-W_6x09.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-DElGPxCZ.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-CDlLT0fw.js → Marketplace-Dzn2guvI.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DdwOyszN.js → McpTools-uPict-KL.js} +6 -6
- package/src/assets/web-panel/assets/{Memory-DKQnkKuG.js → Memory-BCw43kOb.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DGseqlzJ.js → MobileBridge-DmV8Wv7F.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-COBbEOlj.js → MobileProjects-C-HaCmSM.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-O61Vwyli.js → Mtc-C6jxJ34Y.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-Dw2SyasS.js → MtcAudit-Za8ExwhQ.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-hSu_l8kf.js → Multisig-KKDbuEgb.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CX1Epurr.js → NLProgramming-Dj97zbCk.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-R9xX8o8_.js → Notes-DBXvMraU.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-jtSNLCuK.js → NotificationSettings-DOxITLYl.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-7DVjZwMM.js +1 -0
- package/src/assets/web-panel/assets/{Organization-Pu2WO-U3.js → Organization-D1i73mjb.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-BjnqUyFp.js → Overflow-kW6Q3rvg.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-jjkvz44c.js → P2P-C-v_8p2v.js} +2 -2
- package/src/assets/web-panel/assets/PdhVaultBrowser-tUbbY64W.js +7 -0
- package/src/assets/web-panel/assets/{Permissions-D4yDpivd.js → Permissions-DkHxEZ58.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-0qsL6iH1.js → PersonalDataHub-DWuMPGrK.js} +5 -5
- package/src/assets/web-panel/assets/{Pipeline-Cx7Ybrk2.js → Pipeline-30IYXWWm.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-PbfR_M_V.js → Privacy-DZNQ4RrH.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CyV6Gsub.js → ProjectInit-De2MCElf.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-C7KZ2Wdk.js → ProjectSettings-5uHk9wsC.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-DQDjO-yK.js → Projects-XjDfrFLb.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-DyFVBBMX.js → Providers-Bgwj6gcE.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-CzB1BC9_.js → QrScannerModal-gKql9umv.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-Bk636mC1.js → QuickAsk-BWvmwkh7.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DoWxHIT3.js → Recommend-Uok4hZPd.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-oOVIv9wq.js → RemoteSession-DLJzTk9a.js} +3 -3
- package/src/assets/web-panel/assets/{Reputation-BucoReGG.js → Reputation-ZVwIPMSO.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DzmOdJU7.js → Row-CN04HxWR.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-C1tK7KGU.js → RssFeed-BdSrEG4g.js} +3 -3
- package/src/assets/web-panel/assets/{Search-CSnInOqk.js → Search-5PwVd0IR.js} +1 -1
- package/src/assets/web-panel/assets/{Security-dlejr15l.js → Security-DdBo4zjN.js} +3 -3
- package/src/assets/web-panel/assets/{Services-BioPZuNh.js → Services-D3letU7M.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DWMibwpC.js → Skeleton--Jb83f1v.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-BAAnx1B2.js → Skills-Bg-CulHX.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-jWNp4hNO.js → Sla-7QOa7S-X.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-y5kqKha-.js → SpeechSettings-DQwppjRw.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-B9GNsc3e.js → SyncSettings-dNPQGL-s.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-BDBYKRen.js → Tasks-Ggs-Z48q.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DRiE5eaN.js → Templates-78TYHgRO.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BGlAvExF.js → Tenant-xHwwv9vO.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-B8uzrbNY.js → Terminal-BJXSJfm4.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-Df48iveY.js → TimelineRenderer-fLbjRhJw.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CcmHndN6.js → Tokens-oMSBSBmB.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DPkWxmQ2.js → Trigger-CfnY_wX_.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DTb5a5AV.js → Trust-AdyTgYKO.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-CBmg5vch.js → UkeySign-C5K_81dS.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-B0mZrz4O.js → VideoEditing-vcGVDqki.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-Bxs7_mpd.js → Wallet-hhOc3hlY.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-C8x3cMxU.js → WebAuthn-CVloC11s.js} +3 -3
- package/src/assets/web-panel/assets/{WorkflowEditor-DPrNznrP.js → WorkflowEditor-CYeE32R5.js} +1 -1
- package/src/assets/web-panel/assets/{chat-D3bYWu7S.js → chat-CNROR5bN.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DY-PwMu4.js → colors-Cjx-cnwU.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-LDk10Kw6.js → compact-item-DOIfelvD.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-C-2MZnFq.js → createContext-DfA7UZ8u.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-ChcCBGs7.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BQIBjcnB.js → hasIn-BMSCzcfM.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQVb2dWp.js → index-5-HXgOX5.js} +1 -1
- package/src/assets/web-panel/assets/{index-BdXnYMCZ.js → index-AWg-0M1S.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cx6D_dx2.js → index-B4iYzpkL.js} +1 -1
- package/src/assets/web-panel/assets/{index-_HRO_cIu.js → index-B5Udeb_6.js} +1 -1
- package/src/assets/web-panel/assets/{index-DlRYqt3S.js → index-B7gf77CJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BrnRO9yE.js → index-BEMwBBHk.js} +1 -1
- package/src/assets/web-panel/assets/{index-VCh5zNJo.js → index-BM1y_9hM.js} +1 -1
- package/src/assets/web-panel/assets/{index-BRI3ngNg.js → index-BUonmKSE.js} +1 -1
- package/src/assets/web-panel/assets/{index-C3GNvExG.js → index-BbQ2eQ8H.js} +1 -1
- package/src/assets/web-panel/assets/{index-DSvo_bp0.js → index-BemJ2u_E.js} +1 -1
- package/src/assets/web-panel/assets/{index-L6qixyLc.js → index-BolaOQxx.js} +1 -1
- package/src/assets/web-panel/assets/{index-BI4KgcYC.js → index-BxOk9vkG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CY4KnaO9.js → index-By61ojUx.js} +1 -1
- package/src/assets/web-panel/assets/{index-L3smcoQX.js → index-C2K9SNVX.js} +1 -1
- package/src/assets/web-panel/assets/{index-BX2b9Orc.js → index-C3L5gdiD.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cz4b6Z_7.js → index-C7xn0Nub.js} +1 -1
- package/src/assets/web-panel/assets/{index-COG61sRv.js → index-CAnvhwg6.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7FC8cP2.js → index-CEKMcOyU.js} +1 -1
- package/src/assets/web-panel/assets/{index-BN-xdSWZ.js → index-CNC5onm4.js} +1 -1
- package/src/assets/web-panel/assets/{index-DY2FIrmz.js → index-CVL27kXC.js} +1 -1
- package/src/assets/web-panel/assets/{index-CiTvBAr-.js → index-CVm1_i3t.js} +1 -1
- package/src/assets/web-panel/assets/{index-DhRt9cYJ.js → index-CdYBye8B.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5pgtZnY.js → index-ClNMrqEY.js} +1 -1
- package/src/assets/web-panel/assets/{index-7xKu7Fns.js → index-Cs6l7uRu.js} +1 -1
- package/src/assets/web-panel/assets/{index-D4K1weKc.js → index-CshQbilz.js} +1 -1
- package/src/assets/web-panel/assets/{index-k7C_oDCO.js → index-Cyweh0Hy.js} +1 -1
- package/src/assets/web-panel/assets/{index-D3Oq-DLL.js → index-DBpWXFed.js} +3 -3
- package/src/assets/web-panel/assets/{index-DYyLJPlF.js → index-DHtpHZAQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dh-tkpKQ.js → index-DLmn0BoK.js} +1 -1
- package/src/assets/web-panel/assets/{index-8JC9BJa1.js → index-DPAWBrMf.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dd2Km97U.js → index-DjEsxO5h.js} +1 -1
- package/src/assets/web-panel/assets/{index-KFZO_lcx.js → index-FBc_CJBd.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTIZpPtG.js → index-Icdoqmzt.js} +1 -1
- package/src/assets/web-panel/assets/index-Q2IajL54.js +1 -0
- package/src/assets/web-panel/assets/index-bRkHgOMc.js +1 -0
- package/src/assets/web-panel/assets/{index-lmm9p-jm.js → index-dCd8cSUq.js} +1 -1
- package/src/assets/web-panel/assets/{index-HasPt4-h.js → index-m2vG9gG_.js} +1 -1
- package/src/assets/web-panel/assets/{index-CaslYch6.js → index-mTpHoEzr.js} +1 -1
- package/src/assets/web-panel/assets/{index-CbR6ROOa.js → index-ti_lnTWl.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-zQPSHf2C.js → initDefaultProps-Bu8k7H1Y.js} +1 -1
- package/src/assets/web-panel/assets/{motion-CKCStGVk.js → motion-s3iVK41k.js} +1 -1
- package/src/assets/web-panel/assets/{move-BufVDGYF.js → move-B6B-E5Jz.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DcaFxy5D.js → mtc-parser-BiYgTidA.js} +1 -1
- package/src/assets/web-panel/assets/{omit-EScpkGdo.js → omit-pU1RdUDA.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-72hG_qfT.js → pickAttrs-Cdoq_SeM.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-CMFWiH20.js → placementArrow-DIuvAn2K.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DKQHS41p.js → responsiveObserve-8w16YR2D.js} +1 -1
- package/src/assets/web-panel/assets/{slide-QVAFHTIQ.js → slide-VZ01cZe3.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DIz38e2G.js → statusUtils-lh8YMN8Z.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-zK5cyLby.js → styleChecker-CmDyreec.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DhZngk62.js → useFlexGapSupport-hiJWMU5y.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-BvV2cvkg.js → useFs-Thnu-Iaj.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CRmxb9l3.js → usePersonalDataHub-LBdOxjwY.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CgQIbobP.js → vnode-Cg6lFY_X.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-D4XrrO0R.js → zoom-DVfsR1YO.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/cli-help.js +181 -0
- package/src/command-help-index.json +181 -0
- package/src/command-manifest.json +1107 -175
- package/src/command-surface-policy.js +197 -0
- package/src/commands/agent.js +98 -33
- package/src/commands/artifacts.js +400 -2
- package/src/commands/background-session.js +41 -3
- package/src/commands/checkpoint-managed.js +1059 -0
- package/src/commands/checkpoint-restore-recovery.js +1476 -0
- package/src/commands/checkpoint.js +950 -22
- package/src/commands/compact.js +101 -25
- package/src/commands/config.js +506 -20
- package/src/commands/doctor.js +4 -1
- package/src/commands/hub.js +1 -1
- package/src/commands/mcp.js +688 -24
- package/src/commands/pack.js +16 -2
- package/src/commands/plugin.js +20 -12
- package/src/commands/session-mcp-recovery.js +218 -0
- package/src/commands/session.js +470 -110
- package/src/commands/setup.js +36 -10
- package/src/commands/skill.js +181 -110
- package/src/commands/status.js +21 -5
- package/src/commands/team-distributed.js +3324 -0
- package/src/commands/team.js +1130 -74
- package/src/commands/update.js +68 -6
- package/src/constants.js +11 -0
- package/src/data/changelog.json +26 -0
- package/src/gateways/ws/action-protocol.js +10 -0
- package/src/gateways/ws/session-protocol.js +555 -27
- package/src/gateways/ws/ws-agent-handler.js +541 -14
- package/src/gateways/ws/ws-session-gateway.js +129 -22
- package/src/gateways/ws/ws-session-state.js +431 -0
- package/src/harness/background-task-manager.js +233 -36
- package/src/harness/jsonl-session-store.js +2551 -167
- package/src/harness/mcp-client.js +802 -61
- package/src/harness/prompt-compressor.js +227 -27
- package/src/harness/session-index.js +89 -38
- package/src/harness/session-list-index.js +254 -0
- package/src/harness/structured-handoff.js +410 -0
- package/src/lazy-dispatch.js +747 -84
- package/src/lib/__tests__/logger.test.js +50 -2
- package/src/lib/advisor-runtime.js +1047 -0
- package/src/lib/agent-core.js +2 -0
- package/src/lib/agent-sandbox.js +120 -26
- package/src/lib/agent-session-export.js +20 -0
- package/src/lib/agent-team/task-lease.js +271 -3
- package/src/lib/agent-team/team-adjudication.js +1501 -0
- package/src/lib/agent-team/team-budget.js +4 -1
- package/src/lib/agent-team/team-control-store.js +582 -0
- package/src/lib/agent-team/team-distributed-queue.js +5246 -0
- package/src/lib/agent-team/team-process-checkpoint.js +211 -0
- package/src/lib/agent-team/team-runner.js +543 -24
- package/src/lib/agent-team/team-worktree.js +1403 -95
- package/src/lib/artifact-store.js +131 -8
- package/src/lib/background-agent-supervisor.js +185 -13
- package/src/lib/background-interaction-journal.js +37 -9
- package/src/lib/background-launch-profile.js +1058 -0
- package/src/lib/checkpoint-restore-already-completed-controller.js +902 -0
- package/src/lib/checkpoint-restore-orchestrator.js +987 -0
- package/src/lib/checkpoint-restore-partial-rollback-controller.js +1953 -0
- package/src/lib/checkpoint-restore-recovery-controller.js +689 -0
- package/src/lib/checkpoint-restore-recovery.js +884 -0
- package/src/lib/checkpoint-restore-retention.js +126 -0
- package/src/lib/checkpoint-restore-saga.js +5079 -0
- package/src/lib/checkpoint-restore-session-recovery.js +1178 -0
- package/src/lib/checkpoint-restore-workspace-target-verifier.js +270 -0
- package/src/lib/checkpoint-store.js +1688 -97
- package/src/lib/checkpoint-timeline-authority.js +522 -0
- package/src/lib/checkpoint-timeline.js +642 -0
- package/src/lib/config-keys.js +18 -119
- package/src/lib/config-manager.js +446 -27
- package/src/lib/config-redaction.js +57 -0
- package/src/lib/config-schema.js +853 -0
- package/src/lib/cowork-mcp-tools.js +5 -0
- package/src/lib/cowork-task-runner.js +458 -63
- package/src/lib/delivery-coordinator.js +1234 -0
- package/src/lib/delivery-evidence.js +450 -0
- package/src/lib/doctor-checkup.js +199 -14
- package/src/lib/executable-path.js +34 -0
- package/src/lib/fatal-handler.js +16 -0
- package/src/lib/file-checkpoint.js +4909 -186
- package/src/lib/file-lines.js +125 -0
- package/src/lib/headless-config-command.js +16 -5
- package/src/lib/hook-runner.cjs +139 -38
- package/src/lib/hooks-v2-workspace-context.js +10 -0
- package/src/lib/impacted-gate-selector.js +424 -0
- package/src/lib/interaction-adapter.js +62 -0
- package/src/lib/jsonl-session-store.js +1 -35
- package/src/lib/logger.js +66 -14
- package/src/lib/managed-checkpoint-render.js +60 -0
- package/src/lib/managed-tool-checkpoint.js +194 -0
- package/src/lib/mcp-call-ledger-store.js +1069 -0
- package/src/lib/mcp-call-ledger.js +747 -0
- package/src/lib/mcp-client.js +5 -0
- package/src/lib/mcp-conflict-scheduler.js +468 -0
- package/src/lib/mcp-effect-contract.js +102 -0
- package/src/lib/mcp-headers-helper-trust.js +145 -0
- package/src/lib/mcp-headers-helper.js +616 -0
- package/src/lib/mcp-host-recovery-runtime.js +131 -0
- package/src/lib/mcp-ledger-recovery-admission.js +1065 -0
- package/src/lib/mcp-recovery-adjudication.js +443 -0
- package/src/lib/output-context.js +86 -0
- package/src/lib/packer/file-identity.js +170 -0
- package/src/lib/packer/index.js +3 -0
- package/src/lib/packer/native-update-state.js +277 -0
- package/src/lib/packer/pack-update-applier.js +1983 -136
- package/src/lib/packer/pack-update-checker.js +34 -0
- package/src/lib/packer/pack-update-downloader.js +875 -86
- package/src/lib/packer/pack-update-signature.js +86 -0
- package/src/lib/packer/pkg-config-generator.js +6 -0
- package/src/lib/paths.js +65 -10
- package/src/lib/plan-mode.js +831 -14
- package/src/lib/plan-persistence.js +540 -0
- package/src/lib/plugin-runtime/hooks.js +227 -69
- package/src/lib/plugin-runtime/plugin-options.js +189 -44
- package/src/lib/pr-automation-policy.js +146 -8
- package/src/lib/process-execution-broker/credential-agent.js +7 -0
- package/src/lib/process-execution-broker/index.js +491 -13
- package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +28 -1
- package/src/lib/process-execution-broker/linux-generic-bwrap.js +28 -4
- package/src/lib/process-execution-broker/platform-sandbox.js +92 -70
- package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
- package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
- package/src/lib/process-execution-broker/windows-sandbox.cs +284 -19
- package/src/lib/process-execution-broker/workspace-transaction.js +4459 -0
- package/src/lib/project-instructions.js +165 -29
- package/src/lib/project-mcp-trust.js +67 -0
- package/src/lib/public-ide-capability-manifest.js +272 -0
- package/src/lib/public-ide-capability-validation.js +351 -0
- package/src/lib/secret-store.js +267 -43
- package/src/lib/secure-file-identity.js +433 -0
- package/src/lib/secure-fs.js +1089 -0
- package/src/lib/security-audit.js +58 -0
- package/src/lib/service-manager.js +34 -13
- package/src/lib/session-budget-runtime.js +1452 -0
- package/src/lib/session-host-snapshot.js +388 -0
- package/src/lib/session-message-provenance.js +429 -0
- package/src/lib/session-projection.js +804 -0
- package/src/lib/session-resource-budget.js +1495 -0
- package/src/lib/settings-hooks.cjs +138 -22
- package/src/lib/settings-loader.cjs +25 -2
- package/src/lib/side-effect-ledger-store.js +45 -23
- package/src/lib/skill-execution-identity.js +217 -0
- package/src/lib/skill-loader.js +299 -18
- package/src/lib/sub-agent-context.js +236 -28
- package/src/lib/sub-agent-registry.js +5 -0
- package/src/lib/subagent-contract.js +94 -0
- package/src/lib/todo-manager.js +185 -18
- package/src/lib/todo-persistence.js +417 -0
- package/src/lib/turn-binding-store.js +41 -23
- package/src/lib/turn-binding.js +160 -2
- package/src/lib/with-file-lock.js +165 -24
- package/src/lib/ws-chat-handler.js +21 -0
- package/src/program-base.js +18 -0
- package/src/repl/advisor-command.js +103 -0
- package/src/repl/agent-repl.js +2175 -146
- package/src/repl/btw-command.js +93 -10
- package/src/repl/clipboard-image.js +119 -0
- package/src/repl/config-summary.js +64 -108
- package/src/repl/prompt-editor.js +233 -0
- package/src/repl/prompt-interactions.js +344 -0
- package/src/repl/prompt-stash.js +354 -0
- package/src/repl/prompt-suggestions.js +312 -0
- package/src/repl/repl-keybindings.js +227 -0
- package/src/repl/session-recap.js +217 -0
- package/src/repl/slash-command-registry.js +22 -0
- package/src/repl/terminal-layout.js +88 -0
- package/src/runtime/agent-core.js +1630 -447
- package/src/runtime/coding-agent-contract-shared.cjs +13 -1
- package/src/runtime/coding-agent-managed-tool-policy.cjs +7 -2
- package/src/runtime/diagnostics.js +32 -13
- package/src/runtime/headless-runner.js +310 -118
- package/src/runtime/headless-stream.js +356 -95
- package/src/runtime/mcp-config.js +148 -14
- package/src/runtime/policies/agent-policy.js +14 -0
- package/src/runtime/status-diagnostics-lite.js +137 -0
- package/src/assets/web-panel/assets/MarkdownRenderer-7iWGwZ-r.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-BU2Yqq5H.js +0 -1
- package/src/assets/web-panel/assets/PdhVaultBrowser-69ca_WeR.js +0 -7
- package/src/assets/web-panel/assets/devWarning-B4dXIJqD.js +0 -1
- package/src/assets/web-panel/assets/index-C-C7DaG6.js +0 -1
- package/src/assets/web-panel/assets/index-DkfWzWtd.js +0 -1
|
@@ -0,0 +1,1452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-local sharing and a revisioned sidecar for SessionResourceBudget.
|
|
3
|
+
*
|
|
4
|
+
* Callers in one CLI process can reuse the same budget object. Cooperating
|
|
5
|
+
* processes detect stale writes through a file lock and revision CAS. This
|
|
6
|
+
* sidecar is not a machine-wide authority or an anti-rollback mechanism.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import crypto from "node:crypto";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { sessionPath } from "../harness/jsonl-session-store.js";
|
|
13
|
+
import {
|
|
14
|
+
normalizeSessionResourceBudgetSnapshot,
|
|
15
|
+
SessionResourceBudget,
|
|
16
|
+
} from "./session-resource-budget.js";
|
|
17
|
+
import {
|
|
18
|
+
sameFileStatIdentity,
|
|
19
|
+
samePathHandleDirectoryIdentity,
|
|
20
|
+
samePathHandleFileIdentity,
|
|
21
|
+
} from "./secure-file-identity.js";
|
|
22
|
+
import { withFileLock } from "./with-file-lock.js";
|
|
23
|
+
|
|
24
|
+
export const SESSION_BUDGET_SIDECAR_VERSION = 1;
|
|
25
|
+
|
|
26
|
+
const DEFAULT_REGISTRY = new Map();
|
|
27
|
+
const MAX_SIDECAR_BYTES = 1024 * 1024;
|
|
28
|
+
const MAX_IN_FLIGHT_RESOURCES = 1024;
|
|
29
|
+
const USAGE_UNKNOWN_SUFFIX = ".usage-unknown.json";
|
|
30
|
+
const BIGINT_STAT_OPTIONS = Object.freeze({ bigint: true });
|
|
31
|
+
const OPAQUE_AUTHORITY_ID_PATTERNS = Object.freeze({
|
|
32
|
+
work: /^work-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
|
|
33
|
+
tool: /^tool-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
|
|
34
|
+
usage:
|
|
35
|
+
/^usage-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function isRecord(value) {
|
|
39
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
40
|
+
const proto = Object.getPrototypeOf(value);
|
|
41
|
+
return proto === Object.prototype || proto === null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function runtimeError(operation, filePath, cause, code = null) {
|
|
45
|
+
const detail = cause?.message || String(cause || "unknown error");
|
|
46
|
+
const error = new Error(
|
|
47
|
+
`Session budget ${operation} failed (${filePath}): ${detail}`,
|
|
48
|
+
{ cause },
|
|
49
|
+
);
|
|
50
|
+
error.name = "SessionBudgetPersistenceError";
|
|
51
|
+
error.code = code || `ERR_SESSION_BUDGET_${operation.toUpperCase()}`;
|
|
52
|
+
error.filePath = filePath;
|
|
53
|
+
return error;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hasTypedOpaqueAuthorityIds(inFlight) {
|
|
57
|
+
return (
|
|
58
|
+
inFlight.work.every(
|
|
59
|
+
(entry) =>
|
|
60
|
+
typeof entry?.id === "string" &&
|
|
61
|
+
(OPAQUE_AUTHORITY_ID_PATTERNS.work.test(entry.id) ||
|
|
62
|
+
OPAQUE_AUTHORITY_ID_PATTERNS.usage.test(entry.id)),
|
|
63
|
+
) &&
|
|
64
|
+
inFlight.tools.every(
|
|
65
|
+
(entry) =>
|
|
66
|
+
typeof entry?.id === "string" &&
|
|
67
|
+
OPAQUE_AUTHORITY_ID_PATTERNS.tool.test(entry.id),
|
|
68
|
+
)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function canonicalizeSnapshot(snapshot, filePath, operation = "snapshot") {
|
|
73
|
+
if (
|
|
74
|
+
!isRecord(snapshot?.inFlight) ||
|
|
75
|
+
!Array.isArray(snapshot.inFlight.work) ||
|
|
76
|
+
!Array.isArray(snapshot.inFlight.tools) ||
|
|
77
|
+
snapshot.inFlight.work.length + snapshot.inFlight.tools.length >
|
|
78
|
+
MAX_IN_FLIGHT_RESOURCES
|
|
79
|
+
) {
|
|
80
|
+
throw runtimeError(
|
|
81
|
+
operation,
|
|
82
|
+
filePath,
|
|
83
|
+
new TypeError(
|
|
84
|
+
`in-flight resource count exceeds ${MAX_IN_FLIGHT_RESOURCES}`,
|
|
85
|
+
),
|
|
86
|
+
"ERR_SESSION_BUDGET_IN_FLIGHT_LIMIT",
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (!hasTypedOpaqueAuthorityIds(snapshot.inFlight)) {
|
|
90
|
+
throw runtimeError(
|
|
91
|
+
operation,
|
|
92
|
+
filePath,
|
|
93
|
+
new TypeError("in-flight authority ids must be typed opaque ids"),
|
|
94
|
+
"ERR_SESSION_BUDGET_NON_OPAQUE_AUTHORITY",
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
let canonical;
|
|
98
|
+
try {
|
|
99
|
+
canonical = normalizeSessionResourceBudgetSnapshot(snapshot);
|
|
100
|
+
} catch (cause) {
|
|
101
|
+
throw runtimeError(operation, filePath, cause);
|
|
102
|
+
}
|
|
103
|
+
if (!hasTypedOpaqueAuthorityIds(canonical.inFlight)) {
|
|
104
|
+
throw runtimeError(
|
|
105
|
+
operation,
|
|
106
|
+
filePath,
|
|
107
|
+
new TypeError("in-flight authority ids must be typed opaque ids"),
|
|
108
|
+
"ERR_SESSION_BUDGET_NON_OPAQUE_AUTHORITY",
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return canonical;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function validateRecord(value, sessionId, filePath) {
|
|
115
|
+
if (
|
|
116
|
+
!isRecord(value) ||
|
|
117
|
+
value.version !== SESSION_BUDGET_SIDECAR_VERSION ||
|
|
118
|
+
value.sessionId !== sessionId ||
|
|
119
|
+
!Number.isSafeInteger(value.revision) ||
|
|
120
|
+
value.revision < 1 ||
|
|
121
|
+
!isRecord(value.snapshot)
|
|
122
|
+
) {
|
|
123
|
+
throw runtimeError(
|
|
124
|
+
"corrupt",
|
|
125
|
+
filePath,
|
|
126
|
+
new TypeError("invalid sidecar envelope"),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const snapshot = canonicalizeSnapshot(value.snapshot, filePath, "corrupt");
|
|
130
|
+
const writerPid = Number(value.writer?.pid);
|
|
131
|
+
return {
|
|
132
|
+
version: SESSION_BUDGET_SIDECAR_VERSION,
|
|
133
|
+
sessionId,
|
|
134
|
+
revision: value.revision,
|
|
135
|
+
storedAt: typeof value.storedAt === "string" ? value.storedAt : null,
|
|
136
|
+
writer:
|
|
137
|
+
Number.isSafeInteger(writerPid) && writerPid > 0
|
|
138
|
+
? { pid: writerPid }
|
|
139
|
+
: null,
|
|
140
|
+
snapshot,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function usageSettlementEntries(snapshot) {
|
|
145
|
+
return snapshot.inFlight.work.filter(
|
|
146
|
+
(entry) => entry.kind === "usage-settlement",
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function usageUnknownMarkerIdentity(record) {
|
|
151
|
+
if (!record) return null;
|
|
152
|
+
const digest = crypto
|
|
153
|
+
.createHash("sha256")
|
|
154
|
+
.update(
|
|
155
|
+
JSON.stringify({
|
|
156
|
+
version: record.version,
|
|
157
|
+
sessionId: record.sessionId,
|
|
158
|
+
revision: record.revision,
|
|
159
|
+
storedAt: record.storedAt,
|
|
160
|
+
writer: record.writer,
|
|
161
|
+
snapshot: record.snapshot,
|
|
162
|
+
}),
|
|
163
|
+
)
|
|
164
|
+
.digest("hex");
|
|
165
|
+
return { revision: record.revision, digest };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function normalizeUsageUnknownMarkerIdentity(value, filePath) {
|
|
169
|
+
if (
|
|
170
|
+
!isRecord(value) ||
|
|
171
|
+
!Number.isSafeInteger(value.revision) ||
|
|
172
|
+
value.revision < 1 ||
|
|
173
|
+
typeof value.digest !== "string" ||
|
|
174
|
+
!/^[a-f0-9]{64}$/.test(value.digest)
|
|
175
|
+
) {
|
|
176
|
+
throw runtimeError(
|
|
177
|
+
"marker",
|
|
178
|
+
filePath,
|
|
179
|
+
new TypeError("invalid usage-unknown marker identity"),
|
|
180
|
+
"ERR_SESSION_BUDGET_MARKER_IDENTITY",
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return { revision: value.revision, digest: value.digest };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function sameUsageUnknownMarkerIdentity(left, right) {
|
|
187
|
+
return Boolean(
|
|
188
|
+
left &&
|
|
189
|
+
right &&
|
|
190
|
+
left.revision === right.revision &&
|
|
191
|
+
left.digest === right.digest,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function mergeUsageUnknownSnapshot(baseSnapshot, markerSnapshot, filePath) {
|
|
196
|
+
const base = canonicalizeSnapshot(baseSnapshot, filePath, "corrupt");
|
|
197
|
+
const marker = canonicalizeSnapshot(markerSnapshot, filePath, "corrupt");
|
|
198
|
+
const markerEntries = usageSettlementEntries(marker);
|
|
199
|
+
if (markerEntries.length === 0) {
|
|
200
|
+
throw runtimeError(
|
|
201
|
+
"corrupt",
|
|
202
|
+
filePath,
|
|
203
|
+
new TypeError("usage-unknown marker has no pending settlement"),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const work = new Map(base.inFlight.work.map((entry) => [entry.id, entry]));
|
|
207
|
+
for (const entry of markerEntries) {
|
|
208
|
+
const existing = work.get(entry.id);
|
|
209
|
+
if (existing && JSON.stringify(existing) !== JSON.stringify(entry)) {
|
|
210
|
+
throw runtimeError(
|
|
211
|
+
"corrupt",
|
|
212
|
+
filePath,
|
|
213
|
+
new TypeError("conflicting usage-unknown settlement identity"),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
work.set(entry.id, { ...entry });
|
|
217
|
+
if (work.size + base.inFlight.tools.length > MAX_IN_FLIGHT_RESOURCES) {
|
|
218
|
+
throw runtimeError(
|
|
219
|
+
"corrupt",
|
|
220
|
+
filePath,
|
|
221
|
+
new TypeError(
|
|
222
|
+
`in-flight resource count exceeds ${MAX_IN_FLIGHT_RESOURCES}`,
|
|
223
|
+
),
|
|
224
|
+
"ERR_SESSION_BUDGET_IN_FLIGHT_LIMIT",
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return canonicalizeSnapshot(
|
|
229
|
+
{
|
|
230
|
+
...base,
|
|
231
|
+
inFlight: {
|
|
232
|
+
work: [...work.values()],
|
|
233
|
+
tools: base.inFlight.tools,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
filePath,
|
|
237
|
+
"corrupt",
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function hasPreciseIdentity(stat) {
|
|
242
|
+
if (!stat) return false;
|
|
243
|
+
if (typeof stat.dev === "bigint" && typeof stat.ino === "bigint") {
|
|
244
|
+
return stat.dev >= 0n && stat.ino > 0n;
|
|
245
|
+
}
|
|
246
|
+
return (
|
|
247
|
+
Number.isSafeInteger(stat.dev) &&
|
|
248
|
+
stat.dev >= 0 &&
|
|
249
|
+
Number.isSafeInteger(stat.ino) &&
|
|
250
|
+
stat.ino > 0
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function sameIdentity(left, right) {
|
|
255
|
+
return Boolean(
|
|
256
|
+
hasPreciseIdentity(left) &&
|
|
257
|
+
hasPreciseIdentity(right) &&
|
|
258
|
+
typeof left.dev === typeof right.dev &&
|
|
259
|
+
typeof left.ino === typeof right.ino &&
|
|
260
|
+
left.dev === right.dev &&
|
|
261
|
+
left.ino === right.ino,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function sameOpenedIdentity(left, right) {
|
|
266
|
+
return Boolean(
|
|
267
|
+
hasPreciseIdentity(left) &&
|
|
268
|
+
hasPreciseIdentity(right) &&
|
|
269
|
+
sameFileStatIdentity(left, right),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function samePathHandleIdentity(pathStat, handleStat, expectedDevice, runtime) {
|
|
274
|
+
return Boolean(
|
|
275
|
+
hasPreciseIdentity(pathStat) &&
|
|
276
|
+
hasPreciseIdentity(handleStat) &&
|
|
277
|
+
samePathHandleFileIdentity(pathStat, handleStat, expectedDevice, runtime),
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function samePathHandleParentIdentity(
|
|
282
|
+
pathStat,
|
|
283
|
+
handleStat,
|
|
284
|
+
expectedDevice,
|
|
285
|
+
runtime,
|
|
286
|
+
) {
|
|
287
|
+
return Boolean(
|
|
288
|
+
hasPreciseIdentity(pathStat) &&
|
|
289
|
+
hasPreciseIdentity(handleStat) &&
|
|
290
|
+
samePathHandleDirectoryIdentity(
|
|
291
|
+
pathStat,
|
|
292
|
+
handleStat,
|
|
293
|
+
expectedDevice,
|
|
294
|
+
runtime,
|
|
295
|
+
),
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function lstatIdentity(fileSystem, target) {
|
|
300
|
+
return fileSystem.lstatSync(target, BIGINT_STAT_OPTIONS);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function statIdentity(fileSystem, target) {
|
|
304
|
+
return fileSystem.statSync(target, BIGINT_STAT_OPTIONS);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function fstatIdentity(fileSystem, descriptor) {
|
|
308
|
+
return fileSystem.fstatSync(descriptor, BIGINT_STAT_OPTIONS);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function statSizeExceedsLimit(stat, limit) {
|
|
312
|
+
if (typeof stat?.size === "bigint") {
|
|
313
|
+
return stat.size < 0n || stat.size > BigInt(limit);
|
|
314
|
+
}
|
|
315
|
+
return (
|
|
316
|
+
!Number.isSafeInteger(stat?.size) || stat.size < 0 || stat.size > limit
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function ensurePrivateOwnership(filePath, stat, kind) {
|
|
321
|
+
const predicate = kind === "directory" ? stat.isDirectory : stat.isFile;
|
|
322
|
+
if (
|
|
323
|
+
typeof predicate !== "function" ||
|
|
324
|
+
!predicate.call(stat) ||
|
|
325
|
+
stat.isSymbolicLink?.()
|
|
326
|
+
) {
|
|
327
|
+
throw runtimeError(
|
|
328
|
+
"read",
|
|
329
|
+
filePath,
|
|
330
|
+
new Error(`${kind} must be a regular non-symlink ${kind}`),
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
const insecureMode =
|
|
334
|
+
typeof stat.mode === "bigint"
|
|
335
|
+
? (stat.mode & 0o077n) !== 0n
|
|
336
|
+
: !Number.isSafeInteger(stat.mode) || (stat.mode & 0o077) !== 0;
|
|
337
|
+
if (process.platform !== "win32" && insecureMode) {
|
|
338
|
+
throw runtimeError(
|
|
339
|
+
"read",
|
|
340
|
+
filePath,
|
|
341
|
+
new Error(`${kind} permissions must not grant group/other access`),
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
if (
|
|
345
|
+
process.platform !== "win32" &&
|
|
346
|
+
typeof process.getuid === "function" &&
|
|
347
|
+
(typeof stat.uid === "bigint"
|
|
348
|
+
? stat.uid !== BigInt(process.getuid())
|
|
349
|
+
: !Number.isSafeInteger(stat.uid) || stat.uid !== process.getuid())
|
|
350
|
+
) {
|
|
351
|
+
throw runtimeError("read", filePath, new Error(`${kind} owner mismatch`));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function openVerifiedParent(
|
|
356
|
+
filePath,
|
|
357
|
+
fileSystem,
|
|
358
|
+
{
|
|
359
|
+
platform = process.platform,
|
|
360
|
+
uvVersion = process.versions.uv,
|
|
361
|
+
allowUnsupportedPlatformForTests = false,
|
|
362
|
+
} = {},
|
|
363
|
+
) {
|
|
364
|
+
const directory = path.dirname(filePath);
|
|
365
|
+
const runtime = { platform, uvVersion };
|
|
366
|
+
let before;
|
|
367
|
+
try {
|
|
368
|
+
before = lstatIdentity(fileSystem, directory);
|
|
369
|
+
ensurePrivateOwnership(directory, before, "directory");
|
|
370
|
+
} catch (cause) {
|
|
371
|
+
if (cause?.name === "SessionBudgetPersistenceError") throw cause;
|
|
372
|
+
throw runtimeError("read", directory, cause);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
let authorityDescriptor = null;
|
|
376
|
+
let descriptor = null;
|
|
377
|
+
let descriptorRoot = null;
|
|
378
|
+
let trustedDevice = before.dev;
|
|
379
|
+
if (
|
|
380
|
+
platform === "linux" ||
|
|
381
|
+
(platform === "win32" && allowUnsupportedPlatformForTests)
|
|
382
|
+
) {
|
|
383
|
+
try {
|
|
384
|
+
const flags =
|
|
385
|
+
fs.constants.O_RDONLY |
|
|
386
|
+
(platform === "linux" ? fs.constants.O_DIRECTORY || 0 : 0) |
|
|
387
|
+
(fs.constants.O_NOFOLLOW || 0);
|
|
388
|
+
if (platform === "win32") {
|
|
389
|
+
const authorityPath = path.parse(path.resolve(directory)).root;
|
|
390
|
+
authorityDescriptor = fileSystem.openSync(
|
|
391
|
+
authorityPath,
|
|
392
|
+
fs.constants.O_RDONLY,
|
|
393
|
+
);
|
|
394
|
+
const authority = fstatIdentity(fileSystem, authorityDescriptor);
|
|
395
|
+
if (
|
|
396
|
+
!authority.isDirectory() ||
|
|
397
|
+
!hasPreciseIdentity(authority) ||
|
|
398
|
+
authority.dev === 0n
|
|
399
|
+
) {
|
|
400
|
+
throw new Error("parent volume identity is unavailable");
|
|
401
|
+
}
|
|
402
|
+
trustedDevice = authority.dev;
|
|
403
|
+
}
|
|
404
|
+
descriptor = fileSystem.openSync(directory, flags);
|
|
405
|
+
const opened = fstatIdentity(fileSystem, descriptor);
|
|
406
|
+
ensurePrivateOwnership(directory, opened, "directory");
|
|
407
|
+
if (
|
|
408
|
+
!samePathHandleParentIdentity(before, opened, trustedDevice, runtime)
|
|
409
|
+
) {
|
|
410
|
+
throw new Error("parent directory identity changed during open");
|
|
411
|
+
}
|
|
412
|
+
trustedDevice = opened.dev;
|
|
413
|
+
if (platform === "linux") {
|
|
414
|
+
descriptorRoot = `/proc/self/fd/${descriptor}`;
|
|
415
|
+
const projected = statIdentity(fileSystem, descriptorRoot);
|
|
416
|
+
if (
|
|
417
|
+
!samePathHandleParentIdentity(
|
|
418
|
+
projected,
|
|
419
|
+
opened,
|
|
420
|
+
trustedDevice,
|
|
421
|
+
runtime,
|
|
422
|
+
)
|
|
423
|
+
) {
|
|
424
|
+
throw new Error("could not bind parent directory through its dirfd");
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
} catch (cause) {
|
|
428
|
+
if (descriptor !== null) {
|
|
429
|
+
try {
|
|
430
|
+
fileSystem.closeSync(descriptor);
|
|
431
|
+
} catch {
|
|
432
|
+
// Preserve the identity failure.
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (authorityDescriptor !== null) {
|
|
436
|
+
try {
|
|
437
|
+
fileSystem.closeSync(authorityDescriptor);
|
|
438
|
+
} catch {
|
|
439
|
+
// Preserve the identity failure.
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (cause?.name === "SessionBudgetPersistenceError") throw cause;
|
|
443
|
+
throw runtimeError("read", directory, cause);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
resolve(childPath) {
|
|
449
|
+
const resolved = path.resolve(childPath);
|
|
450
|
+
if (path.dirname(resolved) !== path.resolve(directory)) {
|
|
451
|
+
throw runtimeError(
|
|
452
|
+
"path",
|
|
453
|
+
childPath,
|
|
454
|
+
new Error("sidecar child escaped its verified parent"),
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
if (descriptorRoot !== null) {
|
|
458
|
+
return path.posix.join(descriptorRoot, path.basename(resolved));
|
|
459
|
+
}
|
|
460
|
+
if (allowUnsupportedPlatformForTests) return resolved;
|
|
461
|
+
throw runtimeError(
|
|
462
|
+
"unsupported",
|
|
463
|
+
childPath,
|
|
464
|
+
new Error(`durable sidecars are unsupported on ${platform}`),
|
|
465
|
+
"ERR_SESSION_BUDGET_DURABLE_STORE_UNSUPPORTED",
|
|
466
|
+
);
|
|
467
|
+
},
|
|
468
|
+
verify() {
|
|
469
|
+
let current;
|
|
470
|
+
try {
|
|
471
|
+
current = lstatIdentity(fileSystem, directory);
|
|
472
|
+
ensurePrivateOwnership(directory, current, "directory");
|
|
473
|
+
if (descriptor !== null) {
|
|
474
|
+
const opened = fstatIdentity(fileSystem, descriptor);
|
|
475
|
+
if (
|
|
476
|
+
!samePathHandleParentIdentity(
|
|
477
|
+
current,
|
|
478
|
+
opened,
|
|
479
|
+
trustedDevice,
|
|
480
|
+
runtime,
|
|
481
|
+
)
|
|
482
|
+
) {
|
|
483
|
+
throw new Error("opened parent directory identity changed");
|
|
484
|
+
}
|
|
485
|
+
} else if (!sameIdentity(before, current)) {
|
|
486
|
+
throw new Error("parent directory identity changed");
|
|
487
|
+
}
|
|
488
|
+
} catch (cause) {
|
|
489
|
+
if (cause?.name === "SessionBudgetPersistenceError") throw cause;
|
|
490
|
+
throw runtimeError("read", directory, cause);
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
sync() {
|
|
494
|
+
if (descriptor !== null && platform === "linux") {
|
|
495
|
+
fileSystem.fsyncSync(descriptor);
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
trustedDevice,
|
|
499
|
+
runtime,
|
|
500
|
+
close() {
|
|
501
|
+
try {
|
|
502
|
+
if (descriptor !== null) {
|
|
503
|
+
fileSystem.closeSync(descriptor);
|
|
504
|
+
descriptor = null;
|
|
505
|
+
}
|
|
506
|
+
} finally {
|
|
507
|
+
if (authorityDescriptor !== null) {
|
|
508
|
+
fileSystem.closeSync(authorityDescriptor);
|
|
509
|
+
authorityDescriptor = null;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function readRecordAt(filePath, sessionId, fileSystem = fs, ioOptions = {}) {
|
|
517
|
+
let parent;
|
|
518
|
+
try {
|
|
519
|
+
parent = openVerifiedParent(filePath, fileSystem, ioOptions);
|
|
520
|
+
} catch (error) {
|
|
521
|
+
if (error?.cause?.code === "ENOENT") return null;
|
|
522
|
+
throw error;
|
|
523
|
+
}
|
|
524
|
+
let descriptor = null;
|
|
525
|
+
let serialized;
|
|
526
|
+
try {
|
|
527
|
+
const physicalPath = parent.resolve(filePath);
|
|
528
|
+
let before;
|
|
529
|
+
try {
|
|
530
|
+
before = lstatIdentity(fileSystem, physicalPath);
|
|
531
|
+
} catch (cause) {
|
|
532
|
+
if (cause?.code === "ENOENT") {
|
|
533
|
+
parent.verify();
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
throw cause;
|
|
537
|
+
}
|
|
538
|
+
ensurePrivateOwnership(filePath, before, "file");
|
|
539
|
+
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0);
|
|
540
|
+
descriptor = fileSystem.openSync(physicalPath, flags);
|
|
541
|
+
const opened = fstatIdentity(fileSystem, descriptor);
|
|
542
|
+
ensurePrivateOwnership(filePath, opened, "file");
|
|
543
|
+
if (
|
|
544
|
+
!samePathHandleIdentity(
|
|
545
|
+
before,
|
|
546
|
+
opened,
|
|
547
|
+
parent.trustedDevice,
|
|
548
|
+
parent.runtime,
|
|
549
|
+
)
|
|
550
|
+
) {
|
|
551
|
+
throw new Error("sidecar identity changed during open");
|
|
552
|
+
}
|
|
553
|
+
if (statSizeExceedsLimit(opened, MAX_SIDECAR_BYTES)) {
|
|
554
|
+
throw new Error("sidecar exceeds maximum size");
|
|
555
|
+
}
|
|
556
|
+
const pathAfterOpen = lstatIdentity(fileSystem, physicalPath);
|
|
557
|
+
ensurePrivateOwnership(filePath, pathAfterOpen, "file");
|
|
558
|
+
if (
|
|
559
|
+
!samePathHandleIdentity(
|
|
560
|
+
pathAfterOpen,
|
|
561
|
+
opened,
|
|
562
|
+
parent.trustedDevice,
|
|
563
|
+
parent.runtime,
|
|
564
|
+
)
|
|
565
|
+
) {
|
|
566
|
+
throw new Error("sidecar path changed after open");
|
|
567
|
+
}
|
|
568
|
+
serialized = fileSystem.readFileSync(descriptor, "utf8");
|
|
569
|
+
const openedAfterRead = fstatIdentity(fileSystem, descriptor);
|
|
570
|
+
if (
|
|
571
|
+
statSizeExceedsLimit(openedAfterRead, MAX_SIDECAR_BYTES) ||
|
|
572
|
+
Buffer.byteLength(serialized, "utf8") > MAX_SIDECAR_BYTES
|
|
573
|
+
) {
|
|
574
|
+
throw new Error("sidecar exceeds maximum UTF-8 byte size");
|
|
575
|
+
}
|
|
576
|
+
const pathAfterRead = lstatIdentity(fileSystem, physicalPath);
|
|
577
|
+
ensurePrivateOwnership(filePath, pathAfterRead, "file");
|
|
578
|
+
if (
|
|
579
|
+
!sameOpenedIdentity(opened, openedAfterRead) ||
|
|
580
|
+
!samePathHandleIdentity(
|
|
581
|
+
pathAfterRead,
|
|
582
|
+
openedAfterRead,
|
|
583
|
+
parent.trustedDevice,
|
|
584
|
+
parent.runtime,
|
|
585
|
+
)
|
|
586
|
+
) {
|
|
587
|
+
throw new Error("sidecar identity changed during read");
|
|
588
|
+
}
|
|
589
|
+
parent.verify();
|
|
590
|
+
} catch (cause) {
|
|
591
|
+
if (cause?.name === "SessionBudgetPersistenceError") throw cause;
|
|
592
|
+
throw runtimeError("read", filePath, cause);
|
|
593
|
+
} finally {
|
|
594
|
+
if (descriptor !== null) {
|
|
595
|
+
try {
|
|
596
|
+
fileSystem.closeSync(descriptor);
|
|
597
|
+
} catch {
|
|
598
|
+
// The read/identity result remains authoritative.
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
parent.close();
|
|
602
|
+
}
|
|
603
|
+
try {
|
|
604
|
+
return validateRecord(JSON.parse(serialized), sessionId, filePath);
|
|
605
|
+
} catch (cause) {
|
|
606
|
+
if (cause?.name === "SessionBudgetPersistenceError") throw cause;
|
|
607
|
+
throw runtimeError("corrupt", filePath, cause);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function writeRecordAt(filePath, value, fileSystem = fs, ioOptions = {}) {
|
|
612
|
+
let serialized;
|
|
613
|
+
try {
|
|
614
|
+
serialized = `${JSON.stringify(value, null, 2)}\n`;
|
|
615
|
+
} catch (cause) {
|
|
616
|
+
throw runtimeError("write", filePath, cause);
|
|
617
|
+
}
|
|
618
|
+
if (Buffer.byteLength(serialized, "utf8") > MAX_SIDECAR_BYTES) {
|
|
619
|
+
throw runtimeError(
|
|
620
|
+
"write",
|
|
621
|
+
filePath,
|
|
622
|
+
new Error("sidecar exceeds maximum UTF-8 byte size"),
|
|
623
|
+
"ERR_SESSION_BUDGET_SIDECAR_TOO_LARGE",
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
const directory = path.dirname(filePath);
|
|
627
|
+
const parent = openVerifiedParent(filePath, fileSystem, ioOptions);
|
|
628
|
+
const temporary = path.join(
|
|
629
|
+
directory,
|
|
630
|
+
`.${path.basename(filePath)}.${process.pid}.${crypto.randomUUID()}.tmp`,
|
|
631
|
+
);
|
|
632
|
+
const physicalTemporary = parent.resolve(temporary);
|
|
633
|
+
const physicalFilePath = parent.resolve(filePath);
|
|
634
|
+
let descriptor = null;
|
|
635
|
+
let renamed = false;
|
|
636
|
+
let temporaryIdentity = null;
|
|
637
|
+
let temporaryPathIdentity = null;
|
|
638
|
+
try {
|
|
639
|
+
descriptor = fileSystem.openSync(physicalTemporary, "wx", 0o600);
|
|
640
|
+
fileSystem.writeFileSync(descriptor, serialized, "utf8");
|
|
641
|
+
fileSystem.fsyncSync(descriptor);
|
|
642
|
+
temporaryIdentity = fstatIdentity(fileSystem, descriptor);
|
|
643
|
+
ensurePrivateOwnership(temporary, temporaryIdentity, "file");
|
|
644
|
+
temporaryPathIdentity = lstatIdentity(fileSystem, physicalTemporary);
|
|
645
|
+
ensurePrivateOwnership(temporary, temporaryPathIdentity, "file");
|
|
646
|
+
if (
|
|
647
|
+
!samePathHandleIdentity(
|
|
648
|
+
temporaryPathIdentity,
|
|
649
|
+
temporaryIdentity,
|
|
650
|
+
parent.trustedDevice,
|
|
651
|
+
parent.runtime,
|
|
652
|
+
)
|
|
653
|
+
) {
|
|
654
|
+
throw new Error("temporary sidecar identity changed before commit");
|
|
655
|
+
}
|
|
656
|
+
fileSystem.closeSync(descriptor);
|
|
657
|
+
descriptor = null;
|
|
658
|
+
fileSystem.renameSync(physicalTemporary, physicalFilePath);
|
|
659
|
+
renamed = true;
|
|
660
|
+
const committed = lstatIdentity(fileSystem, physicalFilePath);
|
|
661
|
+
ensurePrivateOwnership(filePath, committed, "file");
|
|
662
|
+
// Rename may legitimately change ctime. Both samples come from the path
|
|
663
|
+
// API, so exact BigInt dev+ino continuity is the stable commit identity.
|
|
664
|
+
if (!sameIdentity(temporaryPathIdentity, committed)) {
|
|
665
|
+
throw new Error("committed sidecar identity mismatch");
|
|
666
|
+
}
|
|
667
|
+
parent.verify();
|
|
668
|
+
|
|
669
|
+
parent.sync();
|
|
670
|
+
parent.verify();
|
|
671
|
+
} catch (cause) {
|
|
672
|
+
if (descriptor !== null) {
|
|
673
|
+
try {
|
|
674
|
+
fileSystem.closeSync(descriptor);
|
|
675
|
+
} catch {
|
|
676
|
+
// Preserve the original write failure.
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (!renamed) {
|
|
680
|
+
try {
|
|
681
|
+
fileSystem.unlinkSync(physicalTemporary);
|
|
682
|
+
} catch {
|
|
683
|
+
// Best-effort orphan cleanup only.
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
throw runtimeError("write", filePath, cause);
|
|
687
|
+
} finally {
|
|
688
|
+
parent.close();
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
export function sessionBudgetPath(sessionId) {
|
|
693
|
+
return `${sessionPath(sessionId)}.budget.json`;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
export class SessionBudgetSidecarStore {
|
|
697
|
+
constructor({
|
|
698
|
+
resolvePath = sessionBudgetPath,
|
|
699
|
+
fileSystem = fs,
|
|
700
|
+
lock = withFileLock,
|
|
701
|
+
lockOptions = {},
|
|
702
|
+
now = () => Date.now(),
|
|
703
|
+
platform = process.platform,
|
|
704
|
+
uvVersion = process.versions.uv,
|
|
705
|
+
allowUnsupportedPlatformForTests = false,
|
|
706
|
+
} = {}) {
|
|
707
|
+
this._resolvePath = resolvePath;
|
|
708
|
+
this._fs = fileSystem;
|
|
709
|
+
this._lock = lock;
|
|
710
|
+
this._lockOptions = lockOptions;
|
|
711
|
+
this._now = now;
|
|
712
|
+
this._platform = String(platform);
|
|
713
|
+
this._allowUnsupportedPlatformForTests =
|
|
714
|
+
allowUnsupportedPlatformForTests === true;
|
|
715
|
+
this._ioOptions = {
|
|
716
|
+
platform: this._platform,
|
|
717
|
+
uvVersion: String(uvVersion || ""),
|
|
718
|
+
allowUnsupportedPlatformForTests: this._allowUnsupportedPlatformForTests,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
_assertSupported(filePath) {
|
|
723
|
+
if (this._platform !== "linux" && !this._allowUnsupportedPlatformForTests) {
|
|
724
|
+
throw runtimeError(
|
|
725
|
+
"unsupported",
|
|
726
|
+
filePath,
|
|
727
|
+
new Error(
|
|
728
|
+
`durable session budget sidecars require Linux dirfd-relative I/O; ${this._platform} is unsupported`,
|
|
729
|
+
),
|
|
730
|
+
"ERR_SESSION_BUDGET_DURABLE_STORE_UNSUPPORTED",
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
pathForSession(sessionId) {
|
|
736
|
+
return this._resolvePath(String(sessionId));
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
usageUnknownPathForSession(sessionId) {
|
|
740
|
+
return `${this.pathForSession(sessionId)}${USAGE_UNKNOWN_SUFFIX}`;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
read(sessionId) {
|
|
744
|
+
const id = String(sessionId);
|
|
745
|
+
const filePath = this.pathForSession(id);
|
|
746
|
+
this._assertSupported(filePath);
|
|
747
|
+
const markerPath = this.usageUnknownPathForSession(id);
|
|
748
|
+
try {
|
|
749
|
+
lstatIdentity(this._fs, path.dirname(filePath));
|
|
750
|
+
} catch (cause) {
|
|
751
|
+
if (cause?.code === "ENOENT") return null;
|
|
752
|
+
throw runtimeError("read", path.dirname(filePath), cause);
|
|
753
|
+
}
|
|
754
|
+
return this._lock(
|
|
755
|
+
filePath,
|
|
756
|
+
() => {
|
|
757
|
+
const current = readRecordAt(filePath, id, this._fs, this._ioOptions);
|
|
758
|
+
const marker = readRecordAt(markerPath, id, this._fs, this._ioOptions);
|
|
759
|
+
if (!marker) return current;
|
|
760
|
+
const snapshot = mergeUsageUnknownSnapshot(
|
|
761
|
+
current?.snapshot ?? marker.snapshot,
|
|
762
|
+
marker.snapshot,
|
|
763
|
+
markerPath,
|
|
764
|
+
);
|
|
765
|
+
return {
|
|
766
|
+
...(current || marker),
|
|
767
|
+
// A marker can survive the first canonical intent write. In that
|
|
768
|
+
// case the main sidecar is absent and its CAS authority is null.
|
|
769
|
+
revision: current?.revision ?? null,
|
|
770
|
+
snapshot,
|
|
771
|
+
usageUnknown: true,
|
|
772
|
+
usageUnknownMarker: usageUnknownMarkerIdentity(marker),
|
|
773
|
+
};
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
timeoutMs: 2000,
|
|
777
|
+
staleMs: 30_000,
|
|
778
|
+
failIfUnavailable: true,
|
|
779
|
+
...this._lockOptions,
|
|
780
|
+
},
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
write(sessionId, snapshot, { expectedRevision = null } = {}) {
|
|
785
|
+
return this._write(sessionId, snapshot, { expectedRevision });
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
finalizeUsageUnknown(
|
|
789
|
+
sessionId,
|
|
790
|
+
snapshot,
|
|
791
|
+
{ expectedRevision = null, expectedUsageUnknownMarker = null } = {},
|
|
792
|
+
) {
|
|
793
|
+
return this._write(
|
|
794
|
+
sessionId,
|
|
795
|
+
snapshot,
|
|
796
|
+
{ expectedRevision },
|
|
797
|
+
{ resolveUsageUnknown: true, expectedUsageUnknownMarker },
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
_write(
|
|
802
|
+
sessionId,
|
|
803
|
+
snapshot,
|
|
804
|
+
{ expectedRevision = null } = {},
|
|
805
|
+
{ resolveUsageUnknown = false, expectedUsageUnknownMarker = null } = {},
|
|
806
|
+
) {
|
|
807
|
+
const id = String(sessionId);
|
|
808
|
+
const filePath = this.pathForSession(id);
|
|
809
|
+
this._assertSupported(filePath);
|
|
810
|
+
if (
|
|
811
|
+
expectedRevision !== null &&
|
|
812
|
+
(!Number.isSafeInteger(expectedRevision) || expectedRevision < 1)
|
|
813
|
+
) {
|
|
814
|
+
throw runtimeError(
|
|
815
|
+
"revision",
|
|
816
|
+
filePath,
|
|
817
|
+
new TypeError(
|
|
818
|
+
"expected revision must be null or a positive safe integer",
|
|
819
|
+
),
|
|
820
|
+
"ERR_SESSION_BUDGET_UNSAFE_REVISION",
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
const canonicalSnapshot = canonicalizeSnapshot(
|
|
824
|
+
snapshot,
|
|
825
|
+
filePath,
|
|
826
|
+
"snapshot",
|
|
827
|
+
);
|
|
828
|
+
const expectedMarker = resolveUsageUnknown
|
|
829
|
+
? normalizeUsageUnknownMarkerIdentity(
|
|
830
|
+
expectedUsageUnknownMarker,
|
|
831
|
+
this.usageUnknownPathForSession(id),
|
|
832
|
+
)
|
|
833
|
+
: null;
|
|
834
|
+
try {
|
|
835
|
+
this._fs.mkdirSync(path.dirname(filePath), {
|
|
836
|
+
recursive: true,
|
|
837
|
+
mode: 0o700,
|
|
838
|
+
});
|
|
839
|
+
} catch (cause) {
|
|
840
|
+
throw runtimeError("prepare", filePath, cause);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const result = this._lock(
|
|
844
|
+
filePath,
|
|
845
|
+
() => {
|
|
846
|
+
const current = readRecordAt(filePath, id, this._fs, this._ioOptions);
|
|
847
|
+
const marker = readRecordAt(
|
|
848
|
+
this.usageUnknownPathForSession(id),
|
|
849
|
+
id,
|
|
850
|
+
this._fs,
|
|
851
|
+
this._ioOptions,
|
|
852
|
+
);
|
|
853
|
+
const actualMarker = usageUnknownMarkerIdentity(marker);
|
|
854
|
+
const clean = usageSettlementEntries(canonicalSnapshot).length === 0;
|
|
855
|
+
if (marker && clean && !resolveUsageUnknown) {
|
|
856
|
+
throw runtimeError(
|
|
857
|
+
"recovery",
|
|
858
|
+
filePath,
|
|
859
|
+
new Error("unknown usage must be adjudicated before a clean write"),
|
|
860
|
+
"ERR_SESSION_BUDGET_RECOVERY_REQUIRED",
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
if (resolveUsageUnknown && !marker) {
|
|
864
|
+
throw runtimeError(
|
|
865
|
+
"recovery",
|
|
866
|
+
filePath,
|
|
867
|
+
new Error("usage-unknown marker is required for finalization"),
|
|
868
|
+
"ERR_SESSION_BUDGET_RECOVERY_REQUIRED",
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
if (
|
|
872
|
+
resolveUsageUnknown &&
|
|
873
|
+
!sameUsageUnknownMarkerIdentity(expectedMarker, actualMarker)
|
|
874
|
+
) {
|
|
875
|
+
throw runtimeError(
|
|
876
|
+
"conflict",
|
|
877
|
+
this.usageUnknownPathForSession(id),
|
|
878
|
+
new Error("usage-unknown marker changed after it was observed"),
|
|
879
|
+
"ERR_SESSION_BUDGET_MARKER_CONFLICT",
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
if (resolveUsageUnknown && !clean) {
|
|
883
|
+
throw runtimeError(
|
|
884
|
+
"recovery",
|
|
885
|
+
filePath,
|
|
886
|
+
new Error("usage finalization snapshot must be clean"),
|
|
887
|
+
"ERR_SESSION_BUDGET_RECOVERY_REQUIRED",
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
const actualRevision = current?.revision ?? null;
|
|
891
|
+
if (actualRevision !== expectedRevision) {
|
|
892
|
+
throw runtimeError(
|
|
893
|
+
"conflict",
|
|
894
|
+
filePath,
|
|
895
|
+
new Error(
|
|
896
|
+
`expected revision ${String(expectedRevision)}, found ${String(actualRevision)}`,
|
|
897
|
+
),
|
|
898
|
+
"ERR_SESSION_BUDGET_CONFLICT",
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
if (actualRevision === Number.MAX_SAFE_INTEGER) {
|
|
902
|
+
throw runtimeError(
|
|
903
|
+
"revision",
|
|
904
|
+
filePath,
|
|
905
|
+
new Error("sidecar revision is exhausted"),
|
|
906
|
+
"ERR_SESSION_BUDGET_UNSAFE_REVISION",
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
const revision = (actualRevision || 0) + 1;
|
|
910
|
+
const record = {
|
|
911
|
+
version: SESSION_BUDGET_SIDECAR_VERSION,
|
|
912
|
+
sessionId: id,
|
|
913
|
+
revision,
|
|
914
|
+
storedAt: new Date(this._now()).toISOString(),
|
|
915
|
+
writer: { pid: process.pid },
|
|
916
|
+
snapshot: canonicalSnapshot,
|
|
917
|
+
};
|
|
918
|
+
writeRecordAt(filePath, record, this._fs, this._ioOptions);
|
|
919
|
+
if (resolveUsageUnknown) {
|
|
920
|
+
this._clearUsageUnknownLocked(id, expectedMarker);
|
|
921
|
+
}
|
|
922
|
+
return {
|
|
923
|
+
revision,
|
|
924
|
+
filePath,
|
|
925
|
+
...(resolveUsageUnknown ? { usageUnknownMarker: null } : {}),
|
|
926
|
+
};
|
|
927
|
+
},
|
|
928
|
+
{
|
|
929
|
+
timeoutMs: 2000,
|
|
930
|
+
staleMs: 30_000,
|
|
931
|
+
failIfUnavailable: true,
|
|
932
|
+
...this._lockOptions,
|
|
933
|
+
},
|
|
934
|
+
);
|
|
935
|
+
return result;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
markUsageUnknown(sessionId, snapshot) {
|
|
939
|
+
const id = String(sessionId);
|
|
940
|
+
const filePath = this.pathForSession(id);
|
|
941
|
+
this._assertSupported(filePath);
|
|
942
|
+
const markerPath = this.usageUnknownPathForSession(id);
|
|
943
|
+
const canonicalSnapshot = canonicalizeSnapshot(
|
|
944
|
+
snapshot,
|
|
945
|
+
markerPath,
|
|
946
|
+
"snapshot",
|
|
947
|
+
);
|
|
948
|
+
if (usageSettlementEntries(canonicalSnapshot).length === 0) {
|
|
949
|
+
throw runtimeError(
|
|
950
|
+
"marker",
|
|
951
|
+
markerPath,
|
|
952
|
+
new TypeError("usage-unknown marker requires a pending settlement"),
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
try {
|
|
956
|
+
this._fs.mkdirSync(path.dirname(markerPath), {
|
|
957
|
+
recursive: true,
|
|
958
|
+
mode: 0o700,
|
|
959
|
+
});
|
|
960
|
+
} catch (cause) {
|
|
961
|
+
throw runtimeError("prepare", markerPath, cause);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
return this._lock(
|
|
965
|
+
filePath,
|
|
966
|
+
() => {
|
|
967
|
+
const current = readRecordAt(markerPath, id, this._fs, this._ioOptions);
|
|
968
|
+
if (current?.revision === Number.MAX_SAFE_INTEGER) {
|
|
969
|
+
throw runtimeError(
|
|
970
|
+
"revision",
|
|
971
|
+
markerPath,
|
|
972
|
+
new Error("usage-unknown marker revision is exhausted"),
|
|
973
|
+
"ERR_SESSION_BUDGET_UNSAFE_REVISION",
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
const merged = current
|
|
977
|
+
? mergeUsageUnknownSnapshot(
|
|
978
|
+
canonicalSnapshot,
|
|
979
|
+
current.snapshot,
|
|
980
|
+
markerPath,
|
|
981
|
+
)
|
|
982
|
+
: canonicalSnapshot;
|
|
983
|
+
const revision = (current?.revision || 0) + 1;
|
|
984
|
+
const record = {
|
|
985
|
+
version: SESSION_BUDGET_SIDECAR_VERSION,
|
|
986
|
+
sessionId: id,
|
|
987
|
+
revision,
|
|
988
|
+
storedAt: new Date(this._now()).toISOString(),
|
|
989
|
+
writer: { pid: process.pid },
|
|
990
|
+
snapshot: merged,
|
|
991
|
+
};
|
|
992
|
+
writeRecordAt(markerPath, record, this._fs, this._ioOptions);
|
|
993
|
+
const committed = readRecordAt(
|
|
994
|
+
markerPath,
|
|
995
|
+
id,
|
|
996
|
+
this._fs,
|
|
997
|
+
this._ioOptions,
|
|
998
|
+
);
|
|
999
|
+
const usageUnknownMarker = usageUnknownMarkerIdentity(committed);
|
|
1000
|
+
if (current) {
|
|
1001
|
+
// Preserve the newly reported settlement, but do not let a runtime
|
|
1002
|
+
// that did not observe the prior marker later finalize both. The
|
|
1003
|
+
// caller fails closed and a fresh recovery host must see the union.
|
|
1004
|
+
throw runtimeError(
|
|
1005
|
+
"conflict",
|
|
1006
|
+
markerPath,
|
|
1007
|
+
new Error("usage-unknown marker changed after it was observed"),
|
|
1008
|
+
"ERR_SESSION_BUDGET_MARKER_CONFLICT",
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
return { revision, filePath: markerPath, usageUnknownMarker };
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
timeoutMs: 2000,
|
|
1015
|
+
staleMs: 30_000,
|
|
1016
|
+
failIfUnavailable: true,
|
|
1017
|
+
...this._lockOptions,
|
|
1018
|
+
},
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
clearUsageUnknown(sessionId, { expectedUsageUnknownMarker = null } = {}) {
|
|
1023
|
+
const id = String(sessionId);
|
|
1024
|
+
const filePath = this.pathForSession(id);
|
|
1025
|
+
this._assertSupported(filePath);
|
|
1026
|
+
const markerPath = this.usageUnknownPathForSession(id);
|
|
1027
|
+
const expectedMarker = normalizeUsageUnknownMarkerIdentity(
|
|
1028
|
+
expectedUsageUnknownMarker,
|
|
1029
|
+
markerPath,
|
|
1030
|
+
);
|
|
1031
|
+
return this._lock(
|
|
1032
|
+
filePath,
|
|
1033
|
+
() => this._clearUsageUnknownLocked(id, expectedMarker),
|
|
1034
|
+
{
|
|
1035
|
+
timeoutMs: 2000,
|
|
1036
|
+
staleMs: 30_000,
|
|
1037
|
+
failIfUnavailable: true,
|
|
1038
|
+
...this._lockOptions,
|
|
1039
|
+
},
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
_clearUsageUnknownLocked(sessionId, expectedUsageUnknownMarker) {
|
|
1044
|
+
const id = String(sessionId);
|
|
1045
|
+
const markerPath = this.usageUnknownPathForSession(id);
|
|
1046
|
+
const marker = readRecordAt(markerPath, id, this._fs, this._ioOptions);
|
|
1047
|
+
const actualMarker = usageUnknownMarkerIdentity(marker);
|
|
1048
|
+
if (
|
|
1049
|
+
!sameUsageUnknownMarkerIdentity(expectedUsageUnknownMarker, actualMarker)
|
|
1050
|
+
) {
|
|
1051
|
+
throw runtimeError(
|
|
1052
|
+
"conflict",
|
|
1053
|
+
markerPath,
|
|
1054
|
+
new Error("usage-unknown marker changed before clear"),
|
|
1055
|
+
"ERR_SESSION_BUDGET_MARKER_CONFLICT",
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
const parent = openVerifiedParent(markerPath, this._fs, this._ioOptions);
|
|
1059
|
+
const tombstone = `${markerPath}.${process.pid}.${crypto.randomUUID()}.clear`;
|
|
1060
|
+
const physicalMarkerPath = parent.resolve(markerPath);
|
|
1061
|
+
const physicalTombstone = parent.resolve(tombstone);
|
|
1062
|
+
try {
|
|
1063
|
+
const before = lstatIdentity(this._fs, physicalMarkerPath);
|
|
1064
|
+
ensurePrivateOwnership(markerPath, before, "file");
|
|
1065
|
+
this._fs.renameSync(physicalMarkerPath, physicalTombstone);
|
|
1066
|
+
const moved = lstatIdentity(this._fs, physicalTombstone);
|
|
1067
|
+
ensurePrivateOwnership(tombstone, moved, "file");
|
|
1068
|
+
if (!sameIdentity(before, moved)) {
|
|
1069
|
+
throw new Error("usage-unknown marker identity changed during clear");
|
|
1070
|
+
}
|
|
1071
|
+
this._fs.unlinkSync(physicalTombstone);
|
|
1072
|
+
parent.verify();
|
|
1073
|
+
parent.sync();
|
|
1074
|
+
parent.verify();
|
|
1075
|
+
return true;
|
|
1076
|
+
} catch (cause) {
|
|
1077
|
+
throw runtimeError("clear", markerPath, cause);
|
|
1078
|
+
} finally {
|
|
1079
|
+
parent.close();
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
class SharedSessionBudgetRuntime {
|
|
1085
|
+
constructor({
|
|
1086
|
+
sessionId,
|
|
1087
|
+
key,
|
|
1088
|
+
store,
|
|
1089
|
+
budget,
|
|
1090
|
+
revision,
|
|
1091
|
+
usageUnknownMarker,
|
|
1092
|
+
registry,
|
|
1093
|
+
}) {
|
|
1094
|
+
this.sessionId = sessionId;
|
|
1095
|
+
this.key = key;
|
|
1096
|
+
this.store = store;
|
|
1097
|
+
this.budget = budget;
|
|
1098
|
+
this.revision = revision;
|
|
1099
|
+
this.usageUnknownMarker = usageUnknownMarker || null;
|
|
1100
|
+
this.registry = registry;
|
|
1101
|
+
this.references = 0;
|
|
1102
|
+
this.observers = new Set();
|
|
1103
|
+
this.persistenceError = null;
|
|
1104
|
+
this._persisting = false;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
observe(callback) {
|
|
1108
|
+
if (typeof callback !== "function") return () => false;
|
|
1109
|
+
this.observers.add(callback);
|
|
1110
|
+
return () => this.observers.delete(callback);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
onBudgetEvent(event) {
|
|
1114
|
+
for (const observer of this.observers) {
|
|
1115
|
+
try {
|
|
1116
|
+
const pending = observer(event);
|
|
1117
|
+
void Promise.resolve(pending).catch(() => {});
|
|
1118
|
+
} catch {
|
|
1119
|
+
// Observability never weakens the budget authority.
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
assertMutationAllowed() {
|
|
1125
|
+
this.budget._assertAuthorityMutationAllowed?.();
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
_persistTransaction(write) {
|
|
1129
|
+
this.assertMutationAllowed();
|
|
1130
|
+
if (this.persistenceError) throw this.persistenceError;
|
|
1131
|
+
if (this._persisting) {
|
|
1132
|
+
const error = runtimeError(
|
|
1133
|
+
"reentrant",
|
|
1134
|
+
this.store.pathForSession(this.sessionId),
|
|
1135
|
+
new Error("authority persistence cannot re-enter"),
|
|
1136
|
+
"ERR_SESSION_BUDGET_REENTRANT_PERSISTENCE",
|
|
1137
|
+
);
|
|
1138
|
+
this.persistenceError = error;
|
|
1139
|
+
throw error;
|
|
1140
|
+
}
|
|
1141
|
+
this._persisting = true;
|
|
1142
|
+
try {
|
|
1143
|
+
const result = write();
|
|
1144
|
+
if (this.persistenceError) throw this.persistenceError;
|
|
1145
|
+
if (!Number.isSafeInteger(result?.revision) || result.revision < 1) {
|
|
1146
|
+
throw runtimeError(
|
|
1147
|
+
"write",
|
|
1148
|
+
this.store.pathForSession(this.sessionId),
|
|
1149
|
+
new TypeError("store returned an invalid revision"),
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
this.revision = result.revision;
|
|
1153
|
+
if (Object.hasOwn(result, "usageUnknownMarker")) {
|
|
1154
|
+
this.usageUnknownMarker = result.usageUnknownMarker
|
|
1155
|
+
? normalizeUsageUnknownMarkerIdentity(
|
|
1156
|
+
result.usageUnknownMarker,
|
|
1157
|
+
this.store.pathForSession(this.sessionId),
|
|
1158
|
+
)
|
|
1159
|
+
: null;
|
|
1160
|
+
}
|
|
1161
|
+
return this.revision;
|
|
1162
|
+
} catch (error) {
|
|
1163
|
+
this.persistenceError = error;
|
|
1164
|
+
throw error;
|
|
1165
|
+
} finally {
|
|
1166
|
+
this._persisting = false;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
persistSnapshot(snapshot) {
|
|
1171
|
+
return this._persistTransaction(() =>
|
|
1172
|
+
this.store.write(this.sessionId, snapshot, {
|
|
1173
|
+
expectedRevision: this.revision,
|
|
1174
|
+
}),
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
persist() {
|
|
1179
|
+
this.assertMutationAllowed();
|
|
1180
|
+
return this.persistSnapshot(this.budget.snapshot());
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
persistAuthority(change) {
|
|
1184
|
+
if (change.type === "budget:usage-settlement-started") {
|
|
1185
|
+
return this._persistTransaction(() => {
|
|
1186
|
+
if (typeof this.store.markUsageUnknown !== "function") {
|
|
1187
|
+
throw runtimeError(
|
|
1188
|
+
"marker",
|
|
1189
|
+
this.store.pathForSession(this.sessionId),
|
|
1190
|
+
new Error("store cannot persist unknown usage intent"),
|
|
1191
|
+
"ERR_SESSION_BUDGET_MARKER_UNAVAILABLE",
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
const marker = this.store.markUsageUnknown(
|
|
1195
|
+
this.sessionId,
|
|
1196
|
+
change.snapshot,
|
|
1197
|
+
);
|
|
1198
|
+
const usageUnknownMarker = normalizeUsageUnknownMarkerIdentity(
|
|
1199
|
+
marker?.usageUnknownMarker,
|
|
1200
|
+
this.store.pathForSession(this.sessionId),
|
|
1201
|
+
);
|
|
1202
|
+
const result = this.store.write(this.sessionId, change.snapshot, {
|
|
1203
|
+
expectedRevision: this.revision,
|
|
1204
|
+
});
|
|
1205
|
+
return {
|
|
1206
|
+
...result,
|
|
1207
|
+
usageUnknownMarker,
|
|
1208
|
+
};
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
if (change.type === "budget:usage-recorded") {
|
|
1213
|
+
return this._persistTransaction(() => {
|
|
1214
|
+
if (typeof this.store.finalizeUsageUnknown !== "function") {
|
|
1215
|
+
throw runtimeError(
|
|
1216
|
+
"finalize",
|
|
1217
|
+
this.store.pathForSession(this.sessionId),
|
|
1218
|
+
new Error("store cannot durably finalize unknown usage"),
|
|
1219
|
+
"ERR_SESSION_BUDGET_MARKER_UNAVAILABLE",
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
return this.store.finalizeUsageUnknown(
|
|
1223
|
+
this.sessionId,
|
|
1224
|
+
change.snapshot,
|
|
1225
|
+
{
|
|
1226
|
+
expectedRevision: this.revision,
|
|
1227
|
+
expectedUsageUnknownMarker: this.usageUnknownMarker,
|
|
1228
|
+
},
|
|
1229
|
+
);
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if (change.type === "budget:recovery-adjudicated") {
|
|
1234
|
+
return this._persistTransaction(() => {
|
|
1235
|
+
if (this.usageUnknownMarker) {
|
|
1236
|
+
if (typeof this.store.finalizeUsageUnknown !== "function") {
|
|
1237
|
+
throw runtimeError(
|
|
1238
|
+
"finalize",
|
|
1239
|
+
this.store.pathForSession(this.sessionId),
|
|
1240
|
+
new Error("store cannot durably adjudicate unknown usage"),
|
|
1241
|
+
"ERR_SESSION_BUDGET_MARKER_UNAVAILABLE",
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
return this.store.finalizeUsageUnknown(
|
|
1245
|
+
this.sessionId,
|
|
1246
|
+
change.snapshot,
|
|
1247
|
+
{
|
|
1248
|
+
expectedRevision: this.revision,
|
|
1249
|
+
expectedUsageUnknownMarker: this.usageUnknownMarker,
|
|
1250
|
+
},
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
return this.store.write(this.sessionId, change.snapshot, {
|
|
1254
|
+
expectedRevision: this.revision,
|
|
1255
|
+
});
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
return this.persistSnapshot(change.snapshot);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
retain({ limits = {}, onEvent = null, signal = null } = {}) {
|
|
1263
|
+
this.budget.tightenLimits?.(limits);
|
|
1264
|
+
let unlinkSignal = () => {};
|
|
1265
|
+
let signalFailure = null;
|
|
1266
|
+
let retaining = true;
|
|
1267
|
+
if (signal) {
|
|
1268
|
+
const abort = () => {
|
|
1269
|
+
try {
|
|
1270
|
+
this.budget.abort(signal.reason || "host-aborted", {
|
|
1271
|
+
reason: "host-aborted",
|
|
1272
|
+
});
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
signalFailure = error;
|
|
1275
|
+
// Once a handle exists, the budget has already failed closed and its
|
|
1276
|
+
// status exposes the durable error. Do not turn a later EventTarget
|
|
1277
|
+
// callback into an uncaught process exception.
|
|
1278
|
+
if (!retaining) return;
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
if (signal.aborted) abort();
|
|
1282
|
+
else {
|
|
1283
|
+
unlinkSignal = () => {
|
|
1284
|
+
try {
|
|
1285
|
+
signal.removeEventListener?.("abort", abort);
|
|
1286
|
+
} catch {
|
|
1287
|
+
// Listener cleanup must not retain the shared runtime forever.
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
try {
|
|
1291
|
+
signal.addEventListener?.("abort", abort, { once: true });
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
// A custom EventTarget may register and then throw. Best-effort
|
|
1294
|
+
// unlink closes that leak before openSessionBudget unwinds.
|
|
1295
|
+
unlinkSignal();
|
|
1296
|
+
throw error;
|
|
1297
|
+
}
|
|
1298
|
+
// Custom signals need not provide the browser's atomic add/check
|
|
1299
|
+
// behavior. Close the narrow race before retaining process state.
|
|
1300
|
+
if (signal.aborted) abort();
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
retaining = false;
|
|
1304
|
+
if (signalFailure) {
|
|
1305
|
+
unlinkSignal();
|
|
1306
|
+
throw signalFailure;
|
|
1307
|
+
}
|
|
1308
|
+
const unobserve = this.observe(onEvent);
|
|
1309
|
+
this.references += 1;
|
|
1310
|
+
let closed = false;
|
|
1311
|
+
return new SessionBudgetRuntimeHandle(this, () => {
|
|
1312
|
+
if (closed) return false;
|
|
1313
|
+
this.assertMutationAllowed();
|
|
1314
|
+
const released = this.release();
|
|
1315
|
+
// A failed final persist leaves this handle live and the runtime
|
|
1316
|
+
// registered but poisoned. Only successful release may detach the
|
|
1317
|
+
// handle's observer/signal capabilities.
|
|
1318
|
+
closed = true;
|
|
1319
|
+
unlinkSignal();
|
|
1320
|
+
unobserve();
|
|
1321
|
+
return released;
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
release() {
|
|
1326
|
+
this.assertMutationAllowed();
|
|
1327
|
+
if (this.references <= 0) return false;
|
|
1328
|
+
if (this.references > 1) {
|
|
1329
|
+
this.references -= 1;
|
|
1330
|
+
return true;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
try {
|
|
1334
|
+
this.persist();
|
|
1335
|
+
} catch (error) {
|
|
1336
|
+
this.budget._failClosedAuthorityPersistence(error, "runtime-close");
|
|
1337
|
+
throw error;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
this.references = 0;
|
|
1341
|
+
this.budget._revokeRuntimeAuthority();
|
|
1342
|
+
if (this.registry.get(this.key) === this) this.registry.delete(this.key);
|
|
1343
|
+
return true;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
export class SessionBudgetRuntimeHandle {
|
|
1348
|
+
constructor(runtime, close) {
|
|
1349
|
+
this.sessionId = runtime.sessionId;
|
|
1350
|
+
this.budget = runtime.budget;
|
|
1351
|
+
this.filePath = runtime.store.pathForSession(runtime.sessionId);
|
|
1352
|
+
this._runtime = runtime;
|
|
1353
|
+
this._close = close;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
persist() {
|
|
1357
|
+
return this._runtime.persist();
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
status() {
|
|
1361
|
+
return {
|
|
1362
|
+
...this.budget.status(),
|
|
1363
|
+
persistenceRevision: this._runtime.revision,
|
|
1364
|
+
persistenceError: this._runtime.persistenceError?.message || null,
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
close() {
|
|
1369
|
+
return this._close();
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
function registryKey(store, sessionId) {
|
|
1374
|
+
const filePath = path.resolve(store.pathForSession(sessionId));
|
|
1375
|
+
return process.platform === "win32" ? filePath.toLowerCase() : filePath;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/**
|
|
1379
|
+
* Open (or reuse) one process-local budget instance for a logical session.
|
|
1380
|
+
* Restored dirty snapshots remain blocked until the caller explicitly invokes
|
|
1381
|
+
* `budget.adjudicateRecovery({ abandoned: exactIds })`.
|
|
1382
|
+
*/
|
|
1383
|
+
export function openSessionBudget(
|
|
1384
|
+
sessionId,
|
|
1385
|
+
{
|
|
1386
|
+
limits = {},
|
|
1387
|
+
table = undefined,
|
|
1388
|
+
signal = null,
|
|
1389
|
+
onEvent = null,
|
|
1390
|
+
recoverUnsettled = "require-adjudication",
|
|
1391
|
+
store = new SessionBudgetSidecarStore(),
|
|
1392
|
+
registry = DEFAULT_REGISTRY,
|
|
1393
|
+
now = () => Date.now(),
|
|
1394
|
+
setTimer = (fn, delay) => setTimeout(fn, delay),
|
|
1395
|
+
clearTimer = (timer) => clearTimeout(timer),
|
|
1396
|
+
} = {},
|
|
1397
|
+
) {
|
|
1398
|
+
const id = String(sessionId || "");
|
|
1399
|
+
if (recoverUnsettled !== "require-adjudication") {
|
|
1400
|
+
throw new TypeError(
|
|
1401
|
+
"session budget runtime requires explicit recovery adjudication",
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
// sessionBudgetPath/sessionPath performs the canonical segment validation.
|
|
1405
|
+
const key = registryKey(store, id);
|
|
1406
|
+
const existing = registry.get(key);
|
|
1407
|
+
if (existing) return existing.retain({ limits, onEvent, signal });
|
|
1408
|
+
|
|
1409
|
+
const stored = store.read(id);
|
|
1410
|
+
let runtime = null;
|
|
1411
|
+
const dispatch = (event) => runtime?.onBudgetEvent(event);
|
|
1412
|
+
const persistAuthority = (change) => runtime?.persistAuthority(change);
|
|
1413
|
+
const common = {
|
|
1414
|
+
table,
|
|
1415
|
+
now,
|
|
1416
|
+
setTimer,
|
|
1417
|
+
clearTimer,
|
|
1418
|
+
onEvent: dispatch,
|
|
1419
|
+
onAuthorityChange: persistAuthority,
|
|
1420
|
+
};
|
|
1421
|
+
const budget = stored
|
|
1422
|
+
? SessionResourceBudget.restore(stored.snapshot, {
|
|
1423
|
+
...common,
|
|
1424
|
+
overrides: limits,
|
|
1425
|
+
recoverUnsettled: "require-adjudication",
|
|
1426
|
+
})
|
|
1427
|
+
: new SessionResourceBudget({ ...limits, ...common });
|
|
1428
|
+
runtime = new SharedSessionBudgetRuntime({
|
|
1429
|
+
sessionId: id,
|
|
1430
|
+
key,
|
|
1431
|
+
store,
|
|
1432
|
+
budget,
|
|
1433
|
+
revision: stored?.revision ?? null,
|
|
1434
|
+
usageUnknownMarker: stored?.usageUnknownMarker ?? null,
|
|
1435
|
+
registry,
|
|
1436
|
+
});
|
|
1437
|
+
registry.set(key, runtime);
|
|
1438
|
+
try {
|
|
1439
|
+
// Persist creation, restore/tightening and an exact clean/dirty snapshot
|
|
1440
|
+
// before returning a handle.
|
|
1441
|
+
runtime.persist();
|
|
1442
|
+
return runtime.retain({ onEvent, signal });
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
registry.delete(key);
|
|
1445
|
+
budget.dispose();
|
|
1446
|
+
throw error;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
export function sessionBudgetRuntimeCount(registry = DEFAULT_REGISTRY) {
|
|
1451
|
+
return registry.size;
|
|
1452
|
+
}
|