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.
Files changed (166) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-lS1R0Cza.js → AIOps-BiHNUlbU.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BmlgBPsj.js → ActionButton-D-UFjDRQ.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-CNKczv1S.js → Analytics-DazVsDgW.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-BuZGJARQ.js → AppLayout-DXh9UfNJ.js} +5 -5
  6. package/src/assets/web-panel/assets/{Artifacts-Dm73Sl-O.js → Artifacts-CZ6tsceY.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-BGMW7A5e.js → Audit-RyxnBr_h.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-BQ6M54JR.js → BackgroundAgents-BWS51Q6X.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-DBzGP0bR.js → Backup-BRtPcUul.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-D1mLCoOV.js → BaseInput-CGd4vZYY.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-BUfcN7p8.js → Chat-h8ijVwCg.js} +4 -4
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-CY5m8ZHg.js → ChatBubbleRenderer-D7tcdDS9.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-Du0Yg5Pi.js → Checkbox-NyF6dPl8.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-B2RXziHW.js → Codegen-Ci4OOxr5.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-DWhekOrR.js → Col-CwTkc9BM.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-HQhrkon4.js → Community-DW_tM82m.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-izHNb4uz.js → Compact-DEvHRV4S.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-BDwLy41-.js → Compliance-BbjBEEfD.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-DFJhLa7K.js → Cowork-10IXBaBa.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-DYY9RG7-.js → Cron-BZkbpd5I.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-rr7tJTtv.js → Crosschain-CFQ8L_6v.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-D2sfHRLi.js → DID-BDF6AkXe.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-q_eGdhha.js → Dashboard-CoOdWMi4.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-Cufrh2Bz.js → Dropdown-DEsGmu8Y.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-B-kdv10f.js → EmailListRenderer-BS89SWlK.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-D21XjgfG.js → FamilyGuardDashboard-C1FpCHiI.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-CAwRTLZH.js → Federation-BzvpVmLA.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-vx3daNDF.js → FormItemContext-BeC259PQ.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-CRWY2wU4.js → GenericCardRenderer-azgG3bmV.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-Q6SwauyD.js → Git-BmSSowc5.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-BJ1q4IQZ.js → Governance-CTBCSyQZ.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-CSWjROgD.js → Inference-DA5xyk4a.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-fzuQsWuY.js → KnowledgeGraph-D1gPHNig.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-DVF8Mnns.js → Logs-BPmLf1AR.js} +2 -2
  35. package/src/assets/web-panel/assets/MarkdownRenderer-SVLOg49H.js +1 -0
  36. package/src/assets/web-panel/assets/{Marketplace-BQP7LOA4.js → Marketplace-DtUFcR0E.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-BNlJCmNc.js → McpTools-DS_kqBv-.js} +4 -4
  38. package/src/assets/web-panel/assets/{Memory-DLLg5WQ4.js → Memory-Fvh7XL7V.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-CR2skNAF.js → MobileBridge-C7itAP45.js} +1 -1
  40. package/src/assets/web-panel/assets/MobileProjects-DGbP36-3.js +1 -0
  41. package/src/assets/web-panel/assets/{Mtc-B17pgqMI.js → Mtc-B57MiNLK.js} +6 -6
  42. package/src/assets/web-panel/assets/{MtcAudit-4UmV5cIL.js → MtcAudit-BXfCUcM7.js} +4 -4
  43. package/src/assets/web-panel/assets/{Multisig-DoMEXEcU.js → Multisig-yaJ-ZcF6.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-h9DMXFX4.js → NLProgramming-CoaUCKrO.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-Cihn-xX8.js → Notes-BKLM_65R.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-BE6ZGa3R.js → NotificationSettings-DoDtK9uZ.js} +1 -1
  47. package/src/assets/web-panel/assets/OrderTableRenderer-BJ28TGr-.js +1 -0
  48. package/src/assets/web-panel/assets/{Organization-B2vpwmjb.js → Organization-MuGsdoIb.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-C4z28R0-.js → Overflow-BLUN9J4R.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-BVlFkwhW.js → P2P-DWuoBUPd.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-BXbEdL5B.js → PdhVaultBrowser-CUi37YPj.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-C28xs4lp.js → Permissions-BYQg2pcr.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub-Dgl_rBjM.js → PersonalDataHub-CAxoV2xu.js} +6 -6
  54. package/src/assets/web-panel/assets/{Pipeline-3QUIhrKI.js → Pipeline-B33wGvIO.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-8Gh33OZ5.js → Privacy-A8yXTORP.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-BNmg4tZi.js → ProjectInit-D49I2I1O.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-vgMfeCf9.js → ProjectSettings-Ci73jJDw.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-BhtAVilc.js → Projects-CF-yD6Cn.js} +1 -1
  59. package/src/assets/web-panel/assets/Providers-D-etEhg5.js +1 -0
  60. package/src/assets/web-panel/assets/{QrScannerModal-BhfTF0oS.js → QrScannerModal-Brza0ykJ.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-C746HEAj.js → QuickAsk-DFydhjxL.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-BSej8Eo0.js → Recommend-DLF8_NhA.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-f3gmnlG3.js → RemoteSession-dyuStW3x.js} +3 -3
  64. package/src/assets/web-panel/assets/{Reputation-CyvybpkN.js → Reputation-5F3tA6yw.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-DwV2PeYn.js → Row-Ca2A57S5.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-11GrzjZy.js → RssFeed-B0bxBkq_.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-BjKXkrAY.js → Search-CFNvHalZ.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-eWiFb9Yk.js → Security-DSoH3Hav.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-CdCsMAtt.js → Services-BHpfEmBQ.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-4Q2UTKQ5.js → Skeleton-Dsh-QNmn.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-v4rtYrRj.js → Skills-BhT1NkSU.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-CRhsFvd_.js → Sla-DY9snPeJ.js} +1 -1
  73. package/src/assets/web-panel/assets/SpeechSettings-CWaX0_Cx.js +1 -0
  74. package/src/assets/web-panel/assets/{SyncSettings-D16zonl7.js → SyncSettings-B690y_TC.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-Xxozz0i_.js → Tasks-2N-w3zWd.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-CXJpPtCT.js → Templates-BjwQOQYC.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-XRV0L1ZF.js → Tenant-CUqHErxY.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-CYuO1i_w.js → Terminal-C13rXvNy.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-BXqitWsu.js → TimelineRenderer-CpbWcya_.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-BvPrDHIy.js → Tokens-BXHLGXYC.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-DhCrSTf5.js → Trigger-1e5i65o3.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-DmsuZD6K.js → Trust-FLGqNHs5.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-D_yrts5C.js → UkeySign-BbaR_LJB.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-B01ihPrm.js → VideoEditing-NOp4RIov.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-DpUjKa-N.js → Wallet-B7DW7bG4.js} +3 -3
  86. package/src/assets/web-panel/assets/{WebAuthn-CL07yON6.js → WebAuthn-BptSUWix.js} +5 -5
  87. package/src/assets/web-panel/assets/{WorkflowEditor-CLMflnRr.js → WorkflowEditor-CAJnDOXS.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-DtUqe8h_.js → chat-I8LgG1-Z.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-DPKrgVfy.js → colors-CWe7SOVD.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-BTt2GtOm.js → compact-item-Cx3u3CA3.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-D_VeUc1J.js → createContext-CgD5ArwY.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-DLv_872g.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-1kndiylj.js → hasIn-BfQWiXN8.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-brCInrzq.js → index-3iKP9OTJ.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-l1BETrDb.js → index-87gMxqhN.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-DxdTdBix.js → index-B1cX4Rxu.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-DdkplL9R.js → index-B6yChzt1.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BHpBMWCh.js → index-BREnPKGo.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-CzjkKUmR.js → index-BUAR7RY-.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-D95OFht7.js → index-Bi24S7Dw.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-8nz_fmlj.js → index-BiI-xPRH.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-2eyAXE3d.js → index-BuEQSorC.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-GsoUQ2M7.js → index-Bx7BNIRH.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CX81qSM4.js → index-C-rIKccl.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-CcO04eDn.js → index-C0nDKhWv.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-Bhudc_Ez.js → index-C0ztiqtB.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DfGl8CqG.js → index-C1UjHdgZ.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-BaZAbw9A.js → index-CAUnl4Jv.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-B9KvcS7R.js → index-CMu16vPC.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-m31Ka8hZ.js → index-CNYFQxAW.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-Dwqmtk65.js → index-CQDKYbnv.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-fk2Rjacw.js → index-CWZ3REg6.js} +3 -3
  113. package/src/assets/web-panel/assets/{index-DsJCDUJG.js → index-CcX4FSPf.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-BAXfRKfG.js → index-Cd79NQAx.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-BlDNVgBt.js → index-CdRkCwm4.js} +1 -1
  116. package/src/assets/web-panel/assets/index-CdiGCibe.js +1 -0
  117. package/src/assets/web-panel/assets/{index-4bna2_SR.js → index-Cy_cCJlr.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-dVc-CXY4.js → index-DAVy98u7.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-ocf06EVQ.js → index-DCJEY2T0.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-Dbbymxpj.js → index-DFb-XIKy.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CHG-KcS4.js → index-DGMYTN5W.js} +1 -1
  122. package/src/assets/web-panel/assets/index-D_0z-gyz.js +1 -0
  123. package/src/assets/web-panel/assets/{index-By5n8up9.js → index-Dcdm_Z2c.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-FzdwcSy2.js → index-DeizoQVZ.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DFIZ_1Sf.js → index-DlIyt1qF.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-BepLZwgB.js → index-DmgtY58H.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-sDoMHTNA.js → index-DmrdUwzU.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-DJ8vXGOa.js → index-Doc0Ftpd.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-CauoBQkH.js → index-DsQcURkA.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-CtF7EN49.js → index-DslTuqro.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-DfKBRdSS.js → index-V-kIHzAV.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-DG2MZQ3f.js → index-e8BM1yY1.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-CvyYJd7z.js → initDefaultProps-DiBszVdG.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-DYZkhUdz.js → motion-BGIFFozv.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-slnJRVT1.js → move-D5W7mp7B.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-DVc0CKmy.js → mtc-parser-JFIlxJ7F.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-BCXAtHCq.js → omit-CQ3o6LIU.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-DBsemz_K.js → pickAttrs-R8MIlXo3.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-BRtkJ-nv.js → placementArrow-sW7ylSky.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-Cebq7EZn.js → responsiveObserve-Cnl39Vbz.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-DT7qvId2.js → slide-sn8tFd14.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-pQiITLkG.js → statusUtils-IKKY_QxZ.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-c-19BVKQ.js → styleChecker-BNpV4Nqs.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-DsS5-0bE.js → useFlexGapSupport-BWyT_eoU.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-CxGiMe5n.js → useFs-D09p6_U9.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-D9btVID5.js → usePersonalDataHub-CzW3TkJZ.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-CUYFsFEN.js → vnode-3s3WoAKB.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-CXmn_35j.js → zoom-Cjy_673s.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/data/changelog.json +13 -0
  151. package/src/lib/background-agent-keeper-protocol.js +75 -4
  152. package/src/lib/background-agent-launch-barrier.js +74 -0
  153. package/src/lib/background-agent-supervisor.js +149 -5
  154. package/src/lib/background-turn-bootstrap-protocol.js +4 -0
  155. package/src/lib/with-file-lock.js +175 -26
  156. package/src/workers/background-agent-keeper.js +112 -19
  157. package/src/workers/background-agent-worker.js +383 -134
  158. package/src/workers/background-turn-bootstrap.js +13 -8
  159. package/src/assets/web-panel/assets/MarkdownRenderer-BLif4QSg.js +0 -1
  160. package/src/assets/web-panel/assets/MobileProjects-BxVavObl.js +0 -1
  161. package/src/assets/web-panel/assets/OrderTableRenderer-CnwojoSI.js +0 -1
  162. package/src/assets/web-panel/assets/Providers-CXoOa8eX.js +0 -1
  163. package/src/assets/web-panel/assets/SpeechSettings-DLjKsDVu.js +0 -1
  164. package/src/assets/web-panel/assets/devWarning-Bj5nUW2v.js +0 -1
  165. package/src/assets/web-panel/assets/index-CPcM-eqo.js +0 -1
  166. 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
- * marker inside the lock directory so a delayed contender cannot delete a
19
- * replacement lock. Strict callers may use bounded jittered retries and an
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 critical section; receives whether the lock was held
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 (!publishedRelease.published && !isOwnerAlive(incumbent)) {
107
- if (reclaimOwnedDirectory(_fs, lockDir, incumbent, isOwnerAlive)) {
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({ locked: held });
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(_fs, lockDir, owner, isOwnerAlive);
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, owner, ownerAlive) {
365
- const markerPath = path.join(lockDir, `.reclaim-${owner.token}`);
366
- try {
367
- writeOwnerMarker(_fs, markerPath, owner);
368
- } catch (error) {
369
- if (error?.code === "ENOENT" || error?.code === "EEXIST") return false;
370
- throw error;
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, owner) || ownerAlive(owner)) {
374
- removeOwnMarker(_fs, markerPath, owner);
467
+ if (!sameOwner(current, incumbent) || ownerAlive(incumbent)) {
468
+ removeOwnMarker(_fs, claimPath, claimant);
375
469
  return false;
376
470
  }
377
- _fs.rmSync(lockDir, { recursive: true, force: true });
378
- return true;
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
- if (!sameOwner(readOwner(_fs, markerPath), owner)) {
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(_fs, lockDir, owner, ownerAlive) {
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
- return false;
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 false;
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" || error?.code === "EEXIST") return false;
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 { readFileSync, unlinkSync, writeSync } from "node:fs";
13
- import { resolve } from "node:path";
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
- currentTime - workerHeartbeatAt >
141
- BACKGROUND_AGENT_KEEPER_HEARTBEAT_TIMEOUT_MS;
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
- (armedTurn &&
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, while the PID/start anchor remains the
157
- // immediate path on platforms that report death promptly.
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 deadline =
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,