chainlesschain 0.163.8 → 0.164.0
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 +4 -1
- package/scripts/macos-mcp-launcher-build.mjs +317 -0
- package/scripts/macos-mcp-launcher-postinstall.sh +54 -0
- package/scripts/macos-mcp-launcher-preinstall.sh +61 -0
- package/src/assets/web-panel/assets/{AIOps-CavM7W3k.js → AIOps-D2MEn7fI.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CUvZGabQ.js → ActionButton-lCW_4Gf0.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DeBnWnbe.js → Analytics-Bs7i-nsS.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DbI5kxvd.js → AppLayout-CK7B6S_j.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-D93bigKm.js → Artifacts-C8ILjpyo.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CBNvfUZa.js → Audit-Bb9cNaWc.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-CJLyh_YU.js → BackgroundAgents-sC9ooXIf.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-D_0yoehD.js → Backup-D-DrsplT.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-TJxmbObW.js → BaseInput-uCNZpwSK.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CE0dto1q.js → Chat-CsDK1uE9.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CZ5-yYiD.js → ChatBubbleRenderer-OqSDt50w.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-Ck1RVIVo.js → Checkbox-JhTXM1-K.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-qpoyf6DK.js → Codegen-BbzZLnyD.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DUyPWjIE.js → Col-D4VW4m4j.js} +1 -1
- package/src/assets/web-panel/assets/{Community-D_S1BemS.js → Community-BXrYawxV.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BjxeK-Gc.js → Compact-DCihGy1u.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-Dd3CEJsu.js → Compliance-DlyZ-lk8.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-B4QG7-ad.js → Cowork-DvGZrhbO.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-BtZnids5.js → Cron-DUncZi8h.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-CHPfCBFD.js → Crosschain-C_hLuUcm.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DT8DCEOK.js → DID-b6YSUqxr.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-COQab5_U.js → Dashboard-uvM15Cfb.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BY1PsQuU.js → Dropdown-D1-Nd4qY.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-CsuAuaq3.js → EmailListRenderer-8ApvL6Zc.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BqpinD-j.js → FamilyGuardDashboard-LYJt8811.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-D0gkfQFv.js → Federation-BbI3HGpv.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BqgE-axK.js → FormItemContext-DSoEKuFu.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-D0hJ2KvC.js → GenericCardRenderer-B0S9OWd5.js} +1 -1
- package/src/assets/web-panel/assets/{Git-CmMB0uPY.js → Git-DIs9Zhf3.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BnVEI0Dk.js → Governance-jzZXrxcl.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-YTAMihRP.js → Inference-JUPjf2ZI.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-0ax7VHop.js → KnowledgeGraph-DxnfJvlP.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-B9TRcokL.js → Logs-iXiz8BEE.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-GmB6DOr6.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-B6CSuD_H.js → Marketplace-Y3Xch312.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-CjMXvOVU.js → McpTools-D7kGGbO7.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-CXRF71U3.js → Memory-Do_hVxzh.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DQgNp_oS.js → MobileBridge-CtrR0Tas.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-BL0Czcoj.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-6hDTRPe1.js → Mtc-qo5QCTGd.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-BlVHU1DS.js → MtcAudit-cq5mnjYj.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-DP9pzEbV.js → Multisig-Bq0MAQHw.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-Djorh8EV.js → NLProgramming-DdWE6Vi2.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-oLZ-JkUg.js → Notes-BxMBFcIp.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-Cg288VH3.js → NotificationSettings-RTrAcbUe.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-Bb1O3Ofr.js → OrderTableRenderer-DJbpGqzT.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-CBdo-bHs.js → Organization-SQzIPdqC.js} +2 -2
- package/src/assets/web-panel/assets/{Overflow-qZdnVumx.js → Overflow-02yECh22.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-D4kNfGNQ.js → P2P-BNg4pNCM.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-DnlMemM7.js → PdhVaultBrowser-BxPJZsD3.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-DHucHFBJ.js → Permissions-CLnDnPAz.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-BovlOIPR.js → PersonalDataHub-C5m0AjGf.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-Deru3CQY.js → Pipeline-VwOf5Dhv.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-Bq5oTTbU.js → Privacy-CC0W2hZd.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BfzxVWAz.js → ProjectInit-Dz893Bob.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CKC6XBtH.js → ProjectSettings-CxQkvl0k.js} +2 -2
- package/src/assets/web-panel/assets/Projects-DDAjkUkT.js +1 -0
- package/src/assets/web-panel/assets/{Providers-C_jx4TWX.js → Providers-DeB9IkxK.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BdIHeVGC.js → QrScannerModal-OCeoaswn.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-BwZwAcPD.js → QuickAsk-DenotaAj.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BNTt0L-m.js → Recommend-Cies_jjI.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-Biyn2-Ro.js → RemoteSession-Dxzl5n2T.js} +3 -3
- package/src/assets/web-panel/assets/{Reputation-D6VpG32B.js → Reputation-CW0mvxgZ.js} +1 -1
- package/src/assets/web-panel/assets/{Row-D_74OvQQ.js → Row-SEd32qZU.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BTr0a-BQ.js → RssFeed-BSwnJ3Lm.js} +3 -3
- package/src/assets/web-panel/assets/{Search-C3MCFoNU.js → Search-xe2m8bkC.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CqW9aJ6s.js → Security-Mq2F9Nro.js} +3 -3
- package/src/assets/web-panel/assets/{Services-Cw5GNaHF.js → Services-BKGw2V3P.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DZps5qeY.js → Skeleton-DW95-Rxf.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-C6NQ1znT.js → Skills-CHgChCv_.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DFP8G2Sh.js → Sla-tKh6k3R7.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-ESt6MB6v.js → SpeechSettings-D6UL3dA5.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-BHnN98aM.js → SyncSettings-D0Fw8fVL.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-NiSCaOf-.js → Tasks-20x1GB3D.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DsKFhW2C.js → Templates-DJ-lts-a.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CQ0E0ze5.js → Tenant-BFdGpbs7.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-D_rBu_by.js → Terminal-Xpx61vHL.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-D3Vm4Po4.js → TimelineRenderer-C6UIvyZQ.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-DNTxRH6q.js → Tokens-Dellq08f.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-Dgprz1ms.js → Trigger-VKw8d0J-.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-B2rUnVBD.js → Trust-DNftdKT5.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DEGQNc6_.js → UkeySign-DX9VAKJI.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-jlsAx3Eh.js → VideoEditing-B2h3jO8q.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-8jQih1ij.js → Wallet-CjGkof7f.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-XLeO6xIB.js → WebAuthn-2Zfcu55P.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-De_dgDAG.js → WorkflowEditor-B9eZ8UxZ.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CI19R_Sk.js → chat-CQNfD-YO.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DsYTOYN3.js → colors-D4usyFT2.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CM3-t_Qr.js → compact-item-s4TpjvjX.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BjxlBRZv.js → createContext-BRCSvYxj.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DKJjSWRn.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-RDaWX_4i.js → hasIn-AdkUbkV-.js} +1 -1
- package/src/assets/web-panel/assets/{index-BN6UPD2E.js → index--00U6vXZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CiECJi-Q.js → index-B2V-eKfK.js} +1 -1
- package/src/assets/web-panel/assets/{index-CnHH7bnD.js → index-B4JJCfQ2.js} +1 -1
- package/src/assets/web-panel/assets/{index-CjTrRt13.js → index-BIoG5yYf.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dxge-xWl.js → index-BMcimU-2.js} +1 -1
- package/src/assets/web-panel/assets/{index-9R6BxAr0.js → index-BPhkFZ1I.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHNYPO0b.js → index-BQ3DXBtD.js} +3 -3
- package/src/assets/web-panel/assets/{index-Cpqm1qfU.js → index-Bad4VNvz.js} +1 -1
- package/src/assets/web-panel/assets/{index-B9O9YyWE.js → index-BgiXR_-_.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5REnKt8.js → index-BtobShro.js} +1 -1
- package/src/assets/web-panel/assets/{index-D6sm5-Xq.js → index-Bw2MEnUk.js} +1 -1
- package/src/assets/web-panel/assets/{index-DcPFB8qJ.js → index-C1mJtE6q.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKJr-TZp.js → index-C484EJiS.js} +1 -1
- package/src/assets/web-panel/assets/{index-Drlnwr8F.js → index-C5KAQFPQ.js} +1 -1
- package/src/assets/web-panel/assets/index-C61cnl3K.js +1 -0
- package/src/assets/web-panel/assets/{index-Dsij_KsN.js → index-C7Y8Aluc.js} +1 -1
- package/src/assets/web-panel/assets/{index-IeiL9RKc.js → index-CCsM_Ybx.js} +1 -1
- package/src/assets/web-panel/assets/{index-B-Z8eFUJ.js → index-CJCFe2Qf.js} +1 -1
- package/src/assets/web-panel/assets/{index-CyOGN6a5.js → index-CJVEOa4i.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0PMlbMs.js → index-CJdQrZth.js} +1 -1
- package/src/assets/web-panel/assets/{index-nAH87mQP.js → index-CKC1t3Ic.js} +1 -1
- package/src/assets/web-panel/assets/{index-BGqqJfL0.js → index-CQhAVU5t.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cd9OJhHG.js → index-CYdoQsa1.js} +1 -1
- package/src/assets/web-panel/assets/{index-D0zcy54x.js → index-Cduu2Hhr.js} +1 -1
- package/src/assets/web-panel/assets/{index-DIUnNOjG.js → index-CkwiSLhU.js} +1 -1
- package/src/assets/web-panel/assets/{index-CVQMeiwH.js → index-Cvyt-zhm.js} +1 -1
- package/src/assets/web-panel/assets/{index-BlT2VAEK.js → index-D2GjI2oe.js} +1 -1
- package/src/assets/web-panel/assets/index-D4XFHxBr.js +1 -0
- package/src/assets/web-panel/assets/{index-C0yx-jtQ.js → index-DARsAyUp.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bi46Xu4u.js → index-DVKfQ0Ux.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cd3euP_4.js → index-DZT36ZYq.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCnZyIMA.js → index-D_XBHopw.js} +1 -1
- package/src/assets/web-panel/assets/{index-C4gkzxkw.js → index-DdCB70SS.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7WBt_4T.js → index-DzRjHxEv.js} +1 -1
- package/src/assets/web-panel/assets/{index-DX7tuWVJ.js → index-MMN9V6JF.js} +1 -1
- package/src/assets/web-panel/assets/{index-DoQA0-52.js → index-RfMr79MW.js} +1 -1
- package/src/assets/web-panel/assets/{index-DXXtGMUJ.js → index-_IRVsV9X.js} +1 -1
- package/src/assets/web-panel/assets/{index-pPL66J85.js → index-nXyk8GZ9.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyIWssPC.js → index-s_buFMEG.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BTL88of4.js → initDefaultProps-Blmpk7HL.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BUB2pXwB.js → motion-DPBJMv1n.js} +1 -1
- package/src/assets/web-panel/assets/{move-egCkkOMm.js → move-ButdzCM7.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DAWhZC8c.js → mtc-parser-cyRC_QCe.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CvOxe_6d.js → omit-1epSV7hz.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DZUSLZ5Z.js → pickAttrs-DuPY8xJs.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BERxGNMY.js → placementArrow-6XZqarhe.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DZuDfZ9n.js → responsiveObserve-B40pXBWM.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DO756sgI.js → slide-jbZ9Wu4n.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BRQlIz28.js → statusUtils-BKQQcVy4.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DCSfdybl.js → styleChecker-NiKfWjUT.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-LD8dWVve.js → useFlexGapSupport-bQUCvPeb.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-BX9uW2LK.js → useFs-CUGji7ph.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BPSlPuHU.js → usePersonalDataHub-4veyN2N2.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-BmOILC9V.js → vnode-1-nLug-Z.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-1iKowKGg.js → zoom-dhCeRDMo.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +3 -3
- package/src/commands/cowork.js +174 -19
- package/src/commands/permissions.js +240 -16
- package/src/commands/plugin.js +1293 -56
- package/src/commands/session-location.js +276 -0
- package/src/commands/session.js +2 -0
- package/src/data/changelog.json +15 -2
- package/src/gateways/ws/action-protocol.js +35 -10
- package/src/gateways/ws/remote-session-protocol.js +24 -1
- package/src/gateways/ws/ws-agent-handler.js +12 -0
- package/src/gateways/ws/ws-server.js +48 -12
- package/src/harness/jsonl-session-store.js +341 -24
- package/src/harness/mcp-client.js +8 -1
- package/src/harness/remote-session-audit.js +1 -0
- package/src/harness/remote-session-crypto.js +210 -2
- package/src/harness/remote-session-registry.js +159 -6
- package/src/harness/session-list-index.js +233 -14
- package/src/lib/agent-authority.js +6 -0
- package/src/lib/background-agent-keeper-client.js +248 -0
- package/src/lib/background-agent-keeper-protocol.js +147 -0
- package/src/lib/background-agent-supervisor.js +520 -40
- package/src/lib/background-interaction-resolver.js +12 -0
- package/src/lib/background-turn-bootstrap-protocol.js +126 -0
- package/src/lib/cowork-workflow.js +336 -16
- package/src/lib/dynamic-workflow-facade.js +648 -0
- package/src/lib/execution-location-contract.js +551 -0
- package/src/lib/execution-location-runtime.js +130 -0
- package/src/lib/ide-context.js +94 -6
- package/src/lib/mcp-stdio-package-materialization.js +166 -14
- package/src/lib/packer/pkg-config-generator.js +2 -0
- package/src/lib/permission-authority.js +77 -0
- package/src/lib/permission-side-effect-center.js +495 -0
- package/src/lib/plugin-runtime/install.js +514 -13
- package/src/lib/plugin-runtime/marketplace-artifact-readback.js +900 -0
- package/src/lib/plugin-runtime/marketplace-catalog.js +1181 -0
- package/src/lib/plugin-runtime/marketplace-impact.js +408 -0
- package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +1567 -0
- package/src/lib/plugin-security.js +60 -3
- package/src/lib/process-execution-broker/index.js +1095 -57
- package/src/lib/process-execution-broker/linux-bwrap-descriptor-launch.js +375 -0
- package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +90 -16
- package/src/lib/process-execution-broker/linux-generic-bwrap.js +159 -18
- package/src/lib/process-execution-broker/macos-mcp-launcher-contract.js +278 -0
- package/src/lib/process-execution-broker/macos-mcp-launcher-protocol.json +23 -0
- package/src/lib/process-execution-broker/macos-mcp-launcher.c +1497 -0
- package/src/lib/process-execution-broker/mcp-fd-entry-bootstrap.js +37 -0
- package/src/lib/process-execution-broker/platform-sandbox.js +2330 -279
- 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 +50 -4
- package/src/lib/public-ide-capability-manifest.js +3 -0
- package/src/lib/remote-approval-bridge.js +123 -17
- package/src/lib/remote-membership-authority.js +632 -0
- package/src/lib/remote-membership-coordinator.js +2409 -0
- package/src/lib/remote-membership-host-store.js +1750 -0
- package/src/lib/scoped-permission-store.js +369 -0
- package/src/lib/session-anti-rollback-anchor.js +262 -5
- package/src/lib/sub-agent-context.js +4 -0
- package/src/lib/workflow-definition-contract.js +149 -0
- package/src/repl/agent-repl.js +41 -7
- package/src/repl/chat-repl.js +2 -0
- package/src/repl/clipboard-image.js +735 -16
- package/src/repl/ide-status.js +1 -1
- package/src/repl/prompt-interactions.js +59 -6
- package/src/runtime/agent-core.js +83 -3
- package/src/runtime/headless-runner.js +30 -25
- package/src/runtime/headless-stream.js +32 -25
- package/src/workers/background-agent-keeper.js +498 -0
- package/src/workers/background-agent-worker.js +358 -13
- package/src/workers/background-turn-bootstrap.js +143 -0
- package/src/assets/web-panel/assets/MarkdownRenderer-yySN5YNM.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-DlPwcCbq.js +0 -1
- package/src/assets/web-panel/assets/Projects-CMolGhae.js +0 -1
- package/src/assets/web-panel/assets/devWarning-S1ld2LwU.js +0 -1
- package/src/assets/web-panel/assets/index-Drz6yq0m.js +0 -1
- package/src/assets/web-panel/assets/index-uWiUs7S_.js +0 -1
|
@@ -9,14 +9,23 @@
|
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
appendFileSync,
|
|
12
|
+
closeSync,
|
|
13
|
+
constants as fsConstants,
|
|
12
14
|
existsSync,
|
|
15
|
+
fstatSync,
|
|
16
|
+
fsyncSync,
|
|
17
|
+
lstatSync,
|
|
18
|
+
openSync,
|
|
13
19
|
readFileSync,
|
|
20
|
+
renameSync,
|
|
14
21
|
rmSync,
|
|
15
22
|
writeFileSync,
|
|
16
23
|
} from "node:fs";
|
|
17
|
-
import {
|
|
24
|
+
import { randomUUID } from "node:crypto";
|
|
25
|
+
import { basename, dirname, join } from "node:path";
|
|
18
26
|
import { iterateFileLinesReverseSync } from "../lib/file-lines.js";
|
|
19
27
|
import { ensurePrivateFile } from "../lib/secure-fs.js";
|
|
28
|
+
import { isAffectedWindowsZeroDeviceStatRuntime } from "../lib/secure-file-identity.js";
|
|
20
29
|
|
|
21
30
|
export const SESSION_INDEX_SCHEMA = 2;
|
|
22
31
|
export const SESSION_INDEX_FILE = ".sessions-index-v2.ndjson";
|
|
@@ -31,6 +40,14 @@ export const SESSION_GENERATION_AUTHORITY_SCHEMA =
|
|
|
31
40
|
// Keeping the injection immediately around the production sidecar/journal
|
|
32
41
|
// boundary makes that gate deterministic without replacing either write.
|
|
33
42
|
export const _sessionScaleFaultHooks = Object.seal({
|
|
43
|
+
afterMetaTempFsync: null,
|
|
44
|
+
afterMetaRename: null,
|
|
45
|
+
afterMetaDirectoryFsync: null,
|
|
46
|
+
afterTombstoneTempFsync: null,
|
|
47
|
+
afterTombstoneRename: null,
|
|
48
|
+
afterTombstoneDirectoryFsync: null,
|
|
49
|
+
afterTombstoneRemoval: null,
|
|
50
|
+
afterTombstoneRemovalDirectoryFsync: null,
|
|
34
51
|
afterMetaSnapshot: null,
|
|
35
52
|
});
|
|
36
53
|
|
|
@@ -40,6 +57,157 @@ function runSessionScaleFaultHook(name, payload) {
|
|
|
40
57
|
if (typeof hook === "function") hook(payload);
|
|
41
58
|
}
|
|
42
59
|
|
|
60
|
+
function annotateSnapshotFailure(
|
|
61
|
+
cause,
|
|
62
|
+
{ targetPath, renamed, directorySynced },
|
|
63
|
+
) {
|
|
64
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
65
|
+
if (!error.commitState) {
|
|
66
|
+
// Before rename the public snapshot is unchanged. POSIX needs the parent
|
|
67
|
+
// directory fsync to make the rename durable; Windows exposes no usable
|
|
68
|
+
// directory fsync through Node, so a flushed atomic rename is its complete
|
|
69
|
+
// host boundary. A hook after that boundary models response loss.
|
|
70
|
+
error.commitState = !renamed
|
|
71
|
+
? "not-committed"
|
|
72
|
+
: directorySynced
|
|
73
|
+
? "committed"
|
|
74
|
+
: "unknown";
|
|
75
|
+
}
|
|
76
|
+
if (!error.persistenceTarget) error.persistenceTarget = targetPath;
|
|
77
|
+
return error;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function syncParentDirectory(targetPath) {
|
|
81
|
+
if (process.platform === "win32") return false;
|
|
82
|
+
let descriptor = null;
|
|
83
|
+
try {
|
|
84
|
+
const flags =
|
|
85
|
+
fsConstants.O_RDONLY |
|
|
86
|
+
Number(fsConstants.O_DIRECTORY ?? 0) |
|
|
87
|
+
Number(fsConstants.O_NOFOLLOW ?? 0);
|
|
88
|
+
descriptor = openSync(dirname(targetPath), flags);
|
|
89
|
+
if (!fstatSync(descriptor).isDirectory()) {
|
|
90
|
+
throw new Error("Session snapshot parent is not a directory");
|
|
91
|
+
}
|
|
92
|
+
fsyncSync(descriptor);
|
|
93
|
+
return true;
|
|
94
|
+
} finally {
|
|
95
|
+
if (descriptor !== null) closeSync(descriptor);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function assertSnapshotPathMatchesDescriptor(
|
|
100
|
+
filePath,
|
|
101
|
+
descriptorStats,
|
|
102
|
+
serialized,
|
|
103
|
+
) {
|
|
104
|
+
const published = lstatSync(filePath, { bigint: true });
|
|
105
|
+
const usefulIdentity =
|
|
106
|
+
descriptorStats.dev !== 0n ||
|
|
107
|
+
descriptorStats.ino !== 0n ||
|
|
108
|
+
published.dev !== 0n ||
|
|
109
|
+
published.ino !== 0n;
|
|
110
|
+
const deviceMatches =
|
|
111
|
+
String(descriptorStats.dev) === String(published.dev) ||
|
|
112
|
+
(isAffectedWindowsZeroDeviceStatRuntime() &&
|
|
113
|
+
String(descriptorStats.dev) !== "0" &&
|
|
114
|
+
String(published.dev) === "0");
|
|
115
|
+
const identityMatches =
|
|
116
|
+
deviceMatches &&
|
|
117
|
+
String(descriptorStats.ino) === String(published.ino) &&
|
|
118
|
+
String(descriptorStats.mode) === String(published.mode);
|
|
119
|
+
const expectedBytes = BigInt(Buffer.byteLength(serialized, "utf8"));
|
|
120
|
+
if (
|
|
121
|
+
published.isSymbolicLink() ||
|
|
122
|
+
!published.isFile() ||
|
|
123
|
+
(usefulIdentity && !identityMatches) ||
|
|
124
|
+
descriptorStats.size !== expectedBytes ||
|
|
125
|
+
published.size !== expectedBytes ||
|
|
126
|
+
readFileSync(filePath, "utf8") !== serialized
|
|
127
|
+
) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
"Session snapshot path no longer identifies the flushed payload",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Publish one authority snapshot without ever exposing a partial JSON file.
|
|
136
|
+
* The temporary file lives beside the destination so rename remains atomic.
|
|
137
|
+
* Activity ordering is intentionally outside this helper: that journal is a
|
|
138
|
+
* rebuildable cache and must not be confused with the durable snapshot.
|
|
139
|
+
*/
|
|
140
|
+
function writeSnapshotDurably(
|
|
141
|
+
filePath,
|
|
142
|
+
serialized,
|
|
143
|
+
{ kind, sessionId, status, afterTempFsync, afterRename, afterDirectoryFsync },
|
|
144
|
+
) {
|
|
145
|
+
const temporaryPath = join(
|
|
146
|
+
dirname(filePath),
|
|
147
|
+
`.${basename(filePath)}.${process.pid}.${randomUUID()}.tmp`,
|
|
148
|
+
);
|
|
149
|
+
const payload = Object.freeze({
|
|
150
|
+
kind,
|
|
151
|
+
sessionId,
|
|
152
|
+
status,
|
|
153
|
+
targetPath: filePath,
|
|
154
|
+
temporaryPath,
|
|
155
|
+
});
|
|
156
|
+
let descriptor = null;
|
|
157
|
+
let descriptorStats = null;
|
|
158
|
+
let renamed = false;
|
|
159
|
+
let directorySynced = process.platform === "win32";
|
|
160
|
+
try {
|
|
161
|
+
descriptor = openSync(temporaryPath, "wx", 0o600);
|
|
162
|
+
writeFileSync(descriptor, serialized, "utf8");
|
|
163
|
+
ensurePrivateFile(temporaryPath);
|
|
164
|
+
fsyncSync(descriptor);
|
|
165
|
+
descriptorStats = fstatSync(descriptor, { bigint: true });
|
|
166
|
+
runSessionScaleFaultHook(afterTempFsync, payload);
|
|
167
|
+
assertSnapshotPathMatchesDescriptor(
|
|
168
|
+
temporaryPath,
|
|
169
|
+
descriptorStats,
|
|
170
|
+
serialized,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// Both files are in the same directory; rename atomically replaces an old
|
|
174
|
+
// snapshot or publishes a first generation without a partial target.
|
|
175
|
+
// The temp already has the final owner-only permissions before publication.
|
|
176
|
+
renameSync(temporaryPath, filePath);
|
|
177
|
+
renamed = true;
|
|
178
|
+
assertSnapshotPathMatchesDescriptor(filePath, descriptorStats, serialized);
|
|
179
|
+
closeSync(descriptor);
|
|
180
|
+
descriptor = null;
|
|
181
|
+
runSessionScaleFaultHook(afterRename, payload);
|
|
182
|
+
if (process.platform !== "win32") {
|
|
183
|
+
directorySynced = syncParentDirectory(filePath);
|
|
184
|
+
runSessionScaleFaultHook(afterDirectoryFsync, payload);
|
|
185
|
+
}
|
|
186
|
+
} catch (cause) {
|
|
187
|
+
throw annotateSnapshotFailure(cause, {
|
|
188
|
+
targetPath: filePath,
|
|
189
|
+
renamed,
|
|
190
|
+
directorySynced,
|
|
191
|
+
});
|
|
192
|
+
} finally {
|
|
193
|
+
if (descriptor !== null) {
|
|
194
|
+
try {
|
|
195
|
+
closeSync(descriptor);
|
|
196
|
+
} catch {
|
|
197
|
+
// Preserve the persistence failure that stopped publication.
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (!renamed) {
|
|
201
|
+
try {
|
|
202
|
+
rmSync(temporaryPath, { force: true });
|
|
203
|
+
} catch {
|
|
204
|
+
// A crash may leave an ignored same-directory temp file. Never replace
|
|
205
|
+
// the original persistence failure with best-effort cleanup failure.
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
43
211
|
export function sessionMetaPath(dir, sessionId) {
|
|
44
212
|
return join(dir, `${sessionId}.meta.json`);
|
|
45
213
|
}
|
|
@@ -189,6 +357,7 @@ function writeMetaSnapshot(dir, meta) {
|
|
|
189
357
|
// that publication fails. Remove it only after a live generation snapshot
|
|
190
358
|
// commits, so crashes can cause an extra candidate read but never hide a
|
|
191
359
|
// successfully committed tombstone.
|
|
360
|
+
let tombstonePublished = false;
|
|
192
361
|
if (meta.deleted === true) {
|
|
193
362
|
const generation = normalizeSessionGenerationAuthority(meta.generation);
|
|
194
363
|
const marker = {
|
|
@@ -206,21 +375,71 @@ function writeMetaSnapshot(dir, meta) {
|
|
|
206
375
|
Number(meta.deleted_at_ms || meta.updated_at_ms) || 0,
|
|
207
376
|
),
|
|
208
377
|
};
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
378
|
+
writeSnapshotDurably(tombstoneMarker, `${JSON.stringify(marker)}\n`, {
|
|
379
|
+
kind: "tombstone",
|
|
380
|
+
sessionId: meta.id,
|
|
381
|
+
status: "deleted",
|
|
382
|
+
afterTempFsync: "afterTombstoneTempFsync",
|
|
383
|
+
afterRename: "afterTombstoneRename",
|
|
384
|
+
afterDirectoryFsync: "afterTombstoneDirectoryFsync",
|
|
212
385
|
});
|
|
213
|
-
|
|
386
|
+
tombstonePublished = true;
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
writeSnapshotDurably(filePath, `${JSON.stringify(meta)}\n`, {
|
|
390
|
+
kind: "meta",
|
|
391
|
+
sessionId: meta.id,
|
|
392
|
+
status: meta.deleted === true ? "deleted" : "live",
|
|
393
|
+
afterTempFsync: "afterMetaTempFsync",
|
|
394
|
+
afterRename: "afterMetaRename",
|
|
395
|
+
afterDirectoryFsync: "afterMetaDirectoryFsync",
|
|
396
|
+
});
|
|
397
|
+
} catch (cause) {
|
|
398
|
+
// A durable marker is already a conservative deletion fence. If the meta
|
|
399
|
+
// publication then fails, the composite mutation is partial/unknown even
|
|
400
|
+
// when the old meta path itself was left byte-for-byte unchanged.
|
|
401
|
+
if (tombstonePublished && cause?.commitState === "not-committed") {
|
|
402
|
+
cause.commitState = "unknown";
|
|
403
|
+
}
|
|
404
|
+
throw cause;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (meta.deleted !== true && existsSync(tombstoneMarker)) {
|
|
408
|
+
const payload = Object.freeze({
|
|
409
|
+
kind: "tombstone-removal",
|
|
410
|
+
sessionId: meta.id,
|
|
411
|
+
status: "live",
|
|
412
|
+
targetPath: tombstoneMarker,
|
|
413
|
+
parentDirectory: dirname(tombstoneMarker),
|
|
414
|
+
});
|
|
415
|
+
let removed = false;
|
|
416
|
+
let directorySynced = process.platform === "win32";
|
|
417
|
+
try {
|
|
418
|
+
rmSync(tombstoneMarker, { force: true });
|
|
419
|
+
removed = true;
|
|
420
|
+
runSessionScaleFaultHook("afterTombstoneRemoval", payload);
|
|
421
|
+
if (process.platform !== "win32") {
|
|
422
|
+
directorySynced = syncParentDirectory(tombstoneMarker);
|
|
423
|
+
runSessionScaleFaultHook(
|
|
424
|
+
"afterTombstoneRemovalDirectoryFsync",
|
|
425
|
+
payload,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
} catch (cause) {
|
|
429
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
430
|
+
if (!error.commitState) {
|
|
431
|
+
// The live meta was already durably published. Retaining the marker is
|
|
432
|
+
// fail-closed; removing it without a POSIX directory barrier is an
|
|
433
|
+
// unknown publication, while a post-barrier hook is response loss.
|
|
434
|
+
error.commitState =
|
|
435
|
+
removed && directorySynced ? "committed" : "unknown";
|
|
436
|
+
}
|
|
437
|
+
if (!error.persistenceTarget) {
|
|
438
|
+
error.persistenceTarget = tombstoneMarker;
|
|
439
|
+
}
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
214
442
|
}
|
|
215
|
-
// The owning transcript lock serializes this derived snapshot. It need not
|
|
216
|
-
// use a second temp+rename protocol: interruption can only corrupt a cache,
|
|
217
|
-
// which readSessionMeta rejects and rebuildSessionMeta regenerates.
|
|
218
|
-
writeFileSync(filePath, `${JSON.stringify(meta)}\n`, {
|
|
219
|
-
encoding: "utf8",
|
|
220
|
-
mode: 0o600,
|
|
221
|
-
});
|
|
222
|
-
ensurePrivateFile(filePath);
|
|
223
|
-
if (meta.deleted !== true) rmSync(tombstoneMarker, { force: true });
|
|
224
443
|
}
|
|
225
444
|
|
|
226
445
|
/**
|
|
@@ -266,6 +266,12 @@ export function describeAuthorityChain(envelope = {}) {
|
|
|
266
266
|
const parts = [`origin=${envelope.origin || "unknown"}`];
|
|
267
267
|
if (envelope.principalId) parts.push(`principal=${envelope.principalId}`);
|
|
268
268
|
if (envelope.sessionId) parts.push(`session=${envelope.sessionId}`);
|
|
269
|
+
if (envelope.remoteSessionId)
|
|
270
|
+
parts.push(`remote-session=${envelope.remoteSessionId}`);
|
|
271
|
+
if (envelope.sessionEpoch)
|
|
272
|
+
parts.push(`session-epoch=${envelope.sessionEpoch}`);
|
|
273
|
+
if (envelope.membershipEpoch)
|
|
274
|
+
parts.push(`membership-epoch=${envelope.membershipEpoch}`);
|
|
269
275
|
if (envelope.parentAgentId) parts.push(`parent=${envelope.parentAgentId}`);
|
|
270
276
|
if (envelope.correlationId) parts.push(`corr=${envelope.correlationId}`);
|
|
271
277
|
parts.push(`authority=${authorityForOrigin(envelope.origin, envelope)}`);
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { createNdjsonReader } from "./background-session-transport.js";
|
|
4
|
+
import {
|
|
5
|
+
BACKGROUND_AGENT_KEEPER_ARM,
|
|
6
|
+
BACKGROUND_AGENT_KEEPER_ARMED,
|
|
7
|
+
BACKGROUND_AGENT_KEEPER_HELLO,
|
|
8
|
+
BACKGROUND_AGENT_KEEPER_PROTOCOL_VERSION,
|
|
9
|
+
BACKGROUND_AGENT_KEEPER_READY,
|
|
10
|
+
BACKGROUND_AGENT_KEEPER_RETIRE,
|
|
11
|
+
BACKGROUND_AGENT_KEEPER_RETIRED,
|
|
12
|
+
createBackgroundAgentKeeperMessage,
|
|
13
|
+
normalizeBackgroundAgentKeeperHello,
|
|
14
|
+
normalizeBackgroundAgentKeeperTurn,
|
|
15
|
+
resolveBackgroundAgentKeeperRetireTimeoutMs,
|
|
16
|
+
sameBackgroundAgentKeeperTurn,
|
|
17
|
+
} from "./background-agent-keeper-protocol.js";
|
|
18
|
+
|
|
19
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 30_000;
|
|
20
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 10_000;
|
|
21
|
+
|
|
22
|
+
function keeperError(message, code = "BACKGROUND_AGENT_KEEPER_UNAVAILABLE") {
|
|
23
|
+
const error = new Error(message);
|
|
24
|
+
error.code = code;
|
|
25
|
+
return error;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeMessage(socket, message) {
|
|
29
|
+
if (!socket || socket.destroyed) {
|
|
30
|
+
throw keeperError("background agent keeper channel is closed");
|
|
31
|
+
}
|
|
32
|
+
socket.write(`${JSON.stringify(message)}\n`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function connectOnce(pipePath) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const socket = net.createConnection(pipePath);
|
|
38
|
+
const onError = (error) => {
|
|
39
|
+
socket.destroy();
|
|
40
|
+
reject(error);
|
|
41
|
+
};
|
|
42
|
+
socket.once("error", onError);
|
|
43
|
+
socket.once("connect", () => {
|
|
44
|
+
socket.removeListener("error", onError);
|
|
45
|
+
resolve(socket);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function connectWithDeadline(pipePath, timeoutMs) {
|
|
51
|
+
const deadline = Date.now() + timeoutMs;
|
|
52
|
+
let lastError = null;
|
|
53
|
+
while (Date.now() < deadline) {
|
|
54
|
+
try {
|
|
55
|
+
return await connectOnce(pipePath);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
lastError = error;
|
|
58
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
throw keeperError(
|
|
62
|
+
`timed out connecting to background agent keeper: ${lastError?.message || "unavailable"}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function connectBackgroundAgentKeeper(options = {}) {
|
|
67
|
+
const hello = normalizeBackgroundAgentKeeperHello(options);
|
|
68
|
+
const connectTimeoutMs = Math.max(
|
|
69
|
+
1,
|
|
70
|
+
Number(options.connectTimeoutMs) || DEFAULT_CONNECT_TIMEOUT_MS,
|
|
71
|
+
);
|
|
72
|
+
const requestTimeoutMs = Math.max(
|
|
73
|
+
1,
|
|
74
|
+
Number(options.requestTimeoutMs) || DEFAULT_REQUEST_TIMEOUT_MS,
|
|
75
|
+
);
|
|
76
|
+
const retireTimeoutMs = resolveBackgroundAgentKeeperRetireTimeoutMs(
|
|
77
|
+
options.retireTimeoutMs,
|
|
78
|
+
);
|
|
79
|
+
const socket = await connectWithDeadline(options.pipePath, connectTimeoutMs);
|
|
80
|
+
const pending = new Map();
|
|
81
|
+
let ready = false;
|
|
82
|
+
let closing = false;
|
|
83
|
+
let activeTurn = null;
|
|
84
|
+
let readyResolve;
|
|
85
|
+
let readyReject;
|
|
86
|
+
const readyPromise = new Promise((resolve, reject) => {
|
|
87
|
+
readyResolve = resolve;
|
|
88
|
+
readyReject = reject;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const failPending = (error) => {
|
|
92
|
+
readyReject(error);
|
|
93
|
+
for (const entry of pending.values()) {
|
|
94
|
+
clearTimeout(entry.timer);
|
|
95
|
+
entry.reject(error);
|
|
96
|
+
}
|
|
97
|
+
pending.clear();
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
socket.on(
|
|
101
|
+
"data",
|
|
102
|
+
createNdjsonReader(
|
|
103
|
+
(message) => {
|
|
104
|
+
if (
|
|
105
|
+
message?.protocolVersion !== BACKGROUND_AGENT_KEEPER_PROTOCOL_VERSION
|
|
106
|
+
) {
|
|
107
|
+
socket.destroy(
|
|
108
|
+
keeperError("background agent keeper protocol mismatch"),
|
|
109
|
+
);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (!ready) {
|
|
113
|
+
if (
|
|
114
|
+
message.type !== BACKGROUND_AGENT_KEEPER_READY ||
|
|
115
|
+
message.id !== hello.id ||
|
|
116
|
+
message.workerGeneration !== hello.workerGeneration
|
|
117
|
+
) {
|
|
118
|
+
socket.destroy(
|
|
119
|
+
keeperError("background agent keeper ready binding mismatch"),
|
|
120
|
+
);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
ready = true;
|
|
124
|
+
readyResolve();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const entry = pending.get(message.requestId);
|
|
128
|
+
if (!entry) return;
|
|
129
|
+
if (message.type === "background-agent-keeper-error") {
|
|
130
|
+
pending.delete(message.requestId);
|
|
131
|
+
clearTimeout(entry.timer);
|
|
132
|
+
entry.reject(
|
|
133
|
+
keeperError(
|
|
134
|
+
message.message || "background agent keeper rejected request",
|
|
135
|
+
message.code || "BACKGROUND_AGENT_KEEPER_REJECTED",
|
|
136
|
+
),
|
|
137
|
+
);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (message.type !== entry.expectedType) return;
|
|
141
|
+
if (!sameBackgroundAgentKeeperTurn(message, entry.turn)) {
|
|
142
|
+
socket.destroy(
|
|
143
|
+
keeperError("background agent keeper response binding mismatch"),
|
|
144
|
+
);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
pending.delete(message.requestId);
|
|
148
|
+
clearTimeout(entry.timer);
|
|
149
|
+
entry.resolve(message);
|
|
150
|
+
},
|
|
151
|
+
(error) => socket.destroy(error),
|
|
152
|
+
),
|
|
153
|
+
);
|
|
154
|
+
socket.on("error", (error) => failPending(error));
|
|
155
|
+
socket.on("close", () => {
|
|
156
|
+
const error = keeperError("background agent keeper channel disconnected");
|
|
157
|
+
failPending(error);
|
|
158
|
+
if (!closing) options.onDisconnect?.(error, activeTurn);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
writeMessage(
|
|
162
|
+
socket,
|
|
163
|
+
createBackgroundAgentKeeperMessage(BACKGROUND_AGENT_KEEPER_HELLO, hello),
|
|
164
|
+
);
|
|
165
|
+
const helloTimer = setTimeout(
|
|
166
|
+
() =>
|
|
167
|
+
socket.destroy(
|
|
168
|
+
keeperError("timed out waiting for background agent keeper ready"),
|
|
169
|
+
),
|
|
170
|
+
requestTimeoutMs,
|
|
171
|
+
);
|
|
172
|
+
helloTimer.unref?.();
|
|
173
|
+
await readyPromise.finally(() => clearTimeout(helloTimer));
|
|
174
|
+
|
|
175
|
+
const request = (
|
|
176
|
+
type,
|
|
177
|
+
expectedType,
|
|
178
|
+
turnValue,
|
|
179
|
+
timeoutMs = requestTimeoutMs,
|
|
180
|
+
) => {
|
|
181
|
+
const turn = normalizeBackgroundAgentKeeperTurn(turnValue);
|
|
182
|
+
const requestId = randomBytes(16).toString("hex");
|
|
183
|
+
return new Promise((resolve, reject) => {
|
|
184
|
+
const timer = setTimeout(() => {
|
|
185
|
+
pending.delete(requestId);
|
|
186
|
+
reject(
|
|
187
|
+
keeperError(
|
|
188
|
+
`timed out waiting for background agent keeper ${type} after ${timeoutMs}ms`,
|
|
189
|
+
),
|
|
190
|
+
);
|
|
191
|
+
}, timeoutMs);
|
|
192
|
+
timer.unref?.();
|
|
193
|
+
pending.set(requestId, { expectedType, reject, resolve, timer, turn });
|
|
194
|
+
try {
|
|
195
|
+
writeMessage(
|
|
196
|
+
socket,
|
|
197
|
+
createBackgroundAgentKeeperMessage(type, { requestId, ...turn }),
|
|
198
|
+
);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
pending.delete(requestId);
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
reject(error);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
return Object.freeze({
|
|
208
|
+
async arm(turn) {
|
|
209
|
+
const normalized = normalizeBackgroundAgentKeeperTurn(turn);
|
|
210
|
+
if (activeTurn) {
|
|
211
|
+
throw keeperError(
|
|
212
|
+
"background agent keeper is already armed",
|
|
213
|
+
"BACKGROUND_AGENT_KEEPER_BUSY",
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
await request(
|
|
217
|
+
BACKGROUND_AGENT_KEEPER_ARM,
|
|
218
|
+
BACKGROUND_AGENT_KEEPER_ARMED,
|
|
219
|
+
normalized,
|
|
220
|
+
);
|
|
221
|
+
activeTurn = normalized;
|
|
222
|
+
return normalized;
|
|
223
|
+
},
|
|
224
|
+
async retire(turn) {
|
|
225
|
+
const normalized = normalizeBackgroundAgentKeeperTurn(turn);
|
|
226
|
+
if (!sameBackgroundAgentKeeperTurn(normalized, activeTurn)) {
|
|
227
|
+
throw keeperError(
|
|
228
|
+
"background agent keeper retire binding mismatch",
|
|
229
|
+
"BACKGROUND_AGENT_KEEPER_BINDING_MISMATCH",
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
await request(
|
|
233
|
+
BACKGROUND_AGENT_KEEPER_RETIRE,
|
|
234
|
+
BACKGROUND_AGENT_KEEPER_RETIRED,
|
|
235
|
+
normalized,
|
|
236
|
+
retireTimeoutMs,
|
|
237
|
+
);
|
|
238
|
+
activeTurn = null;
|
|
239
|
+
return true;
|
|
240
|
+
},
|
|
241
|
+
close() {
|
|
242
|
+
if (closing) return;
|
|
243
|
+
closing = true;
|
|
244
|
+
socket.end();
|
|
245
|
+
},
|
|
246
|
+
activeTurn: () => activeTurn,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
export const BACKGROUND_AGENT_KEEPER_PROTOCOL_VERSION = 1;
|
|
4
|
+
export const BACKGROUND_AGENT_KEEPER_HELLO = "background-agent-keeper-hello";
|
|
5
|
+
export const BACKGROUND_AGENT_KEEPER_READY = "background-agent-keeper-ready";
|
|
6
|
+
export const BACKGROUND_AGENT_KEEPER_ARM = "background-agent-keeper-arm";
|
|
7
|
+
export const BACKGROUND_AGENT_KEEPER_ARMED = "background-agent-keeper-armed";
|
|
8
|
+
export const BACKGROUND_AGENT_KEEPER_RETIRE = "background-agent-keeper-retire";
|
|
9
|
+
export const BACKGROUND_AGENT_KEEPER_RETIRED =
|
|
10
|
+
"background-agent-keeper-retired";
|
|
11
|
+
|
|
12
|
+
// RETIRE is the only keeper request that performs destructive OS cleanup. Its
|
|
13
|
+
// deadline is deliberately independent from the short HELLO/ARM request
|
|
14
|
+
// deadline and is derived from every synchronous operation on the longest
|
|
15
|
+
// Windows path:
|
|
16
|
+
//
|
|
17
|
+
// 2 targets * (taskkill + WMIC + PowerShell) +
|
|
18
|
+
// 2 durable state-lock acquisitions + cleanup confirmation + scheduling
|
|
19
|
+
// margin = 70 seconds.
|
|
20
|
+
//
|
|
21
|
+
// Keeping these limits in the shared protocol contract prevents either side
|
|
22
|
+
// from silently reintroducing the old 10-second client / longer keeper race.
|
|
23
|
+
export const BACKGROUND_AGENT_KEEPER_CLEANUP_TARGET_LIMIT = 2;
|
|
24
|
+
export const BACKGROUND_AGENT_KEEPER_TASKKILL_TIMEOUT_MS = 10_000;
|
|
25
|
+
export const BACKGROUND_AGENT_KEEPER_WMIC_TIMEOUT_MS = 5_000;
|
|
26
|
+
export const BACKGROUND_AGENT_KEEPER_POWERSHELL_TIMEOUT_MS = 10_000;
|
|
27
|
+
export const BACKGROUND_AGENT_KEEPER_STATE_LOCK_TIMEOUT_MS = 5_000;
|
|
28
|
+
export const BACKGROUND_AGENT_KEEPER_CLEANUP_CONFIRM_TIMEOUT_MS = 2_000;
|
|
29
|
+
export const BACKGROUND_AGENT_KEEPER_RETIRE_TIMEOUT_MARGIN_MS = 8_000;
|
|
30
|
+
export const BACKGROUND_AGENT_KEEPER_RETIRE_TIMEOUT_MS =
|
|
31
|
+
BACKGROUND_AGENT_KEEPER_CLEANUP_TARGET_LIMIT *
|
|
32
|
+
(BACKGROUND_AGENT_KEEPER_TASKKILL_TIMEOUT_MS +
|
|
33
|
+
BACKGROUND_AGENT_KEEPER_WMIC_TIMEOUT_MS +
|
|
34
|
+
BACKGROUND_AGENT_KEEPER_POWERSHELL_TIMEOUT_MS) +
|
|
35
|
+
2 * BACKGROUND_AGENT_KEEPER_STATE_LOCK_TIMEOUT_MS +
|
|
36
|
+
BACKGROUND_AGENT_KEEPER_CLEANUP_CONFIRM_TIMEOUT_MS +
|
|
37
|
+
BACKGROUND_AGENT_KEEPER_RETIRE_TIMEOUT_MARGIN_MS;
|
|
38
|
+
|
|
39
|
+
export function resolveBackgroundAgentKeeperRetireTimeoutMs(value) {
|
|
40
|
+
const requested = Number(value);
|
|
41
|
+
if (!Number.isFinite(requested) || requested <= 0) {
|
|
42
|
+
return BACKGROUND_AGENT_KEEPER_RETIRE_TIMEOUT_MS;
|
|
43
|
+
}
|
|
44
|
+
return Math.min(
|
|
45
|
+
BACKGROUND_AGENT_KEEPER_RETIRE_TIMEOUT_MS,
|
|
46
|
+
Math.max(1, Math.floor(requested)),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const SAFE_ID = /^[A-Za-z0-9_-]{1,256}$/u;
|
|
51
|
+
const SAFE_TOKEN = /^[a-f0-9]{64}$/u;
|
|
52
|
+
|
|
53
|
+
function requiredSafeString(value, label, pattern = SAFE_ID) {
|
|
54
|
+
const normalized = typeof value === "string" ? value.trim() : "";
|
|
55
|
+
if (!pattern.test(normalized)) {
|
|
56
|
+
throw new TypeError(`invalid background agent keeper ${label}`);
|
|
57
|
+
}
|
|
58
|
+
return normalized;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function requiredPositiveInteger(value, label) {
|
|
62
|
+
const normalized = Number(value);
|
|
63
|
+
if (!Number.isSafeInteger(normalized) || normalized < 1) {
|
|
64
|
+
throw new TypeError(`invalid background agent keeper ${label}`);
|
|
65
|
+
}
|
|
66
|
+
return normalized;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function requiredTimestamp(value, label) {
|
|
70
|
+
const normalized = Number(value);
|
|
71
|
+
if (!Number.isFinite(normalized) || normalized <= 0) {
|
|
72
|
+
throw new TypeError(`invalid background agent keeper ${label}`);
|
|
73
|
+
}
|
|
74
|
+
return normalized;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function backgroundAgentKeeperPipePath(id, directory) {
|
|
78
|
+
const safeId = requiredSafeString(id, "id");
|
|
79
|
+
if (process.platform === "win32") {
|
|
80
|
+
return `\\\\.\\pipe\\cc-bg-keeper-${safeId}`;
|
|
81
|
+
}
|
|
82
|
+
return join(directory, `${safeId}.keeper.sock`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function normalizeBackgroundAgentKeeperAuthority(value = {}) {
|
|
86
|
+
return Object.freeze({
|
|
87
|
+
id: requiredSafeString(value.id, "id"),
|
|
88
|
+
workerGeneration: requiredSafeString(
|
|
89
|
+
value.workerGeneration,
|
|
90
|
+
"worker generation",
|
|
91
|
+
),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function normalizeBackgroundAgentKeeperHello(value = {}) {
|
|
96
|
+
return Object.freeze({
|
|
97
|
+
...normalizeBackgroundAgentKeeperAuthority(value),
|
|
98
|
+
token: requiredSafeString(value.token, "token", SAFE_TOKEN),
|
|
99
|
+
workerPid: requiredPositiveInteger(value.workerPid, "worker pid"),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function normalizeBackgroundAgentKeeperTurn(value = {}) {
|
|
104
|
+
return Object.freeze({
|
|
105
|
+
...normalizeBackgroundAgentKeeperAuthority(value),
|
|
106
|
+
turnLaunchToken: requiredSafeString(
|
|
107
|
+
value.turnLaunchToken,
|
|
108
|
+
"turn launch token",
|
|
109
|
+
),
|
|
110
|
+
attempt: requiredPositiveInteger(value.attempt, "turn attempt"),
|
|
111
|
+
agentPid: requiredPositiveInteger(value.agentPid, "agent pid"),
|
|
112
|
+
agentStartedAt: requiredTimestamp(
|
|
113
|
+
value.agentStartedAt,
|
|
114
|
+
"agent start anchor",
|
|
115
|
+
),
|
|
116
|
+
agentRuntimePid: requiredPositiveInteger(
|
|
117
|
+
value.agentRuntimePid,
|
|
118
|
+
"agent runtime pid",
|
|
119
|
+
),
|
|
120
|
+
agentRuntimeStartedAt: requiredTimestamp(
|
|
121
|
+
value.agentRuntimeStartedAt,
|
|
122
|
+
"agent runtime start anchor",
|
|
123
|
+
),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function sameBackgroundAgentKeeperTurn(left, right) {
|
|
128
|
+
let normalizedLeft;
|
|
129
|
+
let normalizedRight;
|
|
130
|
+
try {
|
|
131
|
+
normalizedLeft = normalizeBackgroundAgentKeeperTurn(left);
|
|
132
|
+
normalizedRight = normalizeBackgroundAgentKeeperTurn(right);
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
return Object.keys(normalizedLeft).every(
|
|
137
|
+
(key) => normalizedLeft[key] === normalizedRight[key],
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function createBackgroundAgentKeeperMessage(type, payload = {}) {
|
|
142
|
+
return Object.freeze({
|
|
143
|
+
type,
|
|
144
|
+
protocolVersion: BACKGROUND_AGENT_KEEPER_PROTOCOL_VERSION,
|
|
145
|
+
...payload,
|
|
146
|
+
});
|
|
147
|
+
}
|