chainlesschain 0.163.6 → 0.163.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-DDIUofA8.js → AIOps-axqQfwv8.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-Bm-5xYRn.js → ActionButton-DV-Ro0By.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-D5UH4ljI.js → Analytics-Bt72WEVu.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-D_ifjOOP.js → AppLayout-BtepG55E.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-BY1mXdC7.js → Artifacts-B7KaJ7S_.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-Bujr4Ml3.js → Audit-FNFaUg7A.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-sioKSTcC.js → BackgroundAgents-DFb6J2wQ.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DDOAK8RK.js → Backup-CC7YBhyj.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CtCsVVke.js → BaseInput-RkgTNVtj.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DpNj0vnd.js → Chat-hHEmGZy-.js} +4 -4
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CZta3Ftr.js → ChatBubbleRenderer-CAP9YhHv.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-DoHVNSXq.js → Checkbox-z5wXOQP1.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-DlVl7Qmv.js → Codegen-DVaDJnxy.js} +1 -1
- package/src/assets/web-panel/assets/{Col-GIqoPYty.js → Col-CJmaRDR5.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CIAPEVji.js → Community-DF29_2bI.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BN7JiI_h.js → Compact-DtkQkBzn.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CqUWCM9B.js → Compliance-xoHDxp7I.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-A4iPp7A4.js → Cowork-BUm-R7ht.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-B2zeXXWb.js → Cron-Cgl14ZkB.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-BR2xmWr8.js → Crosschain-BNcbmoqh.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BzFDjrIv.js → DID-Bwja0Zo9.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-DKtDo3yA.js → Dashboard-Bk5XjEdJ.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-RNx43_YG.js → Dropdown-Ci36Oitq.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-SwMt4Rcy.js → EmailListRenderer--U04ZN4q.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-CsYebZ4X.js → FamilyGuardDashboard-B2gNPR2s.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-BmRKvBTy.js → Federation-C4KghYn7.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-CVxGTA-0.js → FormItemContext-QL7x_Q_0.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-x2iXpyB7.js → GenericCardRenderer-F3Ryltwm.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DfqbkiOP.js → Git-2HassK5k.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-RuHyyj5O.js → Governance-DoBIp7oe.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-Bn7uZ7rI.js → Inference--W01QhZ-.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DlP7XefZ.js → KnowledgeGraph-DmvwqGpT.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BYNdOiCd.js → Logs-BBJ4yAel.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-aMtgN-R6.js → MarkdownRenderer-BLCZMzhJ.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-JQYmDqM8.js → Marketplace-D5lNngSI.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-Cyx4yZ-9.js → McpTools-CkEVOMKH.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-VDuGZTN2.js → Memory-B1Ow1xVE.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-_yKo97gi.js → MobileBridge-SgYbUfb6.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-CXRXW3q7.js → MobileProjects-BWdOvQAj.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-acTGtfLH.js → Mtc-DcTXSSma.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-ClpjXfkY.js → MtcAudit-Dt3WG4oj.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-_6Tn3o96.js → Multisig-B9B38UZx.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CK7pbZzP.js → NLProgramming-B0S4GGvG.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-Czy6V_P8.js → Notes-DZ5U58Ng.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-Cqx-UVv3.js → NotificationSettings-PjL5ZW34.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-C69WDxGy.js → OrderTableRenderer-DrAX2ESt.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-DFnA9vLQ.js → Organization-Cv1GYs-c.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-ReZkk2yT.js → Overflow-Bbkp_PL3.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-DzjSI0aI.js → P2P-BD8QUgoZ.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-DKTf5xIP.js → PdhVaultBrowser-BO-2PeaB.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-DhQ5klhR.js → Permissions-D_OPVa02.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-CUL1iHrW.js → PersonalDataHub-mh-pEmaB.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-DU88iu4N.js → Pipeline-6JC2EDdb.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-Bw-sYfyK.js → Privacy-CUshzUAG.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-Bq3Q5y0z.js → ProjectInit-Cs2Aalri.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-C8spzpho.js → ProjectSettings-1ak49tcj.js} +2 -2
- package/src/assets/web-panel/assets/Projects-BV4oApsm.js +1 -0
- package/src/assets/web-panel/assets/{Providers-Bf4eTP9W.js → Providers-CXXxvafH.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-Dme9BDU1.js → QrScannerModal-PTcDsF58.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CIXfv1rE.js → QuickAsk-CDwucrJw.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CIxvdeZ-.js → Recommend-CJlLVRXR.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-C6k7MExI.js → RemoteSession-DlN-ETsO.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-CISDz2bl.js → Reputation-DI23LR6m.js} +1 -1
- package/src/assets/web-panel/assets/{Row-CdnDxXyf.js → Row-Cl7UQMrM.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-C-QpRHsl.js → RssFeed-CexHNfh8.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DBjaGzSn.js → Search-BneBWvRN.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CQKukOeE.js → Security-DX7Phgrp.js} +4 -4
- package/src/assets/web-panel/assets/{Services-DZz86jWb.js → Services-BWXO8Mte.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-jb8RMK0d.js → Skeleton-DCyOjKPJ.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CdY76fZG.js → Skills-CXqERsTB.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-C_CSiZX9.js → Sla-bhbrbz8E.js} +1 -1
- package/src/assets/web-panel/assets/SpeechSettings-3IPU0HBZ.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-qvimGO0O.js → SyncSettings-DBNTQLbM.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-Dng5LPJC.js → Tasks-quP0bkN-.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-Bwqq5Wll.js → Templates-BouWmgho.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-DNWuwP7Z.js → Tenant-qv0D9s7z.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-B-6Othpp.js → Terminal-BxvbE8BB.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-DKpKZfzx.js → TimelineRenderer-D-IeLnJ2.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-DzfGMd2J.js → Tokens-CFMKKyVG.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BXwUi74J.js → Trigger-L8as3UuJ.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BNzUq4ui.js → Trust-MxmA4VZG.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-BYugxww4.js → UkeySign-C0VvJYRi.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DdauhtSJ.js → VideoEditing-Bf5QZj8N.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CSC9bCe_.js → Wallet-BIJ9riRz.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-Q8AN3bTO.js → WebAuthn-B8p9Mhkw.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-d-LBGCuA.js → WorkflowEditor-BVsuaPen.js} +1 -1
- package/src/assets/web-panel/assets/{chat-BLVDCgsZ.js → chat-BfkjoKpV.js} +1 -1
- package/src/assets/web-panel/assets/{colors-BZlPSEHl.js → colors-DeAlsWK8.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-IWseP6R6.js → compact-item-Cs97bz5j.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-DnVRV3Ao.js → createContext-BqbG60LH.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DPty0mGN.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DLQZA_Wn.js → hasIn-i_RY0b1X.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_N4-Gq9.js → index-0-agYlWL.js} +1 -1
- package/src/assets/web-panel/assets/{index-DB0MJgF_.js → index-79V_zv1s.js} +1 -1
- package/src/assets/web-panel/assets/{index-BOZJBTWZ.js → index-B-3tEpTc.js} +1 -1
- package/src/assets/web-panel/assets/{index-DUq0c7gq.js → index-B-JVTrpr.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dy0R5ZED.js → index-B1CbOH1i.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bxp7uDD8.js → index-B5UkyA3c.js} +1 -1
- package/src/assets/web-panel/assets/{index-DpVrK6fr.js → index-BESLFt4q.js} +1 -1
- package/src/assets/web-panel/assets/{index-CT9vVN-X.js → index-BOVkf1K3.js} +1 -1
- package/src/assets/web-panel/assets/{index-DOoQpuQY.js → index-BYUVuwKt.js} +1 -1
- package/src/assets/web-panel/assets/{index-CINJQEj6.js → index-BcUXAB2s.js} +1 -1
- package/src/assets/web-panel/assets/{index-CarMTIyQ.js → index-BkBi5TmC.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5dgnilC.js → index-Bm7lLR4c.js} +1 -1
- package/src/assets/web-panel/assets/index-Bt7g0Vq_.js +1 -0
- package/src/assets/web-panel/assets/{index-CbQNL0ue.js → index-BuLQobzW.js} +1 -1
- package/src/assets/web-panel/assets/{index-CpBaJ3TD.js → index-CE2VtTlp.js} +1 -1
- package/src/assets/web-panel/assets/{index-jWb70ZgZ.js → index-CED2PL_y.js} +1 -1
- package/src/assets/web-panel/assets/{index-BEu2lMBy.js → index-CG3PRyAL.js} +1 -1
- package/src/assets/web-panel/assets/{index-DocEhinm.js → index-CIG8fQ2_.js} +1 -1
- package/src/assets/web-panel/assets/{index-D_LQodeJ.js → index-COdGA-8n.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZpYeqrh.js → index-CeWoNHau.js} +1 -1
- package/src/assets/web-panel/assets/{index-gt8bNefW.js → index-Cf3WCLmy.js} +1 -1
- package/src/assets/web-panel/assets/{index-N_GoWZX4.js → index-CgsqdnRV.js} +1 -1
- package/src/assets/web-panel/assets/index-ChVT0p9u.js +1 -0
- package/src/assets/web-panel/assets/{index-CZDekj7T.js → index-D2B4VPyP.js} +1 -1
- package/src/assets/web-panel/assets/{index-YSw_gxlZ.js → index-D9evtvg2.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTbxp4UH.js → index-DUUrv141.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bbj2NS_I.js → index-DiSkx4UB.js} +3 -3
- package/src/assets/web-panel/assets/{index-BZs5bBnk.js → index-Dk2qouAz.js} +1 -1
- package/src/assets/web-panel/assets/{index-DalA0TKC.js → index-Dk7-v4hP.js} +1 -1
- package/src/assets/web-panel/assets/{index-1jeAudHd.js → index-DmbfhJUP.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1ifZtXo.js → index-DqBgBpgp.js} +1 -1
- package/src/assets/web-panel/assets/{index-BhA3QEni.js → index-PT_5sBMA.js} +1 -1
- package/src/assets/web-panel/assets/{index-BSjilZ9L.js → index-U4f5izHz.js} +1 -1
- package/src/assets/web-panel/assets/{index-C2FqfMvU.js → index-YXgLng0K.js} +1 -1
- package/src/assets/web-panel/assets/{index-COev0W2g.js → index-ZxO_-E8a.js} +1 -1
- package/src/assets/web-panel/assets/{index-M7wxfAbm.js → index-dIHr4B_u.js} +1 -1
- package/src/assets/web-panel/assets/{index-DjWnONdy.js → index-gyFpvL7l.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dkwsi1Kn.js → index-s1opAL-M.js} +1 -1
- package/src/assets/web-panel/assets/{index-hwVDeoC0.js → index-yObj1O4n.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-Co67Kqmb.js → initDefaultProps-C9h9yKL3.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BMOxQSF_.js → motion-BvmLsPdu.js} +1 -1
- package/src/assets/web-panel/assets/{move-C5fWDw-N.js → move-CoL4ekU1.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BJevpL0e.js → mtc-parser-C4pMy6xV.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BCzNU8s6.js → omit-DPuJfNxx.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-CybWvDPk.js → pickAttrs-D0KEeaK2.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BMWS9HnW.js → placementArrow-BpkyINxt.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-BoL2UnGY.js → responsiveObserve-DQI_wOq8.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DWNfXraU.js → slide-DYfdDiRg.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BquTM3tX.js → statusUtils-B5dtvb0D.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BEkCleim.js → styleChecker-CF310mSj.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-Oecnq7EA.js → useFlexGapSupport-D1Qp4BLp.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CkDffJ4F.js → useFs-C7_iyvJK.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-qbvKXesB.js → usePersonalDataHub-DZUt_fIm.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-C6v8n6mA.js → vnode-D3dsi0lZ.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-4CTeFILV.js → zoom-WdbOlvdQ.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +2 -2
- package/src/commands/agent.js +90 -1
- package/src/commands/chat.js +11 -2
- package/src/commands/compact.js +106 -23
- package/src/commands/loop.js +28 -10
- package/src/commands/routine.js +1 -1
- package/src/commands/scheduler-daemon.js +359 -2
- package/src/commands/session-observability.js +305 -0
- package/src/commands/session.js +38 -0
- package/src/data/changelog.json +14 -1
- package/src/gateways/ws/ws-agent-handler.js +439 -5
- package/src/harness/jsonl-session-store.js +463 -125
- package/src/harness/prompt-compressor.js +86 -13
- package/src/harness/provider-backed-compaction.js +100 -27
- package/src/lib/advisor-runtime.js +3 -1
- package/src/lib/agent-schedule-store.js +414 -4
- package/src/lib/agent-team/team-adjudication.js +40 -1
- package/src/lib/automation-engine.js +494 -0
- package/src/lib/background-agent-supervisor.js +793 -215
- package/src/lib/background-command-argv.js +24 -0
- package/src/lib/background-interaction-journal.js +361 -52
- package/src/lib/background-interaction-resolver.js +32 -0
- package/src/lib/background-launch-profile.js +2 -0
- package/src/lib/causal-observability.js +2661 -0
- package/src/lib/chat-session-admission.js +76 -0
- package/src/lib/cost-budget.js +5 -7
- package/src/lib/cowork/ab-comparator-cli.js +11 -0
- package/src/lib/cowork/debate-review-cli.js +12 -0
- package/src/lib/cowork-adapter.js +159 -38
- package/src/lib/cowork-cron.js +357 -1
- package/src/lib/cowork-task-runner.js +112 -17
- package/src/lib/delivery-coordinator.js +142 -0
- package/src/lib/direct-model-usage.js +121 -0
- package/src/lib/file-lines.js +183 -47
- package/src/lib/llm-pricing.js +12 -17
- package/src/lib/loop.js +26 -2
- package/src/lib/routine-store.js +380 -21
- package/src/lib/runtime-usage-ledger.js +182 -0
- package/src/lib/scheduler-kernel/agenda-adapter.js +308 -3
- package/src/lib/scheduler-kernel/automation-adapter.js +410 -8
- package/src/lib/scheduler-kernel/automation-event-adapter.js +36 -0
- package/src/lib/scheduler-kernel/cowork-cron-adapter.js +285 -3
- package/src/lib/scheduler-kernel/loop-adapter.js +541 -6
- package/src/lib/scheduler-kernel/migration-admin.js +346 -0
- package/src/lib/scheduler-kernel/migration-rollback-admin.js +932 -0
- package/src/lib/scheduler-kernel/routine-adapter.js +394 -20
- package/src/lib/scheduler-kernel/runtime.js +63 -1
- package/src/lib/scheduler-kernel/source-locator-path.js +142 -0
- package/src/lib/scheduler-kernel/store.js +2586 -85
- package/src/lib/session-host-lease.js +171 -2
- package/src/lib/session-host-snapshot.js +14 -3
- package/src/lib/session-persistence-failure.js +1 -0
- package/src/lib/session-tail.js +179 -34
- package/src/lib/session-transcript-structure.js +96 -0
- package/src/lib/sub-agent-context.js +328 -22
- package/src/lib/ws-agent-handler.js +4 -1
- package/src/repl/agent-repl.js +732 -91
- package/src/repl/chat-repl.js +5 -1
- package/src/runtime/agent-core.js +1272 -207
- package/src/runtime/fallback-model.js +5 -0
- package/src/runtime/headless-runner.js +324 -82
- package/src/runtime/headless-stream.js +232 -26
- package/src/runtime/policies/agent-policy.js +4 -0
- package/src/workers/background-agent-worker.js +403 -96
- package/src/assets/web-panel/assets/Projects-Dd-xuPIc.js +0 -1
- package/src/assets/web-panel/assets/SpeechSettings-C7tT9iad.js +0 -1
- package/src/assets/web-panel/assets/devWarning-Jzu62CLC.js +0 -1
- package/src/assets/web-panel/assets/index-Zb2MeDSy.js +0 -1
- package/src/assets/web-panel/assets/index-fikqml4k.js +0 -1
|
@@ -17,6 +17,10 @@ import { fileURLToPath } from "node:url";
|
|
|
17
17
|
import { getHomeDir } from "./paths.js";
|
|
18
18
|
import { withFileLock } from "./with-file-lock.js";
|
|
19
19
|
import { deriveSessionState } from "./session-lifecycle.js";
|
|
20
|
+
import {
|
|
21
|
+
getSessionPresence,
|
|
22
|
+
SESSION_PRESENCE,
|
|
23
|
+
} from "../harness/jsonl-session-store.js";
|
|
20
24
|
import {
|
|
21
25
|
finishAgentWorktree,
|
|
22
26
|
validateAgentWorktree,
|
|
@@ -25,10 +29,12 @@ import { rejectPendingBackgroundInteractions } from "./background-interaction-jo
|
|
|
25
29
|
import executionBroker from "./process-execution-broker/index.js";
|
|
26
30
|
import {
|
|
27
31
|
agentPrintArgument,
|
|
32
|
+
assertBackgroundArgvDurable,
|
|
28
33
|
stripFirstTurnPromptArgv,
|
|
29
34
|
} from "./background-command-argv.js";
|
|
30
35
|
import { terminateOwnedProcessTree } from "./process-tree-termination.js";
|
|
31
36
|
import { sendAgentNotification } from "./agent-notify.js";
|
|
37
|
+
import { withSessionHostRecoveryLease } from "./session-host-lease.js";
|
|
32
38
|
import {
|
|
33
39
|
buildNeedsInputNotification,
|
|
34
40
|
claimNeedsInputNotification,
|
|
@@ -172,6 +178,83 @@ function defaultReadProcessStartTimeMs(pid) {
|
|
|
172
178
|
return null;
|
|
173
179
|
}
|
|
174
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Read the POSIX process state letter (`R`, `S`, `Z`, ...), or null when it
|
|
183
|
+
* cannot be determined. A successfully signalled child can remain as a zombie
|
|
184
|
+
* until its parent returns to the event loop; `kill(pid, 0)` still reports that
|
|
185
|
+
* PID as present even though it can no longer execute any code.
|
|
186
|
+
*/
|
|
187
|
+
function defaultReadProcessState(pid) {
|
|
188
|
+
const target = Number(pid);
|
|
189
|
+
if (
|
|
190
|
+
process.platform === "win32" ||
|
|
191
|
+
!Number.isInteger(target) ||
|
|
192
|
+
target <= 0
|
|
193
|
+
) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const result = runSupervisorCommand(
|
|
198
|
+
"ps",
|
|
199
|
+
["-o", "stat=", "-p", String(target)],
|
|
200
|
+
{ encoding: "utf8", timeout: 5_000 },
|
|
201
|
+
"background-agent:process-state",
|
|
202
|
+
);
|
|
203
|
+
if (result.error || result.status !== 0) return null;
|
|
204
|
+
const state = String(result.stdout || "")
|
|
205
|
+
.trim()
|
|
206
|
+
.charAt(0)
|
|
207
|
+
.toUpperCase();
|
|
208
|
+
return /^[A-Z]$/.test(state) ? state : null;
|
|
209
|
+
} catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Read every state in the POSIX process group whose id is `pgid`.
|
|
216
|
+
*
|
|
217
|
+
* Both procps (Linux) and BSD ps (macOS) support repeated `-o` fields and an
|
|
218
|
+
* empty header (`field=`). Enumerating the table is more portable than the
|
|
219
|
+
* incompatible Linux/BSD process-group selector flags. `null` means the
|
|
220
|
+
* snapshot could not be trusted; callers must keep treating the group as live.
|
|
221
|
+
*/
|
|
222
|
+
function defaultReadProcessGroupStates(pgid) {
|
|
223
|
+
const target = Number(pgid);
|
|
224
|
+
if (
|
|
225
|
+
process.platform === "win32" ||
|
|
226
|
+
!Number.isInteger(target) ||
|
|
227
|
+
target <= 0
|
|
228
|
+
) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
const result = runSupervisorCommand(
|
|
233
|
+
"ps",
|
|
234
|
+
["-A", "-o", "pgid=", "-o", "stat="],
|
|
235
|
+
{
|
|
236
|
+
encoding: "utf8",
|
|
237
|
+
timeout: 5_000,
|
|
238
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
239
|
+
},
|
|
240
|
+
"background-agent:process-group-state",
|
|
241
|
+
);
|
|
242
|
+
if (result.error || result.status !== 0) return null;
|
|
243
|
+
const states = [];
|
|
244
|
+
for (const line of String(result.stdout || "").split(/\r?\n/)) {
|
|
245
|
+
if (!line.trim()) continue;
|
|
246
|
+
const match = /^\s*(\d+)\s+([A-Za-z])/.exec(line);
|
|
247
|
+
// Unexpected output (including an unsuppressed header) makes the whole
|
|
248
|
+
// snapshot untrusted instead of accidentally declaring a group dead.
|
|
249
|
+
if (!match) return null;
|
|
250
|
+
if (Number(match[1]) === target) states.push(match[2].toUpperCase());
|
|
251
|
+
}
|
|
252
|
+
return states;
|
|
253
|
+
} catch {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
175
258
|
/**
|
|
176
259
|
* Kill a whole process tree (Gap 2 orphan reclaim). Windows: `taskkill /T`.
|
|
177
260
|
* POSIX: negative-pid group signal (the worker spawns the agent child
|
|
@@ -219,7 +302,11 @@ export const _deps = {
|
|
|
219
302
|
// "worker-death" CI flake: a stop-flow test recorded pid=process.pid and
|
|
220
303
|
// SIGTERMed its own vitest worker).
|
|
221
304
|
kill: (pid, signal) => process.kill(pid, signal),
|
|
305
|
+
getSessionPresence,
|
|
222
306
|
readProcessStartTimeMs: defaultReadProcessStartTimeMs,
|
|
307
|
+
readProcessState: defaultReadProcessState,
|
|
308
|
+
readProcessGroupStates: defaultReadProcessGroupStates,
|
|
309
|
+
processExitWaitDeadlineMs: 2_000,
|
|
223
310
|
killProcessTree: defaultKillProcessTree,
|
|
224
311
|
terminateOwnedProcessTree,
|
|
225
312
|
};
|
|
@@ -271,7 +358,7 @@ export function stopBackgroundAgentChildTree(
|
|
|
271
358
|
}
|
|
272
359
|
|
|
273
360
|
/** Cached probe (default impl only — injected probes run uncached for tests). */
|
|
274
|
-
function processStartTimeMs(pid) {
|
|
361
|
+
function processStartTimeMs(pid, options = {}) {
|
|
275
362
|
if (_deps.readProcessStartTimeMs !== defaultReadProcessStartTimeMs) {
|
|
276
363
|
const raw = _deps.readProcessStartTimeMs(pid);
|
|
277
364
|
const n = Number(raw);
|
|
@@ -279,6 +366,11 @@ function processStartTimeMs(pid) {
|
|
|
279
366
|
}
|
|
280
367
|
const key = Number(pid);
|
|
281
368
|
const at = Date.now();
|
|
369
|
+
if (options.fresh === true) {
|
|
370
|
+
const value = defaultReadProcessStartTimeMs(key);
|
|
371
|
+
_startTimeCache.set(key, { at, value });
|
|
372
|
+
return value;
|
|
373
|
+
}
|
|
282
374
|
const hit = _startTimeCache.get(key);
|
|
283
375
|
if (hit && at - hit.at < START_TIME_CACHE_TTL_MS) return hit.value;
|
|
284
376
|
const value = defaultReadProcessStartTimeMs(key);
|
|
@@ -365,7 +457,13 @@ function mergeBackgroundAgentState(current, requested) {
|
|
|
365
457
|
// clobber window from "any caller's RMW span" to microseconds; rename/pin
|
|
366
458
|
// additionally verify-and-retry on top of this.
|
|
367
459
|
if (current) {
|
|
368
|
-
|
|
460
|
+
const explicitHeartbeatStop =
|
|
461
|
+
current.status === "lost" &&
|
|
462
|
+
current.lostReason === "heartbeat-stale" &&
|
|
463
|
+
requested.status === "stopped" &&
|
|
464
|
+
current.stopRequestedAt &&
|
|
465
|
+
requested.stopRequestedAt === current.stopRequestedAt;
|
|
466
|
+
if (TERMINAL_STATUSES.has(current.status) && !explicitHeartbeatStop) {
|
|
369
467
|
next = {
|
|
370
468
|
...next,
|
|
371
469
|
status: current.status,
|
|
@@ -420,6 +518,9 @@ function mergeBackgroundAgentState(current, requested) {
|
|
|
420
518
|
next = {
|
|
421
519
|
...next,
|
|
422
520
|
...(current.sessionId ? { sessionId: current.sessionId } : {}),
|
|
521
|
+
...(current.sessionBootstrapExpected !== undefined
|
|
522
|
+
? { sessionBootstrapExpected: current.sessionBootstrapExpected }
|
|
523
|
+
: {}),
|
|
423
524
|
...(current.cwd ? { cwd: current.cwd } : {}),
|
|
424
525
|
...(current.startedAt !== undefined
|
|
425
526
|
? { startedAt: current.startedAt }
|
|
@@ -806,6 +907,36 @@ export function isProcessAlive(pid) {
|
|
|
806
907
|
}
|
|
807
908
|
}
|
|
808
909
|
|
|
910
|
+
function isProcessExecutionAlive(pid) {
|
|
911
|
+
if (!isProcessAlive(pid)) return false;
|
|
912
|
+
const state = _deps.readProcessState(pid);
|
|
913
|
+
// Z = zombie, X = dead. Both retain a PID table entry temporarily but have
|
|
914
|
+
// no executable process body. Unknown probe results fail closed as alive.
|
|
915
|
+
return state !== "Z" && state !== "X";
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function inspectProcessGroupExecution(pgid) {
|
|
919
|
+
const states = _deps.readProcessGroupStates(pgid);
|
|
920
|
+
if (!Array.isArray(states)) return { status: "unknown" };
|
|
921
|
+
return {
|
|
922
|
+
status: states.some((state) => state !== "Z" && state !== "X")
|
|
923
|
+
? "alive"
|
|
924
|
+
: "dead",
|
|
925
|
+
states,
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function isSignalledTargetExecutionAlive(target) {
|
|
930
|
+
if (isProcessExecutionAlive(target.pid)) return true;
|
|
931
|
+
if (process.platform === "win32" || target.signalledAsGroup !== true) {
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
934
|
+
// The detached group leader can be reaped (or remain a zombie) while a tool
|
|
935
|
+
// grandchild in the same group still executes. Unknown snapshots fail
|
|
936
|
+
// closed as alive so interaction recovery never races an orphaned tool.
|
|
937
|
+
return inspectProcessGroupExecution(target.pid).status !== "dead";
|
|
938
|
+
}
|
|
939
|
+
|
|
809
940
|
/**
|
|
810
941
|
* Is the live process at `pid` the SAME process the state file recorded at
|
|
811
942
|
* `expectedStartedAtMs` — or a pid-reusing stranger? (Gap 1.)
|
|
@@ -819,7 +950,7 @@ export function isProcessAlive(pid) {
|
|
|
819
950
|
* born after the original died)
|
|
820
951
|
*/
|
|
821
952
|
export function isSameProcess(pid, expectedStartedAtMs, options = {}) {
|
|
822
|
-
if (!
|
|
953
|
+
if (!isProcessExecutionAlive(pid)) return false;
|
|
823
954
|
const expected = Number(expectedStartedAtMs);
|
|
824
955
|
if (!Number.isFinite(expected) || expected <= 0) return true;
|
|
825
956
|
const actual = processStartTimeMs(pid);
|
|
@@ -830,6 +961,117 @@ export function isSameProcess(pid, expectedStartedAtMs, options = {}) {
|
|
|
830
961
|
return actual <= expected + tolerance;
|
|
831
962
|
}
|
|
832
963
|
|
|
964
|
+
/**
|
|
965
|
+
* Destructive process identity is deliberately stricter than read-only
|
|
966
|
+
* reconciliation. A signal is authorized only when the persisted positive
|
|
967
|
+
* creation-time anchor can be compared with a successful, fresh OS probe and
|
|
968
|
+
* both timestamps describe the same launch window. Missing evidence never
|
|
969
|
+
* falls back to pid-only liveness.
|
|
970
|
+
*/
|
|
971
|
+
function inspectDestructiveProcessIdentity(
|
|
972
|
+
pid,
|
|
973
|
+
expectedStartedAtMs,
|
|
974
|
+
options = {},
|
|
975
|
+
) {
|
|
976
|
+
const target = Number(pid);
|
|
977
|
+
if (!Number.isInteger(target) || target <= 0) {
|
|
978
|
+
return { status: "absent", pid: target };
|
|
979
|
+
}
|
|
980
|
+
// A corrupt record must never authorize the daemon-stop process to signal
|
|
981
|
+
// itself. This guard covers every recorded role (including agentPid), not
|
|
982
|
+
// only the primary worker pid checked by the public stop flow.
|
|
983
|
+
if (target === process.pid) {
|
|
984
|
+
return { status: "self", reason: "current-process", pid: target };
|
|
985
|
+
}
|
|
986
|
+
const leaderAlive = isProcessAlive(target);
|
|
987
|
+
const leaderState = leaderAlive ? _deps.readProcessState(target) : null;
|
|
988
|
+
const leaderCanExecute =
|
|
989
|
+
leaderAlive && leaderState !== "Z" && leaderState !== "X";
|
|
990
|
+
if (!leaderCanExecute) {
|
|
991
|
+
if (process.platform === "win32") {
|
|
992
|
+
return { status: "dead", reason: "not-executing", pid: target };
|
|
993
|
+
}
|
|
994
|
+
const group = inspectProcessGroupExecution(target);
|
|
995
|
+
if (group.status === "dead") {
|
|
996
|
+
return {
|
|
997
|
+
status: "dead",
|
|
998
|
+
reason: "process-group-not-executing",
|
|
999
|
+
pid: target,
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
if (group.status === "unknown") {
|
|
1003
|
+
return {
|
|
1004
|
+
status: "unverifiable",
|
|
1005
|
+
reason: "process-group-state-probe-failed",
|
|
1006
|
+
pid: target,
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
if (!leaderAlive) {
|
|
1010
|
+
// A live group whose leader has already disappeared has no remaining
|
|
1011
|
+
// creation-time identity that can authorize signalling. Keep the stop
|
|
1012
|
+
// fenced rather than risking an unrelated, reused process group.
|
|
1013
|
+
return {
|
|
1014
|
+
status: "unverifiable",
|
|
1015
|
+
reason: "process-group-owner-not-verifiable",
|
|
1016
|
+
pid: target,
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
const expected = Number(expectedStartedAtMs);
|
|
1021
|
+
if (!Number.isFinite(expected) || expected <= 0) {
|
|
1022
|
+
return {
|
|
1023
|
+
status: "unverifiable",
|
|
1024
|
+
reason: "started-at-missing",
|
|
1025
|
+
pid: target,
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
const actual = processStartTimeMs(target, { fresh: true });
|
|
1029
|
+
if (actual === null) {
|
|
1030
|
+
return {
|
|
1031
|
+
status: "unverifiable",
|
|
1032
|
+
reason: "creation-time-probe-failed",
|
|
1033
|
+
pid: target,
|
|
1034
|
+
expectedStartedAt: expected,
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
const tolerance = Number.isFinite(Number(options.toleranceMs))
|
|
1038
|
+
? Number(options.toleranceMs)
|
|
1039
|
+
: PID_IDENTITY_TOLERANCE_MS;
|
|
1040
|
+
if (Math.abs(actual - expected) > tolerance) {
|
|
1041
|
+
if (process.platform !== "win32") {
|
|
1042
|
+
const group = inspectProcessGroupExecution(target);
|
|
1043
|
+
// The leader PID can be reused while descendants from its detached
|
|
1044
|
+
// process group still execute under the old PGID. We can prove the new
|
|
1045
|
+
// PID is not ours, but cannot prove that the group is empty unless this
|
|
1046
|
+
// snapshot succeeds. Never recover/delete through that ambiguity.
|
|
1047
|
+
if (group.status !== "dead") {
|
|
1048
|
+
return {
|
|
1049
|
+
status: "unverifiable",
|
|
1050
|
+
reason:
|
|
1051
|
+
group.status === "alive"
|
|
1052
|
+
? "process-group-survived-pid-reuse"
|
|
1053
|
+
: "process-group-state-probe-failed",
|
|
1054
|
+
pid: target,
|
|
1055
|
+
expectedStartedAt: expected,
|
|
1056
|
+
actualStartedAt: actual,
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return {
|
|
1061
|
+
status: "reused",
|
|
1062
|
+
pid: target,
|
|
1063
|
+
expectedStartedAt: expected,
|
|
1064
|
+
actualStartedAt: actual,
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
return {
|
|
1068
|
+
status: "match",
|
|
1069
|
+
pid: target,
|
|
1070
|
+
expectedStartedAt: expected,
|
|
1071
|
+
actualStartedAt: actual,
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
|
|
833
1075
|
/**
|
|
834
1076
|
* Reap a lost worker's recorded agent child (Gap 2). The worker persists
|
|
835
1077
|
* `agentPid` + `agentStartedAt` per turn; when the WORKER dies (crash /
|
|
@@ -839,9 +1081,8 @@ export function isSameProcess(pid, expectedStartedAtMs, options = {}) {
|
|
|
839
1081
|
* - `agentStartedAt` anchor is REQUIRED — unlike the read-only liveness
|
|
840
1082
|
* probe, this path kills, so a legacy state without the anchor fails
|
|
841
1083
|
* CLOSED (no kill) rather than risking a pid-reused stranger;
|
|
842
|
-
* - with the anchor present,
|
|
843
|
-
*
|
|
844
|
-
* anchor already bounds the window to this session's lifetime).
|
|
1084
|
+
* - with the anchor present, the fresh creation-time probe must also succeed
|
|
1085
|
+
* and match; missing or unverifiable identity evidence fails closed.
|
|
845
1086
|
*
|
|
846
1087
|
* @returns {boolean} true when a kill was actually issued
|
|
847
1088
|
*/
|
|
@@ -851,7 +1092,9 @@ export function reclaimOrphanAgentProcess(state) {
|
|
|
851
1092
|
if (agentPid === Number(state?.pid)) return false; // never the worker itself
|
|
852
1093
|
const anchor = Number(state?.agentStartedAt);
|
|
853
1094
|
if (!Number.isFinite(anchor) || anchor <= 0) return false; // fail closed
|
|
854
|
-
if (
|
|
1095
|
+
if (inspectDestructiveProcessIdentity(agentPid, anchor).status !== "match") {
|
|
1096
|
+
return false;
|
|
1097
|
+
}
|
|
855
1098
|
return _deps.killProcessTree(agentPid, "SIGKILL") === true;
|
|
856
1099
|
}
|
|
857
1100
|
|
|
@@ -925,9 +1168,141 @@ function deriveEffectiveBackgroundAgentState(state, options = {}) {
|
|
|
925
1168
|
return next;
|
|
926
1169
|
}
|
|
927
1170
|
|
|
1171
|
+
function backgroundInteractionFallback(state) {
|
|
1172
|
+
const pending = state?.pendingQuestion;
|
|
1173
|
+
if (!pending) return null;
|
|
1174
|
+
const requestId = pending?.requestId || pending?.intId;
|
|
1175
|
+
return {
|
|
1176
|
+
requestId: requestId || null,
|
|
1177
|
+
binding: pending?.binding,
|
|
1178
|
+
payload: {
|
|
1179
|
+
kind: "question",
|
|
1180
|
+
question: pending?.question || pending?.prompt || "",
|
|
1181
|
+
options: pending?.options || null,
|
|
1182
|
+
multiSelect: pending?.multiSelect === true,
|
|
1183
|
+
timeoutMs: pending?.timeoutMs,
|
|
1184
|
+
},
|
|
1185
|
+
createdAt: pending?.askedAt,
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function interactionRecoverySettledForState(state) {
|
|
1190
|
+
const recovery = state?.interactionRecovery;
|
|
1191
|
+
return Boolean(
|
|
1192
|
+
recovery &&
|
|
1193
|
+
["clean", "rejected"].includes(recovery.status) &&
|
|
1194
|
+
Number(recovery.turn) === Number(state.turnCount || 0) &&
|
|
1195
|
+
String(recovery.workerGeneration || "") ===
|
|
1196
|
+
String(state.workerGeneration || ""),
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function persistTerminalInteractionRecovery(state, patch) {
|
|
1201
|
+
const expectedRecovery = state.interactionRecovery || null;
|
|
1202
|
+
const expectedPendingRequestId =
|
|
1203
|
+
state.pendingQuestion?.requestId || state.pendingQuestion?.intId || null;
|
|
1204
|
+
const mutation = mutateBackgroundAgentState(state.id, (current) => {
|
|
1205
|
+
if (
|
|
1206
|
+
!current ||
|
|
1207
|
+
current.status !== state.status ||
|
|
1208
|
+
current.lostReason !== state.lostReason ||
|
|
1209
|
+
current.endedAt !== state.endedAt ||
|
|
1210
|
+
current.workerGeneration !== state.workerGeneration ||
|
|
1211
|
+
current.turnCount !== state.turnCount ||
|
|
1212
|
+
current.agentPid !== state.agentPid ||
|
|
1213
|
+
current.agentStartedAt !== state.agentStartedAt ||
|
|
1214
|
+
(current.pendingQuestion?.requestId ||
|
|
1215
|
+
current.pendingQuestion?.intId ||
|
|
1216
|
+
null) !== expectedPendingRequestId ||
|
|
1217
|
+
(current.interactionRecovery?.status || null) !==
|
|
1218
|
+
(expectedRecovery?.status || null) ||
|
|
1219
|
+
(current.interactionRecovery?.turn ?? null) !==
|
|
1220
|
+
(expectedRecovery?.turn ?? null) ||
|
|
1221
|
+
(current.interactionRecovery?.workerGeneration || null) !==
|
|
1222
|
+
(expectedRecovery?.workerGeneration || null) ||
|
|
1223
|
+
(current.interactionRecovery?.recoveredAt ?? null) !==
|
|
1224
|
+
(expectedRecovery?.recoveredAt ?? null)
|
|
1225
|
+
) {
|
|
1226
|
+
return null;
|
|
1227
|
+
}
|
|
1228
|
+
return { ...current, ...patch };
|
|
1229
|
+
});
|
|
1230
|
+
// Never report an uncommitted recovery patch after the CAS loses a race.
|
|
1231
|
+
// Callers (notably remove/worktree cleanup) must observe the fresh durable
|
|
1232
|
+
// state and retry, rather than acting on a fabricated terminal projection.
|
|
1233
|
+
return mutation.state || state;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function recoverTerminalBackgroundInteractions(
|
|
1237
|
+
state,
|
|
1238
|
+
options = {},
|
|
1239
|
+
{
|
|
1240
|
+
code = "INTERACTION_WORKER_LOST",
|
|
1241
|
+
message = "The background worker was lost before the interaction settled",
|
|
1242
|
+
} = {},
|
|
1243
|
+
) {
|
|
1244
|
+
if (!state?.sessionId) return state;
|
|
1245
|
+
const t = nowMs(options);
|
|
1246
|
+
if (recordedBackgroundProcessesAlive(state)) {
|
|
1247
|
+
return persistTerminalInteractionRecovery(state, {
|
|
1248
|
+
interactionRecovery: {
|
|
1249
|
+
status: "failed",
|
|
1250
|
+
code: "INTERACTION_RECOVERY_CHILD_STILL_RUNNING",
|
|
1251
|
+
message:
|
|
1252
|
+
"The agent child is still alive; interaction recovery will retry after it exits",
|
|
1253
|
+
recoveredAt: t,
|
|
1254
|
+
turn: Number(state.turnCount || 0),
|
|
1255
|
+
workerGeneration: state.workerGeneration || null,
|
|
1256
|
+
},
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
const fallbackRequest = backgroundInteractionFallback(state);
|
|
1261
|
+
try {
|
|
1262
|
+
const recovery = withSessionHostRecoveryLease(state.sessionId, () =>
|
|
1263
|
+
rejectPendingBackgroundInteractions(state.sessionId, state.id, {
|
|
1264
|
+
fallbackRequest,
|
|
1265
|
+
code,
|
|
1266
|
+
message,
|
|
1267
|
+
}),
|
|
1268
|
+
);
|
|
1269
|
+
return persistTerminalInteractionRecovery(state, {
|
|
1270
|
+
phase: null,
|
|
1271
|
+
pendingQuestion: null,
|
|
1272
|
+
interactionRecovery: {
|
|
1273
|
+
status: recovery.changed ? "rejected" : "clean",
|
|
1274
|
+
requestIds: recovery.rejected.map((record) => record.requestId),
|
|
1275
|
+
recoveredAt: t,
|
|
1276
|
+
turn: Number(state.turnCount || 0),
|
|
1277
|
+
workerGeneration: state.workerGeneration || null,
|
|
1278
|
+
},
|
|
1279
|
+
});
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
return persistTerminalInteractionRecovery(state, {
|
|
1282
|
+
interactionRecovery: {
|
|
1283
|
+
status: "failed",
|
|
1284
|
+
code: error?.code || "INTERACTION_RECOVERY_FAILED",
|
|
1285
|
+
message: error?.message || String(error),
|
|
1286
|
+
recoveredAt: t,
|
|
1287
|
+
turn: Number(state.turnCount || 0),
|
|
1288
|
+
workerGeneration: state.workerGeneration || null,
|
|
1289
|
+
},
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
928
1294
|
export function effectiveBackgroundAgentState(state, options = {}) {
|
|
929
1295
|
if (!state) return null;
|
|
930
|
-
if (state.status !== "running")
|
|
1296
|
+
if (state.status !== "running") {
|
|
1297
|
+
if (
|
|
1298
|
+
state.sessionId &&
|
|
1299
|
+
!interactionRecoverySettledForState(state) &&
|
|
1300
|
+
["lost", "failed", "stopped", "completed"].includes(state.status)
|
|
1301
|
+
) {
|
|
1302
|
+
return recoverTerminalBackgroundInteractions(state, options);
|
|
1303
|
+
}
|
|
1304
|
+
return state;
|
|
1305
|
+
}
|
|
931
1306
|
// OS identity probes can take seconds on Windows. Perform them outside the
|
|
932
1307
|
// state lock, then use the exact observed identity/heartbeat as a CAS fence
|
|
933
1308
|
// inside the short persistence transaction.
|
|
@@ -975,91 +1350,9 @@ export function effectiveBackgroundAgentState(state, options = {}) {
|
|
|
975
1350
|
} catch {
|
|
976
1351
|
/* best-effort */
|
|
977
1352
|
}
|
|
978
|
-
if (previous?.sessionId) {
|
|
979
|
-
const t = nowMs(options);
|
|
980
|
-
const stateBeforeRecovery = next;
|
|
981
|
-
const pending = previous.pendingQuestion;
|
|
982
|
-
const requestId = pending?.requestId || pending?.intId;
|
|
983
|
-
try {
|
|
984
|
-
const recovery = rejectPendingBackgroundInteractions(
|
|
985
|
-
previous.sessionId,
|
|
986
|
-
previous.id,
|
|
987
|
-
{
|
|
988
|
-
fallbackRequest: requestId
|
|
989
|
-
? {
|
|
990
|
-
requestId,
|
|
991
|
-
binding: pending?.binding,
|
|
992
|
-
payload: {
|
|
993
|
-
kind: "question",
|
|
994
|
-
question: pending?.question || pending?.prompt || "",
|
|
995
|
-
options: pending?.options || null,
|
|
996
|
-
multiSelect: pending?.multiSelect === true,
|
|
997
|
-
timeoutMs: pending?.timeoutMs,
|
|
998
|
-
},
|
|
999
|
-
createdAt: pending?.askedAt,
|
|
1000
|
-
}
|
|
1001
|
-
: null,
|
|
1002
|
-
code: "INTERACTION_WORKER_LOST",
|
|
1003
|
-
message:
|
|
1004
|
-
"The background worker was lost before the interaction settled",
|
|
1005
|
-
},
|
|
1006
|
-
);
|
|
1007
|
-
const recoveredRecord = requestId
|
|
1008
|
-
? recovery.journal.get(requestId)
|
|
1009
|
-
: null;
|
|
1010
|
-
if (
|
|
1011
|
-
recovery.changed ||
|
|
1012
|
-
(recoveredRecord && recoveredRecord.status !== "pending")
|
|
1013
|
-
) {
|
|
1014
|
-
next = {
|
|
1015
|
-
...next,
|
|
1016
|
-
phase: null,
|
|
1017
|
-
pendingQuestion: null,
|
|
1018
|
-
interactionRecovery: {
|
|
1019
|
-
status: "rejected",
|
|
1020
|
-
requestIds: recovery.rejected.map((record) => record.requestId),
|
|
1021
|
-
recoveredAt: t,
|
|
1022
|
-
},
|
|
1023
|
-
};
|
|
1024
|
-
}
|
|
1025
|
-
} catch (error) {
|
|
1026
|
-
next = {
|
|
1027
|
-
...next,
|
|
1028
|
-
interactionRecovery: {
|
|
1029
|
-
status: "failed",
|
|
1030
|
-
code: error?.code || "INTERACTION_RECOVERY_FAILED",
|
|
1031
|
-
message: error?.message || String(error),
|
|
1032
|
-
recoveredAt: t,
|
|
1033
|
-
},
|
|
1034
|
-
};
|
|
1035
|
-
}
|
|
1036
|
-
if (next !== stateBeforeRecovery) {
|
|
1037
|
-
try {
|
|
1038
|
-
const recoveryMutation = mutateBackgroundAgentState(
|
|
1039
|
-
previous.id,
|
|
1040
|
-
(current) => {
|
|
1041
|
-
if (
|
|
1042
|
-
!current ||
|
|
1043
|
-
current.status !== stateBeforeRecovery.status ||
|
|
1044
|
-
current.lostReason !== stateBeforeRecovery.lostReason
|
|
1045
|
-
) {
|
|
1046
|
-
return null;
|
|
1047
|
-
}
|
|
1048
|
-
return {
|
|
1049
|
-
...current,
|
|
1050
|
-
phase: next.phase,
|
|
1051
|
-
pendingQuestion: next.pendingQuestion,
|
|
1052
|
-
interactionRecovery: next.interactionRecovery,
|
|
1053
|
-
};
|
|
1054
|
-
},
|
|
1055
|
-
);
|
|
1056
|
-
next = recoveryMutation.state || next;
|
|
1057
|
-
} catch {
|
|
1058
|
-
/* best-effort; the terminal transition itself is already durable */
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
1353
|
}
|
|
1354
|
+
if (previous?.sessionId)
|
|
1355
|
+
next = recoverTerminalBackgroundInteractions(next, options);
|
|
1063
1356
|
return next;
|
|
1064
1357
|
}
|
|
1065
1358
|
|
|
@@ -1113,28 +1406,40 @@ export function renameBackgroundAgent(id, title, options = {}) {
|
|
|
1113
1406
|
* conversation session is NOT touched — that's `cc session delete`.
|
|
1114
1407
|
*/
|
|
1115
1408
|
export function removeBackgroundAgent(id, options = {}) {
|
|
1116
|
-
let state =
|
|
1117
|
-
now: options.now,
|
|
1118
|
-
heartbeatStaleMs: options.heartbeatStaleMs,
|
|
1119
|
-
});
|
|
1409
|
+
let state = readBackgroundAgentState(id);
|
|
1120
1410
|
if (!state) throw new Error(`Background agent not found: ${id}`);
|
|
1121
|
-
|
|
1411
|
+
const explicitlyStoppable =
|
|
1412
|
+
state.status === "running" ||
|
|
1413
|
+
(state.status === "lost" && state.lostReason === "heartbeat-stale");
|
|
1414
|
+
if (!explicitlyStoppable) {
|
|
1415
|
+
state = effectiveBackgroundAgentState(state, {
|
|
1416
|
+
now: options.now,
|
|
1417
|
+
heartbeatStaleMs: options.heartbeatStaleMs,
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
if (explicitlyStoppable) {
|
|
1122
1421
|
if (options.force !== true) {
|
|
1123
1422
|
throw new Error(
|
|
1124
1423
|
`${id} is still running — stop it first (cc daemon stop ${id}) or pass --force`,
|
|
1125
1424
|
);
|
|
1126
1425
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
/* best-effort — the record removal below is the point of rm --force */
|
|
1426
|
+
const stopped = stopBackgroundAgent(id);
|
|
1427
|
+
state = readBackgroundAgentState(id) || stopped;
|
|
1428
|
+
if (state.status === "running" || stopped.stopPending === true) {
|
|
1429
|
+
const error = new Error(
|
|
1430
|
+
`${id} cannot be removed until process termination and interaction recovery complete`,
|
|
1431
|
+
);
|
|
1432
|
+
error.code = "BACKGROUND_AGENT_REMOVE_RECOVERY_PENDING";
|
|
1433
|
+
throw error;
|
|
1136
1434
|
}
|
|
1137
1435
|
}
|
|
1436
|
+
if (state.sessionId && !interactionRecoverySettledForState(state)) {
|
|
1437
|
+
const error = new Error(
|
|
1438
|
+
`${id} cannot be removed while interaction recovery is incomplete`,
|
|
1439
|
+
);
|
|
1440
|
+
error.code = "BACKGROUND_AGENT_REMOVE_RECOVERY_PENDING";
|
|
1441
|
+
throw error;
|
|
1442
|
+
}
|
|
1138
1443
|
const worktree = cleanupBackgroundAgentWorktree(state, {
|
|
1139
1444
|
keepWorktree: options.keepWorktree === true,
|
|
1140
1445
|
});
|
|
@@ -1145,6 +1450,7 @@ export function removeBackgroundAgent(id, options = {}) {
|
|
|
1145
1450
|
current.launchFinalizationUncertain !== true &&
|
|
1146
1451
|
!current.turnLaunchIntent &&
|
|
1147
1452
|
current.turnLaunchFinalizationUncertain !== true &&
|
|
1453
|
+
(!current.sessionId || interactionRecoverySettledForState(current)) &&
|
|
1148
1454
|
activeWorktreeProcesses(current).length === 0,
|
|
1149
1455
|
);
|
|
1150
1456
|
if (!deleted) throw new Error(`Background agent not found: ${id}`);
|
|
@@ -1622,7 +1928,24 @@ export function launchBackgroundAgent({
|
|
|
1622
1928
|
launchProfile,
|
|
1623
1929
|
apiKey,
|
|
1624
1930
|
}) {
|
|
1931
|
+
assertBackgroundArgvDurable(argv);
|
|
1932
|
+
assertBackgroundArgvDurable(followUpArgv, "background follow-up");
|
|
1625
1933
|
assertUsableCwd(cwd);
|
|
1934
|
+
const sessionPresence = _deps.getSessionPresence(sessionId);
|
|
1935
|
+
if (
|
|
1936
|
+
![SESSION_PRESENCE.ABSENT, SESSION_PRESENCE.PRESENT].includes(
|
|
1937
|
+
sessionPresence,
|
|
1938
|
+
)
|
|
1939
|
+
) {
|
|
1940
|
+
const error = new Error(
|
|
1941
|
+
`Background session ${sessionId} cannot launch from canonical presence ${sessionPresence}`,
|
|
1942
|
+
);
|
|
1943
|
+
error.code = "BACKGROUND_SESSION_BOOTSTRAP_EVIDENCE_INVALID";
|
|
1944
|
+
error.sessionId = sessionId;
|
|
1945
|
+
error.presence = sessionPresence;
|
|
1946
|
+
throw error;
|
|
1947
|
+
}
|
|
1948
|
+
const sessionBootstrapExpected = sessionPresence === SESSION_PRESENCE.ABSENT;
|
|
1626
1949
|
const worktreeState = normalizeBackgroundWorktree(worktree, cwd);
|
|
1627
1950
|
const id = createBackgroundAgentId();
|
|
1628
1951
|
const governanceState = normalizeBackgroundGovernance(governance, {
|
|
@@ -1663,6 +1986,7 @@ export function launchBackgroundAgent({
|
|
|
1663
1986
|
argv: initialSecrets.argv,
|
|
1664
1987
|
cwd,
|
|
1665
1988
|
sessionId,
|
|
1989
|
+
sessionBootstrapExpected,
|
|
1666
1990
|
title: title || "Background agent",
|
|
1667
1991
|
cliEntry: cliEntry || process.argv[1],
|
|
1668
1992
|
logFile: logPath(id),
|
|
@@ -1682,6 +2006,7 @@ export function launchBackgroundAgent({
|
|
|
1682
2006
|
id,
|
|
1683
2007
|
workerGeneration,
|
|
1684
2008
|
sessionId,
|
|
2009
|
+
sessionBootstrapExpected,
|
|
1685
2010
|
title: job.title,
|
|
1686
2011
|
cwd,
|
|
1687
2012
|
pid: null,
|
|
@@ -1798,6 +2123,14 @@ export function launchBackgroundAgent({
|
|
|
1798
2123
|
status: "failed",
|
|
1799
2124
|
endedAt: Date.now(),
|
|
1800
2125
|
lostReason: `spawn-error: ${error.code || error.message}`,
|
|
2126
|
+
interactionRecovery: current.sessionId
|
|
2127
|
+
? {
|
|
2128
|
+
status: "failed",
|
|
2129
|
+
code: "INTERACTION_WORKER_SPAWN_ERROR",
|
|
2130
|
+
message: error?.message || String(error),
|
|
2131
|
+
recoveredAt: Date.now(),
|
|
2132
|
+
}
|
|
2133
|
+
: current.interactionRecovery,
|
|
1801
2134
|
launchFinalizationUncertain: false,
|
|
1802
2135
|
});
|
|
1803
2136
|
}
|
|
@@ -1823,6 +2156,15 @@ export function launchBackgroundAgent({
|
|
|
1823
2156
|
lostReason: `worker-exited-before-finalize: exit=${
|
|
1824
2157
|
Number.isInteger(code) ? code : "null"
|
|
1825
2158
|
} signal=${signal || "none"}`,
|
|
2159
|
+
interactionRecovery: current.sessionId
|
|
2160
|
+
? {
|
|
2161
|
+
status: "failed",
|
|
2162
|
+
code: "INTERACTION_WORKER_EXITED_BEFORE_FINALIZE",
|
|
2163
|
+
message:
|
|
2164
|
+
"The background worker exited before interaction recovery completed",
|
|
2165
|
+
recoveredAt: Date.now(),
|
|
2166
|
+
}
|
|
2167
|
+
: current.interactionRecovery,
|
|
1826
2168
|
launchFinalizationUncertain: false,
|
|
1827
2169
|
});
|
|
1828
2170
|
}
|
|
@@ -1831,19 +2173,37 @@ export function launchBackgroundAgent({
|
|
|
1831
2173
|
}
|
|
1832
2174
|
});
|
|
1833
2175
|
}
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
2176
|
+
// Read child.pid before entering the state transaction. A platform wrapper
|
|
2177
|
+
// (notably the Windows Job Object adapter) can return from spawn just as the
|
|
2178
|
+
// real worker claims the state record. Finalize the wrapper pid against the
|
|
2179
|
+
// freshest locked record so a launcher snapshot can never erase a turn the
|
|
2180
|
+
// worker already published (agentPid, pendingQuestion, recovery marker,
|
|
2181
|
+
// turnCount, ...).
|
|
2182
|
+
const spawnedWorkerPid = child.pid;
|
|
2183
|
+
const pidFinalization = mutateBackgroundAgentState(id, (current) => {
|
|
2184
|
+
if (
|
|
2185
|
+
!current ||
|
|
2186
|
+
(current.workerGeneration &&
|
|
2187
|
+
current.workerGeneration !== workerGeneration)
|
|
2188
|
+
) {
|
|
2189
|
+
return null;
|
|
2190
|
+
}
|
|
2191
|
+
return {
|
|
2192
|
+
...current,
|
|
2193
|
+
// Once the real worker has claimed the record its process.pid is the
|
|
2194
|
+
// authoritative identity; otherwise initialize both fields from the
|
|
2195
|
+
// platform child returned by spawn.
|
|
2196
|
+
pid: current.pid ?? spawnedWorkerPid,
|
|
2197
|
+
workerPid: current.workerPid ?? spawnedWorkerPid,
|
|
2198
|
+
launchFinalizationUncertain: false,
|
|
2199
|
+
};
|
|
2200
|
+
});
|
|
2201
|
+
if (!pidFinalization.applied || !pidFinalization.state) {
|
|
2202
|
+
throw new Error(
|
|
2203
|
+
`Background worker ${id} lost its launch-generation state before pid finalization`,
|
|
2204
|
+
);
|
|
2205
|
+
}
|
|
2206
|
+
withPid = pidFinalization.state;
|
|
1847
2207
|
child.unref();
|
|
1848
2208
|
} catch (error) {
|
|
1849
2209
|
postSpawnError ||= error;
|
|
@@ -1866,10 +2226,171 @@ export function readBackgroundAgentLog(id, options = {}) {
|
|
|
1866
2226
|
return lines.slice(-limit).join("\n");
|
|
1867
2227
|
}
|
|
1868
2228
|
|
|
2229
|
+
function recordedBackgroundProcessIdentities(state) {
|
|
2230
|
+
const records = [
|
|
2231
|
+
{ role: "agent", pid: state?.agentPid, startedAt: state?.agentStartedAt },
|
|
2232
|
+
{ role: "worker", pid: state?.pid, startedAt: state?.startedAt },
|
|
2233
|
+
{
|
|
2234
|
+
role: "worker-wrapper",
|
|
2235
|
+
pid: state?.workerPid,
|
|
2236
|
+
startedAt: state?.startedAt,
|
|
2237
|
+
},
|
|
2238
|
+
];
|
|
2239
|
+
const seen = new Set();
|
|
2240
|
+
return records.filter((record) => {
|
|
2241
|
+
const pid = Number(record.pid);
|
|
2242
|
+
if (!Number.isInteger(pid) || pid <= 0 || seen.has(pid)) return false;
|
|
2243
|
+
seen.add(pid);
|
|
2244
|
+
record.pid = pid;
|
|
2245
|
+
return true;
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
function inspectRecordedBackgroundProcesses(state) {
|
|
2250
|
+
return recordedBackgroundProcessIdentities(state).map((record) => ({
|
|
2251
|
+
...record,
|
|
2252
|
+
identity: inspectDestructiveProcessIdentity(record.pid, record.startedAt),
|
|
2253
|
+
}));
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
function recordedBackgroundProcessesAlive(state) {
|
|
2257
|
+
return inspectRecordedBackgroundProcesses(state).some(({ identity }) =>
|
|
2258
|
+
["match", "unverifiable", "self"].includes(identity.status),
|
|
2259
|
+
);
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
function isRecordedProcessTreeExecutionAlive(pid, startedAt) {
|
|
2263
|
+
const identity = inspectDestructiveProcessIdentity(pid, startedAt);
|
|
2264
|
+
// `unverifiable` includes a vanished/zombie POSIX leader whose process group
|
|
2265
|
+
// is still executable or whose group snapshot failed. Both must block
|
|
2266
|
+
// worktree deletion just as a verified live leader does.
|
|
2267
|
+
return ["match", "unverifiable", "self"].includes(identity.status);
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
function signalBackgroundProcessTree(pid, signal = "SIGTERM") {
|
|
2271
|
+
const target = Number(pid);
|
|
2272
|
+
if (process.platform === "win32") {
|
|
2273
|
+
const killed = runSupervisorCommand(
|
|
2274
|
+
"taskkill",
|
|
2275
|
+
["/PID", String(target), "/T", "/F"],
|
|
2276
|
+
{ windowsHide: true, encoding: "utf8" },
|
|
2277
|
+
"background-agent:stop-tree",
|
|
2278
|
+
);
|
|
2279
|
+
if (killed.error || (killed.status !== 0 && isProcessAlive(target))) {
|
|
2280
|
+
throw new Error(
|
|
2281
|
+
killed.error?.message ||
|
|
2282
|
+
killed.stderr ||
|
|
2283
|
+
`taskkill exited ${killed.status}`,
|
|
2284
|
+
);
|
|
2285
|
+
}
|
|
2286
|
+
return "process-tree";
|
|
2287
|
+
}
|
|
2288
|
+
try {
|
|
2289
|
+
_deps.kill(-target, signal);
|
|
2290
|
+
return "process-group";
|
|
2291
|
+
} catch {
|
|
2292
|
+
_deps.kill(target, signal);
|
|
2293
|
+
return "process";
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
function persistStopPending(state, reason, details = {}) {
|
|
2298
|
+
const at = Date.now();
|
|
2299
|
+
const mutation = mutateBackgroundAgentState(state.id, (current) =>
|
|
2300
|
+
current
|
|
2301
|
+
? {
|
|
2302
|
+
...current,
|
|
2303
|
+
phase:
|
|
2304
|
+
reason === "identity-unverifiable"
|
|
2305
|
+
? "stop_identity_unverifiable"
|
|
2306
|
+
: "stop_waiting_for_exit",
|
|
2307
|
+
stopPending: true,
|
|
2308
|
+
stopPendingReason: reason,
|
|
2309
|
+
...(details.identity
|
|
2310
|
+
? { processIdentityError: details.identity }
|
|
2311
|
+
: {}),
|
|
2312
|
+
...(current.sessionId
|
|
2313
|
+
? {
|
|
2314
|
+
interactionRecovery: {
|
|
2315
|
+
status: "failed",
|
|
2316
|
+
code:
|
|
2317
|
+
reason === "identity-unverifiable"
|
|
2318
|
+
? "BACKGROUND_PROCESS_IDENTITY_UNVERIFIABLE"
|
|
2319
|
+
: "INTERACTION_RECOVERY_CHILD_STILL_RUNNING",
|
|
2320
|
+
message:
|
|
2321
|
+
details.message ||
|
|
2322
|
+
"Process termination is still pending; interaction recovery has not started",
|
|
2323
|
+
recoveredAt: at,
|
|
2324
|
+
turn: Number(current.turnCount || 0),
|
|
2325
|
+
workerGeneration: current.workerGeneration || null,
|
|
2326
|
+
},
|
|
2327
|
+
}
|
|
2328
|
+
: {}),
|
|
2329
|
+
}
|
|
2330
|
+
: null,
|
|
2331
|
+
);
|
|
2332
|
+
return {
|
|
2333
|
+
...(mutation.state || state),
|
|
2334
|
+
stopped: false,
|
|
2335
|
+
stopPending: true,
|
|
2336
|
+
stopPendingReason: reason,
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
function waitForSignalledProcessesToExit(
|
|
2341
|
+
targets,
|
|
2342
|
+
deadlineMs = _deps.processExitWaitDeadlineMs,
|
|
2343
|
+
) {
|
|
2344
|
+
const configuredDeadline = Number(deadlineMs);
|
|
2345
|
+
const waitMs = Number.isFinite(configuredDeadline)
|
|
2346
|
+
? Math.max(0, configuredDeadline)
|
|
2347
|
+
: 2_000;
|
|
2348
|
+
const deadline = Date.now() + waitMs;
|
|
2349
|
+
while (targets.some(isSignalledTargetExecutionAlive)) {
|
|
2350
|
+
if (Date.now() >= deadline) break;
|
|
2351
|
+
sleepSyncMs(10);
|
|
2352
|
+
}
|
|
2353
|
+
return targets.filter((target) => {
|
|
2354
|
+
if (!isSignalledTargetExecutionAlive(target)) return false;
|
|
2355
|
+
const identity = inspectDestructiveProcessIdentity(
|
|
2356
|
+
target.pid,
|
|
2357
|
+
target.startedAt,
|
|
2358
|
+
);
|
|
2359
|
+
return ["match", "unverifiable", "self"].includes(identity.status);
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
function recoverStoppedInteractions(state) {
|
|
2364
|
+
if (!state?.sessionId) return { state, recovery: null };
|
|
2365
|
+
const recovery = withSessionHostRecoveryLease(state.sessionId, () =>
|
|
2366
|
+
rejectPendingBackgroundInteractions(state.sessionId, state.id, {
|
|
2367
|
+
fallbackRequest: backgroundInteractionFallback(state),
|
|
2368
|
+
code: "INTERACTION_STOPPED",
|
|
2369
|
+
message:
|
|
2370
|
+
"The background agent was stopped before the interaction settled",
|
|
2371
|
+
}),
|
|
2372
|
+
);
|
|
2373
|
+
return {
|
|
2374
|
+
state,
|
|
2375
|
+
recovery: {
|
|
2376
|
+
status: recovery.changed ? "rejected" : "clean",
|
|
2377
|
+
requestIds: recovery.rejected.map((record) => record.requestId),
|
|
2378
|
+
recoveredAt: Date.now(),
|
|
2379
|
+
turn: Number(state.turnCount || 0),
|
|
2380
|
+
workerGeneration: state.workerGeneration || null,
|
|
2381
|
+
},
|
|
2382
|
+
};
|
|
2383
|
+
}
|
|
2384
|
+
|
|
1869
2385
|
export function stopBackgroundAgent(id) {
|
|
1870
|
-
|
|
2386
|
+
// Explicit stop starts from the raw durable record. Read-only reconciliation
|
|
2387
|
+
// may already have classified a stale heartbeat as `lost`, but a verified
|
|
2388
|
+
// live worker still owns a process tree and must remain stoppable.
|
|
2389
|
+
let state = readBackgroundAgentState(id);
|
|
1871
2390
|
if (!state) throw new Error(`Background agent not found: ${id}`);
|
|
1872
|
-
|
|
2391
|
+
const staleHeartbeatOwner =
|
|
2392
|
+
state.status === "lost" && state.lostReason === "heartbeat-stale";
|
|
2393
|
+
if (state.status !== "running" && !staleHeartbeatOwner) {
|
|
1873
2394
|
// Gap 2: stopping an already-lost session still reaps a leaked agent
|
|
1874
2395
|
// child recorded before the worker died (identity-guarded inside).
|
|
1875
2396
|
if (state.status === "lost") {
|
|
@@ -1878,18 +2399,34 @@ export function stopBackgroundAgent(id) {
|
|
|
1878
2399
|
} catch {
|
|
1879
2400
|
/* best-effort */
|
|
1880
2401
|
}
|
|
2402
|
+
if (
|
|
2403
|
+
state.sessionId &&
|
|
2404
|
+
!["clean", "rejected"].includes(state.interactionRecovery?.status)
|
|
2405
|
+
) {
|
|
2406
|
+
state = recoverTerminalBackgroundInteractions(state);
|
|
2407
|
+
}
|
|
1881
2408
|
}
|
|
1882
2409
|
return { ...state, stopped: false };
|
|
1883
2410
|
}
|
|
1884
2411
|
const stopRequestedAt = Date.now();
|
|
1885
2412
|
const fence = mutateBackgroundAgentState(id, (current) => {
|
|
1886
|
-
if (
|
|
2413
|
+
if (
|
|
2414
|
+
!current ||
|
|
2415
|
+
(current.status !== "running" &&
|
|
2416
|
+
!(
|
|
2417
|
+
current.status === "lost" && current.lostReason === "heartbeat-stale"
|
|
2418
|
+
))
|
|
2419
|
+
) {
|
|
2420
|
+
return null;
|
|
2421
|
+
}
|
|
1887
2422
|
return {
|
|
1888
2423
|
...current,
|
|
1889
2424
|
stopRequestedAt: current.stopRequestedAt || stopRequestedAt,
|
|
1890
2425
|
stopRequestedBy: "user",
|
|
1891
2426
|
phase: "stopping",
|
|
1892
2427
|
transport: null,
|
|
2428
|
+
stopPending: false,
|
|
2429
|
+
stopPendingReason: null,
|
|
1893
2430
|
};
|
|
1894
2431
|
});
|
|
1895
2432
|
if (!fence.applied) {
|
|
@@ -1897,10 +2434,7 @@ export function stopBackgroundAgent(id) {
|
|
|
1897
2434
|
return { ...fence.state, stopped: false };
|
|
1898
2435
|
}
|
|
1899
2436
|
state = fence.state;
|
|
1900
|
-
if (
|
|
1901
|
-
state.turnLaunchIntent ||
|
|
1902
|
-
state.turnLaunchFinalizationUncertain === true
|
|
1903
|
-
) {
|
|
2437
|
+
if (state.launchFinalizationUncertain === true || state.turnLaunchIntent) {
|
|
1904
2438
|
// The worker has durably announced that native spawn is prepared or that
|
|
1905
2439
|
// post-spawn pid persistence is unresolved. Killing the worker from this
|
|
1906
2440
|
// snapshot could strand its detached child before ownership is published.
|
|
@@ -1910,9 +2444,11 @@ export function stopBackgroundAgent(id) {
|
|
|
1910
2444
|
...state,
|
|
1911
2445
|
stopped: false,
|
|
1912
2446
|
stopPending: true,
|
|
1913
|
-
stopPendingReason: state.
|
|
1914
|
-
? "
|
|
1915
|
-
:
|
|
2447
|
+
stopPendingReason: state.launchFinalizationUncertain
|
|
2448
|
+
? "launch-finalization"
|
|
2449
|
+
: state.turnLaunchIntent
|
|
2450
|
+
? "turn-launch-intent"
|
|
2451
|
+
: "turn-launch-finalization",
|
|
1916
2452
|
};
|
|
1917
2453
|
}
|
|
1918
2454
|
// Self-pid guard: a worker record whose pid is the CURRENT process is
|
|
@@ -1927,90 +2463,136 @@ export function stopBackgroundAgent(id) {
|
|
|
1927
2463
|
endedAt: Date.now(),
|
|
1928
2464
|
lostReason: "self-pid-corrupt-record",
|
|
1929
2465
|
};
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
// Gap 1: last-instant identity re-check before the kill — never
|
|
1934
|
-
// taskkill/SIGTERM a pid the OS has re-assigned to an unrelated process.
|
|
1935
|
-
if (!isSameProcess(state.pid, state.startedAt)) {
|
|
1936
|
-
try {
|
|
1937
|
-
reclaimOrphanAgentProcess(state);
|
|
1938
|
-
} catch {
|
|
1939
|
-
/* best-effort */
|
|
2466
|
+
let written = writeBackgroundAgentState(lost) || lost;
|
|
2467
|
+
if (written.sessionId) {
|
|
2468
|
+
written = recoverTerminalBackgroundInteractions(written);
|
|
1940
2469
|
}
|
|
1941
|
-
const lost = {
|
|
1942
|
-
...state,
|
|
1943
|
-
status: "lost",
|
|
1944
|
-
endedAt: Date.now(),
|
|
1945
|
-
lostReason: "pid-reused",
|
|
1946
|
-
};
|
|
1947
|
-
const written = writeBackgroundAgentState(lost) || lost;
|
|
1948
2470
|
return { ...written, stopped: false };
|
|
1949
2471
|
}
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
2472
|
+
let postKill = state;
|
|
2473
|
+
let workerPidReused = false;
|
|
2474
|
+
const signalledPids = new Set();
|
|
2475
|
+
// A worker may publish a just-spawned child identity while the stop fence is
|
|
2476
|
+
// being observed. Re-read once after the first termination round so that a
|
|
2477
|
+
// newly durable, exact-owned agent tree is also terminated before recovery.
|
|
2478
|
+
for (let round = 0; round < 2; round++) {
|
|
2479
|
+
const inspected = inspectRecordedBackgroundProcesses(postKill);
|
|
2480
|
+
const unverifiable = inspected.filter(({ identity }) =>
|
|
2481
|
+
["unverifiable", "self"].includes(identity.status),
|
|
2482
|
+
);
|
|
2483
|
+
if (unverifiable.length > 0) {
|
|
2484
|
+
return persistStopPending(postKill, "identity-unverifiable", {
|
|
2485
|
+
identity: unverifiable.map(({ role, pid, identity }) => ({
|
|
2486
|
+
role,
|
|
2487
|
+
pid,
|
|
2488
|
+
reason: identity.reason || identity.status,
|
|
2489
|
+
})),
|
|
2490
|
+
message:
|
|
2491
|
+
"Process identity could not be verified; no destructive signal was issued",
|
|
2492
|
+
});
|
|
2493
|
+
}
|
|
2494
|
+
const primaryWorker =
|
|
2495
|
+
inspected.find(({ role }) => role === "worker") ||
|
|
2496
|
+
inspected.find(({ role }) => role === "worker-wrapper");
|
|
2497
|
+
if (primaryWorker?.identity.status === "reused") workerPidReused = true;
|
|
2498
|
+
const targets = inspected.filter(
|
|
2499
|
+
({ pid, identity }) =>
|
|
2500
|
+
identity.status === "match" && !signalledPids.has(pid),
|
|
2501
|
+
);
|
|
2502
|
+
if (targets.length === 0) break;
|
|
2503
|
+
try {
|
|
2504
|
+
// The child is detached into its own group and therefore gets its own
|
|
2505
|
+
// exact-identity signal before the worker tree is terminated.
|
|
2506
|
+
for (const target of targets) {
|
|
2507
|
+
target.signalledAsGroup =
|
|
2508
|
+
signalBackgroundProcessTree(target.pid, "SIGTERM") ===
|
|
2509
|
+
"process-group";
|
|
2510
|
+
signalledPids.add(target.pid);
|
|
1965
2511
|
}
|
|
1966
|
-
}
|
|
2512
|
+
} catch (error) {
|
|
1967
2513
|
try {
|
|
1968
|
-
|
|
2514
|
+
mutateBackgroundAgentState(id, (current) =>
|
|
2515
|
+
current
|
|
2516
|
+
? {
|
|
2517
|
+
...current,
|
|
2518
|
+
phase: "stop_failed",
|
|
2519
|
+
stopPending: true,
|
|
2520
|
+
stopPendingReason: "process-termination",
|
|
2521
|
+
stopError: error?.message || String(error),
|
|
2522
|
+
}
|
|
2523
|
+
: null,
|
|
2524
|
+
);
|
|
1969
2525
|
} catch {
|
|
1970
|
-
|
|
1971
|
-
}
|
|
1972
|
-
// The worker detaches the agent child into its OWN process group (so a
|
|
1973
|
-
// crashed worker's child can be group-killed) — which also means the
|
|
1974
|
-
// worker-group SIGTERM above no longer reaches it. Kill the agent group
|
|
1975
|
-
// explicitly, identity-guarded via its agentStartedAt anchor.
|
|
1976
|
-
const agentPid = Number(state.agentPid);
|
|
1977
|
-
const anchor = Number(state.agentStartedAt);
|
|
1978
|
-
if (
|
|
1979
|
-
Number.isInteger(agentPid) &&
|
|
1980
|
-
agentPid > 0 &&
|
|
1981
|
-
agentPid !== Number(state.pid) &&
|
|
1982
|
-
agentPid !== process.pid &&
|
|
1983
|
-
Number.isFinite(anchor) &&
|
|
1984
|
-
anchor > 0 &&
|
|
1985
|
-
isSameProcess(agentPid, anchor)
|
|
1986
|
-
) {
|
|
1987
|
-
try {
|
|
1988
|
-
_deps.kill(-agentPid, "SIGTERM");
|
|
1989
|
-
} catch {
|
|
1990
|
-
try {
|
|
1991
|
-
_deps.kill(agentPid, "SIGTERM");
|
|
1992
|
-
} catch {
|
|
1993
|
-
/* already gone */
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
2526
|
+
/* the durable stopRequestedAt fence already blocks new turns */
|
|
1996
2527
|
}
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
try {
|
|
2000
|
-
mutateBackgroundAgentState(id, (current) =>
|
|
2001
|
-
current
|
|
2002
|
-
? {
|
|
2003
|
-
...current,
|
|
2004
|
-
phase: "stop_failed",
|
|
2005
|
-
stopError: error?.message || String(error),
|
|
2006
|
-
}
|
|
2007
|
-
: null,
|
|
2528
|
+
throw new Error(
|
|
2529
|
+
`Failed to stop background agent ${id}: ${error.message}`,
|
|
2008
2530
|
);
|
|
2009
|
-
} catch {
|
|
2010
|
-
/* the durable stopRequestedAt fence already blocks new turns */
|
|
2011
2531
|
}
|
|
2012
|
-
|
|
2532
|
+
const stillAlive = waitForSignalledProcessesToExit(targets);
|
|
2533
|
+
postKill = readBackgroundAgentState(id) || postKill;
|
|
2534
|
+
if (stillAlive.length > 0) {
|
|
2535
|
+
return persistStopPending(postKill, "process-exit", {
|
|
2536
|
+
message:
|
|
2537
|
+
"Process termination is still pending; interaction recovery has not started",
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2540
|
+
postKill = readBackgroundAgentState(id) || postKill;
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
if (recordedBackgroundProcessesAlive(postKill)) {
|
|
2544
|
+
return persistStopPending(postKill, "process-exit");
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
if (workerPidReused) {
|
|
2548
|
+
const lostMutation = mutateBackgroundAgentState(id, (current) =>
|
|
2549
|
+
current
|
|
2550
|
+
? {
|
|
2551
|
+
...current,
|
|
2552
|
+
status: "lost",
|
|
2553
|
+
endedAt: current.endedAt || Date.now(),
|
|
2554
|
+
lostReason: "pid-reused",
|
|
2555
|
+
phase: null,
|
|
2556
|
+
stopPending: false,
|
|
2557
|
+
stopPendingReason: null,
|
|
2558
|
+
}
|
|
2559
|
+
: null,
|
|
2560
|
+
);
|
|
2561
|
+
let lost = lostMutation.state || postKill;
|
|
2562
|
+
if (lost.sessionId) lost = recoverTerminalBackgroundInteractions(lost);
|
|
2563
|
+
return { ...lost, stopped: false };
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
let stopRecovery = null;
|
|
2567
|
+
try {
|
|
2568
|
+
stopRecovery = recoverStoppedInteractions(postKill).recovery;
|
|
2569
|
+
} catch (error) {
|
|
2570
|
+
const failed = mutateBackgroundAgentState(id, (current) =>
|
|
2571
|
+
current
|
|
2572
|
+
? {
|
|
2573
|
+
...current,
|
|
2574
|
+
phase: "stop_recovery_failed",
|
|
2575
|
+
interactionRecovery: {
|
|
2576
|
+
status: "failed",
|
|
2577
|
+
code: error?.code || "INTERACTION_RECOVERY_FAILED",
|
|
2578
|
+
message: error?.message || String(error),
|
|
2579
|
+
recoveredAt: Date.now(),
|
|
2580
|
+
turn: Number(current.turnCount || 0),
|
|
2581
|
+
workerGeneration: current.workerGeneration || null,
|
|
2582
|
+
},
|
|
2583
|
+
stopPending: true,
|
|
2584
|
+
stopPendingReason: "interaction-recovery",
|
|
2585
|
+
}
|
|
2586
|
+
: null,
|
|
2587
|
+
);
|
|
2588
|
+
return {
|
|
2589
|
+
...(failed.state || postKill),
|
|
2590
|
+
stopped: false,
|
|
2591
|
+
stopPending: true,
|
|
2592
|
+
stopPendingReason: "interaction-recovery",
|
|
2593
|
+
};
|
|
2013
2594
|
}
|
|
2595
|
+
|
|
2014
2596
|
// Build the terminal write from the freshest record under the lock. In
|
|
2015
2597
|
// particular, never spread the pre-kill fence snapshot over a worker's
|
|
2016
2598
|
// concurrently persisted process-tree termination confirmation.
|
|
@@ -2021,23 +2603,19 @@ export function stopBackgroundAgent(id) {
|
|
|
2021
2603
|
status: "stopped",
|
|
2022
2604
|
endedAt: current.endedAt || Date.now(),
|
|
2023
2605
|
stoppedByUser: true,
|
|
2606
|
+
lostReason: null,
|
|
2024
2607
|
phase: null,
|
|
2608
|
+
pendingQuestion: null,
|
|
2025
2609
|
transport: null,
|
|
2026
2610
|
stopError: null,
|
|
2611
|
+
stopPending: false,
|
|
2612
|
+
stopPendingReason: null,
|
|
2613
|
+
processIdentityError: null,
|
|
2614
|
+
...(stopRecovery ? { interactionRecovery: stopRecovery } : {}),
|
|
2027
2615
|
}
|
|
2028
2616
|
: null,
|
|
2029
2617
|
);
|
|
2030
2618
|
const written = terminal.state || state;
|
|
2031
|
-
// A turn-spawn persistence failure may publish its agent pid after the
|
|
2032
|
-
// stopper took the first snapshot. Re-read/merge above preserves that
|
|
2033
|
-
// uncertainty identity; make one final identity-guarded tree reap attempt.
|
|
2034
|
-
if (written.agentPid) {
|
|
2035
|
-
try {
|
|
2036
|
-
reclaimOrphanAgentProcess(written);
|
|
2037
|
-
} catch {
|
|
2038
|
-
/* retained agentPid + uncertainty fence block destructive cleanup */
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
2041
2619
|
return { ...written, stopped: written.status === "stopped" };
|
|
2042
2620
|
}
|
|
2043
2621
|
|
|
@@ -2046,14 +2624,14 @@ function activeWorktreeProcesses(state) {
|
|
|
2046
2624
|
if (
|
|
2047
2625
|
Number.isInteger(Number(state?.pid)) &&
|
|
2048
2626
|
Number(state.pid) > 0 &&
|
|
2049
|
-
|
|
2627
|
+
isRecordedProcessTreeExecutionAlive(state.pid, state.startedAt)
|
|
2050
2628
|
) {
|
|
2051
2629
|
active.push("worker");
|
|
2052
2630
|
}
|
|
2053
2631
|
if (
|
|
2054
2632
|
Number.isInteger(Number(state?.agentPid)) &&
|
|
2055
2633
|
Number(state.agentPid) > 0 &&
|
|
2056
|
-
|
|
2634
|
+
isRecordedProcessTreeExecutionAlive(state.agentPid, state.agentStartedAt)
|
|
2057
2635
|
) {
|
|
2058
2636
|
active.push("agent");
|
|
2059
2637
|
}
|