chainlesschain 0.162.189 → 0.162.197
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 +84 -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-DbZft8w0.js → AIOps-D3Gu1a5d.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DoOZpwfN.js → ActionButton-DrwAoKaz.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-B0J0KLTW.js → Analytics-C7Q-gd_3.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-D9eIR0qq.js → AppLayout-DgBtae-n.js} +4 -4
- package/src/assets/web-panel/assets/{Artifacts-BnZXEVeo.js → Artifacts-pQ6LcA-W.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-X1ce4Bjn.js → Audit-DK61tocB.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-BvWR-y5v.js → BackgroundAgents-BOKgDFRh.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BublSGEd.js → Backup-Bo9G4vTP.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CR_77Hso.js → BaseInput-Do_dE9_v.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DixW52tq.js → Chat-COVvZzOL.js} +6 -6
- package/src/assets/web-panel/assets/ChatBubbleRenderer-BUUyJ5QO.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-CCv1C87K.js → Checkbox-Z9MJMolt.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-B03Cc3dd.js → Codegen-BVzWKVbf.js} +1 -1
- package/src/assets/web-panel/assets/{Col-CA06-6pU.js → Col-C8MnYcbT.js} +1 -1
- package/src/assets/web-panel/assets/{Community-Bj881J52.js → Community-DQxqw15l.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-UZLTPoaL.js → Compact-BAUmrh3y.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-kGwzRb_0.js → Compliance-jPAkQ3zK.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-KundoYM8.js → Cowork-BBQyxbh8.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-BYZyoqDe.js → Cron-W5koq8Z5.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-BhrrppBX.js → Crosschain-BQhnu5hS.js} +1 -1
- package/src/assets/web-panel/assets/{DID-CdC6EguZ.js → DID-DD7PlI18.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CcQx3J4y.js → Dashboard-w_ILuwKI.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BpijtEzQ.js → Dropdown-qiDUAyAo.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-DfZjeZTe.js → EmailListRenderer-PDeBcI5H.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BFYSf69C.js → FamilyGuardDashboard-SuArscRM.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-Okdi3pJm.js → Federation-BCovZaUn.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-mH-HebKW.js → FormItemContext-DjlFdVFk.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-xivV69x7.js → GenericCardRenderer-DoEZ7fcS.js} +1 -1
- package/src/assets/web-panel/assets/{Git-1rh_YBwg.js → Git-CV0-6GPI.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-eAXkNxdY.js → Governance-B_SFN50w.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CS6dMVTN.js → Inference-heI0Eo6k.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-Bq_TnkvD.js → KnowledgeGraph-Cz5INS5p.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-KchHtiHt.js → Logs-BSFxiHkg.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-1mNozZ8v.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-CsmGcX5X.js → Marketplace-0TWTpE7k.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BaPFnnyz.js → McpTools-CV4-wBLm.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-2m-sUUf7.js → Memory-x5bH5rw5.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-D5nVZBRu.js → MobileBridge-DaBmUu9G.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-Bo8MLZvT.js → MobileProjects-DqnoFPgR.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-3GDvamGa.js → Mtc-B_zKEBif.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-D2zGuZCH.js → MtcAudit-BCfKD2yg.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-BBVQ4t6t.js → Multisig-DgYfj6c_.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-BIOCyOGp.js → NLProgramming-Dz09lFUm.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DVwBGpzc.js → Notes-Cuj6ugBE.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BDnHfIfX.js → NotificationSettings-DK2WXjhg.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-C7Ek20ag.js → OrderTableRenderer-D1IAOQoo.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-D3SRKaa7.js → Organization-9fqnPvTw.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-C2B8ahyn.js → Overflow-n5xHkhow.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-2iwQzG65.js → P2P-B5AUWwH-.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-CoPiWqMN.js → PdhVaultBrowser-CoUDV6dy.js} +4 -4
- package/src/assets/web-panel/assets/{Permissions-yV5XNXo5.js → Permissions-DPRmQf_H.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-uCDJhMUF.js → PersonalDataHub-B37bsbEN.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-hN7ucMOA.js → Pipeline-CCAuPqu9.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-CgU0ptN4.js → Privacy-B81msZdv.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CfYPiT6j.js → ProjectInit-B-Py2NbF.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-B0A2C4nw.js → ProjectSettings-CYsneiRB.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BE5C0ajn.js → Projects-Y975Mh6R.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-CstQPf8W.js → Providers-uO-10aP1.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-Co3z_ynk.js → QrScannerModal-Bj_6gJ0l.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-DMTS6pQ6.js → QuickAsk-BgpBCMPE.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-ETw4odUa.js → Recommend-DhUW3YUA.js} +1 -1
- package/src/assets/web-panel/assets/RemoteSession-DdeyfbkA.js +5 -0
- package/src/assets/web-panel/assets/{Reputation-BTVqQgKq.js → Reputation-fxW421xd.js} +1 -1
- package/src/assets/web-panel/assets/{Row-CBKKwwtO.js → Row-De73w1og.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BHUQ438I.js → RssFeed-3US_Q_lj.js} +2 -2
- package/src/assets/web-panel/assets/{Search-B4NqeUsF.js → Search-DbZ_DU74.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BRjoYG5_.js → Security-DoIU1vMF.js} +4 -4
- package/src/assets/web-panel/assets/{Services-CxQzu3p3.js → Services-DB_RGqpc.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-BKzwwmWO.js → Skeleton-DnzLaayC.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DrsFcgth.js → Skills-VzVXHENF.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-D21tmXCH.js → Sla-DMfvjCAI.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-CjyjLfp8.js → SpeechSettings-DQQOYWbF.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-Cu45-tnx.js → SyncSettings-Bmx3FG1P.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-oMcILYOj.js → Tasks-DOQyfb-2.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-Bh-Zcr5R.js → Templates-D6hTRT7c.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-68ns63aI.js → Tenant-CTPQZAzs.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-BLJkFykE.js → Terminal-B7vII3oH.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-xcrJLq-O.js → TimelineRenderer-BkiFJeXV.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-C6PpFJtf.js → Tokens-BPPdQMNo.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BaKA7tdM.js → Trigger-BxG8JAEI.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BzhA57Jc.js → Trust-DHD7nMpM.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-4_U3JkIR.js → UkeySign-B4BJpU47.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-162AANxX.js → VideoEditing-CRiGpFx2.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-B1viuql_.js → Wallet-BSDSNiem.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-ZVWazhCy.js → WebAuthn-DLTUh5Zv.js} +5 -5
- package/src/assets/web-panel/assets/WorkflowEditor-tW2MwjhD.js +1 -0
- package/src/assets/web-panel/assets/{chat-DmrHNuwq.js → chat-SgsLQ3FC.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DAJ_M-PK.js → colors-BHjjLIKO.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-uIzmYwLs.js → compact-item-VceErdKq.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-D_ZmuLwb.js → createContext-BZ332MYl.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DosMaL-k.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-CDt_shTL.js → hasIn-CBTjJOjn.js} +1 -1
- package/src/assets/web-panel/assets/index-0O5ePnUq.js +1 -0
- package/src/assets/web-panel/assets/{index-rXdwUeE3.js → index-1n7xjKDU.js} +1 -1
- package/src/assets/web-panel/assets/{index-BAvSmJXW.js → index-29wVzi-k.js} +1 -1
- package/src/assets/web-panel/assets/{index-C16fYKFD.js → index-B2u26AS3.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5WcdJH2.js → index-B40dQn0G.js} +3 -3
- package/src/assets/web-panel/assets/{index-CaelyMgF.js → index-B5KAGxNo.js} +1 -1
- package/src/assets/web-panel/assets/{index-OzTxFIJP.js → index-BC1xuQgX.js} +1 -1
- package/src/assets/web-panel/assets/{index-CzXqvMuP.js → index-BETEjuNr.js} +1 -1
- package/src/assets/web-panel/assets/{index-clSX4PEc.js → index-BOtvPdHt.js} +1 -1
- package/src/assets/web-panel/assets/{index-saf3VNeP.js → index-BPXFLYdW.js} +1 -1
- package/src/assets/web-panel/assets/{index-DdUMcwRB.js → index-BQSmZGgs.js} +1 -1
- package/src/assets/web-panel/assets/{index-DfSSy9uy.js → index-BRWhLbWF.js} +1 -1
- package/src/assets/web-panel/assets/{index-GcKOHD_c.js → index-BTlc-Bve.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHvlm0In.js → index-BY1wexYG.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cvak6Oe2.js → index-BcUenmCX.js} +1 -1
- package/src/assets/web-panel/assets/{index-CKyd472S.js → index-BeiqkxJG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DvFhhe6r.js → index-BhXZEUAc.js} +1 -1
- package/src/assets/web-panel/assets/{index-DBFmcaCQ.js → index-By5jBtkV.js} +1 -1
- package/src/assets/web-panel/assets/{index-DqDSF2RW.js → index-CLc19alH.js} +1 -1
- package/src/assets/web-panel/assets/{index-DeGBoLQA.js → index-CMVvxIS8.js} +1 -1
- package/src/assets/web-panel/assets/{index-FyplHuNt.js → index-C_B9rFQE.js} +1 -1
- package/src/assets/web-panel/assets/{index-CeUpKjvi.js → index-CaIS_TNX.js} +1 -1
- package/src/assets/web-panel/assets/{index-BRa4kJ_S.js → index-CawpdGTW.js} +1 -1
- package/src/assets/web-panel/assets/index-ChAzPigF.js +1 -0
- package/src/assets/web-panel/assets/{index-QsgfSZrV.js → index-CpumS98o.js} +1 -1
- package/src/assets/web-panel/assets/{index-BDF6EQBs.js → index-Cr4rjlW5.js} +1 -1
- package/src/assets/web-panel/assets/{index-rS3gl_68.js → index-CrUgDQkf.js} +1 -1
- package/src/assets/web-panel/assets/{index-D4n1lGTB.js → index-CtjLcCNU.js} +1 -1
- package/src/assets/web-panel/assets/{index-ifxmu8SY.js → index-DUkVN794.js} +1 -1
- package/src/assets/web-panel/assets/{index-BTCXf1t-.js → index-D_SePDWj.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bbxjfstq.js → index-DaCCy6lq.js} +1 -1
- package/src/assets/web-panel/assets/{index-D9OE8B3d.js → index-DdefYBSw.js} +1 -1
- package/src/assets/web-panel/assets/{index-CG0lY4h3.js → index-DePOeMDM.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bx4d6c1k.js → index-DgPvuIn2.js} +1 -1
- package/src/assets/web-panel/assets/{index-Z8P4BexE.js → index-FJ3zC9LK.js} +1 -1
- package/src/assets/web-panel/assets/{index-ByzGvpR9.js → index-_PHrIO4O.js} +1 -1
- package/src/assets/web-panel/assets/{index-DwfvPe-j.js → index-fU7wNHDB.js} +1 -1
- package/src/assets/web-panel/assets/{index-CcZ9xJI9.js → index-iYQNU9za.js} +1 -1
- package/src/assets/web-panel/assets/{index-CbD_WW_6.js → index-vIXOpcjD.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-pmfaD-aK.js → initDefaultProps-D1oZNz0D.js} +1 -1
- package/src/assets/web-panel/assets/{motion-CaQ2CpYF.js → motion-C8k6doMO.js} +1 -1
- package/src/assets/web-panel/assets/{move-CkKTqNbI.js → move-DJ4qSbYt.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-Dd4xsgRI.js → mtc-parser-CdpJD85j.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BFuA5YEs.js → omit-B0V4slKA.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DDfxOAJc.js → pickAttrs-Di5NiW1F.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-Be595JCp.js → placementArrow-B5OMHgEM.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-7byljUM3.js → responsiveObserve-C6U9IWql.js} +1 -1
- package/src/assets/web-panel/assets/{slide-CTWfOTaY.js → slide-BMyTH3rN.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CNZOuIxM.js → statusUtils-C7d6BG7Z.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DwOcBV78.js → styleChecker-CR50QnQy.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-B6MNwAO9.js → useFlexGapSupport-DBEeoMb8.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CzyQjH9r.js → useFs-3r099cjv.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-Dxh3KiVA.js → usePersonalDataHub-C5IYNdTK.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DVfZiS5N.js → vnode-DmEOLRHf.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-BYme7fbY.js → zoom-DnpUiVCB.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 +1271 -175
- package/src/command-surface-policy.js +312 -0
- package/src/commands/agent.js +76 -33
- package/src/commands/artifacts.js +400 -2
- package/src/commands/background-session.js +41 -3
- package/src/commands/checkpoint-restore-recovery.js +1476 -0
- package/src/commands/checkpoint.js +947 -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/update.js +68 -6
- package/src/constants.js +11 -0
- package/src/data/changelog.json +14 -1
- 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 +1 -0
- package/src/lib/agent-sandbox.js +120 -26
- package/src/lib/agent-session-export.js +20 -0
- package/src/lib/agent-team/team-runner.js +230 -8
- 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/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 +53 -8
- package/src/lib/process-execution-broker/workspace-transaction.js +976 -73
- 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 +278 -0
- package/src/lib/public-ide-capability-validation.js +351 -0
- package/src/lib/secret-store.js +267 -43
- 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/with-file-lock.js +25 -1
- 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 +2332 -154
- 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 +404 -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 +25 -4
- package/src/repl/terminal-layout.js +88 -0
- package/src/runtime/agent-core.js +1264 -325
- 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 +298 -118
- package/src/runtime/headless-stream.js +353 -95
- package/src/runtime/mcp-config.js +148 -14
- package/src/runtime/status-diagnostics-lite.js +137 -0
- package/src/assets/web-panel/assets/ChatBubbleRenderer-DDHDpBBA.js +0 -1
- package/src/assets/web-panel/assets/MarkdownRenderer-TabqHW9B.js +0 -1
- package/src/assets/web-panel/assets/RemoteSession-B0rkE09d.js +0 -5
- package/src/assets/web-panel/assets/WorkflowEditor-tzdyUk-1.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DIrncrV1.js +0 -1
- package/src/assets/web-panel/assets/index-D-Swwqld.js +0 -1
- package/src/assets/web-panel/assets/index-FMZAMKy8.js +0 -1
|
@@ -58,6 +58,7 @@ export const WORKSPACE_TRANSACTION_ERROR = Object.freeze({
|
|
|
58
58
|
RESOURCE_LIMIT: "WORKSPACE_TRANSACTION_RESOURCE_LIMIT",
|
|
59
59
|
SNAPSHOT_RACE: "WORKSPACE_TRANSACTION_SNAPSHOT_RACE",
|
|
60
60
|
LOCKED: "WORKSPACE_TRANSACTION_LOCKED",
|
|
61
|
+
WORKSPACE_LOCK_TIMEOUT: "WORKSPACE_LOCK_TIMEOUT",
|
|
61
62
|
RECOVERY_REQUIRED: "WORKSPACE_TRANSACTION_RECOVERY_REQUIRED",
|
|
62
63
|
LOCK_CORRUPT: "WORKSPACE_TRANSACTION_LOCK_CORRUPT",
|
|
63
64
|
LOCK_OWNERSHIP_LOST: "WORKSPACE_TRANSACTION_LOCK_OWNERSHIP_LOST",
|
|
@@ -88,6 +89,19 @@ export const DEFAULT_WORKSPACE_TRANSACTION_LIMITS = Object.freeze({
|
|
|
88
89
|
});
|
|
89
90
|
|
|
90
91
|
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,191}$/;
|
|
92
|
+
const CHECKPOINT_RESTORE_LOCK_PURPOSE = "checkpoint-restore";
|
|
93
|
+
const WORKSPACE_LOCK_OWNER_KEYS = Object.freeze([
|
|
94
|
+
"identityPolicy",
|
|
95
|
+
"pid",
|
|
96
|
+
"purpose",
|
|
97
|
+
"startedAt",
|
|
98
|
+
"token",
|
|
99
|
+
"transactionId",
|
|
100
|
+
"workspaceRoot",
|
|
101
|
+
]);
|
|
102
|
+
const WORKSPACE_LOCK_RETENTION_REQUEST = Symbol(
|
|
103
|
+
"workspace-lock-retention-request",
|
|
104
|
+
);
|
|
91
105
|
const TERMINAL_STATES = new Set([
|
|
92
106
|
WORKSPACE_TRANSACTION_STATE.COMMITTED,
|
|
93
107
|
WORKSPACE_TRANSACTION_STATE.ROLLED_BACK,
|
|
@@ -275,6 +289,23 @@ function assertNotFilesystemRoot(root) {
|
|
|
275
289
|
}
|
|
276
290
|
}
|
|
277
291
|
|
|
292
|
+
function assertCanonicalWorkspaceDirectory(value) {
|
|
293
|
+
const requested =
|
|
294
|
+
typeof value === "string" && value.trim() !== ""
|
|
295
|
+
? path.resolve(value)
|
|
296
|
+
: value;
|
|
297
|
+
const canonical = assertSafeDirectory(value, "workspace root");
|
|
298
|
+
assertNotFilesystemRoot(canonical);
|
|
299
|
+
if (!samePath(requested, canonical)) {
|
|
300
|
+
throw codedError(
|
|
301
|
+
WORKSPACE_TRANSACTION_ERROR.PATH_ESCAPE,
|
|
302
|
+
"workspace root must be supplied as its canonical real path",
|
|
303
|
+
{ workspaceRoot: requested, resolvedPath: canonical },
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
return canonical;
|
|
307
|
+
}
|
|
308
|
+
|
|
278
309
|
function normalizeBoundedText(value, label, max = 256) {
|
|
279
310
|
if (typeof value !== "string") {
|
|
280
311
|
throw codedError(
|
|
@@ -1283,7 +1314,49 @@ function defaultLockDir() {
|
|
|
1283
1314
|
);
|
|
1284
1315
|
}
|
|
1285
1316
|
|
|
1286
|
-
|
|
1317
|
+
const DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS = 5_000;
|
|
1318
|
+
const DEFAULT_WORKSPACE_LOCK_RETRY_MS = 25;
|
|
1319
|
+
const MAX_WORKSPACE_LOCK_TIMEOUT_MS = 60_000;
|
|
1320
|
+
const MAX_WORKSPACE_LOCK_RETRY_MS = 1_000;
|
|
1321
|
+
|
|
1322
|
+
function boundedWorkspaceLockDelay(value, fallback, label, maximum) {
|
|
1323
|
+
const resolved = value === undefined ? fallback : Number(value);
|
|
1324
|
+
if (!Number.isSafeInteger(resolved) || resolved <= 0 || resolved > maximum) {
|
|
1325
|
+
throw codedError(
|
|
1326
|
+
WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
|
|
1327
|
+
`${label} must be a positive safe integer no greater than ${maximum}`,
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
return resolved;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function workspaceLockSleepSync(milliseconds) {
|
|
1334
|
+
try {
|
|
1335
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
1336
|
+
} catch {
|
|
1337
|
+
const deadline = Date.now() + milliseconds;
|
|
1338
|
+
while (Date.now() < deadline) {
|
|
1339
|
+
// SharedArrayBuffer is unavailable; retain a bounded synchronous wait.
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function workspaceLockClockValue(now) {
|
|
1345
|
+
const value = Number(now());
|
|
1346
|
+
if (!Number.isFinite(value)) {
|
|
1347
|
+
throw codedError(
|
|
1348
|
+
WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
|
|
1349
|
+
"workspace lock clock must return a finite number",
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
return value;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function assertOwnedPrivateDirectory(
|
|
1356
|
+
dir,
|
|
1357
|
+
label,
|
|
1358
|
+
{ repairPermissions = true } = {},
|
|
1359
|
+
) {
|
|
1287
1360
|
const entry = fs.lstatSync(dir);
|
|
1288
1361
|
if (entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
1289
1362
|
throw codedError(
|
|
@@ -1304,7 +1377,7 @@ function assertOwnedPrivateDirectory(dir, label) {
|
|
|
1304
1377
|
);
|
|
1305
1378
|
}
|
|
1306
1379
|
if (process.platform !== "win32") {
|
|
1307
|
-
fs.chmodSync(dir, 0o700);
|
|
1380
|
+
if (repairPermissions) fs.chmodSync(dir, 0o700);
|
|
1308
1381
|
const secured = fs.lstatSync(dir);
|
|
1309
1382
|
if ((Number(secured.mode) & 0o077) !== 0) {
|
|
1310
1383
|
throw codedError(
|
|
@@ -1331,10 +1404,49 @@ function ownerIsValid(owner) {
|
|
|
1331
1404
|
SAFE_ID.test(owner.transactionId) &&
|
|
1332
1405
|
typeof owner.workspaceRoot === "string" &&
|
|
1333
1406
|
path.isAbsolute(owner.workspaceRoot) &&
|
|
1407
|
+
(owner.purpose === undefined ||
|
|
1408
|
+
(typeof owner.purpose === "string" &&
|
|
1409
|
+
owner.purpose.length >= 1 &&
|
|
1410
|
+
owner.purpose.length <= 128 &&
|
|
1411
|
+
!owner.purpose.includes("\0"))) &&
|
|
1334
1412
|
owner.identityPolicy === "pid-only-fail-closed"
|
|
1335
1413
|
);
|
|
1336
1414
|
}
|
|
1337
1415
|
|
|
1416
|
+
function hasExactWorkspaceLockOwnerShape(owner) {
|
|
1417
|
+
return (
|
|
1418
|
+
ownerIsValid(owner) &&
|
|
1419
|
+
Object.keys(owner).sort().join("\0") ===
|
|
1420
|
+
WORKSPACE_LOCK_OWNER_KEYS.join("\0")
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function sameExactWorkspaceLockOwner(left, right) {
|
|
1425
|
+
return (
|
|
1426
|
+
hasExactWorkspaceLockOwnerShape(left) &&
|
|
1427
|
+
hasExactWorkspaceLockOwnerShape(right) &&
|
|
1428
|
+
canonicalWorkspaceEvidence(left) === canonicalWorkspaceEvidence(right)
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
function workspaceLockOwnerProjection(owner) {
|
|
1433
|
+
if (!ownerIsValid(owner)) {
|
|
1434
|
+
throw codedError(
|
|
1435
|
+
WORKSPACE_TRANSACTION_ERROR.LOCK_CORRUPT,
|
|
1436
|
+
"workspace lock owner cannot be projected safely",
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
return Object.freeze({
|
|
1440
|
+
pid: owner.pid,
|
|
1441
|
+
startedAt: owner.startedAt,
|
|
1442
|
+
token: owner.token,
|
|
1443
|
+
transactionId: owner.transactionId,
|
|
1444
|
+
workspaceRoot: owner.workspaceRoot,
|
|
1445
|
+
purpose: owner.purpose,
|
|
1446
|
+
identityPolicy: owner.identityPolicy,
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1338
1450
|
function readLockOwner(lockPath) {
|
|
1339
1451
|
const ownerPath = path.join(lockPath, "owner.json");
|
|
1340
1452
|
try {
|
|
@@ -1363,6 +1475,7 @@ function sameOwner(left, right) {
|
|
|
1363
1475
|
left.token === right.token &&
|
|
1364
1476
|
left.transactionId === right.transactionId &&
|
|
1365
1477
|
samePath(left.workspaceRoot, right.workspaceRoot) &&
|
|
1478
|
+
left.purpose === right.purpose &&
|
|
1366
1479
|
left.identityPolicy === right.identityPolicy
|
|
1367
1480
|
);
|
|
1368
1481
|
}
|
|
@@ -1387,10 +1500,14 @@ class WorkspaceLifetimeLock {
|
|
|
1387
1500
|
isProcessAlive,
|
|
1388
1501
|
now,
|
|
1389
1502
|
ownerToken,
|
|
1503
|
+
purpose = "workspace-transaction",
|
|
1390
1504
|
reclaimDead = false,
|
|
1391
1505
|
expectedDeadOwner = null,
|
|
1506
|
+
requireExactExpectedDeadOwner = false,
|
|
1392
1507
|
},
|
|
1393
1508
|
) {
|
|
1509
|
+
const normalizedTransactionId = normalizeId(transactionId);
|
|
1510
|
+
const normalizedPurpose = normalizeBoundedText(purpose, "purpose", 128);
|
|
1394
1511
|
ensurePrivateDirectory(lockRoot);
|
|
1395
1512
|
const lockPath = path.join(lockRoot, workspaceLockName(workspaceRoot));
|
|
1396
1513
|
const ownerPath = path.join(lockPath, "owner.json");
|
|
@@ -1398,13 +1515,21 @@ class WorkspaceLifetimeLock {
|
|
|
1398
1515
|
pid: process.pid,
|
|
1399
1516
|
startedAt: now(),
|
|
1400
1517
|
token: ownerToken(),
|
|
1401
|
-
transactionId,
|
|
1518
|
+
transactionId: normalizedTransactionId,
|
|
1402
1519
|
workspaceRoot,
|
|
1520
|
+
purpose: normalizedPurpose,
|
|
1403
1521
|
// A live PID with an unavailable creation-time proof is never reclaimed.
|
|
1404
1522
|
// This may require manual recovery after PID reuse, but cannot kill or
|
|
1405
1523
|
// roll back underneath an unrelated process.
|
|
1406
1524
|
identityPolicy: "pid-only-fail-closed",
|
|
1407
1525
|
};
|
|
1526
|
+
if (!ownerIsValid(owner)) {
|
|
1527
|
+
throw codedError(
|
|
1528
|
+
WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
|
|
1529
|
+
"workspace lock owner identity is invalid",
|
|
1530
|
+
{ transactionId: normalizedTransactionId },
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1408
1533
|
|
|
1409
1534
|
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
1410
1535
|
try {
|
|
@@ -1443,10 +1568,12 @@ class WorkspaceLifetimeLock {
|
|
|
1443
1568
|
},
|
|
1444
1569
|
);
|
|
1445
1570
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1571
|
+
const expectedDeadOwnerMatches = expectedDeadOwner
|
|
1572
|
+
? requireExactExpectedDeadOwner
|
|
1573
|
+
? sameExactWorkspaceLockOwner(expectedDeadOwner, incumbent)
|
|
1574
|
+
: sameOwner(expectedDeadOwner, incumbent)
|
|
1575
|
+
: true;
|
|
1576
|
+
if (!reclaimDead || !expectedDeadOwnerMatches) {
|
|
1450
1577
|
throw codedError(
|
|
1451
1578
|
WORKSPACE_TRANSACTION_ERROR.RECOVERY_REQUIRED,
|
|
1452
1579
|
`workspace has a crashed transaction requiring recovery: ${workspaceRoot}`,
|
|
@@ -1457,7 +1584,9 @@ class WorkspaceLifetimeLock {
|
|
|
1457
1584
|
},
|
|
1458
1585
|
);
|
|
1459
1586
|
}
|
|
1460
|
-
WorkspaceLifetimeLock._reclaim(lockPath, incumbent, isProcessAlive
|
|
1587
|
+
WorkspaceLifetimeLock._reclaim(lockPath, incumbent, isProcessAlive, {
|
|
1588
|
+
requireExactOwner: requireExactExpectedDeadOwner,
|
|
1589
|
+
});
|
|
1461
1590
|
}
|
|
1462
1591
|
}
|
|
1463
1592
|
throw codedError(
|
|
@@ -1467,13 +1596,21 @@ class WorkspaceLifetimeLock {
|
|
|
1467
1596
|
);
|
|
1468
1597
|
}
|
|
1469
1598
|
|
|
1470
|
-
static _reclaim(
|
|
1599
|
+
static _reclaim(
|
|
1600
|
+
lockPath,
|
|
1601
|
+
observed,
|
|
1602
|
+
isProcessAlive,
|
|
1603
|
+
{ requireExactOwner = false } = {},
|
|
1604
|
+
) {
|
|
1471
1605
|
const marker = path.join(lockPath, `.reclaim-${observed.token}`);
|
|
1472
1606
|
atomicWrite(marker, `${canonicalWorkspaceEvidence(observed)}\n`, {
|
|
1473
1607
|
maxBytes: 16 * 1024,
|
|
1474
1608
|
});
|
|
1475
1609
|
const current = readLockOwner(lockPath);
|
|
1476
|
-
|
|
1610
|
+
const ownerMatches = requireExactOwner
|
|
1611
|
+
? sameExactWorkspaceLockOwner(current, observed)
|
|
1612
|
+
: sameOwner(current, observed);
|
|
1613
|
+
if (!ownerMatches || isProcessAlive(observed.pid)) {
|
|
1477
1614
|
try {
|
|
1478
1615
|
fs.rmSync(marker, { force: true });
|
|
1479
1616
|
} catch {
|
|
@@ -1536,8 +1673,12 @@ function assertNoOverlappingWorkspaceLock(
|
|
|
1536
1673
|
lockDir,
|
|
1537
1674
|
workspaceRoot,
|
|
1538
1675
|
isProcessAlive,
|
|
1676
|
+
{ expectedIgnoredOwner = null, requireExactIgnoredOwner = false } = {},
|
|
1539
1677
|
) {
|
|
1540
1678
|
ensurePrivateDirectory(lockDir);
|
|
1679
|
+
const ignoredLockPath = expectedIgnoredOwner
|
|
1680
|
+
? path.join(lockDir, workspaceLockName(expectedIgnoredOwner.workspaceRoot))
|
|
1681
|
+
: null;
|
|
1541
1682
|
for (const name of fs.readdirSync(lockDir).sort()) {
|
|
1542
1683
|
if (name === "coordination.lock") continue;
|
|
1543
1684
|
const candidate = path.join(lockDir, name);
|
|
@@ -1562,6 +1703,24 @@ function assertNoOverlappingWorkspaceLock(
|
|
|
1562
1703
|
{ path: candidate },
|
|
1563
1704
|
);
|
|
1564
1705
|
}
|
|
1706
|
+
if (ignoredLockPath && samePath(candidate, ignoredLockPath)) {
|
|
1707
|
+
const ignoredOwnerMatches = requireExactIgnoredOwner
|
|
1708
|
+
? sameExactWorkspaceLockOwner(owner, expectedIgnoredOwner)
|
|
1709
|
+
: sameOwner(owner, expectedIgnoredOwner);
|
|
1710
|
+
if (!ignoredOwnerMatches) {
|
|
1711
|
+
throw codedError(
|
|
1712
|
+
WORKSPACE_TRANSACTION_ERROR.LOCK_CORRUPT,
|
|
1713
|
+
"workspace recovery lock owner changed during registry validation",
|
|
1714
|
+
{
|
|
1715
|
+
workspaceRoot,
|
|
1716
|
+
lockPath: candidate,
|
|
1717
|
+
expectedTransactionId: expectedIgnoredOwner.transactionId,
|
|
1718
|
+
observedTransactionId: owner.transactionId,
|
|
1719
|
+
},
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
continue;
|
|
1723
|
+
}
|
|
1565
1724
|
if (!workspacesOverlap(owner.workspaceRoot, workspaceRoot)) continue;
|
|
1566
1725
|
const alive = isProcessAlive(owner.pid);
|
|
1567
1726
|
throw codedError(
|
|
@@ -1582,6 +1741,778 @@ function assertNoOverlappingWorkspaceLock(
|
|
|
1582
1741
|
}
|
|
1583
1742
|
}
|
|
1584
1743
|
|
|
1744
|
+
function preflightWorkspaceLockRoot(lockDir, workspaceRoot) {
|
|
1745
|
+
const requestedLockDir = path.resolve(lockDir);
|
|
1746
|
+
const projectedLockDir = canonicalPathThroughExistingAncestor(
|
|
1747
|
+
requestedLockDir,
|
|
1748
|
+
"lock directory",
|
|
1749
|
+
);
|
|
1750
|
+
if (pathsOverlap(projectedLockDir, workspaceRoot)) {
|
|
1751
|
+
throw codedError(
|
|
1752
|
+
WORKSPACE_TRANSACTION_ERROR.INVALID_PATH,
|
|
1753
|
+
"workspace transaction lock directory must be disjoint from the workspace",
|
|
1754
|
+
{
|
|
1755
|
+
workspaceRoot,
|
|
1756
|
+
lockDir: requestedLockDir,
|
|
1757
|
+
resolvedLockDir: projectedLockDir,
|
|
1758
|
+
},
|
|
1759
|
+
);
|
|
1760
|
+
}
|
|
1761
|
+
return { requestedLockDir, projectedLockDir };
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function ensureWorkspaceLockRoot(lockDir, workspaceRoot) {
|
|
1765
|
+
preflightWorkspaceLockRoot(lockDir, workspaceRoot);
|
|
1766
|
+
const canonicalLockDir = assertSafeDirectory(lockDir, "lock directory", {
|
|
1767
|
+
create: true,
|
|
1768
|
+
});
|
|
1769
|
+
if (pathsOverlap(canonicalLockDir, workspaceRoot)) {
|
|
1770
|
+
throw codedError(
|
|
1771
|
+
WORKSPACE_TRANSACTION_ERROR.INVALID_PATH,
|
|
1772
|
+
"workspace transaction lock directory must be disjoint from the workspace",
|
|
1773
|
+
{ workspaceRoot, lockDir: canonicalLockDir },
|
|
1774
|
+
);
|
|
1775
|
+
}
|
|
1776
|
+
assertOwnedPrivateDirectory(canonicalLockDir, "lock directory");
|
|
1777
|
+
return canonicalLockDir;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
function inspectWorkspaceLockRoot(lockDir, workspaceRoot) {
|
|
1781
|
+
const { requestedLockDir } = preflightWorkspaceLockRoot(
|
|
1782
|
+
lockDir,
|
|
1783
|
+
workspaceRoot,
|
|
1784
|
+
);
|
|
1785
|
+
try {
|
|
1786
|
+
fs.lstatSync(requestedLockDir);
|
|
1787
|
+
} catch (cause) {
|
|
1788
|
+
if (cause?.code === "ENOENT") return null;
|
|
1789
|
+
throw codedError(
|
|
1790
|
+
WORKSPACE_TRANSACTION_ERROR.INVALID_PATH,
|
|
1791
|
+
`lock directory is unavailable: ${requestedLockDir}`,
|
|
1792
|
+
{ path: requestedLockDir },
|
|
1793
|
+
cause,
|
|
1794
|
+
);
|
|
1795
|
+
}
|
|
1796
|
+
const canonicalLockDir = assertSafeDirectory(
|
|
1797
|
+
requestedLockDir,
|
|
1798
|
+
"lock directory",
|
|
1799
|
+
);
|
|
1800
|
+
if (pathsOverlap(canonicalLockDir, workspaceRoot)) {
|
|
1801
|
+
throw codedError(
|
|
1802
|
+
WORKSPACE_TRANSACTION_ERROR.INVALID_PATH,
|
|
1803
|
+
"workspace transaction lock directory must be disjoint from the workspace",
|
|
1804
|
+
{ workspaceRoot, lockDir: canonicalLockDir },
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
assertOwnedPrivateDirectory(canonicalLockDir, "lock directory", {
|
|
1808
|
+
repairPermissions: false,
|
|
1809
|
+
});
|
|
1810
|
+
return canonicalLockDir;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
function withWorkspaceRegistryCoordination(
|
|
1814
|
+
{
|
|
1815
|
+
lockDir,
|
|
1816
|
+
timeoutMs,
|
|
1817
|
+
retryMs = DEFAULT_WORKSPACE_LOCK_RETRY_MS,
|
|
1818
|
+
now = () => Date.now(),
|
|
1819
|
+
sleep = workspaceLockSleepSync,
|
|
1820
|
+
isProcessAlive = processAlive,
|
|
1821
|
+
ownerToken = () => randomUUID(),
|
|
1822
|
+
},
|
|
1823
|
+
callback,
|
|
1824
|
+
) {
|
|
1825
|
+
return withFileLock(path.join(lockDir, "coordination"), callback, {
|
|
1826
|
+
failIfUnavailable: true,
|
|
1827
|
+
timeoutMs,
|
|
1828
|
+
retryMs: Math.min(retryMs, timeoutMs),
|
|
1829
|
+
maxRetryMs: Math.min(retryMs, timeoutMs),
|
|
1830
|
+
retryJitterMs: 0,
|
|
1831
|
+
_now: now,
|
|
1832
|
+
_sleep: sleep,
|
|
1833
|
+
_random: () => 0,
|
|
1834
|
+
_isProcessAlive: isProcessAlive,
|
|
1835
|
+
_ownerToken: ownerToken,
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
function acquireWorkspaceLifetimeLockOnce({
|
|
1840
|
+
lockDir,
|
|
1841
|
+
workspaceRoot,
|
|
1842
|
+
operationId,
|
|
1843
|
+
purpose,
|
|
1844
|
+
isProcessAlive,
|
|
1845
|
+
now,
|
|
1846
|
+
ownerToken,
|
|
1847
|
+
coordinationTimeoutMs,
|
|
1848
|
+
coordinationNow = () => Date.now(),
|
|
1849
|
+
coordinationSleep = workspaceLockSleepSync,
|
|
1850
|
+
coordinationIsProcessAlive = processAlive,
|
|
1851
|
+
coordinationOwnerToken = () => randomUUID(),
|
|
1852
|
+
}) {
|
|
1853
|
+
const canonicalLockDir = ensureWorkspaceLockRoot(lockDir, workspaceRoot);
|
|
1854
|
+
let lock = null;
|
|
1855
|
+
withWorkspaceRegistryCoordination(
|
|
1856
|
+
{
|
|
1857
|
+
lockDir: canonicalLockDir,
|
|
1858
|
+
timeoutMs: coordinationTimeoutMs,
|
|
1859
|
+
now: coordinationNow,
|
|
1860
|
+
sleep: coordinationSleep,
|
|
1861
|
+
isProcessAlive: coordinationIsProcessAlive,
|
|
1862
|
+
ownerToken: coordinationOwnerToken,
|
|
1863
|
+
},
|
|
1864
|
+
() => {
|
|
1865
|
+
assertNoOverlappingWorkspaceLock(
|
|
1866
|
+
canonicalLockDir,
|
|
1867
|
+
workspaceRoot,
|
|
1868
|
+
isProcessAlive,
|
|
1869
|
+
);
|
|
1870
|
+
lock = WorkspaceLifetimeLock.acquire(
|
|
1871
|
+
canonicalLockDir,
|
|
1872
|
+
workspaceRoot,
|
|
1873
|
+
operationId,
|
|
1874
|
+
{
|
|
1875
|
+
isProcessAlive,
|
|
1876
|
+
now,
|
|
1877
|
+
ownerToken,
|
|
1878
|
+
purpose,
|
|
1879
|
+
},
|
|
1880
|
+
);
|
|
1881
|
+
},
|
|
1882
|
+
);
|
|
1883
|
+
if (!lock) {
|
|
1884
|
+
throw codedError(
|
|
1885
|
+
WORKSPACE_TRANSACTION_ERROR.LOCK_CORRUPT,
|
|
1886
|
+
"workspace lock coordination completed without an ownership lease",
|
|
1887
|
+
{ workspaceRoot, operationId },
|
|
1888
|
+
);
|
|
1889
|
+
}
|
|
1890
|
+
return lock;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
function acquireRecoveredWorkspaceLifetimeLock({
|
|
1894
|
+
lockDir,
|
|
1895
|
+
workspaceRoot,
|
|
1896
|
+
operationId,
|
|
1897
|
+
expectedDeadOwner,
|
|
1898
|
+
expectedPurpose = null,
|
|
1899
|
+
acquiredPurpose = "workspace-transaction",
|
|
1900
|
+
requireExactOwnerShape = false,
|
|
1901
|
+
isProcessAlive,
|
|
1902
|
+
now,
|
|
1903
|
+
ownerToken,
|
|
1904
|
+
coordinationTimeoutMs = DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS,
|
|
1905
|
+
coordinationRetryMs = DEFAULT_WORKSPACE_LOCK_RETRY_MS,
|
|
1906
|
+
coordinationNow = () => Date.now(),
|
|
1907
|
+
coordinationSleep = workspaceLockSleepSync,
|
|
1908
|
+
coordinationIsProcessAlive = processAlive,
|
|
1909
|
+
coordinationOwnerToken = () => randomUUID(),
|
|
1910
|
+
}) {
|
|
1911
|
+
const canonicalLockDir = ensureWorkspaceLockRoot(lockDir, workspaceRoot);
|
|
1912
|
+
if (
|
|
1913
|
+
!ownerIsValid(expectedDeadOwner) ||
|
|
1914
|
+
!samePath(expectedDeadOwner.workspaceRoot, workspaceRoot) ||
|
|
1915
|
+
(expectedPurpose !== null &&
|
|
1916
|
+
expectedDeadOwner.purpose !== expectedPurpose) ||
|
|
1917
|
+
(requireExactOwnerShape &&
|
|
1918
|
+
!hasExactWorkspaceLockOwnerShape(expectedDeadOwner))
|
|
1919
|
+
) {
|
|
1920
|
+
throw codedError(
|
|
1921
|
+
WORKSPACE_TRANSACTION_ERROR.LOCK_CORRUPT,
|
|
1922
|
+
"workspace recovery requires an exact valid owner observation",
|
|
1923
|
+
{ workspaceRoot, operationId },
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
const lockPath = path.join(
|
|
1927
|
+
canonicalLockDir,
|
|
1928
|
+
workspaceLockName(workspaceRoot),
|
|
1929
|
+
);
|
|
1930
|
+
let lock = null;
|
|
1931
|
+
withWorkspaceRegistryCoordination(
|
|
1932
|
+
{
|
|
1933
|
+
lockDir: canonicalLockDir,
|
|
1934
|
+
timeoutMs: coordinationTimeoutMs,
|
|
1935
|
+
retryMs: coordinationRetryMs,
|
|
1936
|
+
now: coordinationNow,
|
|
1937
|
+
sleep: coordinationSleep,
|
|
1938
|
+
isProcessAlive: coordinationIsProcessAlive,
|
|
1939
|
+
ownerToken: coordinationOwnerToken,
|
|
1940
|
+
},
|
|
1941
|
+
() => {
|
|
1942
|
+
const currentOwner = readLockOwner(lockPath);
|
|
1943
|
+
const currentOwnerMatches = requireExactOwnerShape
|
|
1944
|
+
? sameExactWorkspaceLockOwner(currentOwner, expectedDeadOwner)
|
|
1945
|
+
: sameOwner(currentOwner, expectedDeadOwner);
|
|
1946
|
+
if (!currentOwnerMatches) {
|
|
1947
|
+
throw codedError(
|
|
1948
|
+
WORKSPACE_TRANSACTION_ERROR.LOCK_CORRUPT,
|
|
1949
|
+
"workspace recovery owner changed before exact reclaim",
|
|
1950
|
+
{
|
|
1951
|
+
workspaceRoot,
|
|
1952
|
+
operationId,
|
|
1953
|
+
expectedTransactionId: expectedDeadOwner.transactionId,
|
|
1954
|
+
observedTransactionId: currentOwner?.transactionId || null,
|
|
1955
|
+
},
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
if (isProcessAlive(currentOwner.pid)) {
|
|
1959
|
+
throw codedError(
|
|
1960
|
+
WORKSPACE_TRANSACTION_ERROR.RECOVERY_REQUIRED,
|
|
1961
|
+
"workspace recovery owner became live before exact reclaim",
|
|
1962
|
+
{
|
|
1963
|
+
workspaceRoot,
|
|
1964
|
+
ownerPid: currentOwner.pid,
|
|
1965
|
+
ownerTransactionId: currentOwner.transactionId,
|
|
1966
|
+
},
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1969
|
+
assertNoOverlappingWorkspaceLock(
|
|
1970
|
+
canonicalLockDir,
|
|
1971
|
+
workspaceRoot,
|
|
1972
|
+
isProcessAlive,
|
|
1973
|
+
{
|
|
1974
|
+
expectedIgnoredOwner: currentOwner,
|
|
1975
|
+
requireExactIgnoredOwner: requireExactOwnerShape,
|
|
1976
|
+
},
|
|
1977
|
+
);
|
|
1978
|
+
lock = WorkspaceLifetimeLock.acquire(
|
|
1979
|
+
canonicalLockDir,
|
|
1980
|
+
workspaceRoot,
|
|
1981
|
+
operationId,
|
|
1982
|
+
{
|
|
1983
|
+
isProcessAlive,
|
|
1984
|
+
now,
|
|
1985
|
+
ownerToken,
|
|
1986
|
+
purpose: acquiredPurpose,
|
|
1987
|
+
reclaimDead: true,
|
|
1988
|
+
expectedDeadOwner: currentOwner,
|
|
1989
|
+
requireExactExpectedDeadOwner: requireExactOwnerShape,
|
|
1990
|
+
},
|
|
1991
|
+
);
|
|
1992
|
+
},
|
|
1993
|
+
);
|
|
1994
|
+
if (!lock) {
|
|
1995
|
+
throw codedError(
|
|
1996
|
+
WORKSPACE_TRANSACTION_ERROR.LOCK_CORRUPT,
|
|
1997
|
+
"workspace recovery coordination completed without an ownership lease",
|
|
1998
|
+
{ workspaceRoot, operationId },
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
return lock;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
function publicWorkspaceLockRuntime(options) {
|
|
2005
|
+
const canonicalLockDir = path.resolve(defaultLockDir());
|
|
2006
|
+
const requestedLockDir = path.resolve(options.lockDir || canonicalLockDir);
|
|
2007
|
+
const permitsTestInjection =
|
|
2008
|
+
process.env.NODE_ENV === "test" &&
|
|
2009
|
+
options.allowNonCanonicalLockDirForTests === true;
|
|
2010
|
+
const hasRuntimeInjection = [
|
|
2011
|
+
"_now",
|
|
2012
|
+
"_sleep",
|
|
2013
|
+
"_isProcessAlive",
|
|
2014
|
+
"_ownerToken",
|
|
2015
|
+
].some((key) => Object.hasOwn(options, key));
|
|
2016
|
+
if (
|
|
2017
|
+
(!samePath(requestedLockDir, canonicalLockDir) || hasRuntimeInjection) &&
|
|
2018
|
+
!permitsTestInjection
|
|
2019
|
+
) {
|
|
2020
|
+
throw codedError(
|
|
2021
|
+
WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
|
|
2022
|
+
"workspace lock authority and runtime hooks are test-only",
|
|
2023
|
+
{ requestedLockDir, canonicalLockDir },
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
2026
|
+
const now = options._now || (() => Date.now());
|
|
2027
|
+
const sleep = options._sleep || workspaceLockSleepSync;
|
|
2028
|
+
const isProcessAlive = options._isProcessAlive || processAlive;
|
|
2029
|
+
const ownerToken = options._ownerToken || (() => randomUUID());
|
|
2030
|
+
for (const [label, value] of [
|
|
2031
|
+
["_now", now],
|
|
2032
|
+
["_sleep", sleep],
|
|
2033
|
+
["_isProcessAlive", isProcessAlive],
|
|
2034
|
+
["_ownerToken", ownerToken],
|
|
2035
|
+
]) {
|
|
2036
|
+
if (typeof value !== "function") {
|
|
2037
|
+
throw codedError(
|
|
2038
|
+
WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
|
|
2039
|
+
`${label} must be a function`,
|
|
2040
|
+
);
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
return {
|
|
2044
|
+
lockDir: requestedLockDir,
|
|
2045
|
+
now,
|
|
2046
|
+
sleep,
|
|
2047
|
+
isProcessAlive,
|
|
2048
|
+
ownerToken,
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
function isLiveWorkspaceLockConflict(error) {
|
|
2053
|
+
return (
|
|
2054
|
+
error?.code === WORKSPACE_TRANSACTION_ERROR.OVERLAPPING_WORKSPACE ||
|
|
2055
|
+
error?.code === WORKSPACE_TRANSACTION_ERROR.LOCKED ||
|
|
2056
|
+
error?.code === "STATE_LOCK_UNAVAILABLE"
|
|
2057
|
+
);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
function asyncWorkspaceLockCallbackError(cause = null) {
|
|
2061
|
+
return codedError(
|
|
2062
|
+
WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
|
|
2063
|
+
"workspace lock callback must complete synchronously and cannot return a thenable",
|
|
2064
|
+
{},
|
|
2065
|
+
cause,
|
|
2066
|
+
);
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
function isAsyncFunction(callback) {
|
|
2070
|
+
return Object.prototype.toString.call(callback) === "[object AsyncFunction]";
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
function assertSynchronousWorkspaceLockCallback(callback) {
|
|
2074
|
+
if (typeof callback !== "function") {
|
|
2075
|
+
throw codedError(
|
|
2076
|
+
WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
|
|
2077
|
+
"workspace lock callback must be a function",
|
|
2078
|
+
);
|
|
2079
|
+
}
|
|
2080
|
+
if (isAsyncFunction(callback)) throw asyncWorkspaceLockCallbackError();
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
function assertNonThenableWorkspaceLockResult(result) {
|
|
2084
|
+
if (
|
|
2085
|
+
result === null ||
|
|
2086
|
+
(typeof result !== "object" && typeof result !== "function")
|
|
2087
|
+
) {
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
let then;
|
|
2091
|
+
try {
|
|
2092
|
+
then = result.then;
|
|
2093
|
+
} catch (cause) {
|
|
2094
|
+
throw asyncWorkspaceLockCallbackError(cause);
|
|
2095
|
+
}
|
|
2096
|
+
if (typeof then === "function") throw asyncWorkspaceLockCallbackError();
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
function runWorkspaceLockCallbackSync({
|
|
2100
|
+
lock,
|
|
2101
|
+
workspaceRoot,
|
|
2102
|
+
operationId,
|
|
2103
|
+
callback,
|
|
2104
|
+
priorOwner = null,
|
|
2105
|
+
recoveryOfOperationId = operationId,
|
|
2106
|
+
}) {
|
|
2107
|
+
const publicOwner = workspaceLockOwnerProjection(lock.owner);
|
|
2108
|
+
const publicPriorOwner = priorOwner
|
|
2109
|
+
? workspaceLockOwnerProjection(priorOwner)
|
|
2110
|
+
: null;
|
|
2111
|
+
let retentionRequest = null;
|
|
2112
|
+
const lease = Object.freeze({
|
|
2113
|
+
canonicalWorkspaceRoot: workspaceRoot,
|
|
2114
|
+
owner: publicOwner,
|
|
2115
|
+
priorOwner: publicPriorOwner,
|
|
2116
|
+
recoveryOfOperationId,
|
|
2117
|
+
assertOwned: () => lock.assertOwned(),
|
|
2118
|
+
retainForRecovery: (reason) => {
|
|
2119
|
+
lock.assertOwned();
|
|
2120
|
+
if (lock.owner.purpose !== CHECKPOINT_RESTORE_LOCK_PURPOSE) {
|
|
2121
|
+
throw codedError(
|
|
2122
|
+
WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
|
|
2123
|
+
"only checkpoint-restore locks can be retained for recovery",
|
|
2124
|
+
{ operationId, purpose: lock.owner.purpose },
|
|
2125
|
+
);
|
|
2126
|
+
}
|
|
2127
|
+
if (retentionRequest) {
|
|
2128
|
+
throw codedError(
|
|
2129
|
+
WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
|
|
2130
|
+
"workspace lock recovery retention was already requested",
|
|
2131
|
+
{ operationId },
|
|
2132
|
+
);
|
|
2133
|
+
}
|
|
2134
|
+
const normalizedReason = normalizeBoundedText(
|
|
2135
|
+
reason,
|
|
2136
|
+
"workspace lock recovery reason",
|
|
2137
|
+
512,
|
|
2138
|
+
);
|
|
2139
|
+
const request = {
|
|
2140
|
+
error: null,
|
|
2141
|
+
reason: normalizedReason,
|
|
2142
|
+
};
|
|
2143
|
+
const error = codedError(
|
|
2144
|
+
WORKSPACE_TRANSACTION_ERROR.RECOVERY_REQUIRED,
|
|
2145
|
+
"workspace lock retention requested for durable recovery",
|
|
2146
|
+
{
|
|
2147
|
+
workspaceRoot,
|
|
2148
|
+
operationId,
|
|
2149
|
+
retentionReason: normalizedReason,
|
|
2150
|
+
},
|
|
2151
|
+
);
|
|
2152
|
+
request.error = error;
|
|
2153
|
+
Object.defineProperty(error, WORKSPACE_LOCK_RETENTION_REQUEST, {
|
|
2154
|
+
value: request,
|
|
2155
|
+
});
|
|
2156
|
+
retentionRequest = request;
|
|
2157
|
+
throw error;
|
|
2158
|
+
},
|
|
2159
|
+
});
|
|
2160
|
+
let result;
|
|
2161
|
+
let callbackError;
|
|
2162
|
+
let callbackThrew = false;
|
|
2163
|
+
try {
|
|
2164
|
+
result = callback(lease);
|
|
2165
|
+
assertNonThenableWorkspaceLockResult(result);
|
|
2166
|
+
} catch (error) {
|
|
2167
|
+
callbackThrew = true;
|
|
2168
|
+
callbackError = error;
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
const retentionQualified = !!(
|
|
2172
|
+
callbackThrew &&
|
|
2173
|
+
retentionRequest &&
|
|
2174
|
+
callbackError === retentionRequest.error &&
|
|
2175
|
+
callbackError?.code === WORKSPACE_TRANSACTION_ERROR.RECOVERY_REQUIRED &&
|
|
2176
|
+
callbackError?.[WORKSPACE_LOCK_RETENTION_REQUEST] === retentionRequest
|
|
2177
|
+
);
|
|
2178
|
+
let releaseError = null;
|
|
2179
|
+
if (retentionQualified) {
|
|
2180
|
+
try {
|
|
2181
|
+
lock.assertOwned();
|
|
2182
|
+
} catch (error) {
|
|
2183
|
+
releaseError = error;
|
|
2184
|
+
}
|
|
2185
|
+
if (!releaseError) {
|
|
2186
|
+
const retainedOwner = workspaceLockOwnerProjection(lock.owner);
|
|
2187
|
+
throw codedError(
|
|
2188
|
+
WORKSPACE_TRANSACTION_ERROR.RECOVERY_REQUIRED,
|
|
2189
|
+
"workspace lock was retained for durable recovery",
|
|
2190
|
+
{
|
|
2191
|
+
workspaceRoot,
|
|
2192
|
+
operationId,
|
|
2193
|
+
retentionReason: retentionRequest.reason,
|
|
2194
|
+
workspaceLockRetained: true,
|
|
2195
|
+
retainedOwner,
|
|
2196
|
+
priorOwner: publicPriorOwner,
|
|
2197
|
+
newOwner: retainedOwner,
|
|
2198
|
+
recoveryOfOperationId,
|
|
2199
|
+
},
|
|
2200
|
+
callbackError,
|
|
2201
|
+
);
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
if (!releaseError) {
|
|
2205
|
+
try {
|
|
2206
|
+
lock.release();
|
|
2207
|
+
} catch (error) {
|
|
2208
|
+
releaseError = error;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
if (releaseError) {
|
|
2212
|
+
if (callbackThrew) releaseError.callbackError = callbackError;
|
|
2213
|
+
throw releaseError;
|
|
2214
|
+
}
|
|
2215
|
+
if (callbackThrew) throw callbackError;
|
|
2216
|
+
return result;
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
function normalizeCheckpointRestorePurpose(value) {
|
|
2220
|
+
const purpose = normalizeBoundedText(
|
|
2221
|
+
value ?? CHECKPOINT_RESTORE_LOCK_PURPOSE,
|
|
2222
|
+
"purpose",
|
|
2223
|
+
128,
|
|
2224
|
+
);
|
|
2225
|
+
if (purpose !== CHECKPOINT_RESTORE_LOCK_PURPOSE) {
|
|
2226
|
+
throw codedError(
|
|
2227
|
+
WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
|
|
2228
|
+
`workspace recovery only accepts purpose ${CHECKPOINT_RESTORE_LOCK_PURPOSE}`,
|
|
2229
|
+
{ purpose },
|
|
2230
|
+
);
|
|
2231
|
+
}
|
|
2232
|
+
return purpose;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
function normalizeExpectedCheckpointRestoreOwner(
|
|
2236
|
+
value,
|
|
2237
|
+
workspaceRoot,
|
|
2238
|
+
operationId,
|
|
2239
|
+
) {
|
|
2240
|
+
if (
|
|
2241
|
+
!hasExactWorkspaceLockOwnerShape(value) ||
|
|
2242
|
+
value.purpose !== CHECKPOINT_RESTORE_LOCK_PURPOSE ||
|
|
2243
|
+
value.transactionId !== operationId ||
|
|
2244
|
+
!samePath(value.workspaceRoot, workspaceRoot)
|
|
2245
|
+
) {
|
|
2246
|
+
throw codedError(
|
|
2247
|
+
WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
|
|
2248
|
+
"expectedOwner must be the complete exact checkpoint-restore owner for this workspace and operation",
|
|
2249
|
+
{ workspaceRoot, operationId },
|
|
2250
|
+
);
|
|
2251
|
+
}
|
|
2252
|
+
return workspaceLockOwnerProjection(value);
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
/**
|
|
2256
|
+
* Hold the canonical cross-process workspace lock for one synchronous
|
|
2257
|
+
* operation. The coordination lock is held only while inspecting and claiming
|
|
2258
|
+
* the registry; waits for a live overlapping owner happen outside it.
|
|
2259
|
+
*/
|
|
2260
|
+
export function withWorkspaceLockSync(options = {}, callback) {
|
|
2261
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
2262
|
+
throw codedError(
|
|
2263
|
+
WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
|
|
2264
|
+
"workspace lock options must be an object",
|
|
2265
|
+
);
|
|
2266
|
+
}
|
|
2267
|
+
assertSynchronousWorkspaceLockCallback(callback);
|
|
2268
|
+
|
|
2269
|
+
const workspaceRoot = assertCanonicalWorkspaceDirectory(
|
|
2270
|
+
options.workspaceRoot,
|
|
2271
|
+
);
|
|
2272
|
+
const operationId = normalizeId(options.operationId, "operationId");
|
|
2273
|
+
const purpose = normalizeBoundedText(
|
|
2274
|
+
options.purpose ?? "checkpoint-operation",
|
|
2275
|
+
"purpose",
|
|
2276
|
+
128,
|
|
2277
|
+
);
|
|
2278
|
+
const timeoutMs = boundedWorkspaceLockDelay(
|
|
2279
|
+
options.timeoutMs,
|
|
2280
|
+
DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS,
|
|
2281
|
+
"timeoutMs",
|
|
2282
|
+
MAX_WORKSPACE_LOCK_TIMEOUT_MS,
|
|
2283
|
+
);
|
|
2284
|
+
const retryMs = boundedWorkspaceLockDelay(
|
|
2285
|
+
options.retryMs,
|
|
2286
|
+
DEFAULT_WORKSPACE_LOCK_RETRY_MS,
|
|
2287
|
+
"retryMs",
|
|
2288
|
+
MAX_WORKSPACE_LOCK_RETRY_MS,
|
|
2289
|
+
);
|
|
2290
|
+
const runtime = publicWorkspaceLockRuntime(options);
|
|
2291
|
+
const startedAt = workspaceLockClockValue(runtime.now);
|
|
2292
|
+
const deadline = startedAt + timeoutMs;
|
|
2293
|
+
const maxAttempts = Math.ceil(timeoutMs / retryMs) + 1;
|
|
2294
|
+
let attempts = 0;
|
|
2295
|
+
let lock = null;
|
|
2296
|
+
|
|
2297
|
+
while (!lock) {
|
|
2298
|
+
attempts += 1;
|
|
2299
|
+
const currentTime = workspaceLockClockValue(runtime.now);
|
|
2300
|
+
const remaining = Math.max(1, Math.ceil(deadline - currentTime));
|
|
2301
|
+
try {
|
|
2302
|
+
lock = acquireWorkspaceLifetimeLockOnce({
|
|
2303
|
+
lockDir: runtime.lockDir,
|
|
2304
|
+
workspaceRoot,
|
|
2305
|
+
operationId,
|
|
2306
|
+
purpose,
|
|
2307
|
+
isProcessAlive: runtime.isProcessAlive,
|
|
2308
|
+
now: runtime.now,
|
|
2309
|
+
ownerToken: runtime.ownerToken,
|
|
2310
|
+
coordinationTimeoutMs: Math.max(1, Math.min(retryMs, remaining)),
|
|
2311
|
+
coordinationNow: runtime.now,
|
|
2312
|
+
coordinationSleep: runtime.sleep,
|
|
2313
|
+
coordinationIsProcessAlive: runtime.isProcessAlive,
|
|
2314
|
+
coordinationOwnerToken: runtime.ownerToken,
|
|
2315
|
+
});
|
|
2316
|
+
} catch (error) {
|
|
2317
|
+
if (
|
|
2318
|
+
error?.code === "STATE_LOCK_UNAVAILABLE" &&
|
|
2319
|
+
error?.cause?.code === "STATE_LOCK_OWNER_CORRUPT"
|
|
2320
|
+
) {
|
|
2321
|
+
throw codedError(
|
|
2322
|
+
WORKSPACE_TRANSACTION_ERROR.LOCK_CORRUPT,
|
|
2323
|
+
"workspace lock coordination ownership is corrupt",
|
|
2324
|
+
{ workspaceRoot, operationId },
|
|
2325
|
+
error,
|
|
2326
|
+
);
|
|
2327
|
+
}
|
|
2328
|
+
if (!isLiveWorkspaceLockConflict(error)) throw error;
|
|
2329
|
+
const afterAttempt = workspaceLockClockValue(runtime.now);
|
|
2330
|
+
if (afterAttempt >= deadline || attempts >= maxAttempts) {
|
|
2331
|
+
throw codedError(
|
|
2332
|
+
WORKSPACE_TRANSACTION_ERROR.WORKSPACE_LOCK_TIMEOUT,
|
|
2333
|
+
`timed out waiting for the workspace lock: ${workspaceRoot}`,
|
|
2334
|
+
{
|
|
2335
|
+
workspaceRoot,
|
|
2336
|
+
operationId,
|
|
2337
|
+
purpose,
|
|
2338
|
+
timeoutMs,
|
|
2339
|
+
attempts,
|
|
2340
|
+
ownerWorkspaceRoot: error.ownerWorkspaceRoot || workspaceRoot,
|
|
2341
|
+
ownerOperationId:
|
|
2342
|
+
error.ownerTransactionId || error.transactionId || null,
|
|
2343
|
+
ownerPid: error.ownerPid || null,
|
|
2344
|
+
},
|
|
2345
|
+
error,
|
|
2346
|
+
);
|
|
2347
|
+
}
|
|
2348
|
+
runtime.sleep(
|
|
2349
|
+
Math.max(1, Math.min(retryMs, Math.ceil(deadline - afterAttempt))),
|
|
2350
|
+
);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
return runWorkspaceLockCallbackSync({
|
|
2355
|
+
lock,
|
|
2356
|
+
workspaceRoot,
|
|
2357
|
+
operationId,
|
|
2358
|
+
callback,
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
/**
|
|
2363
|
+
* Observe the exact checkpoint-restore owner for a known workspace and saga
|
|
2364
|
+
* operation. This never scans for another owner and never creates or reclaims
|
|
2365
|
+
* lock state; a later recovery takeover must still perform its own exact CAS.
|
|
2366
|
+
*/
|
|
2367
|
+
export function inspectWorkspaceLockOwnerSync(options = {}) {
|
|
2368
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
2369
|
+
throw codedError(
|
|
2370
|
+
WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
|
|
2371
|
+
"workspace lock inspection options must be an object",
|
|
2372
|
+
);
|
|
2373
|
+
}
|
|
2374
|
+
const workspaceRoot = assertCanonicalWorkspaceDirectory(
|
|
2375
|
+
options.workspaceRoot,
|
|
2376
|
+
);
|
|
2377
|
+
const operationId = normalizeId(options.operationId, "operationId");
|
|
2378
|
+
const purpose = normalizeCheckpointRestorePurpose(options.purpose);
|
|
2379
|
+
const runtime = publicWorkspaceLockRuntime(options);
|
|
2380
|
+
const lockDir = inspectWorkspaceLockRoot(runtime.lockDir, workspaceRoot);
|
|
2381
|
+
if (!lockDir) return null;
|
|
2382
|
+
const lockPath = path.join(lockDir, workspaceLockName(workspaceRoot));
|
|
2383
|
+
let entry;
|
|
2384
|
+
try {
|
|
2385
|
+
entry = fs.lstatSync(lockPath);
|
|
2386
|
+
} catch (cause) {
|
|
2387
|
+
if (cause?.code === "ENOENT") return null;
|
|
2388
|
+
throw codedError(
|
|
2389
|
+
WORKSPACE_TRANSACTION_ERROR.LOCK_CORRUPT,
|
|
2390
|
+
"workspace lock cannot be inspected safely",
|
|
2391
|
+
{ workspaceRoot, operationId },
|
|
2392
|
+
cause,
|
|
2393
|
+
);
|
|
2394
|
+
}
|
|
2395
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
2396
|
+
throw codedError(
|
|
2397
|
+
WORKSPACE_TRANSACTION_ERROR.LOCK_CORRUPT,
|
|
2398
|
+
"workspace lock must be a real directory",
|
|
2399
|
+
{ workspaceRoot, operationId, lockPath },
|
|
2400
|
+
);
|
|
2401
|
+
}
|
|
2402
|
+
assertOwnedPrivateDirectory(lockPath, "workspace lock", {
|
|
2403
|
+
repairPermissions: false,
|
|
2404
|
+
});
|
|
2405
|
+
const owner = readLockOwner(lockPath);
|
|
2406
|
+
if (
|
|
2407
|
+
!hasExactWorkspaceLockOwnerShape(owner) ||
|
|
2408
|
+
!samePath(owner.workspaceRoot, workspaceRoot)
|
|
2409
|
+
) {
|
|
2410
|
+
throw codedError(
|
|
2411
|
+
WORKSPACE_TRANSACTION_ERROR.LOCK_CORRUPT,
|
|
2412
|
+
"workspace lock has corrupt or non-canonical ownership",
|
|
2413
|
+
{ workspaceRoot, operationId },
|
|
2414
|
+
);
|
|
2415
|
+
}
|
|
2416
|
+
if (owner.transactionId !== operationId || owner.purpose !== purpose) {
|
|
2417
|
+
throw codedError(
|
|
2418
|
+
WORKSPACE_TRANSACTION_ERROR.RECOVERY_REQUIRED,
|
|
2419
|
+
"workspace is owned by a different durable operation",
|
|
2420
|
+
{
|
|
2421
|
+
workspaceRoot,
|
|
2422
|
+
operationId,
|
|
2423
|
+
purpose,
|
|
2424
|
+
ownerOperationId: owner.transactionId,
|
|
2425
|
+
ownerPurpose: owner.purpose,
|
|
2426
|
+
},
|
|
2427
|
+
);
|
|
2428
|
+
}
|
|
2429
|
+
return workspaceLockOwnerProjection(owner);
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
/**
|
|
2433
|
+
* Replace one exact dead checkpoint-restore owner and hold the replacement for
|
|
2434
|
+
* a synchronous recovery attempt. The saga operation id is deliberately
|
|
2435
|
+
* stable across takeover; an independent recovery-attempt id belongs in the
|
|
2436
|
+
* durable saga journal, not in the workspace lock identity.
|
|
2437
|
+
*/
|
|
2438
|
+
export function withWorkspaceRecoveryLockSync(options = {}, callback) {
|
|
2439
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
2440
|
+
throw codedError(
|
|
2441
|
+
WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
|
|
2442
|
+
"workspace recovery lock options must be an object",
|
|
2443
|
+
);
|
|
2444
|
+
}
|
|
2445
|
+
assertSynchronousWorkspaceLockCallback(callback);
|
|
2446
|
+
const workspaceRoot = assertCanonicalWorkspaceDirectory(
|
|
2447
|
+
options.workspaceRoot,
|
|
2448
|
+
);
|
|
2449
|
+
const operationId = normalizeId(options.operationId, "operationId");
|
|
2450
|
+
const purpose = normalizeCheckpointRestorePurpose(options.purpose);
|
|
2451
|
+
const expectedOwner = normalizeExpectedCheckpointRestoreOwner(
|
|
2452
|
+
options.expectedOwner,
|
|
2453
|
+
workspaceRoot,
|
|
2454
|
+
operationId,
|
|
2455
|
+
);
|
|
2456
|
+
const timeoutMs = boundedWorkspaceLockDelay(
|
|
2457
|
+
options.timeoutMs,
|
|
2458
|
+
DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS,
|
|
2459
|
+
"timeoutMs",
|
|
2460
|
+
MAX_WORKSPACE_LOCK_TIMEOUT_MS,
|
|
2461
|
+
);
|
|
2462
|
+
const retryMs = boundedWorkspaceLockDelay(
|
|
2463
|
+
options.retryMs,
|
|
2464
|
+
DEFAULT_WORKSPACE_LOCK_RETRY_MS,
|
|
2465
|
+
"retryMs",
|
|
2466
|
+
MAX_WORKSPACE_LOCK_RETRY_MS,
|
|
2467
|
+
);
|
|
2468
|
+
const runtime = publicWorkspaceLockRuntime(options);
|
|
2469
|
+
let lock;
|
|
2470
|
+
try {
|
|
2471
|
+
lock = acquireRecoveredWorkspaceLifetimeLock({
|
|
2472
|
+
lockDir: runtime.lockDir,
|
|
2473
|
+
workspaceRoot,
|
|
2474
|
+
operationId,
|
|
2475
|
+
expectedDeadOwner: expectedOwner,
|
|
2476
|
+
expectedPurpose: purpose,
|
|
2477
|
+
acquiredPurpose: purpose,
|
|
2478
|
+
requireExactOwnerShape: true,
|
|
2479
|
+
isProcessAlive: runtime.isProcessAlive,
|
|
2480
|
+
now: runtime.now,
|
|
2481
|
+
ownerToken: runtime.ownerToken,
|
|
2482
|
+
coordinationTimeoutMs: timeoutMs,
|
|
2483
|
+
coordinationRetryMs: retryMs,
|
|
2484
|
+
coordinationNow: runtime.now,
|
|
2485
|
+
coordinationSleep: runtime.sleep,
|
|
2486
|
+
coordinationIsProcessAlive: runtime.isProcessAlive,
|
|
2487
|
+
coordinationOwnerToken: runtime.ownerToken,
|
|
2488
|
+
});
|
|
2489
|
+
} catch (error) {
|
|
2490
|
+
if (error?.code !== "STATE_LOCK_UNAVAILABLE") throw error;
|
|
2491
|
+
if (error?.cause?.code === "STATE_LOCK_OWNER_CORRUPT") {
|
|
2492
|
+
throw codedError(
|
|
2493
|
+
WORKSPACE_TRANSACTION_ERROR.LOCK_CORRUPT,
|
|
2494
|
+
"workspace recovery coordination ownership is corrupt",
|
|
2495
|
+
{ workspaceRoot, operationId },
|
|
2496
|
+
error,
|
|
2497
|
+
);
|
|
2498
|
+
}
|
|
2499
|
+
throw codedError(
|
|
2500
|
+
WORKSPACE_TRANSACTION_ERROR.WORKSPACE_LOCK_TIMEOUT,
|
|
2501
|
+
`timed out waiting for workspace recovery coordination: ${workspaceRoot}`,
|
|
2502
|
+
{ workspaceRoot, operationId, purpose, timeoutMs },
|
|
2503
|
+
error,
|
|
2504
|
+
);
|
|
2505
|
+
}
|
|
2506
|
+
return runWorkspaceLockCallbackSync({
|
|
2507
|
+
lock,
|
|
2508
|
+
workspaceRoot,
|
|
2509
|
+
operationId,
|
|
2510
|
+
callback,
|
|
2511
|
+
priorOwner: expectedOwner,
|
|
2512
|
+
recoveryOfOperationId: operationId,
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
|
|
1585
2516
|
function safeCurrentEntry(root, relative, expectedRootIdentity) {
|
|
1586
2517
|
assertWorkspaceRootIdentity(root, expectedRootIdentity);
|
|
1587
2518
|
const native = nativePath(relative);
|
|
@@ -2779,69 +3710,44 @@ export class WorkspaceTransactionManager {
|
|
|
2779
3710
|
}
|
|
2780
3711
|
|
|
2781
3712
|
_preflightLockRoot(workspaceRoot = null) {
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
{
|
|
2792
|
-
workspaceRoot,
|
|
2793
|
-
lockDir: requestedLockDir,
|
|
2794
|
-
resolvedLockDir: projectedLockDir,
|
|
2795
|
-
},
|
|
2796
|
-
);
|
|
3713
|
+
if (!workspaceRoot) {
|
|
3714
|
+
const requestedLockDir = path.resolve(this.lockDir);
|
|
3715
|
+
return {
|
|
3716
|
+
requestedLockDir,
|
|
3717
|
+
projectedLockDir: canonicalPathThroughExistingAncestor(
|
|
3718
|
+
requestedLockDir,
|
|
3719
|
+
"lock directory",
|
|
3720
|
+
),
|
|
3721
|
+
};
|
|
2797
3722
|
}
|
|
2798
|
-
return
|
|
3723
|
+
return preflightWorkspaceLockRoot(this.lockDir, workspaceRoot);
|
|
2799
3724
|
}
|
|
2800
3725
|
|
|
2801
3726
|
_ensureLockRoot(workspaceRoot = null) {
|
|
2802
|
-
|
|
3727
|
+
if (workspaceRoot) {
|
|
3728
|
+
this.lockDir = ensureWorkspaceLockRoot(this.lockDir, workspaceRoot);
|
|
3729
|
+
return this.lockDir;
|
|
3730
|
+
}
|
|
3731
|
+
this._preflightLockRoot();
|
|
2803
3732
|
this.lockDir = assertSafeDirectory(this.lockDir, "lock directory", {
|
|
2804
3733
|
create: true,
|
|
2805
3734
|
});
|
|
2806
|
-
if (workspaceRoot && pathsOverlap(this.lockDir, workspaceRoot)) {
|
|
2807
|
-
throw codedError(
|
|
2808
|
-
WORKSPACE_TRANSACTION_ERROR.INVALID_PATH,
|
|
2809
|
-
"workspace transaction lock directory must be disjoint from the workspace",
|
|
2810
|
-
{ workspaceRoot, lockDir: this.lockDir },
|
|
2811
|
-
);
|
|
2812
|
-
}
|
|
2813
3735
|
assertOwnedPrivateDirectory(this.lockDir, "lock directory");
|
|
2814
3736
|
return this.lockDir;
|
|
2815
3737
|
}
|
|
2816
3738
|
|
|
2817
3739
|
_acquireWorkspaceLock(workspaceRoot, transactionId) {
|
|
2818
|
-
this._ensureLockRoot(workspaceRoot);
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
this.lockDir,
|
|
2830
|
-
workspaceRoot,
|
|
2831
|
-
transactionId,
|
|
2832
|
-
{
|
|
2833
|
-
isProcessAlive: this._isProcessAlive,
|
|
2834
|
-
now: this._now,
|
|
2835
|
-
ownerToken: this._ownerToken,
|
|
2836
|
-
},
|
|
2837
|
-
);
|
|
2838
|
-
},
|
|
2839
|
-
{
|
|
2840
|
-
failIfUnavailable: true,
|
|
2841
|
-
timeoutMs: 5_000,
|
|
2842
|
-
},
|
|
2843
|
-
);
|
|
2844
|
-
return lock;
|
|
3740
|
+
this.lockDir = this._ensureLockRoot(workspaceRoot);
|
|
3741
|
+
return acquireWorkspaceLifetimeLockOnce({
|
|
3742
|
+
lockDir: this.lockDir,
|
|
3743
|
+
workspaceRoot,
|
|
3744
|
+
operationId: transactionId,
|
|
3745
|
+
purpose: "workspace-transaction",
|
|
3746
|
+
isProcessAlive: this._isProcessAlive,
|
|
3747
|
+
now: this._now,
|
|
3748
|
+
ownerToken: this._ownerToken,
|
|
3749
|
+
coordinationTimeoutMs: DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS,
|
|
3750
|
+
});
|
|
2845
3751
|
}
|
|
2846
3752
|
|
|
2847
3753
|
_preflightStateRoot(workspaceRoot = null) {
|
|
@@ -3484,18 +4390,15 @@ export class WorkspaceTransactionManager {
|
|
|
3484
4390
|
}
|
|
3485
4391
|
let lock;
|
|
3486
4392
|
try {
|
|
3487
|
-
lock =
|
|
3488
|
-
this.lockDir,
|
|
3489
|
-
state.workspaceRoot,
|
|
3490
|
-
state.id,
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
expectedDeadOwner: deadOwner,
|
|
3497
|
-
},
|
|
3498
|
-
);
|
|
4393
|
+
lock = acquireRecoveredWorkspaceLifetimeLock({
|
|
4394
|
+
lockDir: this.lockDir,
|
|
4395
|
+
workspaceRoot: state.workspaceRoot,
|
|
4396
|
+
operationId: state.id,
|
|
4397
|
+
expectedDeadOwner: deadOwner,
|
|
4398
|
+
isProcessAlive: this._isProcessAlive,
|
|
4399
|
+
now: this._now,
|
|
4400
|
+
ownerToken: this._ownerToken,
|
|
4401
|
+
});
|
|
3499
4402
|
state.owner = deepClone(lock.owner);
|
|
3500
4403
|
state.updatedAt = new Date(this._now()).toISOString();
|
|
3501
4404
|
state.stateDigest = stateDigest(state);
|