chainlesschain 0.165.2 → 0.165.3
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-lS1R0Cza.js → AIOps-BiHNUlbU.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BmlgBPsj.js → ActionButton-D-UFjDRQ.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CNKczv1S.js → Analytics-DazVsDgW.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-BuZGJARQ.js → AppLayout-DXh9UfNJ.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-Dm73Sl-O.js → Artifacts-CZ6tsceY.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-BGMW7A5e.js → Audit-RyxnBr_h.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-BQ6M54JR.js → BackgroundAgents-BWS51Q6X.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DBzGP0bR.js → Backup-BRtPcUul.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-D1mLCoOV.js → BaseInput-CGd4vZYY.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-BUfcN7p8.js → Chat-h8ijVwCg.js} +4 -4
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CY5m8ZHg.js → ChatBubbleRenderer-D7tcdDS9.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-Du0Yg5Pi.js → Checkbox-NyF6dPl8.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-B2RXziHW.js → Codegen-Ci4OOxr5.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DWhekOrR.js → Col-CwTkc9BM.js} +1 -1
- package/src/assets/web-panel/assets/{Community-HQhrkon4.js → Community-DW_tM82m.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-izHNb4uz.js → Compact-DEvHRV4S.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-BDwLy41-.js → Compliance-BbjBEEfD.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-DFJhLa7K.js → Cowork-10IXBaBa.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DYY9RG7-.js → Cron-BZkbpd5I.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-rr7tJTtv.js → Crosschain-CFQ8L_6v.js} +1 -1
- package/src/assets/web-panel/assets/{DID-D2sfHRLi.js → DID-BDF6AkXe.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-q_eGdhha.js → Dashboard-CoOdWMi4.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-Cufrh2Bz.js → Dropdown-DEsGmu8Y.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-B-kdv10f.js → EmailListRenderer-BS89SWlK.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-D21XjgfG.js → FamilyGuardDashboard-C1FpCHiI.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CAwRTLZH.js → Federation-BzvpVmLA.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-vx3daNDF.js → FormItemContext-BeC259PQ.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-CRWY2wU4.js → GenericCardRenderer-azgG3bmV.js} +1 -1
- package/src/assets/web-panel/assets/{Git-Q6SwauyD.js → Git-BmSSowc5.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BJ1q4IQZ.js → Governance-CTBCSyQZ.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CSWjROgD.js → Inference-DA5xyk4a.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-fzuQsWuY.js → KnowledgeGraph-D1gPHNig.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DVF8Mnns.js → Logs-BPmLf1AR.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-SVLOg49H.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-BQP7LOA4.js → Marketplace-DtUFcR0E.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BNlJCmNc.js → McpTools-DS_kqBv-.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-DLLg5WQ4.js → Memory-Fvh7XL7V.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-CR2skNAF.js → MobileBridge-C7itAP45.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-DGbP36-3.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-B17pgqMI.js → Mtc-B57MiNLK.js} +6 -6
- package/src/assets/web-panel/assets/{MtcAudit-4UmV5cIL.js → MtcAudit-BXfCUcM7.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-DoMEXEcU.js → Multisig-yaJ-ZcF6.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-h9DMXFX4.js → NLProgramming-CoaUCKrO.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-Cihn-xX8.js → Notes-BKLM_65R.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BE6ZGa3R.js → NotificationSettings-DoDtK9uZ.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-BJ28TGr-.js +1 -0
- package/src/assets/web-panel/assets/{Organization-B2vpwmjb.js → Organization-MuGsdoIb.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-C4z28R0-.js → Overflow-BLUN9J4R.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BVlFkwhW.js → P2P-DWuoBUPd.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-BXbEdL5B.js → PdhVaultBrowser-CUi37YPj.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-C28xs4lp.js → Permissions-BYQg2pcr.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-Dgl_rBjM.js → PersonalDataHub-CAxoV2xu.js} +6 -6
- package/src/assets/web-panel/assets/{Pipeline-3QUIhrKI.js → Pipeline-B33wGvIO.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-8Gh33OZ5.js → Privacy-A8yXTORP.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BNmg4tZi.js → ProjectInit-D49I2I1O.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-vgMfeCf9.js → ProjectSettings-Ci73jJDw.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BhtAVilc.js → Projects-CF-yD6Cn.js} +1 -1
- package/src/assets/web-panel/assets/Providers-D-etEhg5.js +1 -0
- package/src/assets/web-panel/assets/{QrScannerModal-BhfTF0oS.js → QrScannerModal-Brza0ykJ.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-C746HEAj.js → QuickAsk-DFydhjxL.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BSej8Eo0.js → Recommend-DLF8_NhA.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-f3gmnlG3.js → RemoteSession-dyuStW3x.js} +3 -3
- package/src/assets/web-panel/assets/{Reputation-CyvybpkN.js → Reputation-5F3tA6yw.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DwV2PeYn.js → Row-Ca2A57S5.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-11GrzjZy.js → RssFeed-B0bxBkq_.js} +3 -3
- package/src/assets/web-panel/assets/{Search-BjKXkrAY.js → Search-CFNvHalZ.js} +1 -1
- package/src/assets/web-panel/assets/{Security-eWiFb9Yk.js → Security-DSoH3Hav.js} +4 -4
- package/src/assets/web-panel/assets/{Services-CdCsMAtt.js → Services-BHpfEmBQ.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-4Q2UTKQ5.js → Skeleton-Dsh-QNmn.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-v4rtYrRj.js → Skills-BhT1NkSU.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-CRhsFvd_.js → Sla-DY9snPeJ.js} +1 -1
- package/src/assets/web-panel/assets/SpeechSettings-CWaX0_Cx.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-D16zonl7.js → SyncSettings-B690y_TC.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-Xxozz0i_.js → Tasks-2N-w3zWd.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-CXJpPtCT.js → Templates-BjwQOQYC.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-XRV0L1ZF.js → Tenant-CUqHErxY.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-CYuO1i_w.js → Terminal-C13rXvNy.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-BXqitWsu.js → TimelineRenderer-CpbWcya_.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BvPrDHIy.js → Tokens-BXHLGXYC.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DhCrSTf5.js → Trigger-1e5i65o3.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DmsuZD6K.js → Trust-FLGqNHs5.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-D_yrts5C.js → UkeySign-BbaR_LJB.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-B01ihPrm.js → VideoEditing-NOp4RIov.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DpUjKa-N.js → Wallet-B7DW7bG4.js} +3 -3
- package/src/assets/web-panel/assets/{WebAuthn-CL07yON6.js → WebAuthn-BptSUWix.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-CLMflnRr.js → WorkflowEditor-CAJnDOXS.js} +1 -1
- package/src/assets/web-panel/assets/{chat-DtUqe8h_.js → chat-I8LgG1-Z.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DPKrgVfy.js → colors-CWe7SOVD.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BTt2GtOm.js → compact-item-Cx3u3CA3.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-D_VeUc1J.js → createContext-CgD5ArwY.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DLv_872g.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-1kndiylj.js → hasIn-BfQWiXN8.js} +1 -1
- package/src/assets/web-panel/assets/{index-brCInrzq.js → index-3iKP9OTJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-l1BETrDb.js → index-87gMxqhN.js} +1 -1
- package/src/assets/web-panel/assets/{index-DxdTdBix.js → index-B1cX4Rxu.js} +1 -1
- package/src/assets/web-panel/assets/{index-DdkplL9R.js → index-B6yChzt1.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHpBMWCh.js → index-BREnPKGo.js} +1 -1
- package/src/assets/web-panel/assets/{index-CzjkKUmR.js → index-BUAR7RY-.js} +1 -1
- package/src/assets/web-panel/assets/{index-D95OFht7.js → index-Bi24S7Dw.js} +1 -1
- package/src/assets/web-panel/assets/{index-8nz_fmlj.js → index-BiI-xPRH.js} +1 -1
- package/src/assets/web-panel/assets/{index-2eyAXE3d.js → index-BuEQSorC.js} +1 -1
- package/src/assets/web-panel/assets/{index-GsoUQ2M7.js → index-Bx7BNIRH.js} +1 -1
- package/src/assets/web-panel/assets/{index-CX81qSM4.js → index-C-rIKccl.js} +1 -1
- package/src/assets/web-panel/assets/{index-CcO04eDn.js → index-C0nDKhWv.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bhudc_Ez.js → index-C0ztiqtB.js} +1 -1
- package/src/assets/web-panel/assets/{index-DfGl8CqG.js → index-C1UjHdgZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BaZAbw9A.js → index-CAUnl4Jv.js} +1 -1
- package/src/assets/web-panel/assets/{index-B9KvcS7R.js → index-CMu16vPC.js} +1 -1
- package/src/assets/web-panel/assets/{index-m31Ka8hZ.js → index-CNYFQxAW.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dwqmtk65.js → index-CQDKYbnv.js} +1 -1
- package/src/assets/web-panel/assets/{index-fk2Rjacw.js → index-CWZ3REg6.js} +3 -3
- package/src/assets/web-panel/assets/{index-DsJCDUJG.js → index-CcX4FSPf.js} +1 -1
- package/src/assets/web-panel/assets/{index-BAXfRKfG.js → index-Cd79NQAx.js} +1 -1
- package/src/assets/web-panel/assets/{index-BlDNVgBt.js → index-CdRkCwm4.js} +1 -1
- package/src/assets/web-panel/assets/index-CdiGCibe.js +1 -0
- package/src/assets/web-panel/assets/{index-4bna2_SR.js → index-Cy_cCJlr.js} +1 -1
- package/src/assets/web-panel/assets/{index-dVc-CXY4.js → index-DAVy98u7.js} +1 -1
- package/src/assets/web-panel/assets/{index-ocf06EVQ.js → index-DCJEY2T0.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dbbymxpj.js → index-DFb-XIKy.js} +1 -1
- package/src/assets/web-panel/assets/{index-CHG-KcS4.js → index-DGMYTN5W.js} +1 -1
- package/src/assets/web-panel/assets/index-D_0z-gyz.js +1 -0
- package/src/assets/web-panel/assets/{index-By5n8up9.js → index-Dcdm_Z2c.js} +1 -1
- package/src/assets/web-panel/assets/{index-FzdwcSy2.js → index-DeizoQVZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DFIZ_1Sf.js → index-DlIyt1qF.js} +1 -1
- package/src/assets/web-panel/assets/{index-BepLZwgB.js → index-DmgtY58H.js} +1 -1
- package/src/assets/web-panel/assets/{index-sDoMHTNA.js → index-DmrdUwzU.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJ8vXGOa.js → index-Doc0Ftpd.js} +1 -1
- package/src/assets/web-panel/assets/{index-CauoBQkH.js → index-DsQcURkA.js} +1 -1
- package/src/assets/web-panel/assets/{index-CtF7EN49.js → index-DslTuqro.js} +1 -1
- package/src/assets/web-panel/assets/{index-DfKBRdSS.js → index-V-kIHzAV.js} +1 -1
- package/src/assets/web-panel/assets/{index-DG2MZQ3f.js → index-e8BM1yY1.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-CvyYJd7z.js → initDefaultProps-DiBszVdG.js} +1 -1
- package/src/assets/web-panel/assets/{motion-DYZkhUdz.js → motion-BGIFFozv.js} +1 -1
- package/src/assets/web-panel/assets/{move-slnJRVT1.js → move-D5W7mp7B.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DVc0CKmy.js → mtc-parser-JFIlxJ7F.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BCXAtHCq.js → omit-CQ3o6LIU.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DBsemz_K.js → pickAttrs-R8MIlXo3.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BRtkJ-nv.js → placementArrow-sW7ylSky.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-Cebq7EZn.js → responsiveObserve-Cnl39Vbz.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DT7qvId2.js → slide-sn8tFd14.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-pQiITLkG.js → statusUtils-IKKY_QxZ.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-c-19BVKQ.js → styleChecker-BNpV4Nqs.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DsS5-0bE.js → useFlexGapSupport-BWyT_eoU.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CxGiMe5n.js → useFs-D09p6_U9.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-D9btVID5.js → usePersonalDataHub-CzW3TkJZ.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CUYFsFEN.js → vnode-3s3WoAKB.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CXmn_35j.js → zoom-Cjy_673s.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/data/changelog.json +13 -0
- package/src/lib/background-agent-keeper-protocol.js +75 -4
- package/src/lib/background-agent-launch-barrier.js +74 -0
- package/src/lib/background-agent-supervisor.js +149 -5
- package/src/lib/background-turn-bootstrap-protocol.js +4 -0
- package/src/lib/with-file-lock.js +175 -26
- package/src/workers/background-agent-keeper.js +112 -19
- package/src/workers/background-agent-worker.js +383 -134
- package/src/workers/background-turn-bootstrap.js +13 -8
- package/src/assets/web-panel/assets/MarkdownRenderer-BLif4QSg.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-BxVavObl.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-CnwojoSI.js +0 -1
- package/src/assets/web-panel/assets/Providers-CXoOa8eX.js +0 -1
- package/src/assets/web-panel/assets/SpeechSettings-DLjKsDVu.js +0 -1
- package/src/assets/web-panel/assets/devWarning-Bj5nUW2v.js +0 -1
- package/src/assets/web-panel/assets/index-CPcM-eqo.js +0 -1
- package/src/assets/web-panel/assets/index-xeSyVoRe.js +0 -1
|
@@ -11,6 +11,10 @@ export const BACKGROUND_TURN_BOOTSTRAP_READY =
|
|
|
11
11
|
"background-turn-bootstrap-ready";
|
|
12
12
|
export const BACKGROUND_TURN_BOOTSTRAP_RELEASE =
|
|
13
13
|
"background-turn-bootstrap-release";
|
|
14
|
+
// The formal launch gate permits 120 seconds for a 20-worker hosted runner to
|
|
15
|
+
// become ready. Keep the pre-main barrier alive for that same bounded window;
|
|
16
|
+
// worker/IPC death still fails immediately through the disconnect listener.
|
|
17
|
+
export const BACKGROUND_TURN_BOOTSTRAP_RELEASE_TIMEOUT_MS = 120_000;
|
|
14
18
|
|
|
15
19
|
export const BACKGROUND_TURN_BOOTSTRAP_ENV = Object.freeze({
|
|
16
20
|
nonce: "CC_BACKGROUND_TURN_BOOTSTRAP_NONCE",
|
|
@@ -14,14 +14,18 @@
|
|
|
14
14
|
* reclaimed merely because the directory is old, and corrupt ownership fails
|
|
15
15
|
* closed. The default filesystem publishes a fully populated, uniquely staged
|
|
16
16
|
* directory in one rename so a process killed during acquisition cannot expose
|
|
17
|
-
* an ownerless lock. Confirmed-dead owners are reclaimed with an exact-token
|
|
18
|
-
*
|
|
19
|
-
* replacement lock
|
|
17
|
+
* an ownerless lock. Confirmed-dead owners are reclaimed with an exact-token,
|
|
18
|
+
* process-owned claim inside the lock directory so a delayed contender cannot
|
|
19
|
+
* delete a replacement lock and another contender can take over if the first
|
|
20
|
+
* reclaimer is killed. Strict callers may use bounded jittered retries and an
|
|
20
21
|
* after-release yield to avoid Windows sharing transients and fixed-interval
|
|
21
22
|
* lock convoys.
|
|
22
23
|
*
|
|
23
24
|
* @param {string} targetPath the file being guarded (lock is `${targetPath}.lock`)
|
|
24
|
-
* @param {(ctx:{locked:boolean})=>T} fn
|
|
25
|
+
* @param {(ctx:{locked:boolean,publishReleaseAfterPathRemoved:(pendingPath:string)=>boolean})=>T} fn
|
|
26
|
+
* critical section; `publishReleaseAfterPathRemoved` lets a transaction
|
|
27
|
+
* publish an exact, contender-completable handoff before its final atomic
|
|
28
|
+
* rename removes a uniquely named staging path
|
|
25
29
|
* @returns {T} whatever `fn` returns
|
|
26
30
|
*/
|
|
27
31
|
export function withFileLock(targetPath, fn, opts = {}) {
|
|
@@ -54,7 +58,9 @@ export function withFileLock(targetPath, fn, opts = {}) {
|
|
|
54
58
|
};
|
|
55
59
|
let held = false;
|
|
56
60
|
let acquisitionError = null;
|
|
61
|
+
let lastOwnerObservation = null;
|
|
57
62
|
let retryAttempt = 0;
|
|
63
|
+
let releasePublished = false;
|
|
58
64
|
const deadline = _now() + timeoutMs;
|
|
59
65
|
|
|
60
66
|
for (;;) {
|
|
@@ -88,6 +94,17 @@ export function withFileLock(targetPath, fn, opts = {}) {
|
|
|
88
94
|
const incumbent = ownerRead.owner;
|
|
89
95
|
acquisitionError = ownerRead.error;
|
|
90
96
|
if (incumbent) {
|
|
97
|
+
let incumbentAlive = true;
|
|
98
|
+
try {
|
|
99
|
+
incumbentAlive = isOwnerAlive(incumbent);
|
|
100
|
+
} catch {
|
|
101
|
+
// An unavailable identity probe must retain the lock.
|
|
102
|
+
}
|
|
103
|
+
lastOwnerObservation = {
|
|
104
|
+
pid: incumbent.pid,
|
|
105
|
+
startedAt: incumbent.startedAt,
|
|
106
|
+
alive: incumbentAlive,
|
|
107
|
+
};
|
|
91
108
|
// The owner writes this exact-token marker only after its critical
|
|
92
109
|
// section has finished. A Windows sharing violation can then prevent
|
|
93
110
|
// the final recursive removal while the owner process remains alive,
|
|
@@ -102,11 +119,19 @@ export function withFileLock(targetPath, fn, opts = {}) {
|
|
|
102
119
|
owner,
|
|
103
120
|
isOwnerAlive,
|
|
104
121
|
);
|
|
122
|
+
lastOwnerObservation.releasePublished = publishedRelease.published;
|
|
105
123
|
if (publishedRelease.completed) continue;
|
|
106
|
-
if (!
|
|
107
|
-
|
|
124
|
+
if (!incumbentAlive) {
|
|
125
|
+
// A published handoff can still retain its unique staging path when
|
|
126
|
+
// the owner is killed between marker publication and the atomic
|
|
127
|
+
// replacement. No process can commit that staging file after the
|
|
128
|
+
// exact owner is proven dead, so the lock directory is reclaimable
|
|
129
|
+
// whether the transaction committed or not. Leave the private
|
|
130
|
+
// staging file untouched; it cannot affect the guarded target.
|
|
131
|
+
if (
|
|
132
|
+
reclaimOwnedDirectory(_fs, lockDir, incumbent, owner, isOwnerAlive)
|
|
133
|
+
)
|
|
108
134
|
continue;
|
|
109
|
-
}
|
|
110
135
|
}
|
|
111
136
|
} else if (!failIfUnavailable) {
|
|
112
137
|
// Compatibility for legacy best-effort locks that predate owner.json.
|
|
@@ -139,12 +164,21 @@ export function withFileLock(targetPath, fn, opts = {}) {
|
|
|
139
164
|
}
|
|
140
165
|
|
|
141
166
|
if (!held && failIfUnavailable) {
|
|
167
|
+
const ownerDetails = lastOwnerObservation
|
|
168
|
+
? `; owner pid=${lastOwnerObservation.pid}, ageMs=${Math.max(
|
|
169
|
+
0,
|
|
170
|
+
_now() - lastOwnerObservation.startedAt,
|
|
171
|
+
)}, alive=${lastOwnerObservation.alive}, releasePublished=${Boolean(lastOwnerObservation.releasePublished)}`
|
|
172
|
+
: "";
|
|
142
173
|
const error = new Error(
|
|
143
|
-
`Could not acquire state lock: ${targetPath}`,
|
|
174
|
+
`Could not acquire state lock: ${targetPath}${ownerDetails}`,
|
|
144
175
|
acquisitionError ? { cause: acquisitionError } : undefined,
|
|
145
176
|
);
|
|
146
177
|
error.code = "STATE_LOCK_UNAVAILABLE";
|
|
147
178
|
error.attempts = retryAttempt + 1;
|
|
179
|
+
error.lockOwner = lastOwnerObservation
|
|
180
|
+
? Object.freeze({ ...lastOwnerObservation })
|
|
181
|
+
: null;
|
|
148
182
|
throw error;
|
|
149
183
|
}
|
|
150
184
|
|
|
@@ -152,7 +186,32 @@ export function withFileLock(targetPath, fn, opts = {}) {
|
|
|
152
186
|
let bodyThrew = false;
|
|
153
187
|
let result;
|
|
154
188
|
try {
|
|
155
|
-
result = fn({
|
|
189
|
+
result = fn({
|
|
190
|
+
locked: held,
|
|
191
|
+
publishReleaseAfterPathRemoved(pendingPath) {
|
|
192
|
+
if (!held || typeof pendingPath !== "string" || !pendingPath) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
const current = readOwner(_fs, ownerPath);
|
|
196
|
+
if (!sameOwner(current, owner)) return false;
|
|
197
|
+
const markerPath = path.join(lockDir, `.release-${owner.token}`);
|
|
198
|
+
const marker = { ...owner, releaseAfterPathRemoved: pendingPath };
|
|
199
|
+
try {
|
|
200
|
+
writeOwnerMarker(_fs, markerPath, marker);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (error?.code !== "EEXIST") throw error;
|
|
203
|
+
const existing = readOwner(_fs, markerPath);
|
|
204
|
+
if (
|
|
205
|
+
!sameOwner(existing, owner) ||
|
|
206
|
+
existing?.releaseAfterPathRemoved !== pendingPath
|
|
207
|
+
) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
releasePublished = true;
|
|
212
|
+
return true;
|
|
213
|
+
},
|
|
214
|
+
});
|
|
156
215
|
} catch (error) {
|
|
157
216
|
bodyThrew = true;
|
|
158
217
|
bodyError = error;
|
|
@@ -161,7 +220,13 @@ export function withFileLock(targetPath, fn, opts = {}) {
|
|
|
161
220
|
let releaseError = null;
|
|
162
221
|
if (held) {
|
|
163
222
|
try {
|
|
164
|
-
released = releaseOwnedDirectory(
|
|
223
|
+
released = releaseOwnedDirectory(
|
|
224
|
+
_fs,
|
|
225
|
+
lockDir,
|
|
226
|
+
owner,
|
|
227
|
+
isOwnerAlive,
|
|
228
|
+
releasePublished,
|
|
229
|
+
);
|
|
165
230
|
} catch (error) {
|
|
166
231
|
releaseError = error;
|
|
167
232
|
}
|
|
@@ -251,6 +316,15 @@ function directoryExists(_fs, target) {
|
|
|
251
316
|
}
|
|
252
317
|
}
|
|
253
318
|
|
|
319
|
+
function pathStatus(_fs, target) {
|
|
320
|
+
try {
|
|
321
|
+
_fs.statSync(target);
|
|
322
|
+
return "present";
|
|
323
|
+
} catch (error) {
|
|
324
|
+
return error?.code === "ENOENT" ? "absent" : "unknown";
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
254
328
|
function isProcessAlive(pid) {
|
|
255
329
|
try {
|
|
256
330
|
process.kill(pid, 0);
|
|
@@ -361,21 +435,65 @@ function writeOwnerMarker(_fs, markerPath, owner) {
|
|
|
361
435
|
});
|
|
362
436
|
}
|
|
363
437
|
|
|
364
|
-
function reclaimOwnedDirectory(_fs, lockDir,
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
438
|
+
function reclaimOwnedDirectory(_fs, lockDir, incumbent, claimant, ownerAlive) {
|
|
439
|
+
const claimPath = path.join(lockDir, `.reclaim-${incumbent.token}`);
|
|
440
|
+
for (;;) {
|
|
441
|
+
const existingClaim = readOwner(_fs, claimPath);
|
|
442
|
+
if (existingClaim) {
|
|
443
|
+
if (sameOwner(existingClaim, claimant)) break;
|
|
444
|
+
if (!ownerAlive(existingClaim)) {
|
|
445
|
+
if (removeOwnMarker(_fs, claimPath, existingClaim)) continue;
|
|
446
|
+
}
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
// The claim belongs to the contender, not the dead incumbent. If this
|
|
451
|
+
// contender is killed before detaching the directory, another process
|
|
452
|
+
// can prove that exact claimant dead and safely take over.
|
|
453
|
+
writeOwnerMarker(_fs, claimPath, claimant);
|
|
454
|
+
break;
|
|
455
|
+
} catch (error) {
|
|
456
|
+
if (error?.code === "ENOENT") {
|
|
457
|
+
return !directoryExists(_fs, lockDir);
|
|
458
|
+
}
|
|
459
|
+
if (error?.code === "EEXIST" || isTransientLockError(error)) {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
371
464
|
}
|
|
465
|
+
|
|
372
466
|
const current = readOwner(_fs, path.join(lockDir, "owner.json"));
|
|
373
|
-
if (!sameOwner(current,
|
|
374
|
-
removeOwnMarker(_fs,
|
|
467
|
+
if (!sameOwner(current, incumbent) || ownerAlive(incumbent)) {
|
|
468
|
+
removeOwnMarker(_fs, claimPath, claimant);
|
|
375
469
|
return false;
|
|
376
470
|
}
|
|
377
|
-
|
|
378
|
-
|
|
471
|
+
|
|
472
|
+
if (typeof _fs.renameSync !== "function") {
|
|
473
|
+
_fs.rmSync(lockDir, { recursive: true, force: true });
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const cleanupDir = `${lockDir}.reclaimed-${incumbent.token}-${claimant.token}`;
|
|
478
|
+
try {
|
|
479
|
+
// Free the acquisition path atomically before recursive cleanup. A killed
|
|
480
|
+
// reclaimer can therefore leave only uniquely named debris, never a
|
|
481
|
+
// claimant marker that permanently fences the shared lock path.
|
|
482
|
+
_fs.renameSync(lockDir, cleanupDir);
|
|
483
|
+
try {
|
|
484
|
+
_fs.rmSync(cleanupDir, { recursive: true, force: true });
|
|
485
|
+
} catch {
|
|
486
|
+
/* private reclaim debris is harmless after the lock path is detached */
|
|
487
|
+
}
|
|
488
|
+
return true;
|
|
489
|
+
} catch (error) {
|
|
490
|
+
removeOwnMarker(_fs, claimPath, claimant);
|
|
491
|
+
if (error?.code === "ENOENT") {
|
|
492
|
+
return !directoryExists(_fs, lockDir);
|
|
493
|
+
}
|
|
494
|
+
if (isTransientLockError(error)) return false;
|
|
495
|
+
throw error;
|
|
496
|
+
}
|
|
379
497
|
}
|
|
380
498
|
|
|
381
499
|
function reclaimLegacyDirectory(_fs, lockDir, stat) {
|
|
@@ -401,9 +519,23 @@ function reclaimLegacyDirectory(_fs, lockDir, stat) {
|
|
|
401
519
|
|
|
402
520
|
function completePublishedRelease(_fs, lockDir, owner, claimant, ownerAlive) {
|
|
403
521
|
const markerPath = path.join(lockDir, `.release-${owner.token}`);
|
|
404
|
-
|
|
522
|
+
const marker = readOwner(_fs, markerPath);
|
|
523
|
+
if (!sameOwner(marker, owner)) {
|
|
405
524
|
return { published: false, completed: false };
|
|
406
525
|
}
|
|
526
|
+
// A state transaction can stage its exact replacement, publish this marker,
|
|
527
|
+
// and only then atomically rename the staging file over the guarded target.
|
|
528
|
+
// Contenders may complete the release only after that unique staging path is
|
|
529
|
+
// gone. This closes the otherwise unbounded scheduler-preemption window
|
|
530
|
+
// between a committed rename and the owner's first release syscall without
|
|
531
|
+
// ever exposing an in-progress write to a competing critical section.
|
|
532
|
+
if (
|
|
533
|
+
typeof marker.releaseAfterPathRemoved === "string" &&
|
|
534
|
+
marker.releaseAfterPathRemoved &&
|
|
535
|
+
pathStatus(_fs, marker.releaseAfterPathRemoved) !== "absent"
|
|
536
|
+
) {
|
|
537
|
+
return { published: true, completed: false };
|
|
538
|
+
}
|
|
407
539
|
const claimPath = path.join(lockDir, `.release-claim-${owner.token}`);
|
|
408
540
|
for (;;) {
|
|
409
541
|
const existingClaim = readOwner(_fs, claimPath);
|
|
@@ -461,9 +593,20 @@ function completePublishedRelease(_fs, lockDir, owner, claimant, ownerAlive) {
|
|
|
461
593
|
}
|
|
462
594
|
}
|
|
463
595
|
|
|
464
|
-
function releaseOwnedDirectory(
|
|
596
|
+
function releaseOwnedDirectory(
|
|
597
|
+
_fs,
|
|
598
|
+
lockDir,
|
|
599
|
+
owner,
|
|
600
|
+
ownerAlive,
|
|
601
|
+
releaseWasPublished = false,
|
|
602
|
+
) {
|
|
465
603
|
if (!sameOwner(readOwner(_fs, path.join(lockDir, "owner.json")), owner)) {
|
|
466
|
-
|
|
604
|
+
// Once this exact owner published a guarded handoff, a contender is allowed
|
|
605
|
+
// to detach the old directory as soon as the final staging path disappears.
|
|
606
|
+
// A missing or replacement lock therefore proves successful release, not
|
|
607
|
+
// ownership loss. Without that publication the strict legacy verdict stays
|
|
608
|
+
// fail-closed.
|
|
609
|
+
return releaseWasPublished;
|
|
467
610
|
}
|
|
468
611
|
// Atomically move the exact owned directory out of the acquisition path,
|
|
469
612
|
// then remove that uniquely-tokened directory. A replacement owner may
|
|
@@ -482,7 +625,7 @@ function releaseOwnedDirectory(_fs, lockDir, owner, ownerAlive) {
|
|
|
482
625
|
}
|
|
483
626
|
return true;
|
|
484
627
|
} catch (error) {
|
|
485
|
-
if (error?.code === "ENOENT") return
|
|
628
|
+
if (error?.code === "ENOENT") return releaseWasPublished;
|
|
486
629
|
// Windows sharing transients fall back to the marker-based release.
|
|
487
630
|
}
|
|
488
631
|
}
|
|
@@ -490,7 +633,13 @@ function releaseOwnedDirectory(_fs, lockDir, owner, ownerAlive) {
|
|
|
490
633
|
try {
|
|
491
634
|
writeOwnerMarker(_fs, markerPath, owner);
|
|
492
635
|
} catch (error) {
|
|
493
|
-
if (error?.code === "ENOENT"
|
|
636
|
+
if (error?.code === "ENOENT") return releaseWasPublished;
|
|
637
|
+
if (error?.code === "EEXIST") {
|
|
638
|
+
const existing = readOwner(_fs, markerPath);
|
|
639
|
+
if (!sameOwner(existing, owner)) return false;
|
|
640
|
+
return completePublishedRelease(_fs, lockDir, owner, owner, ownerAlive)
|
|
641
|
+
.published;
|
|
642
|
+
}
|
|
494
643
|
throw error;
|
|
495
644
|
}
|
|
496
645
|
if (!sameOwner(readOwner(_fs, path.join(lockDir, "owner.json")), owner)) {
|
|
@@ -9,8 +9,15 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import net from "node:net";
|
|
12
|
-
import {
|
|
13
|
-
|
|
12
|
+
import {
|
|
13
|
+
readFileSync,
|
|
14
|
+
renameSync,
|
|
15
|
+
rmSync,
|
|
16
|
+
unlinkSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
writeSync,
|
|
19
|
+
} from "node:fs";
|
|
20
|
+
import { dirname, resolve } from "node:path";
|
|
14
21
|
import { fileURLToPath } from "node:url";
|
|
15
22
|
import { createNdjsonReader } from "../lib/background-session-transport.js";
|
|
16
23
|
import {
|
|
@@ -28,6 +35,7 @@ import {
|
|
|
28
35
|
BACKGROUND_AGENT_KEEPER_CLEANUP_CONFIRM_TIMEOUT_MS,
|
|
29
36
|
BACKGROUND_AGENT_KEEPER_HELLO,
|
|
30
37
|
BACKGROUND_AGENT_KEEPER_HEARTBEAT,
|
|
38
|
+
BACKGROUND_AGENT_KEEPER_IDENTITY_PROBE_DELAY_MS,
|
|
31
39
|
BACKGROUND_AGENT_KEEPER_HEARTBEAT_TIMEOUT_MS,
|
|
32
40
|
BACKGROUND_AGENT_KEEPER_PERSIST_RETRY_TIMEOUT_MS,
|
|
33
41
|
BACKGROUND_AGENT_KEEPER_PROTOCOL_VERSION,
|
|
@@ -35,12 +43,15 @@ import {
|
|
|
35
43
|
BACKGROUND_AGENT_KEEPER_RETIRE,
|
|
36
44
|
BACKGROUND_AGENT_KEEPER_RETIRED,
|
|
37
45
|
BACKGROUND_AGENT_KEEPER_STATE_LOCK_TIMEOUT_MS,
|
|
46
|
+
backgroundAgentKeeperLaunchClaimPath,
|
|
38
47
|
cleanupBackgroundAgentKeeperPipeDirectory,
|
|
48
|
+
createBackgroundAgentKeeperLaunchClaim,
|
|
39
49
|
createBackgroundAgentKeeperMessage,
|
|
40
50
|
normalizeBackgroundAgentKeeperHello,
|
|
41
51
|
normalizeBackgroundAgentKeeperTurn,
|
|
42
52
|
sameBackgroundAgentKeeperTurn,
|
|
43
53
|
} from "../lib/background-agent-keeper-protocol.js";
|
|
54
|
+
import { waitForBackgroundAgentLaunchBarrier } from "../lib/background-agent-launch-barrier.js";
|
|
44
55
|
|
|
45
56
|
const STARTUP_TIMEOUT_MS = 30_000;
|
|
46
57
|
const HEARTBEAT_INTERVAL_MS = 1_000;
|
|
@@ -132,18 +143,29 @@ export function runBackgroundAgentKeeperHeartbeat({
|
|
|
132
143
|
reportPersistenceFailure = reportKeeperPersistenceFailure,
|
|
133
144
|
}) {
|
|
134
145
|
if (finishing) return false;
|
|
146
|
+
// Startup is deliberately serialized: the keeper publishes its listening
|
|
147
|
+
// claim, then remains read-only until the worker has claimed and
|
|
148
|
+
// authenticated. Persisting a pre-auth heartbeat would reintroduce the
|
|
149
|
+
// first-write lock race the launch barrier prevents.
|
|
150
|
+
if (!workerSocket || !authenticatedHello) return false;
|
|
135
151
|
const currentTime = now();
|
|
152
|
+
const workerHeartbeatAge = Number.isFinite(workerHeartbeatAt)
|
|
153
|
+
? Math.max(0, currentTime - workerHeartbeatAt)
|
|
154
|
+
: 0;
|
|
136
155
|
const workerHeartbeatExpired =
|
|
137
156
|
workerSocket &&
|
|
138
157
|
authenticatedHello &&
|
|
139
158
|
Number.isFinite(workerHeartbeatAt) &&
|
|
140
|
-
|
|
141
|
-
|
|
159
|
+
workerHeartbeatAge > BACKGROUND_AGENT_KEEPER_HEARTBEAT_TIMEOUT_MS;
|
|
160
|
+
const workerIdentityProbeDue =
|
|
161
|
+
armedTurn &&
|
|
162
|
+
Number.isFinite(workerHeartbeatAt) &&
|
|
163
|
+
workerHeartbeatAge > BACKGROUND_AGENT_KEEPER_IDENTITY_PROBE_DELAY_MS;
|
|
142
164
|
if (
|
|
143
165
|
workerSocket &&
|
|
144
166
|
authenticatedHello &&
|
|
145
167
|
(workerHeartbeatExpired ||
|
|
146
|
-
(
|
|
168
|
+
(workerIdentityProbeDue &&
|
|
147
169
|
!workerIdentityAlive(
|
|
148
170
|
authenticatedHello.workerPid,
|
|
149
171
|
authenticatedWorkerStartedAt,
|
|
@@ -153,8 +175,9 @@ export function runBackgroundAgentKeeperHeartbeat({
|
|
|
153
175
|
// after its worker dies if a platform helper retained a duplicate.
|
|
154
176
|
// Socket EOF and PID visibility are therefore not sufficient lifetime
|
|
155
177
|
// signals. The authenticated application heartbeat provides an
|
|
156
|
-
// independent bounded fence
|
|
157
|
-
//
|
|
178
|
+
// independent bounded fence. Delay the PID/start probe until application
|
|
179
|
+
// heartbeats are already stale so healthy Windows keepers do not create a
|
|
180
|
+
// synchronized WMIC/PowerShell probe storm under normal operation.
|
|
158
181
|
finishForWorkerDisconnect();
|
|
159
182
|
workerSocket.destroy();
|
|
160
183
|
return false;
|
|
@@ -239,7 +262,9 @@ export function stopBackgroundAgentKeeperTurnTrees(targets, options = {}) {
|
|
|
239
262
|
leaderAlive ||
|
|
240
263
|
(platform !== "win32" &&
|
|
241
264
|
target.processGroup === true &&
|
|
242
|
-
processTreeExecutionAlive(target.pid, target.startedAt
|
|
265
|
+
processTreeExecutionAlive(target.pid, target.startedAt, {
|
|
266
|
+
processGroup: true,
|
|
267
|
+
}));
|
|
243
268
|
if (residualAlive) {
|
|
244
269
|
failures.push(error?.message || String(error));
|
|
245
270
|
}
|
|
@@ -248,6 +273,46 @@ export function stopBackgroundAgentKeeperTurnTrees(targets, options = {}) {
|
|
|
248
273
|
return failures;
|
|
249
274
|
}
|
|
250
275
|
|
|
276
|
+
export async function waitForBackgroundAgentKeeperTurnTreesToStop(
|
|
277
|
+
targets,
|
|
278
|
+
options = {},
|
|
279
|
+
) {
|
|
280
|
+
const now = options.now || Date.now;
|
|
281
|
+
const sleep =
|
|
282
|
+
options.sleep ||
|
|
283
|
+
((milliseconds) =>
|
|
284
|
+
new Promise((resolvePromise) =>
|
|
285
|
+
setTimeout(resolvePromise, milliseconds),
|
|
286
|
+
));
|
|
287
|
+
const processTreeExecutionAlive =
|
|
288
|
+
options.isProcessTreeExecutionAlive ||
|
|
289
|
+
isBackgroundProcessTreeExecutionAlive;
|
|
290
|
+
const timeoutMs = Math.max(
|
|
291
|
+
1,
|
|
292
|
+
Number(options.timeoutMs) ||
|
|
293
|
+
BACKGROUND_AGENT_KEEPER_CLEANUP_CONFIRM_TIMEOUT_MS,
|
|
294
|
+
);
|
|
295
|
+
// Each strict macOS liveness probe invokes ps. Polling every 10 ms across
|
|
296
|
+
// 20 keepers created a process storm precisely while launchd was trying to
|
|
297
|
+
// reap the killed groups. A 100 ms cadence remains responsive and bounded
|
|
298
|
+
// while leaving the host enough scheduling capacity to establish absence.
|
|
299
|
+
const pollMs = Math.max(1, Number(options.pollMs) || 100);
|
|
300
|
+
const deadline = now() + timeoutMs;
|
|
301
|
+
const observeAlive = () =>
|
|
302
|
+
targets.filter((target) =>
|
|
303
|
+
processTreeExecutionAlive(target.pid, target.startedAt, {
|
|
304
|
+
processGroup: target.processGroup !== false,
|
|
305
|
+
}),
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
let alive = observeAlive();
|
|
309
|
+
while (alive.length > 0 && now() < deadline) {
|
|
310
|
+
await sleep(Math.max(1, Math.min(pollMs, deadline - now())));
|
|
311
|
+
alive = observeAlive();
|
|
312
|
+
}
|
|
313
|
+
return alive;
|
|
314
|
+
}
|
|
315
|
+
|
|
251
316
|
async function cleanupTurn(job, turn, reason) {
|
|
252
317
|
const requestedAt = Date.now();
|
|
253
318
|
const requestedPersistence = await retryKeeperPersistence(() =>
|
|
@@ -275,17 +340,7 @@ async function cleanupTurn(job, turn, reason) {
|
|
|
275
340
|
);
|
|
276
341
|
const failures = stopBackgroundAgentKeeperTurnTrees(targets);
|
|
277
342
|
|
|
278
|
-
const
|
|
279
|
-
Date.now() + BACKGROUND_AGENT_KEEPER_CLEANUP_CONFIRM_TIMEOUT_MS;
|
|
280
|
-
let alive = targets.filter((target) =>
|
|
281
|
-
isBackgroundProcessTreeExecutionAlive(target.pid, target.startedAt),
|
|
282
|
-
);
|
|
283
|
-
while (alive.length > 0 && Date.now() < deadline) {
|
|
284
|
-
await new Promise((resolvePromise) => setTimeout(resolvePromise, 10));
|
|
285
|
-
alive = targets.filter((target) =>
|
|
286
|
-
isBackgroundProcessTreeExecutionAlive(target.pid, target.startedAt),
|
|
287
|
-
);
|
|
288
|
-
}
|
|
343
|
+
const alive = await waitForBackgroundAgentKeeperTurnTreesToStop(targets);
|
|
289
344
|
const confirmed = alive.length === 0 && failures.length === 0;
|
|
290
345
|
let finalPersistenceAttempts = 0;
|
|
291
346
|
const finalPersistence = await retryKeeperPersistence(() => {
|
|
@@ -322,6 +377,24 @@ async function cleanupTurn(job, turn, reason) {
|
|
|
322
377
|
|
|
323
378
|
export async function runBackgroundAgentKeeper(jobFile, options = {}) {
|
|
324
379
|
const job = JSON.parse(readFileSync(jobFile, "utf8"));
|
|
380
|
+
const launchClaimPath = backgroundAgentKeeperLaunchClaimPath(
|
|
381
|
+
job.id,
|
|
382
|
+
job.keeperGeneration,
|
|
383
|
+
options.backgroundAgentsDirectory || dirname(resolve(jobFile)),
|
|
384
|
+
);
|
|
385
|
+
const launchBarrier = await waitForBackgroundAgentLaunchBarrier({
|
|
386
|
+
id: job.id,
|
|
387
|
+
workerGeneration: job.workerGeneration,
|
|
388
|
+
expectedPid: process.pid,
|
|
389
|
+
pidField: "keeperPid",
|
|
390
|
+
readState: options.readBackgroundAgentState || readBackgroundAgentState,
|
|
391
|
+
timeoutMs: Number(options.launchBarrierTimeoutMs) || undefined,
|
|
392
|
+
sleep: options.launchBarrierSleep,
|
|
393
|
+
});
|
|
394
|
+
if (launchBarrier.status !== "ready") {
|
|
395
|
+
removeJobFile(jobFile);
|
|
396
|
+
return { status: `launch-${launchBarrier.status}` };
|
|
397
|
+
}
|
|
325
398
|
removeJobFile(jobFile);
|
|
326
399
|
const startedAt = Date.now();
|
|
327
400
|
const claim = mutateBackgroundAgentState(job.id, (current) => {
|
|
@@ -342,6 +415,25 @@ export async function runBackgroundAgentKeeper(jobFile, options = {}) {
|
|
|
342
415
|
};
|
|
343
416
|
});
|
|
344
417
|
if (!claim.applied) return { status: "unclaimed" };
|
|
418
|
+
const launchClaim = createBackgroundAgentKeeperLaunchClaim({
|
|
419
|
+
id: job.id,
|
|
420
|
+
workerGeneration: job.workerGeneration,
|
|
421
|
+
keeperGeneration: job.keeperGeneration,
|
|
422
|
+
keeperPid: process.pid,
|
|
423
|
+
token: job.token,
|
|
424
|
+
});
|
|
425
|
+
const launchClaimCandidate = `${launchClaimPath}.${process.pid}.tmp`;
|
|
426
|
+
try {
|
|
427
|
+
writeFileSync(launchClaimCandidate, JSON.stringify(launchClaim), {
|
|
428
|
+
encoding: "utf8",
|
|
429
|
+
mode: 0o600,
|
|
430
|
+
flag: "wx",
|
|
431
|
+
});
|
|
432
|
+
renameSync(launchClaimCandidate, launchClaimPath);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
rmSync(launchClaimCandidate, { force: true });
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
345
437
|
|
|
346
438
|
if (process.platform !== "win32") {
|
|
347
439
|
try {
|
|
@@ -686,6 +778,7 @@ export async function runBackgroundAgentKeeper(jobFile, options = {}) {
|
|
|
686
778
|
// Another keeper can still own a sibling socket in the shared namespace.
|
|
687
779
|
}
|
|
688
780
|
}
|
|
781
|
+
rmSync(launchClaimPath, { force: true });
|
|
689
782
|
return {
|
|
690
783
|
...result,
|
|
691
784
|
authenticatedWorkerPid: authenticatedHello?.workerPid || null,
|