chainlesschain 0.163.2 → 0.163.4
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 +3 -2
- package/scripts/command-lifecycle-report.mjs +330 -38
- package/src/assets/web-panel/assets/{AIOps-DnGsmsqJ.js → AIOps-DB5X7qqV.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CrO6fcQ7.js → ActionButton-DxHRtKLk.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DsBdOaP2.js → Analytics-DIH5QrxT.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DmQslKUB.js → AppLayout-uu0oxVz7.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-BvyrT_QQ.js → Artifacts-BNSMiW9Y.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-prndoLiC.js → Audit-DcAzB4_x.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-DOTFyA55.js → BackgroundAgents-ClSsvjIc.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-9zOnPp_8.js → Backup-BMciY0js.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-C4PC6sAG.js → BaseInput-Hn3b57Sh.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-BBAF-W9v.js → Chat-CoSlC53V.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CWhvvloO.js → ChatBubbleRenderer-CFevJ3So.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-BI9iculd.js → Checkbox-B4L4Eu4w.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-3DP9BCKb.js → Codegen-C77FgRxF.js} +1 -1
- package/src/assets/web-panel/assets/{Col-df-2TnJX.js → Col-BrLtJLH3.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CHs8gG_A.js → Community-Be7pXXhA.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CDfF6q7V.js → Compact-DuGPecjo.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CthrDnO7.js → Compliance-CzS66mjP.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-HjIqsWJw.js → Cowork-CEhjCoyK.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-BKc196fb.js → Cron-D7ir8Vja.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-CJnjdc8W.js → Crosschain-BcXmuLxf.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BHUy-8Q2.js → DID-Cto1dtBy.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-QDB7sKeY.js → Dashboard-B7KjgwOX.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-DStcVK11.js → Dropdown-COMqxijg.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-BwSUskMJ.js → EmailListRenderer-BZrya-nf.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-DgF_kurT.js → FamilyGuardDashboard-AmVeMq0J.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-k8wvv-wN.js → Federation-kZU8c-27.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-C11qoq2r.js → FormItemContext-sOV22v2-.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BIIpr9kg.js → GenericCardRenderer-BUuLtGCz.js} +1 -1
- package/src/assets/web-panel/assets/{Git-BlA1exTe.js → Git-K-ZxsLsH.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CYa1ZYM4.js → Governance-D3OEnawI.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-6_kKVIt6.js → Inference-Bll0gRmP.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-CB4AwxYy.js → KnowledgeGraph-DBXwjr53.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-xf7nHFAe.js → Logs-CEzPwEfP.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-Bf3DEqP6.js → MarkdownRenderer-CLmCbHx6.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-aPg8e4Jt.js → Marketplace-BYyRyOPz.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-C-TNlVUf.js → McpTools-DVu8w_rM.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-DMTPIpGa.js → Memory-tenuZ_hO.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BxyvveRq.js → MobileBridge-Bxgy4ozS.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-DyPCrAyQ.js → MobileProjects-Dgx1D6sc.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BdhqNXp6.js → Mtc-CRfJ8r8j.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-Cz-HFxzW.js → MtcAudit-BBJq4Aiz.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-CIUaFNsx.js → Multisig-B3ii9lai.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CWe6-J17.js → NLProgramming-FIi-Cubv.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-vYAKeviU.js → Notes-DIq8MPf1.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-DQL9-2d7.js → NotificationSettings-BFOD9eGd.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-DnXj1jBn.js → OrderTableRenderer--41u7vre.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-B3spU8AN.js → Organization-B_0AWztW.js} +2 -2
- package/src/assets/web-panel/assets/{Overflow-DgEwo7vm.js → Overflow-B6I7TuXv.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-hujMRH8X.js → P2P-Cdf6qens.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-C_qhdg9h.js → PdhVaultBrowser-B_0FcQJY.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-vp7LMcSf.js → Permissions-6nFBjsQp.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-Dxr5Hgnq.js → PersonalDataHub-nQYV7xpn.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-DEPZqqH4.js → Pipeline-CBVod5jm.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-DTpDiKPe.js → Privacy-D52jW1Nt.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-jDJsRWmF.js → ProjectInit-Bdx3BYhf.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-Dhor5RWH.js → ProjectSettings-b7xr4DUa.js} +2 -2
- package/src/assets/web-panel/assets/Projects-DhfifCKx.js +1 -0
- package/src/assets/web-panel/assets/Providers-Dy_PgDMr.js +1 -0
- package/src/assets/web-panel/assets/{QrScannerModal-CjIw-wIb.js → QrScannerModal-FQKSmc3e.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-DX6jcqoi.js → QuickAsk-CSTrhzcF.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DopXkhar.js → Recommend-CxMABOUh.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-BLvqJE9c.js → RemoteSession-Cv2T8yRa.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-DhRj2Me7.js → Reputation-CE1k4Vvu.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DQ53vuPq.js → Row-BNgNcAV_.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BslvwiBD.js → RssFeed-5ZiBZ3ph.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DJKBDNKZ.js → Search-mDOIF1UV.js} +1 -1
- package/src/assets/web-panel/assets/{Security-COhpxHER.js → Security-CtuCTC-e.js} +3 -3
- package/src/assets/web-panel/assets/{Services-dbulD6cI.js → Services-DxFO2Zg7.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-ciIVNfhC.js → Skeleton-CKmKkH9y.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-AiRTkTCk.js → Skills-Bmzl9qkk.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DDMcWYBc.js → Sla-9RqaWHOC.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-BPxWbpXO.js → SpeechSettings-CGdzFIYw.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-BoEOXCyg.js → SyncSettings-CvNuCeBT.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-B_sLdcnw.js +1 -0
- package/src/assets/web-panel/assets/{Templates-UowY8Ixe.js → Templates-D9_YijdK.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-DauSLQXN.js → Tenant-Cuc7BUSP.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-DBj7Jneq.js → Terminal-6VYRnJgd.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-UMrPwoZC.js → TimelineRenderer-GtK8xd8Y.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-4ea96-bn.js → Tokens-BZ94Lt5W.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger--ISCqt4b.js → Trigger-DEcZgDaX.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-D4GVimRJ.js → Trust-CC3z9THe.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-Ci-nXRQc.js → UkeySign-Cvtw8cV3.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-Cjk7mKPw.js → VideoEditing-CL4e6Bn3.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BgOLOO3Z.js → Wallet-ycEemP27.js} +2 -2
- package/src/assets/web-panel/assets/{WebAuthn-CCX0K29i.js → WebAuthn-DgZ4X_aw.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-Ch3zgIsE.js → WorkflowEditor-D0qMhSv7.js} +1 -1
- package/src/assets/web-panel/assets/{chat-DKOU_2Vb.js → chat-DsAg01Zn.js} +1 -1
- package/src/assets/web-panel/assets/{colors-6OVo4u3C.js → colors-B0oqAhrQ.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BH78Hlsq.js → compact-item-CmX0Rabz.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CFRpSbbC.js → createContext-Be4cn1Ub.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CP2qzG6D.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DNQhgcVi.js → hasIn-D5v12kVq.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkN3iTTp.js → index-0eWLkQKT.js} +1 -1
- package/src/assets/web-panel/assets/{index-CkNEMANR.js → index-301ogenv.js} +1 -1
- package/src/assets/web-panel/assets/{index-D3RtTT4K.js → index-5LGY0JbG.js} +1 -1
- package/src/assets/web-panel/assets/{index-dDkoL4Pe.js → index-7VKDn-t1.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHa4WyTZ.js → index-8WvYGnON.js} +1 -1
- package/src/assets/web-panel/assets/{index-CaYqY35Z.js → index-B1YGjOWC.js} +1 -1
- package/src/assets/web-panel/assets/{index-bKm3TUDp.js → index-B3mUHuVc.js} +1 -1
- package/src/assets/web-panel/assets/{index--rq90rL9.js → index-BA4oulaP.js} +1 -1
- package/src/assets/web-panel/assets/{index-DaDgdJYj.js → index-BBREnn1V.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_Z-YoxX.js → index-BBvyHguy.js} +1 -1
- package/src/assets/web-panel/assets/{index-CkqHgG-b.js → index-BKmsS2jj.js} +1 -1
- package/src/assets/web-panel/assets/{index-CrcLHN2F.js → index-BRIqLy1m.js} +1 -1
- package/src/assets/web-panel/assets/{index-BaVE6E8y.js → index-BXGSbelg.js} +3 -3
- package/src/assets/web-panel/assets/{index-BYrnm5Ob.js → index-BaE0Ui07.js} +1 -1
- package/src/assets/web-panel/assets/{index-BDSR7ecX.js → index-BoAIAZ5C.js} +1 -1
- package/src/assets/web-panel/assets/{index-CNuCHHQT.js → index-BqltWojQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Eag8e_mA.js → index-CGiIyTZw.js} +1 -1
- package/src/assets/web-panel/assets/index-CKB0vbJU.js +1 -0
- package/src/assets/web-panel/assets/{index-BepBTpa8.js → index-CZ2wUEgr.js} +1 -1
- package/src/assets/web-panel/assets/{index-HNGGVF6o.js → index-CZpfzcuy.js} +1 -1
- package/src/assets/web-panel/assets/{index-AuBhLqOm.js → index-Ckrk3pIV.js} +1 -1
- package/src/assets/web-panel/assets/{index-7TC3S8SN.js → index-CqoCZ5vR.js} +1 -1
- package/src/assets/web-panel/assets/{index-CFPJ8yzF.js → index-CyWBir7N.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bb6kDrUx.js → index-CyyGTMKd.js} +1 -1
- package/src/assets/web-panel/assets/{index-DKN2noIF.js → index-D0BSkxYQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Du0rnksC.js → index-D5YVopyr.js} +1 -1
- package/src/assets/web-panel/assets/{index-BxRy5LPR.js → index-DKUBj36_.js} +1 -1
- package/src/assets/web-panel/assets/{index-sm7W8AXy.js → index-DM724NgU.js} +1 -1
- package/src/assets/web-panel/assets/{index-BujsOZfc.js → index-DQRsQen0.js} +1 -1
- package/src/assets/web-panel/assets/index-DgoXmwqB.js +1 -0
- package/src/assets/web-panel/assets/{index-Y9MJv28e.js → index-DikJ0Qqe.js} +1 -1
- package/src/assets/web-panel/assets/{index-caye7AOE.js → index-Dn2dSdXx.js} +1 -1
- package/src/assets/web-panel/assets/{index-cdf7fd7d.js → index-DyUaFWEk.js} +1 -1
- package/src/assets/web-panel/assets/{index-BnnXQot_.js → index-Hfe_dEGx.js} +1 -1
- package/src/assets/web-panel/assets/{index-DvHGtNnF.js → index-_JjRLSD0.js} +1 -1
- package/src/assets/web-panel/assets/{index-nbfHl1oV.js → index-frSojpHj.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cu3L4r5l.js → index-jK3ax6Z6.js} +1 -1
- package/src/assets/web-panel/assets/{index-D84id2Y4.js → index-qMuZ-NRR.js} +1 -1
- package/src/assets/web-panel/assets/{index-hPY0fFrd.js → index-q_fQ94SE.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BZ9w6jPL.js → initDefaultProps-D4VdhJ9_.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BnwUiOho.js → motion-CZJA0MQW.js} +1 -1
- package/src/assets/web-panel/assets/{move-ByUKJnNm.js → move-BqEs7Sb1.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-V7yK1KEl.js → mtc-parser-BpHEtsu1.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BEXBBt4j.js → omit-BW94tY9W.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-qDm_46Gi.js → pickAttrs-BnBjPWP9.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BIbhEo_0.js → placementArrow-uUkjQ_hu.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-NbPbDgA_.js → responsiveObserve-B-klUqXK.js} +1 -1
- package/src/assets/web-panel/assets/{slide-BnUWglZ0.js → slide-CvY1cNAs.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BX8_wZI5.js → statusUtils-Ix5sEDT2.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BV4OycQX.js → styleChecker-C8qcXOne.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-Od_H5llY.js → useFlexGapSupport-KNF6Kj8f.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-BWI-PyiX.js → useFs-DGJr345n.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BqMuhG3g.js → usePersonalDataHub-BboN99rI.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DNd23z6f.js → vnode-Bgt8Vgte.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-Cjaxnm7l.js → zoom-BhJtErgt.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +2 -2
- package/src/commands/agenda.js +6 -5
- package/src/commands/agent.js +237 -75
- package/src/commands/agents.js +6 -0
- package/src/commands/artifacts.js +98 -0
- package/src/commands/background-session.js +11 -1
- package/src/commands/mcp.js +218 -31
- package/src/commands/review.js +221 -22
- package/src/commands/session-mcp-recovery.js +5 -5
- package/src/data/changelog.json +27 -1
- package/src/gateways/ws/ws-agent-handler.js +492 -14
- package/src/harness/jsonl-session-store.js +39 -1
- package/src/harness/mcp-client.js +414 -71
- package/src/harness/prompt-compressor.js +24 -0
- package/src/harness/provider-backed-compaction.js +205 -0
- package/src/harness/worktree-isolator.js +26 -3
- package/src/lib/agent-worktree.js +41 -2
- package/src/lib/app-preview.js +3 -0
- package/src/lib/background-agent-supervisor.js +790 -186
- package/src/lib/background-command-argv.js +318 -0
- package/src/lib/background-launch-profile.js +1 -0
- package/src/lib/background-phase-reporter.js +22 -2
- package/src/lib/background-worktree-policy.js +109 -0
- package/src/lib/command-lifecycle-report.js +1340 -145
- package/src/lib/cowork-mcp-tools.js +51 -6
- package/src/lib/cowork-task-runner.js +1 -0
- package/src/lib/delivery-coordinator.js +30 -5
- package/src/lib/delivery-production-adapter.js +1777 -0
- package/src/lib/delivery-production-runner.js +240 -0
- package/src/lib/durable-security-store.js +29 -17
- package/src/lib/execution-trace/trace-context.cjs +179 -0
- package/src/lib/execution-trace/trace-context.js +8 -175
- package/src/lib/mcp-call-ledger-store.js +58 -6
- package/src/lib/mcp-headers-helper.js +76 -8
- package/src/lib/mcp-host-recovery-runtime.js +2 -0
- package/src/lib/mcp-ledger-recovery-admission.js +39 -3
- package/src/lib/mcp-recovery-adjudication.js +256 -19
- package/src/lib/mcp-sandbox-policy.js +184 -0
- package/src/lib/mcp-stdio-capsule-builder-worker.cjs +495 -0
- package/src/lib/mcp-stdio-executable-identity.js +33 -2
- package/src/lib/mcp-stdio-execution-authority.js +163 -0
- package/src/lib/mcp-stdio-immutable-vfs-resolver.cjs +830 -0
- package/src/lib/mcp-stdio-package-materialization.js +721 -333
- package/src/lib/mcp-stdio-workspace-authority.js +252 -0
- package/src/lib/observability/index.js +2 -0
- package/src/lib/observability/otlp-exporter.js +2 -2
- package/src/lib/packer/pack-update-applier.js +8 -1
- package/src/lib/packer/pkg-config-generator.js +34 -0
- package/src/lib/plugin-runtime/sandbox-policy.js +42 -2
- package/src/lib/process-execution-broker/index.js +213 -109
- package/src/lib/process-execution-broker/platform-sandbox.js +1024 -169
- package/src/lib/process-tree-termination.js +17 -1
- package/src/lib/scheduler-kernel/contract.js +315 -0
- package/src/lib/scheduler-kernel/store.js +1345 -0
- package/src/lib/session-host-lease.js +403 -30
- package/src/lib/session-persistence-failure.js +4 -1
- package/src/lib/skill-mcp.js +65 -4
- package/src/lib/sub-agent-context.js +8 -0
- package/src/lib/web-fetch.js +4 -4
- package/src/repl/agent-repl.js +285 -38
- package/src/runtime/agent-core.js +1044 -90
- package/src/runtime/agent-runtime.js +61 -8
- package/src/runtime/headless-runner.js +260 -166
- package/src/runtime/headless-stream.js +254 -25
- package/src/runtime/mcp-config.js +235 -28
- package/src/workers/background-agent-worker.js +492 -71
- package/src/assets/web-panel/assets/Projects-08xUDZGt.js +0 -1
- package/src/assets/web-panel/assets/Providers-BGs47s1a.js +0 -1
- package/src/assets/web-panel/assets/Tasks-CEYFJwmr.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DEMblAIm.js +0 -1
- package/src/assets/web-panel/assets/index-BTvq-36D.js +0 -1
- package/src/assets/web-panel/assets/index-hUpvk6Jl.js +0 -1
|
@@ -15,6 +15,7 @@ import { randomBytes } from "node:crypto";
|
|
|
15
15
|
import { join, resolve } from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { getHomeDir } from "./paths.js";
|
|
18
|
+
import { withFileLock } from "./with-file-lock.js";
|
|
18
19
|
import { deriveSessionState } from "./session-lifecycle.js";
|
|
19
20
|
import {
|
|
20
21
|
finishAgentWorktree,
|
|
@@ -22,6 +23,11 @@ import {
|
|
|
22
23
|
} from "./agent-worktree.js";
|
|
23
24
|
import { rejectPendingBackgroundInteractions } from "./background-interaction-journal.js";
|
|
24
25
|
import executionBroker from "./process-execution-broker/index.js";
|
|
26
|
+
import {
|
|
27
|
+
agentPrintArgument,
|
|
28
|
+
stripFirstTurnPromptArgv,
|
|
29
|
+
} from "./background-command-argv.js";
|
|
30
|
+
import { terminateOwnedProcessTree } from "./process-tree-termination.js";
|
|
25
31
|
import {
|
|
26
32
|
assessBackgroundLaunchProfileCompatibility,
|
|
27
33
|
buildArgvFromBackgroundLaunchProfile,
|
|
@@ -209,6 +215,7 @@ export const _deps = {
|
|
|
209
215
|
kill: (pid, signal) => process.kill(pid, signal),
|
|
210
216
|
readProcessStartTimeMs: defaultReadProcessStartTimeMs,
|
|
211
217
|
killProcessTree: defaultKillProcessTree,
|
|
218
|
+
terminateOwnedProcessTree,
|
|
212
219
|
};
|
|
213
220
|
|
|
214
221
|
/**
|
|
@@ -316,8 +323,27 @@ export function logPath(id) {
|
|
|
316
323
|
|
|
317
324
|
const TERMINAL_STATUSES = new Set(["completed", "failed", "stopped", "lost"]);
|
|
318
325
|
|
|
319
|
-
|
|
320
|
-
const target = statePath(
|
|
326
|
+
function readBackgroundAgentStateResult(id) {
|
|
327
|
+
const target = statePath(id);
|
|
328
|
+
if (!existsSync(target)) return { state: null, missing: true, error: null };
|
|
329
|
+
try {
|
|
330
|
+
const state = JSON.parse(readFileSync(target, "utf8"));
|
|
331
|
+
if (!state || typeof state !== "object" || state.id !== id) {
|
|
332
|
+
throw new Error(`State record does not belong to ${id}`);
|
|
333
|
+
}
|
|
334
|
+
return { state, missing: false, error: null };
|
|
335
|
+
} catch (cause) {
|
|
336
|
+
if (cause?.code === "ENOENT") {
|
|
337
|
+
return { state: null, missing: true, error: null };
|
|
338
|
+
}
|
|
339
|
+
const error = new Error(`Corrupt background agent state: ${id}`, { cause });
|
|
340
|
+
error.code = "BACKGROUND_AGENT_STATE_CORRUPT";
|
|
341
|
+
return { state: null, missing: false, error };
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function mergeBackgroundAgentState(current, requested) {
|
|
346
|
+
let next = requested;
|
|
321
347
|
// Field-aware merge against the freshest on-disk state. The state file has
|
|
322
348
|
// multiple concurrent read-modify-write writers (launcher, worker heartbeat/
|
|
323
349
|
// turn/finalize, rename/pin/stop from other processes) with last-writer-wins
|
|
@@ -332,10 +358,8 @@ export function writeBackgroundAgentState(state) {
|
|
|
332
358
|
// The read happens immediately before the atomic rename, shrinking the
|
|
333
359
|
// clobber window from "any caller's RMW span" to microseconds; rename/pin
|
|
334
360
|
// additionally verify-and-retry on top of this.
|
|
335
|
-
let next = state;
|
|
336
|
-
const current = readBackgroundAgentState(state.id);
|
|
337
361
|
if (current) {
|
|
338
|
-
if (TERMINAL_STATUSES.has(current.status)
|
|
362
|
+
if (TERMINAL_STATUSES.has(current.status)) {
|
|
339
363
|
next = {
|
|
340
364
|
...next,
|
|
341
365
|
status: current.status,
|
|
@@ -384,7 +408,163 @@ export function writeBackgroundAgentState(state) {
|
|
|
384
408
|
configFingerprint: current.configFingerprint,
|
|
385
409
|
};
|
|
386
410
|
}
|
|
411
|
+
// Core same-id identity is immutable even for non-worktree sessions.
|
|
412
|
+
// Stale launcher/phase/finalize writers may update lifecycle fields, but
|
|
413
|
+
// never retarget a conversation, cwd or log record.
|
|
414
|
+
next = {
|
|
415
|
+
...next,
|
|
416
|
+
...(current.sessionId ? { sessionId: current.sessionId } : {}),
|
|
417
|
+
...(current.cwd ? { cwd: current.cwd } : {}),
|
|
418
|
+
...(current.startedAt !== undefined
|
|
419
|
+
? { startedAt: current.startedAt }
|
|
420
|
+
: {}),
|
|
421
|
+
...(current.logFile ? { logFile: current.logFile } : {}),
|
|
422
|
+
};
|
|
423
|
+
// The launch generation is immutable same-id identity. It lets the real
|
|
424
|
+
// worker replace a platform wrapper pid without allowing a stale/different
|
|
425
|
+
// worker to claim this record.
|
|
426
|
+
if (current.workerGeneration) {
|
|
427
|
+
next = { ...next, workerGeneration: current.workerGeneration };
|
|
428
|
+
}
|
|
429
|
+
const claimedWorkerPid = Number(current.workerClaimedPid);
|
|
430
|
+
if (
|
|
431
|
+
current.workerClaimedPid != null &&
|
|
432
|
+
Number.isInteger(claimedWorkerPid) &&
|
|
433
|
+
claimedWorkerPid > 0
|
|
434
|
+
) {
|
|
435
|
+
next = {
|
|
436
|
+
...next,
|
|
437
|
+
workerClaimedPid: claimedWorkerPid,
|
|
438
|
+
workerClaimedAt: current.workerClaimedAt,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
// A turn launch uses a durable prepare/resolve record. The prepare must
|
|
442
|
+
// reach disk before native spawn so stop/remove can distinguish "no turn"
|
|
443
|
+
// from "a child may exist but its pid commit failed". Only the owner that
|
|
444
|
+
// knows the matching token/attempt may resolve it; stale snapshots cannot
|
|
445
|
+
// drop an unresolved intent or resurrect an already-resolved one.
|
|
446
|
+
const currentTurnAttempt = Math.max(
|
|
447
|
+
0,
|
|
448
|
+
Number.isInteger(Number(current.turnLaunchAttempt))
|
|
449
|
+
? Number(current.turnLaunchAttempt)
|
|
450
|
+
: 0,
|
|
451
|
+
);
|
|
452
|
+
const requestedTurnAttempt = Number(next.turnLaunchAttempt);
|
|
453
|
+
const currentTurnIntent = current.turnLaunchIntent;
|
|
454
|
+
const requestedTurnIntent = next.turnLaunchIntent;
|
|
455
|
+
const requestedTurnResolution = next.turnLaunchResolution;
|
|
456
|
+
const resolvesCurrentTurnIntent =
|
|
457
|
+
currentTurnIntent &&
|
|
458
|
+
requestedTurnIntent === null &&
|
|
459
|
+
requestedTurnResolution?.token === currentTurnIntent.token &&
|
|
460
|
+
Number(requestedTurnResolution?.attempt) ===
|
|
461
|
+
Number(currentTurnIntent.attempt) &&
|
|
462
|
+
["spawned", "not-spawned", "terminated"].includes(
|
|
463
|
+
requestedTurnResolution?.outcome,
|
|
464
|
+
);
|
|
465
|
+
const currentTurnResolution = current.turnLaunchResolution;
|
|
466
|
+
const advancesSpawnedTurnResolution =
|
|
467
|
+
!currentTurnIntent &&
|
|
468
|
+
currentTurnResolution?.outcome === "spawned" &&
|
|
469
|
+
requestedTurnResolution?.outcome === "terminated" &&
|
|
470
|
+
requestedTurnResolution.token === currentTurnResolution.token &&
|
|
471
|
+
Number(requestedTurnResolution.attempt) ===
|
|
472
|
+
Number(currentTurnResolution.attempt);
|
|
473
|
+
const startsNewTurnIntent =
|
|
474
|
+
!currentTurnIntent &&
|
|
475
|
+
requestedTurnIntent &&
|
|
476
|
+
Number.isInteger(Number(requestedTurnIntent.attempt)) &&
|
|
477
|
+
Number(requestedTurnIntent.attempt) > currentTurnAttempt &&
|
|
478
|
+
Number(requestedTurnAttempt) === Number(requestedTurnIntent.attempt);
|
|
479
|
+
if (currentTurnIntent && !resolvesCurrentTurnIntent) {
|
|
480
|
+
next = {
|
|
481
|
+
...next,
|
|
482
|
+
turnLaunchAttempt: currentTurnAttempt,
|
|
483
|
+
turnLaunchIntent: currentTurnIntent,
|
|
484
|
+
...(current.turnLaunchResolution !== undefined
|
|
485
|
+
? { turnLaunchResolution: current.turnLaunchResolution }
|
|
486
|
+
: {}),
|
|
487
|
+
};
|
|
488
|
+
} else if (
|
|
489
|
+
!currentTurnIntent &&
|
|
490
|
+
!startsNewTurnIntent &&
|
|
491
|
+
!advancesSpawnedTurnResolution
|
|
492
|
+
) {
|
|
493
|
+
next = {
|
|
494
|
+
...next,
|
|
495
|
+
turnLaunchAttempt: currentTurnAttempt,
|
|
496
|
+
turnLaunchIntent: null,
|
|
497
|
+
...(current.turnLaunchResolution !== undefined
|
|
498
|
+
? { turnLaunchResolution: current.turnLaunchResolution }
|
|
499
|
+
: {}),
|
|
500
|
+
};
|
|
501
|
+
} else if (advancesSpawnedTurnResolution) {
|
|
502
|
+
next = {
|
|
503
|
+
...next,
|
|
504
|
+
turnLaunchAttempt: currentTurnAttempt,
|
|
505
|
+
turnLaunchIntent: null,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
const turnTerminationConfirmed =
|
|
509
|
+
next.turnLaunchFinalizationUncertain === false &&
|
|
510
|
+
next.turnLaunchTermination?.confirmed === true;
|
|
511
|
+
const currentTurnTerminationConfirmed =
|
|
512
|
+
current.turnLaunchFinalizationUncertain === false &&
|
|
513
|
+
current.turnLaunchTermination?.confirmed === true;
|
|
514
|
+
if (currentTurnTerminationConfirmed) {
|
|
515
|
+
// Confirmation is absorbing for this terminal same-id run. A stopper or
|
|
516
|
+
// heartbeat carrying an older snapshot must never restore uncertainty
|
|
517
|
+
// after the owned process tree has been proven gone.
|
|
518
|
+
next = {
|
|
519
|
+
...next,
|
|
520
|
+
turnLaunchFinalizationUncertain: false,
|
|
521
|
+
turnLaunchIntent: null,
|
|
522
|
+
turnLaunchToken: null,
|
|
523
|
+
turnLaunchError: null,
|
|
524
|
+
turnLaunchTermination: current.turnLaunchTermination,
|
|
525
|
+
...(current.turnLaunchResolution !== undefined
|
|
526
|
+
? { turnLaunchResolution: current.turnLaunchResolution }
|
|
527
|
+
: {}),
|
|
528
|
+
};
|
|
529
|
+
} else if (
|
|
530
|
+
current.turnLaunchFinalizationUncertain === true &&
|
|
531
|
+
!turnTerminationConfirmed
|
|
532
|
+
) {
|
|
533
|
+
next = {
|
|
534
|
+
...next,
|
|
535
|
+
agentPid: current.agentPid,
|
|
536
|
+
agentStartedAt: current.agentStartedAt,
|
|
537
|
+
turnLaunchFinalizationUncertain: true,
|
|
538
|
+
turnLaunchError: current.turnLaunchError,
|
|
539
|
+
...(current.turnLaunchToken
|
|
540
|
+
? { turnLaunchToken: current.turnLaunchToken }
|
|
541
|
+
: {}),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
if (current.stopRequestedAt) {
|
|
545
|
+
next = {
|
|
546
|
+
...next,
|
|
547
|
+
stopRequestedAt: current.stopRequestedAt,
|
|
548
|
+
...(current.stopRequestedBy !== undefined
|
|
549
|
+
? { stopRequestedBy: current.stopRequestedBy }
|
|
550
|
+
: {}),
|
|
551
|
+
};
|
|
552
|
+
if (next.status === "completed" || next.status === "failed") {
|
|
553
|
+
next = {
|
|
554
|
+
...next,
|
|
555
|
+
status: "stopped",
|
|
556
|
+
stoppedByUser: true,
|
|
557
|
+
endedAt: next.endedAt || Date.now(),
|
|
558
|
+
phase: null,
|
|
559
|
+
transport: null,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
}
|
|
387
563
|
}
|
|
564
|
+
return next;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function persistBackgroundAgentState(target, next) {
|
|
388
568
|
// Multiple async state writers can run in one process; a pid-only temporary
|
|
389
569
|
// name lets them overwrite each other's staging file. A unique suffix keeps
|
|
390
570
|
// each atomic replacement independent. Windows scanners/readers can also
|
|
@@ -411,14 +591,130 @@ export function writeBackgroundAgentState(state) {
|
|
|
411
591
|
return next;
|
|
412
592
|
}
|
|
413
593
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
594
|
+
/**
|
|
595
|
+
* Strict cross-process read/mutate/write transaction for one supervisor state.
|
|
596
|
+
* Returning null from `updater` leaves the record untouched. Missing and
|
|
597
|
+
* corrupt records fail closed unless an explicit creator opts into missing.
|
|
598
|
+
*/
|
|
599
|
+
export function mutateBackgroundAgentState(id, updater, options = {}) {
|
|
600
|
+
const safe = safeId(id);
|
|
601
|
+
const target = statePath(safe);
|
|
602
|
+
return withFileLock(
|
|
603
|
+
target,
|
|
604
|
+
() => {
|
|
605
|
+
const read = readBackgroundAgentStateResult(safe);
|
|
606
|
+
if (read.error) throw read.error;
|
|
607
|
+
if (read.missing && options.createIfMissing !== true) {
|
|
608
|
+
return { applied: false, state: null, previous: null };
|
|
609
|
+
}
|
|
610
|
+
const requested = updater(read.state);
|
|
611
|
+
if (!requested) {
|
|
612
|
+
return { applied: false, state: read.state, previous: read.state };
|
|
613
|
+
}
|
|
614
|
+
const next = mergeBackgroundAgentState(read.state, {
|
|
615
|
+
...requested,
|
|
616
|
+
id: safe,
|
|
617
|
+
});
|
|
618
|
+
persistBackgroundAgentState(target, next);
|
|
619
|
+
return { applied: true, state: next, previous: read.state };
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
failIfUnavailable: true,
|
|
623
|
+
timeoutMs: options.timeoutMs ?? 5_000,
|
|
624
|
+
},
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
export function writeBackgroundAgentState(state, options = {}) {
|
|
629
|
+
return mutateBackgroundAgentState(state.id, () => state, {
|
|
630
|
+
createIfMissing: options.createIfMissing === true,
|
|
631
|
+
}).state;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Atomically claim/refresh a worker heartbeat. The returned `applied` bit is
|
|
636
|
+
* the only authority to start or continue a turn: a terminal, deleted,
|
|
637
|
+
* corrupt, differently-generated or already-owned record is never revived.
|
|
638
|
+
*/
|
|
639
|
+
export function claimBackgroundAgentHeartbeat(id, heartbeat = {}) {
|
|
640
|
+
const workerPid = Number(heartbeat.workerPid ?? heartbeat.pid);
|
|
641
|
+
const workerGeneration = heartbeat.workerGeneration;
|
|
642
|
+
if (!Number.isInteger(workerPid) || workerPid <= 0) {
|
|
643
|
+
throw new Error(`Invalid background worker pid: ${heartbeat.workerPid}`);
|
|
421
644
|
}
|
|
645
|
+
return mutateBackgroundAgentState(id, (current) => {
|
|
646
|
+
if (!current || current.status !== "running") return null;
|
|
647
|
+
if (current.stopRequestedAt) return null;
|
|
648
|
+
if (
|
|
649
|
+
current.workerGeneration &&
|
|
650
|
+
current.workerGeneration !== workerGeneration
|
|
651
|
+
) {
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
const claimedWorkerPid = Number(current.workerClaimedPid);
|
|
655
|
+
if (
|
|
656
|
+
current.workerClaimedPid != null &&
|
|
657
|
+
Number.isInteger(claimedWorkerPid) &&
|
|
658
|
+
claimedWorkerPid > 0 &&
|
|
659
|
+
claimedWorkerPid !== workerPid
|
|
660
|
+
) {
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
const currentWorkerPid = Number(current.workerPid ?? current.pid);
|
|
664
|
+
if (
|
|
665
|
+
!current.workerGeneration &&
|
|
666
|
+
Number.isInteger(currentWorkerPid) &&
|
|
667
|
+
currentWorkerPid > 0 &&
|
|
668
|
+
currentWorkerPid !== workerPid &&
|
|
669
|
+
current.launchFinalizationUncertain !== true
|
|
670
|
+
) {
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
...current,
|
|
675
|
+
...heartbeat,
|
|
676
|
+
id,
|
|
677
|
+
pid: workerPid,
|
|
678
|
+
workerPid,
|
|
679
|
+
workerClaimedPid:
|
|
680
|
+
current.workerClaimedPid != null &&
|
|
681
|
+
Number.isInteger(claimedWorkerPid) &&
|
|
682
|
+
claimedWorkerPid > 0
|
|
683
|
+
? claimedWorkerPid
|
|
684
|
+
: workerPid,
|
|
685
|
+
workerClaimedAt: current.workerClaimedAt || Date.now(),
|
|
686
|
+
status: "running",
|
|
687
|
+
heartbeatAt: heartbeat.heartbeatAt ?? Date.now(),
|
|
688
|
+
};
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function deleteBackgroundAgentState(id, predicate = () => true) {
|
|
693
|
+
const safe = safeId(id);
|
|
694
|
+
const target = statePath(safe);
|
|
695
|
+
return withFileLock(
|
|
696
|
+
target,
|
|
697
|
+
() => {
|
|
698
|
+
const read = readBackgroundAgentStateResult(safe);
|
|
699
|
+
if (read.error) throw read.error;
|
|
700
|
+
if (!read.state) return null;
|
|
701
|
+
if (!predicate(read.state)) {
|
|
702
|
+
const error = new Error(
|
|
703
|
+
`Background agent state changed before removal: ${safe}`,
|
|
704
|
+
);
|
|
705
|
+
error.code = "BACKGROUND_AGENT_REMOVE_CONFLICT";
|
|
706
|
+
throw error;
|
|
707
|
+
}
|
|
708
|
+
rmSync(target, { force: true });
|
|
709
|
+
return read.state;
|
|
710
|
+
},
|
|
711
|
+
{ failIfUnavailable: true, timeoutMs: 5_000 },
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
export function readBackgroundAgentState(id) {
|
|
716
|
+
const result = readBackgroundAgentStateResult(safeId(id));
|
|
717
|
+
return result.error ? null : result.state;
|
|
422
718
|
}
|
|
423
719
|
|
|
424
720
|
export function isProcessAlive(pid) {
|
|
@@ -507,21 +803,30 @@ function withLifecycleState(state) {
|
|
|
507
803
|
return { ...state, lifecycleState: deriveSessionState(state) };
|
|
508
804
|
}
|
|
509
805
|
|
|
510
|
-
|
|
806
|
+
function deriveEffectiveBackgroundAgentState(state, options = {}) {
|
|
511
807
|
if (!state) return null;
|
|
512
808
|
if (state.status !== "running") return state;
|
|
513
809
|
const t = nowMs(options);
|
|
810
|
+
const staleAfterMs = heartbeatStaleMs(options);
|
|
811
|
+
const heartbeatAt = Number(state.heartbeatAt);
|
|
812
|
+
const launchBootstrapPending =
|
|
813
|
+
state.launchFinalizationUncertain === true &&
|
|
814
|
+
Number.isFinite(heartbeatAt) &&
|
|
815
|
+
t - heartbeatAt <= staleAfterMs;
|
|
514
816
|
let next = state;
|
|
515
|
-
if (
|
|
516
|
-
Number.isFinite(Number(state.heartbeatAt)) &&
|
|
517
|
-
t - Number(state.heartbeatAt) > heartbeatStaleMs(options)
|
|
518
|
-
) {
|
|
817
|
+
if (Number.isFinite(heartbeatAt) && t - heartbeatAt > staleAfterMs) {
|
|
519
818
|
next = {
|
|
520
819
|
...state,
|
|
521
820
|
status: "lost",
|
|
522
821
|
endedAt: state.endedAt || t,
|
|
523
822
|
lostReason: "heartbeat-stale",
|
|
524
823
|
};
|
|
824
|
+
} else if (launchBootstrapPending) {
|
|
825
|
+
// The initial state is intentionally visible before spawn. Until the
|
|
826
|
+
// launcher or worker publishes a concrete pid, a concurrent list/view
|
|
827
|
+
// must not convert that bounded bootstrap window into a terminal `lost`
|
|
828
|
+
// record which the worker can no longer claim. The ordinary heartbeat
|
|
829
|
+
// stale deadline still closes the window above.
|
|
525
830
|
} else if (!isProcessAlive(state.pid)) {
|
|
526
831
|
next = {
|
|
527
832
|
...state,
|
|
@@ -539,42 +844,81 @@ export function effectiveBackgroundAgentState(state, options = {}) {
|
|
|
539
844
|
lostReason: "pid-reused",
|
|
540
845
|
};
|
|
541
846
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
847
|
+
return next;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
export function effectiveBackgroundAgentState(state, options = {}) {
|
|
851
|
+
if (!state) return null;
|
|
852
|
+
if (state.status !== "running") return state;
|
|
853
|
+
// OS identity probes can take seconds on Windows. Perform them outside the
|
|
854
|
+
// state lock, then use the exact observed identity/heartbeat as a CAS fence
|
|
855
|
+
// inside the short persistence transaction.
|
|
856
|
+
const derived = deriveEffectiveBackgroundAgentState(state, options);
|
|
857
|
+
if (options.persist === false || derived === state) return derived;
|
|
858
|
+
|
|
859
|
+
const mutation = mutateBackgroundAgentState(state.id, (current) => {
|
|
554
860
|
if (
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
861
|
+
!current ||
|
|
862
|
+
current.status !== "running" ||
|
|
863
|
+
current.heartbeatAt !== state.heartbeatAt ||
|
|
864
|
+
current.pid !== state.pid ||
|
|
865
|
+
current.workerPid !== state.workerPid ||
|
|
866
|
+
current.startedAt !== state.startedAt ||
|
|
867
|
+
current.workerGeneration !== state.workerGeneration ||
|
|
868
|
+
current.launchFinalizationUncertain !==
|
|
869
|
+
state.launchFinalizationUncertain ||
|
|
870
|
+
current.turnLaunchAttempt !== state.turnLaunchAttempt ||
|
|
871
|
+
current.turnLaunchIntent?.token !== state.turnLaunchIntent?.token ||
|
|
872
|
+
current.turnLaunchFinalizationUncertain !==
|
|
873
|
+
state.turnLaunchFinalizationUncertain ||
|
|
874
|
+
current.stopRequestedAt !== state.stopRequestedAt
|
|
558
875
|
) {
|
|
559
|
-
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
return {
|
|
879
|
+
...current,
|
|
880
|
+
status: derived.status,
|
|
881
|
+
endedAt: derived.endedAt,
|
|
882
|
+
lostReason: derived.lostReason,
|
|
883
|
+
};
|
|
884
|
+
});
|
|
885
|
+
if (!mutation.applied) return mutation.state;
|
|
886
|
+
|
|
887
|
+
const previous = mutation.previous;
|
|
888
|
+
let next = mutation.state;
|
|
889
|
+
// Gap 2: the worker is gone (dead pid or pid-reused stranger) — reap the
|
|
890
|
+
// recorded agent grandchild so a crashed worker never leaves it running
|
|
891
|
+
// unsupervised. A merely-stale heartbeat with a live, verified worker still
|
|
892
|
+
// owns its child, so isSameProcess gates the reclaim.
|
|
893
|
+
const workerGone = !isSameProcess(previous?.pid, previous?.startedAt);
|
|
894
|
+
if (workerGone) {
|
|
895
|
+
try {
|
|
896
|
+
reclaimOrphanAgentProcess(previous);
|
|
897
|
+
} catch {
|
|
898
|
+
/* best-effort */
|
|
899
|
+
}
|
|
900
|
+
if (previous?.sessionId) {
|
|
901
|
+
const t = nowMs(options);
|
|
902
|
+
const stateBeforeRecovery = next;
|
|
903
|
+
const pending = previous.pendingQuestion;
|
|
560
904
|
const requestId = pending?.requestId || pending?.intId;
|
|
561
905
|
try {
|
|
562
906
|
const recovery = rejectPendingBackgroundInteractions(
|
|
563
|
-
|
|
564
|
-
|
|
907
|
+
previous.sessionId,
|
|
908
|
+
previous.id,
|
|
565
909
|
{
|
|
566
910
|
fallbackRequest: requestId
|
|
567
911
|
? {
|
|
568
912
|
requestId,
|
|
569
|
-
binding: pending
|
|
913
|
+
binding: pending?.binding,
|
|
570
914
|
payload: {
|
|
571
915
|
kind: "question",
|
|
572
|
-
question: pending
|
|
573
|
-
options: pending
|
|
574
|
-
multiSelect: pending
|
|
575
|
-
timeoutMs: pending
|
|
916
|
+
question: pending?.question || pending?.prompt || "",
|
|
917
|
+
options: pending?.options || null,
|
|
918
|
+
multiSelect: pending?.multiSelect === true,
|
|
919
|
+
timeoutMs: pending?.timeoutMs,
|
|
576
920
|
},
|
|
577
|
-
createdAt: pending
|
|
921
|
+
createdAt: pending?.askedAt,
|
|
578
922
|
}
|
|
579
923
|
: null,
|
|
580
924
|
code: "INTERACTION_WORKER_LOST",
|
|
@@ -611,12 +955,30 @@ export function effectiveBackgroundAgentState(state, options = {}) {
|
|
|
611
955
|
},
|
|
612
956
|
};
|
|
613
957
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
958
|
+
if (next !== stateBeforeRecovery) {
|
|
959
|
+
try {
|
|
960
|
+
const recoveryMutation = mutateBackgroundAgentState(
|
|
961
|
+
previous.id,
|
|
962
|
+
(current) => {
|
|
963
|
+
if (
|
|
964
|
+
!current ||
|
|
965
|
+
current.status !== stateBeforeRecovery.status ||
|
|
966
|
+
current.lostReason !== stateBeforeRecovery.lostReason
|
|
967
|
+
) {
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
return {
|
|
971
|
+
...current,
|
|
972
|
+
phase: next.phase,
|
|
973
|
+
pendingQuestion: next.pendingQuestion,
|
|
974
|
+
interactionRecovery: next.interactionRecovery,
|
|
975
|
+
};
|
|
976
|
+
},
|
|
977
|
+
);
|
|
978
|
+
next = recoveryMutation.state || next;
|
|
979
|
+
} catch {
|
|
980
|
+
/* best-effort; the terminal transition itself is already durable */
|
|
981
|
+
}
|
|
620
982
|
}
|
|
621
983
|
}
|
|
622
984
|
}
|
|
@@ -658,23 +1020,11 @@ export function renameBackgroundAgent(id, title, options = {}) {
|
|
|
658
1020
|
if (!state) throw new Error(`Background agent not found: ${id}`);
|
|
659
1021
|
const normalized = normalizeBackgroundAgentTitle(title);
|
|
660
1022
|
const renamedAt = typeof options.now === "number" ? options.now : Date.now();
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
let next = { ...state, title: normalized, renamedAt };
|
|
667
|
-
for (let attempt = 0; attempt < 4; attempt++) {
|
|
668
|
-
writeBackgroundAgentState(next);
|
|
669
|
-
sleepSyncMs(15);
|
|
670
|
-
const current = readBackgroundAgentState(id) || next;
|
|
671
|
-
if (current.title === normalized) {
|
|
672
|
-
return { ...current, title: normalized, renamedAt };
|
|
673
|
-
}
|
|
674
|
-
next = { ...current, title: normalized, renamedAt };
|
|
675
|
-
}
|
|
676
|
-
writeBackgroundAgentState(next);
|
|
677
|
-
return next;
|
|
1023
|
+
const mutation = mutateBackgroundAgentState(id, (current) =>
|
|
1024
|
+
current ? { ...current, title: normalized, renamedAt } : null,
|
|
1025
|
+
);
|
|
1026
|
+
if (!mutation.applied) throw new Error(`Background agent not found: ${id}`);
|
|
1027
|
+
return mutation.state;
|
|
678
1028
|
}
|
|
679
1029
|
|
|
680
1030
|
/**
|
|
@@ -710,7 +1060,16 @@ export function removeBackgroundAgent(id, options = {}) {
|
|
|
710
1060
|
const worktree = cleanupBackgroundAgentWorktree(state, {
|
|
711
1061
|
keepWorktree: options.keepWorktree === true,
|
|
712
1062
|
});
|
|
713
|
-
|
|
1063
|
+
const deleted = deleteBackgroundAgentState(
|
|
1064
|
+
id,
|
|
1065
|
+
(current) =>
|
|
1066
|
+
current.status !== "running" &&
|
|
1067
|
+
current.launchFinalizationUncertain !== true &&
|
|
1068
|
+
!current.turnLaunchIntent &&
|
|
1069
|
+
current.turnLaunchFinalizationUncertain !== true &&
|
|
1070
|
+
activeWorktreeProcesses(current).length === 0,
|
|
1071
|
+
);
|
|
1072
|
+
if (!deleted) throw new Error(`Background agent not found: ${id}`);
|
|
714
1073
|
if (options.keepLog !== true) {
|
|
715
1074
|
rmSync(logPath(id), { force: true });
|
|
716
1075
|
}
|
|
@@ -736,18 +1095,11 @@ export function setBackgroundAgentPinned(id, pinned, options = {}) {
|
|
|
736
1095
|
if (!state) throw new Error(`Background agent not found: ${id}`);
|
|
737
1096
|
const value = pinned === true;
|
|
738
1097
|
const pinnedAt = typeof options.now === "number" ? options.now : Date.now();
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
if (current.pinned === value) {
|
|
745
|
-
return { ...current, pinned: value, pinnedAt };
|
|
746
|
-
}
|
|
747
|
-
next = { ...current, pinned: value, pinnedAt };
|
|
748
|
-
}
|
|
749
|
-
writeBackgroundAgentState(next);
|
|
750
|
-
return next;
|
|
1098
|
+
const mutation = mutateBackgroundAgentState(id, (current) =>
|
|
1099
|
+
current ? { ...current, pinned: value, pinnedAt } : null,
|
|
1100
|
+
);
|
|
1101
|
+
if (!mutation.applied) throw new Error(`Background agent not found: ${id}`);
|
|
1102
|
+
return mutation.state;
|
|
751
1103
|
}
|
|
752
1104
|
|
|
753
1105
|
/**
|
|
@@ -763,8 +1115,26 @@ export function setBackgroundAgentPinned(id, pinned, options = {}) {
|
|
|
763
1115
|
* @param {string[]} [opts.positionalTokens] raw positional prompt tokens
|
|
764
1116
|
* @param {string|null} [opts.printValue] inline `-p <value>` text, when the
|
|
765
1117
|
* prompt came from --print
|
|
1118
|
+
* @param {Array<object>} [opts.optionSpecs] captured Commander option grammar
|
|
1119
|
+
* @param {string[]} [opts.commandNames] command name and aliases
|
|
766
1120
|
*/
|
|
1121
|
+
export function insertArgumentsBeforeOptionTerminator(argv, additions) {
|
|
1122
|
+
const out = [...(argv || [])];
|
|
1123
|
+
const index = out.indexOf("--");
|
|
1124
|
+
out.splice(index === -1 ? out.length : index, 0, ...(additions || []));
|
|
1125
|
+
return out;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
767
1128
|
export function buildFollowUpArgv(argv, opts = {}) {
|
|
1129
|
+
if (Array.isArray(opts.optionSpecs) && opts.optionSpecs.length > 0) {
|
|
1130
|
+
return stripFirstTurnPromptArgv(argv, {
|
|
1131
|
+
optionSpecs: opts.optionSpecs,
|
|
1132
|
+
commandNames: opts.commandNames,
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
// Compatibility fallback for stored/direct callers created before option
|
|
1136
|
+
// grammar was persisted by the agent command. New launches always supply
|
|
1137
|
+
// optionSpecs and therefore cannot confuse option values with operands.
|
|
768
1138
|
const positionalLeft = Array.isArray(opts.positionalTokens)
|
|
769
1139
|
? [...opts.positionalTokens]
|
|
770
1140
|
: [];
|
|
@@ -1024,7 +1394,9 @@ export function resumeBackgroundAgent(id, prompt, options = {}) {
|
|
|
1024
1394
|
state.sessionId,
|
|
1025
1395
|
]
|
|
1026
1396
|
: ["agent", "--session", state.sessionId];
|
|
1027
|
-
const argv =
|
|
1397
|
+
const argv = insertArgumentsBeforeOptionTerminator(followUpArgv, [
|
|
1398
|
+
agentPrintArgument(text),
|
|
1399
|
+
]);
|
|
1028
1400
|
return launchBackgroundAgent({
|
|
1029
1401
|
argv,
|
|
1030
1402
|
// Worktree sessions fail closed: never fall back to process.cwd(), which
|
|
@@ -1076,6 +1448,90 @@ function assertUsableCwd(cwd) {
|
|
|
1076
1448
|
}
|
|
1077
1449
|
}
|
|
1078
1450
|
|
|
1451
|
+
/**
|
|
1452
|
+
* A detached worker exists, so the caller must transfer/retain any worktree
|
|
1453
|
+
* even though launcher finalization failed. `instanceof` keeps this ownership
|
|
1454
|
+
* signal typed instead of trusting a forgeable string/code from user input.
|
|
1455
|
+
*/
|
|
1456
|
+
export class BackgroundWorkerStartedError extends Error {
|
|
1457
|
+
constructor(error, { id, workerPid } = {}) {
|
|
1458
|
+
super(
|
|
1459
|
+
`Background worker ${id || "unknown"} started, but launcher finalization failed: ${error?.message || String(error)}`,
|
|
1460
|
+
{ cause: error },
|
|
1461
|
+
);
|
|
1462
|
+
this.name = "BackgroundWorkerStartedError";
|
|
1463
|
+
this.code = "ERR_BACKGROUND_WORKER_STARTED";
|
|
1464
|
+
this.backgroundAgentId = id || null;
|
|
1465
|
+
this.workerPid = Number.isInteger(Number(workerPid))
|
|
1466
|
+
? Number(workerPid)
|
|
1467
|
+
: null;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
export function isBackgroundWorkerStartedError(error) {
|
|
1472
|
+
return error instanceof BackgroundWorkerStartedError;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
async function settleUncertainBackgroundLaunch({
|
|
1476
|
+
id,
|
|
1477
|
+
jobFile,
|
|
1478
|
+
spawnedProcess,
|
|
1479
|
+
error,
|
|
1480
|
+
}) {
|
|
1481
|
+
let termination;
|
|
1482
|
+
try {
|
|
1483
|
+
termination = await _deps.terminateOwnedProcessTree(spawnedProcess, {
|
|
1484
|
+
treeMode: process.platform === "win32" ? "windows-tree" : "posix-group",
|
|
1485
|
+
});
|
|
1486
|
+
} catch {
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
if (termination?.confirmed !== true) return;
|
|
1490
|
+
try {
|
|
1491
|
+
const workerPid = Number(spawnedProcess?.pid);
|
|
1492
|
+
const current = readBackgroundAgentState(id);
|
|
1493
|
+
if (!current || current.launchFinalizationUncertain !== true) return;
|
|
1494
|
+
if (
|
|
1495
|
+
Number.isInteger(workerPid) &&
|
|
1496
|
+
workerPid > 0 &&
|
|
1497
|
+
current.workerPid != null &&
|
|
1498
|
+
Number(current.workerPid) !== workerPid
|
|
1499
|
+
) {
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
const terminal = current.status === "running" ? "failed" : current.status;
|
|
1503
|
+
writeBackgroundAgentState({
|
|
1504
|
+
...current,
|
|
1505
|
+
status: terminal,
|
|
1506
|
+
endedAt: current.endedAt || Date.now(),
|
|
1507
|
+
exitCode: current.exitCode ?? 1,
|
|
1508
|
+
...(current.status === "running"
|
|
1509
|
+
? {
|
|
1510
|
+
error: `background launch failed after spawn: ${
|
|
1511
|
+
error?.message || "unknown post-spawn failure"
|
|
1512
|
+
}`,
|
|
1513
|
+
}
|
|
1514
|
+
: {}),
|
|
1515
|
+
phase: null,
|
|
1516
|
+
transport: null,
|
|
1517
|
+
launchFinalizationUncertain: false,
|
|
1518
|
+
launchTermination: {
|
|
1519
|
+
confirmed: true,
|
|
1520
|
+
treeMode: termination.treeMode || null,
|
|
1521
|
+
closed: termination.closed === true,
|
|
1522
|
+
treeTerminated: termination.treeTerminated === true,
|
|
1523
|
+
settledAt: Date.now(),
|
|
1524
|
+
},
|
|
1525
|
+
});
|
|
1526
|
+
// State is the durable cleanup authority; remove the one-shot job only
|
|
1527
|
+
// after the terminal/ownership evidence is safely persisted.
|
|
1528
|
+
rmSync(jobFile, { force: true });
|
|
1529
|
+
} catch {
|
|
1530
|
+
// Persistence failure must retain the original uncertainty fence and must
|
|
1531
|
+
// never become an unhandled rejection in the launching CLI process.
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1079
1535
|
export function launchBackgroundAgent({
|
|
1080
1536
|
argv,
|
|
1081
1537
|
cwd,
|
|
@@ -1118,12 +1574,14 @@ export function launchBackgroundAgent({
|
|
|
1118
1574
|
const configFingerprint =
|
|
1119
1575
|
fingerprintBackgroundLaunchProfile(launchProfileState);
|
|
1120
1576
|
const dir = backgroundAgentsDir();
|
|
1577
|
+
const workerGeneration = randomBytes(16).toString("hex");
|
|
1121
1578
|
const jobFile = join(dir, `${id}.job.${process.pid}.json`);
|
|
1122
1579
|
const worker = fileURLToPath(
|
|
1123
1580
|
new URL("../workers/background-agent-worker.js", import.meta.url),
|
|
1124
1581
|
);
|
|
1125
1582
|
const job = {
|
|
1126
1583
|
id,
|
|
1584
|
+
workerGeneration,
|
|
1127
1585
|
argv: initialSecrets.argv,
|
|
1128
1586
|
cwd,
|
|
1129
1587
|
sessionId,
|
|
@@ -1144,11 +1602,17 @@ export function launchBackgroundAgent({
|
|
|
1144
1602
|
// by a late launcher write racing the worker.
|
|
1145
1603
|
const state = {
|
|
1146
1604
|
id,
|
|
1605
|
+
workerGeneration,
|
|
1147
1606
|
sessionId,
|
|
1148
1607
|
title: job.title,
|
|
1149
1608
|
cwd,
|
|
1150
1609
|
pid: null,
|
|
1151
1610
|
workerPid: null,
|
|
1611
|
+
workerClaimedPid: null,
|
|
1612
|
+
workerClaimedAt: null,
|
|
1613
|
+
turnLaunchAttempt: 0,
|
|
1614
|
+
turnLaunchIntent: null,
|
|
1615
|
+
turnLaunchResolution: null,
|
|
1152
1616
|
agentPid: null,
|
|
1153
1617
|
agentStartedAt: null,
|
|
1154
1618
|
status: "running",
|
|
@@ -1161,10 +1625,20 @@ export function launchBackgroundAgent({
|
|
|
1161
1625
|
governance: governanceState,
|
|
1162
1626
|
launchProfile: launchProfileState,
|
|
1163
1627
|
configFingerprint,
|
|
1628
|
+
// Cleared only after the launcher (or worker heartbeat) has published a
|
|
1629
|
+
// concrete worker identity. Cleanup treats true as an ownership fence.
|
|
1630
|
+
launchFinalizationUncertain: true,
|
|
1164
1631
|
};
|
|
1165
|
-
|
|
1632
|
+
try {
|
|
1633
|
+
writeBackgroundAgentState(state, { createIfMissing: true });
|
|
1634
|
+
} catch (error) {
|
|
1635
|
+
rmSync(jobFile, { force: true });
|
|
1636
|
+
rmSync(statePath(id), { force: true });
|
|
1637
|
+
throw error;
|
|
1638
|
+
}
|
|
1166
1639
|
let child;
|
|
1167
1640
|
let workerLogFd;
|
|
1641
|
+
let postSpawnError = null;
|
|
1168
1642
|
try {
|
|
1169
1643
|
// Preserve loader/bootstrap failures that happen before the worker can
|
|
1170
1644
|
// open its own append handle. This also gives detached Windows targets
|
|
@@ -1185,73 +1659,123 @@ export function launchBackgroundAgent({
|
|
|
1185
1659
|
shell: false,
|
|
1186
1660
|
});
|
|
1187
1661
|
} catch (error) {
|
|
1662
|
+
if (error?.spawnedProcess) {
|
|
1663
|
+
// The broker can fail a post-spawn sandbox handshake only after native
|
|
1664
|
+
// spawn returned. It requests termination, but this synchronous caller
|
|
1665
|
+
// cannot prove the close fence has settled; retain state/job/worktree so
|
|
1666
|
+
// cleanup never races a process that may still hold cwd.
|
|
1667
|
+
const workerPid = Number(error.spawnedProcess.pid);
|
|
1668
|
+
try {
|
|
1669
|
+
const current = readBackgroundAgentState(id) || state;
|
|
1670
|
+
writeBackgroundAgentState({
|
|
1671
|
+
...current,
|
|
1672
|
+
pid: Number.isInteger(workerPid) && workerPid > 0 ? workerPid : null,
|
|
1673
|
+
workerPid:
|
|
1674
|
+
Number.isInteger(workerPid) && workerPid > 0 ? workerPid : null,
|
|
1675
|
+
launchFinalizationUncertain: true,
|
|
1676
|
+
});
|
|
1677
|
+
} catch {
|
|
1678
|
+
// The initial state already carries the uncertainty fence. Failure to
|
|
1679
|
+
// add a pid must retain that fail-closed marker.
|
|
1680
|
+
}
|
|
1681
|
+
void settleUncertainBackgroundLaunch({
|
|
1682
|
+
id,
|
|
1683
|
+
jobFile,
|
|
1684
|
+
spawnedProcess: error.spawnedProcess,
|
|
1685
|
+
error,
|
|
1686
|
+
});
|
|
1687
|
+
throw new BackgroundWorkerStartedError(error, {
|
|
1688
|
+
id,
|
|
1689
|
+
workerPid,
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1188
1692
|
rmSync(jobFile, { force: true });
|
|
1189
1693
|
rmSync(statePath(id), { force: true });
|
|
1190
1694
|
throw error;
|
|
1191
1695
|
} finally {
|
|
1192
1696
|
if (Number.isInteger(workerLogFd)) {
|
|
1193
|
-
|
|
1697
|
+
try {
|
|
1698
|
+
closeSync(workerLogFd);
|
|
1699
|
+
} catch (error) {
|
|
1700
|
+
// Once spawn returned a child, even fd-finalization errors are
|
|
1701
|
+
// post-transfer: the worker may already be using the worktree.
|
|
1702
|
+
if (child) postSpawnError = error;
|
|
1703
|
+
}
|
|
1194
1704
|
}
|
|
1195
1705
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1706
|
+
let withPid = null;
|
|
1707
|
+
try {
|
|
1708
|
+
// Async spawn failures (EPERM, cwd raced away between the check and the
|
|
1709
|
+
// spawn, …) arrive as an 'error' event on the detached child. Reap them
|
|
1710
|
+
// into the state file instead of leaving a phantom "running" session and
|
|
1711
|
+
// an uncaught exception.
|
|
1712
|
+
if (typeof child.on === "function") {
|
|
1713
|
+
child.on("error", (error) => {
|
|
1714
|
+
try {
|
|
1715
|
+
rmSync(jobFile, { force: true });
|
|
1716
|
+
const current = readBackgroundAgentState(id);
|
|
1717
|
+
if (current && current.status === "running") {
|
|
1718
|
+
writeBackgroundAgentState({
|
|
1719
|
+
...current,
|
|
1720
|
+
status: "failed",
|
|
1721
|
+
endedAt: Date.now(),
|
|
1722
|
+
lostReason: `spawn-error: ${error.code || error.message}`,
|
|
1723
|
+
launchFinalizationUncertain: false,
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
} catch {
|
|
1727
|
+
/* best-effort */
|
|
1212
1728
|
}
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1729
|
+
});
|
|
1730
|
+
child.on("exit", (code, signal) => {
|
|
1731
|
+
try {
|
|
1732
|
+
rmSync(jobFile, { force: true });
|
|
1733
|
+
const current = readBackgroundAgentState(id);
|
|
1734
|
+
// A healthy worker persists completed/failed before exiting. If the
|
|
1735
|
+
// process (or its platform wrapper) exits while the state still says
|
|
1736
|
+
// running, bootstrap or finalization failed and the session must not
|
|
1737
|
+
// remain a phantom live task until heartbeat reconciliation.
|
|
1738
|
+
if (current && current.status === "running") {
|
|
1739
|
+
writeBackgroundAgentState({
|
|
1740
|
+
...current,
|
|
1741
|
+
status: "failed",
|
|
1742
|
+
endedAt: Date.now(),
|
|
1743
|
+
exitCode: Number.isInteger(code) ? code : null,
|
|
1744
|
+
signal: signal || null,
|
|
1745
|
+
lostReason: `worker-exited-before-finalize: exit=${
|
|
1746
|
+
Number.isInteger(code) ? code : "null"
|
|
1747
|
+
} signal=${signal || "none"}`,
|
|
1748
|
+
launchFinalizationUncertain: false,
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
} catch {
|
|
1752
|
+
/* best-effort */
|
|
1236
1753
|
}
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
const current = readBackgroundAgentState(id) || state;
|
|
1757
|
+
withPid = {
|
|
1758
|
+
...current,
|
|
1759
|
+
// A platform wrapper (notably the Windows Job Object adapter) can return
|
|
1760
|
+
// from spawn after the real worker has already claimed the state file.
|
|
1761
|
+
// Preserve that authoritative process.pid instead of racing it back to
|
|
1762
|
+
// the wrapper pid. With an ordinary direct spawn both fields are still
|
|
1763
|
+
// null here and are initialized from child.pid as before.
|
|
1764
|
+
pid: current.pid ?? child.pid,
|
|
1765
|
+
workerPid: current.workerPid ?? child.pid,
|
|
1766
|
+
launchFinalizationUncertain: false,
|
|
1767
|
+
};
|
|
1768
|
+
writeBackgroundAgentState(withPid);
|
|
1769
|
+
child.unref();
|
|
1770
|
+
} catch (error) {
|
|
1771
|
+
postSpawnError ||= error;
|
|
1772
|
+
}
|
|
1773
|
+
if (postSpawnError) {
|
|
1774
|
+
throw new BackgroundWorkerStartedError(postSpawnError, {
|
|
1775
|
+
id,
|
|
1776
|
+
workerPid: child?.pid,
|
|
1240
1777
|
});
|
|
1241
1778
|
}
|
|
1242
|
-
child.unref();
|
|
1243
|
-
const current = readBackgroundAgentState(id) || state;
|
|
1244
|
-
const withPid = {
|
|
1245
|
-
...current,
|
|
1246
|
-
// A platform wrapper (notably the Windows Job Object adapter) can return
|
|
1247
|
-
// from spawn after the real worker has already claimed the state file.
|
|
1248
|
-
// Preserve that authoritative process.pid instead of racing it back to
|
|
1249
|
-
// the wrapper pid. With an ordinary direct spawn both fields are still
|
|
1250
|
-
// null here and are initialized from child.pid as before.
|
|
1251
|
-
pid: current.pid ?? child.pid,
|
|
1252
|
-
workerPid: current.workerPid ?? child.pid,
|
|
1253
|
-
};
|
|
1254
|
-
writeBackgroundAgentState(withPid);
|
|
1255
1779
|
return withPid;
|
|
1256
1780
|
}
|
|
1257
1781
|
|
|
@@ -1265,7 +1789,7 @@ export function readBackgroundAgentLog(id, options = {}) {
|
|
|
1265
1789
|
}
|
|
1266
1790
|
|
|
1267
1791
|
export function stopBackgroundAgent(id) {
|
|
1268
|
-
|
|
1792
|
+
let state = effectiveBackgroundAgentState(readBackgroundAgentState(id));
|
|
1269
1793
|
if (!state) throw new Error(`Background agent not found: ${id}`);
|
|
1270
1794
|
if (state.status !== "running") {
|
|
1271
1795
|
// Gap 2: stopping an already-lost session still reaps a leaked agent
|
|
@@ -1279,6 +1803,40 @@ export function stopBackgroundAgent(id) {
|
|
|
1279
1803
|
}
|
|
1280
1804
|
return { ...state, stopped: false };
|
|
1281
1805
|
}
|
|
1806
|
+
const stopRequestedAt = Date.now();
|
|
1807
|
+
const fence = mutateBackgroundAgentState(id, (current) => {
|
|
1808
|
+
if (!current || current.status !== "running") return null;
|
|
1809
|
+
return {
|
|
1810
|
+
...current,
|
|
1811
|
+
stopRequestedAt: current.stopRequestedAt || stopRequestedAt,
|
|
1812
|
+
stopRequestedBy: "user",
|
|
1813
|
+
phase: "stopping",
|
|
1814
|
+
transport: null,
|
|
1815
|
+
};
|
|
1816
|
+
});
|
|
1817
|
+
if (!fence.applied) {
|
|
1818
|
+
if (!fence.state) throw new Error(`Background agent not found: ${id}`);
|
|
1819
|
+
return { ...fence.state, stopped: false };
|
|
1820
|
+
}
|
|
1821
|
+
state = fence.state;
|
|
1822
|
+
if (
|
|
1823
|
+
state.turnLaunchIntent ||
|
|
1824
|
+
state.turnLaunchFinalizationUncertain === true
|
|
1825
|
+
) {
|
|
1826
|
+
// The worker has durably announced that native spawn is prepared or that
|
|
1827
|
+
// post-spawn pid persistence is unresolved. Killing the worker from this
|
|
1828
|
+
// snapshot could strand its detached child before ownership is published.
|
|
1829
|
+
// The stop fence prevents any new spawn; the live owner must now resolve
|
|
1830
|
+
// no-spawn or terminate the exact owned tree and persist confirmation.
|
|
1831
|
+
return {
|
|
1832
|
+
...state,
|
|
1833
|
+
stopped: false,
|
|
1834
|
+
stopPending: true,
|
|
1835
|
+
stopPendingReason: state.turnLaunchIntent
|
|
1836
|
+
? "turn-launch-intent"
|
|
1837
|
+
: "turn-launch-finalization",
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1282
1840
|
// Self-pid guard: a worker record whose pid is the CURRENT process is
|
|
1283
1841
|
// impossible in legitimate operation (the stopper is never the worker) —
|
|
1284
1842
|
// it means a corrupt/hand-edited state file. Signalling it would SIGTERM /
|
|
@@ -1291,8 +1849,8 @@ export function stopBackgroundAgent(id) {
|
|
|
1291
1849
|
endedAt: Date.now(),
|
|
1292
1850
|
lostReason: "self-pid-corrupt-record",
|
|
1293
1851
|
};
|
|
1294
|
-
writeBackgroundAgentState(lost);
|
|
1295
|
-
return { ...
|
|
1852
|
+
const written = writeBackgroundAgentState(lost) || lost;
|
|
1853
|
+
return { ...written, stopped: false };
|
|
1296
1854
|
}
|
|
1297
1855
|
// Gap 1: last-instant identity re-check before the kill — never
|
|
1298
1856
|
// taskkill/SIGTERM a pid the OS has re-assigned to an unrelated process.
|
|
@@ -1308,64 +1866,101 @@ export function stopBackgroundAgent(id) {
|
|
|
1308
1866
|
endedAt: Date.now(),
|
|
1309
1867
|
lostReason: "pid-reused",
|
|
1310
1868
|
};
|
|
1311
|
-
writeBackgroundAgentState(lost);
|
|
1312
|
-
return { ...
|
|
1869
|
+
const written = writeBackgroundAgentState(lost) || lost;
|
|
1870
|
+
return { ...written, stopped: false };
|
|
1313
1871
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
if (killed.error || (killed.status !== 0 && isProcessAlive(state.pid))) {
|
|
1325
|
-
throw new Error(
|
|
1326
|
-
`Failed to stop background agent ${id}: ${killed.error?.message || killed.stderr || `taskkill exited ${killed.status}`}`,
|
|
1872
|
+
try {
|
|
1873
|
+
if (process.platform === "win32") {
|
|
1874
|
+
const killed = runSupervisorCommand(
|
|
1875
|
+
"taskkill",
|
|
1876
|
+
["/PID", String(state.pid), "/T", "/F"],
|
|
1877
|
+
{
|
|
1878
|
+
windowsHide: true,
|
|
1879
|
+
encoding: "utf8",
|
|
1880
|
+
},
|
|
1881
|
+
"background-agent:stop-tree",
|
|
1327
1882
|
);
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
}
|
|
1335
|
-
// The worker detaches the agent child into its OWN process group (so a
|
|
1336
|
-
// crashed worker's child can be group-killed) — which also means the
|
|
1337
|
-
// worker-group SIGTERM above no longer reaches it. Kill the agent group
|
|
1338
|
-
// explicitly, identity-guarded via its agentStartedAt anchor.
|
|
1339
|
-
const agentPid = Number(state.agentPid);
|
|
1340
|
-
const anchor = Number(state.agentStartedAt);
|
|
1341
|
-
if (
|
|
1342
|
-
Number.isInteger(agentPid) &&
|
|
1343
|
-
agentPid > 0 &&
|
|
1344
|
-
agentPid !== Number(state.pid) &&
|
|
1345
|
-
agentPid !== process.pid &&
|
|
1346
|
-
Number.isFinite(anchor) &&
|
|
1347
|
-
anchor > 0 &&
|
|
1348
|
-
isSameProcess(agentPid, anchor)
|
|
1349
|
-
) {
|
|
1883
|
+
if (killed.error || (killed.status !== 0 && isProcessAlive(state.pid))) {
|
|
1884
|
+
throw new Error(
|
|
1885
|
+
`Failed to stop background agent ${id}: ${killed.error?.message || killed.stderr || `taskkill exited ${killed.status}`}`,
|
|
1886
|
+
);
|
|
1887
|
+
}
|
|
1888
|
+
} else {
|
|
1350
1889
|
try {
|
|
1351
|
-
_deps.kill(-
|
|
1890
|
+
_deps.kill(-Number(state.pid), "SIGTERM");
|
|
1352
1891
|
} catch {
|
|
1892
|
+
_deps.kill(Number(state.pid), "SIGTERM");
|
|
1893
|
+
}
|
|
1894
|
+
// The worker detaches the agent child into its OWN process group (so a
|
|
1895
|
+
// crashed worker's child can be group-killed) — which also means the
|
|
1896
|
+
// worker-group SIGTERM above no longer reaches it. Kill the agent group
|
|
1897
|
+
// explicitly, identity-guarded via its agentStartedAt anchor.
|
|
1898
|
+
const agentPid = Number(state.agentPid);
|
|
1899
|
+
const anchor = Number(state.agentStartedAt);
|
|
1900
|
+
if (
|
|
1901
|
+
Number.isInteger(agentPid) &&
|
|
1902
|
+
agentPid > 0 &&
|
|
1903
|
+
agentPid !== Number(state.pid) &&
|
|
1904
|
+
agentPid !== process.pid &&
|
|
1905
|
+
Number.isFinite(anchor) &&
|
|
1906
|
+
anchor > 0 &&
|
|
1907
|
+
isSameProcess(agentPid, anchor)
|
|
1908
|
+
) {
|
|
1353
1909
|
try {
|
|
1354
|
-
_deps.kill(agentPid, "SIGTERM");
|
|
1910
|
+
_deps.kill(-agentPid, "SIGTERM");
|
|
1355
1911
|
} catch {
|
|
1356
|
-
|
|
1912
|
+
try {
|
|
1913
|
+
_deps.kill(agentPid, "SIGTERM");
|
|
1914
|
+
} catch {
|
|
1915
|
+
/* already gone */
|
|
1916
|
+
}
|
|
1357
1917
|
}
|
|
1358
1918
|
}
|
|
1359
1919
|
}
|
|
1920
|
+
} catch (error) {
|
|
1921
|
+
try {
|
|
1922
|
+
mutateBackgroundAgentState(id, (current) =>
|
|
1923
|
+
current
|
|
1924
|
+
? {
|
|
1925
|
+
...current,
|
|
1926
|
+
phase: "stop_failed",
|
|
1927
|
+
stopError: error?.message || String(error),
|
|
1928
|
+
}
|
|
1929
|
+
: null,
|
|
1930
|
+
);
|
|
1931
|
+
} catch {
|
|
1932
|
+
/* the durable stopRequestedAt fence already blocks new turns */
|
|
1933
|
+
}
|
|
1934
|
+
throw error;
|
|
1360
1935
|
}
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1936
|
+
// Build the terminal write from the freshest record under the lock. In
|
|
1937
|
+
// particular, never spread the pre-kill fence snapshot over a worker's
|
|
1938
|
+
// concurrently persisted process-tree termination confirmation.
|
|
1939
|
+
const terminal = mutateBackgroundAgentState(id, (current) =>
|
|
1940
|
+
current
|
|
1941
|
+
? {
|
|
1942
|
+
...current,
|
|
1943
|
+
status: "stopped",
|
|
1944
|
+
endedAt: current.endedAt || Date.now(),
|
|
1945
|
+
stoppedByUser: true,
|
|
1946
|
+
phase: null,
|
|
1947
|
+
transport: null,
|
|
1948
|
+
stopError: null,
|
|
1949
|
+
}
|
|
1950
|
+
: null,
|
|
1951
|
+
);
|
|
1952
|
+
const written = terminal.state || state;
|
|
1953
|
+
// A turn-spawn persistence failure may publish its agent pid after the
|
|
1954
|
+
// stopper took the first snapshot. Re-read/merge above preserves that
|
|
1955
|
+
// uncertainty identity; make one final identity-guarded tree reap attempt.
|
|
1956
|
+
if (written.agentPid) {
|
|
1957
|
+
try {
|
|
1958
|
+
reclaimOrphanAgentProcess(written);
|
|
1959
|
+
} catch {
|
|
1960
|
+
/* retained agentPid + uncertainty fence block destructive cleanup */
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
return { ...written, stopped: written.status === "stopped" };
|
|
1369
1964
|
}
|
|
1370
1965
|
|
|
1371
1966
|
function activeWorktreeProcesses(state) {
|
|
@@ -1405,6 +2000,15 @@ export function cleanupBackgroundAgentWorktree(state, options = {}) {
|
|
|
1405
2000
|
}
|
|
1406
2001
|
|
|
1407
2002
|
const blockers = [];
|
|
2003
|
+
if (state?.launchFinalizationUncertain === true) {
|
|
2004
|
+
blockers.push("background launch finalization is uncertain");
|
|
2005
|
+
}
|
|
2006
|
+
if (state?.turnLaunchFinalizationUncertain === true) {
|
|
2007
|
+
blockers.push("background turn launch finalization is uncertain");
|
|
2008
|
+
}
|
|
2009
|
+
if (state?.turnLaunchIntent) {
|
|
2010
|
+
blockers.push("background turn launch intent is unresolved");
|
|
2011
|
+
}
|
|
1408
2012
|
const active = activeWorktreeProcesses(state);
|
|
1409
2013
|
if (active.length) blockers.push(`active ${active.join("+")} process`);
|
|
1410
2014
|
if (Number(state?.pendingApprovals || 0) > 0) {
|