chainlesschain 0.162.185 → 0.162.187

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 (184) hide show
  1. package/bin/chainlesschain.js +21 -5
  2. package/package.json +1 -1
  3. package/src/assets/web-panel/assets/{AIOps-Co4ipAKx.js → AIOps-CETaO3s8.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-BpsoDrDq.js → ActionButton-CAe7E9TW.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-DMUqeBik.js → Analytics-BGr2mzwO.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-C6fD_dXr.js → AppLayout-BEoY-NcT.js} +5 -5
  7. package/src/assets/web-panel/assets/{Artifacts-DW0YlQlV.js → Artifacts-DUZivCgt.js} +1 -1
  8. package/src/assets/web-panel/assets/{Audit-DXtHiiFK.js → Audit-C3yBs68k.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-BgmF62U-.js → BackgroundAgents-C9KwiY3O.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-D8vhVLQC.js → Backup-BoSWD_C8.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-D7nCtY2X.js → BaseInput-Dp09GkxL.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-CBZLEV2G.js → Chat-5t9d-Ja0.js} +4 -4
  13. package/src/assets/web-panel/assets/ChatBubbleRenderer-DmVku4GH.js +1 -0
  14. package/src/assets/web-panel/assets/{Checkbox-DJLkeD4M.js → Checkbox-8QJFlkqI.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-Cs6xO8Fd.js → Codegen-BJVFQ3lL.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col-Dtx9wxfT.js → Col-6hdLD0oB.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-DiigJg4n.js → Community-B8opo0uY.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-B67oMDDL.js → Compact-Dv8HudiN.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-BBDl-Jos.js → Compliance-DhxuCS5N.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-Ba98hSsa.js → Cowork-Dggy6VIL.js} +3 -3
  21. package/src/assets/web-panel/assets/{Cron-DBWQZIz7.js → Cron-DSmDqJTT.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-OcrKzxAt.js → Crosschain-7bZq0j5f.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-mW2eSbjM.js → DID-BWY4kh-H.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-BmsDPmKJ.js → Dashboard-bOzPZxlu.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-CVzmCyRx.js → Dropdown-BTFJHqQu.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-vVgqKzhh.js → EmailListRenderer-BC3MOdtT.js} +1 -1
  27. package/src/assets/web-panel/assets/{FamilyGuardDashboard-CWY5t_ZJ.js → FamilyGuardDashboard-CFHAdTTY.js} +1 -1
  28. package/src/assets/web-panel/assets/{Federation-ZGomhjxr.js → Federation-DZ_4aF_n.js} +1 -1
  29. package/src/assets/web-panel/assets/{FormItemContext-DyaknbLr.js → FormItemContext-BZje6UTj.js} +1 -1
  30. package/src/assets/web-panel/assets/{GenericCardRenderer-CmIvN1qr.js → GenericCardRenderer-D7QfCvjN.js} +1 -1
  31. package/src/assets/web-panel/assets/{Git-DQOJIYkG.js → Git-DuG7qANG.js} +2 -2
  32. package/src/assets/web-panel/assets/{Governance-CMiY-EEp.js → Governance-BDmYAgTh.js} +1 -1
  33. package/src/assets/web-panel/assets/{Inference-D6IXuucf.js → Inference-DSXAfc1Q.js} +1 -1
  34. package/src/assets/web-panel/assets/{KnowledgeGraph-6jkTL0hV.js → KnowledgeGraph-DusleniP.js} +1 -1
  35. package/src/assets/web-panel/assets/{Logs-N0jD-349.js → Logs-CpImmA8p.js} +2 -2
  36. package/src/assets/web-panel/assets/MarkdownRenderer-BZ6LJBkk.js +1 -0
  37. package/src/assets/web-panel/assets/{Marketplace-WhYuhBxm.js → Marketplace-DXS8CXkE.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-DTeO28xy.js → McpTools-D151UfUI.js} +5 -5
  39. package/src/assets/web-panel/assets/{Memory-MSHzPSE-.js → Memory-DKDxnoA1.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-Cyqs3J3W.js → MobileBridge-BFZ95iKM.js} +1 -1
  41. package/src/assets/web-panel/assets/MobileProjects-CDAR-grF.js +1 -0
  42. package/src/assets/web-panel/assets/{Mtc-MWBnezNM.js → Mtc-OGJ95xJs.js} +5 -5
  43. package/src/assets/web-panel/assets/{MtcAudit-DQu35Nu-.js → MtcAudit-BmGTo1XC.js} +2 -2
  44. package/src/assets/web-panel/assets/{Multisig-CQS_M-DP.js → Multisig-XumJRqOa.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-WD16vQvY.js → NLProgramming-DHhhRZed.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-DqsiSwOq.js → Notes-B9rGxNNr.js} +3 -3
  47. package/src/assets/web-panel/assets/{NotificationSettings-D3LoxO-P.js → NotificationSettings-DClLbMYV.js} +1 -1
  48. package/src/assets/web-panel/assets/{OrderTableRenderer-C5jPoTd6.js → OrderTableRenderer-I8OohT7N.js} +1 -1
  49. package/src/assets/web-panel/assets/{Organization-48aMBJso.js → Organization-M6WzaYVa.js} +4 -4
  50. package/src/assets/web-panel/assets/{Overflow-j-a2XkhR.js → Overflow-Cb3B5KtM.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-BYpBNYnh.js → P2P-BGk57kya.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-CYCsrrLg.js → PdhVaultBrowser-CXcKyPqK.js} +3 -3
  53. package/src/assets/web-panel/assets/{Permissions-ZRXGXDFb.js → Permissions-hsfJ-Y0e.js} +4 -4
  54. package/src/assets/web-panel/assets/{PersonalDataHub-6OWUPHyL.js → PersonalDataHub-BSBii4js.js} +5 -5
  55. package/src/assets/web-panel/assets/{Pipeline-FcjlDK9E.js → Pipeline-BNw52rey.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-vD_6Tldp.js → Privacy-DUpfue4E.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-B_CHOLXp.js → ProjectInit-C68NsP3h.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-rEiqQ2da.js → ProjectSettings-BJ2It9v4.js} +2 -2
  59. package/src/assets/web-panel/assets/Projects-D2FHLUa-.js +1 -0
  60. package/src/assets/web-panel/assets/{Providers-Dgtt97B4.js → Providers-EnU4VWwS.js} +1 -1
  61. package/src/assets/web-panel/assets/{QrScannerModal-DaGVylZO.js → QrScannerModal-BRt5-B_I.js} +1 -1
  62. package/src/assets/web-panel/assets/{QuickAsk-DP2_2x3u.js → QuickAsk-Jd5L9UL2.js} +1 -1
  63. package/src/assets/web-panel/assets/{Recommend-D29yj4ki.js → Recommend-BNDhoVIu.js} +1 -1
  64. package/src/assets/web-panel/assets/{RemoteSession-Ue2wnBLp.js → RemoteSession-BII41Mel.js} +2 -2
  65. package/src/assets/web-panel/assets/{Reputation-CrxIHqeC.js → Reputation-D1wQPUNv.js} +1 -1
  66. package/src/assets/web-panel/assets/{Row-BgSzJPCn.js → Row-DnjRegul.js} +1 -1
  67. package/src/assets/web-panel/assets/{RssFeed-AwhYOrP9.js → RssFeed-C_T4I-nF.js} +2 -2
  68. package/src/assets/web-panel/assets/{Search-D2shpEcm.js → Search-C4lrxlLh.js} +1 -1
  69. package/src/assets/web-panel/assets/{Security-BtKzhTbT.js → Security-ByYT-VzP.js} +4 -4
  70. package/src/assets/web-panel/assets/{Services-BY0DsUSm.js → Services-BHpyEqB5.js} +2 -2
  71. package/src/assets/web-panel/assets/{Skeleton-B3Sb0UI9.js → Skeleton-C0I23vG-.js} +1 -1
  72. package/src/assets/web-panel/assets/{Skills-Ne5w0bDs.js → Skills-DGZXz22K.js} +1 -1
  73. package/src/assets/web-panel/assets/{Sla-C8VkaP4T.js → Sla-DL_urP9A.js} +1 -1
  74. package/src/assets/web-panel/assets/{SpeechSettings-BQdIEYP9.js → SpeechSettings-6x-hUNQ_.js} +1 -1
  75. package/src/assets/web-panel/assets/{SyncSettings-CwFafrUg.js → SyncSettings-Crk5TCXq.js} +2 -2
  76. package/src/assets/web-panel/assets/{Tasks-yJuKoDlR.js → Tasks-rZGq5PZs.js} +1 -1
  77. package/src/assets/web-panel/assets/{Templates-DYn9qCmx.js → Templates-DT5el8Q8.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tenant-DkKReHvn.js → Tenant-mlgeyMK8.js} +1 -1
  79. package/src/assets/web-panel/assets/{Terminal-r6bd-owt.js → Terminal-C8HEA-lM.js} +2 -2
  80. package/src/assets/web-panel/assets/TimelineRenderer-DvlqBayB.js +1 -0
  81. package/src/assets/web-panel/assets/{Tokens-BnMw7MRi.js → Tokens-C7lDcNmT.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trigger-CTzKp4o9.js → Trigger-CM8nCnIJ.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trust-BBrSZm9U.js → Trust-BQDLUQ9x.js} +1 -1
  84. package/src/assets/web-panel/assets/{UkeySign-D9fDvEMq.js → UkeySign-CVqM0OFE.js} +1 -1
  85. package/src/assets/web-panel/assets/{VideoEditing-DUi5ve-0.js → VideoEditing-BvAcQpzT.js} +1 -1
  86. package/src/assets/web-panel/assets/{Wallet-C8Fi1P8_.js → Wallet-zFVFaAZ2.js} +4 -4
  87. package/src/assets/web-panel/assets/{WebAuthn-s1LppYi2.js → WebAuthn-Cc522ums.js} +5 -5
  88. package/src/assets/web-panel/assets/{WorkflowEditor-DJL2C4lj.js → WorkflowEditor-Bg_wxpDo.js} +1 -1
  89. package/src/assets/web-panel/assets/{chat-D3F2ndOB.js → chat-jg0QwC8K.js} +1 -1
  90. package/src/assets/web-panel/assets/{colors-B7739-e3.js → colors-ALzYz1gO.js} +1 -1
  91. package/src/assets/web-panel/assets/{compact-item-CcJbFTF4.js → compact-item-DXHYUoqi.js} +1 -1
  92. package/src/assets/web-panel/assets/{createContext-CuP_DEFS.js → createContext-BsGsBM5m.js} +1 -1
  93. package/src/assets/web-panel/assets/devWarning-Dy1GJA4S.js +1 -0
  94. package/src/assets/web-panel/assets/{hasIn-6YEXfIik.js → hasIn-BccMLzxA.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-fWgUQZ9d.js → index-08diKcTm.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-CxradkS6.js → index-2VstcImq.js} +19 -19
  97. package/src/assets/web-panel/assets/index-8zbMbbuf.js +1 -0
  98. package/src/assets/web-panel/assets/{index-DDRlBaDt.js → index-BBbbr1YB.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-Dox44j29.js → index-BTTeAiOP.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BYYSLEjn.js → index-BcJzphrP.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CRuoRapN.js → index-BdCmJ_bG.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-BxSqVMGh.js → index-BfF0OGKU.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-D6Ij_2uw.js → index-BfV8RDlg.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-Dsy7_x4s.js → index-BvsII10i.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BsdSnHQE.js → index-C0AbXsDy.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-mfWQRSLn.js → index-C3Rf8wIM.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BIID4XcP.js → index-C3k3ZwkQ.js} +1 -1
  108. package/src/assets/web-panel/assets/index-C97f71oi.js +1 -0
  109. package/src/assets/web-panel/assets/{index-4h3lpWJ3.js → index-CBVIFX-x.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-DHvxXXNA.js → index-CJg6Wsjc.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DjI9MkId.js → index-CLzPzN75.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-D0zdPyTD.js → index-CQ1ORdp-.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-D0K_rFC1.js → index-CSFESurl.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-b9cuvFEv.js → index-CWLWBgfe.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-Nz-MUHD1.js → index-CahDve8M.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DXdnfDVK.js → index-Cg9cOVe9.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-Bh4hcNWW.js → index-Cmg_Npx5.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-Bru_kpZl.js → index-CrZHUsOZ.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-yOZYSNmG.js → index-Cz_s25m3.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-DlSvpNMf.js → index-D9oV2iRR.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-LfRlSmqe.js → index-DgjQ-kdT.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-BOHUo3aM.js → index-Dmk7dD-T.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-D3haorUt.js → index-Dxqiu9-F.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-DV11jpeO.js → index-JS2LXdd0.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-CY0zd_jc.js → index-TeCof1s3.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-DpEKmjoa.js → index-UQXedWQz.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-C3pctdCb.js → index-koT8Q86Z.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-D_75jYWk.js → index-mIQUoUx1.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-Gxw09e9R.js → index-oWltafBn.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BhIKnQAW.js → index-rIy96PeM.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-C3ZA_h1j.js → index-rcwgNMc0.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-DGsE7MQz.js → index-srqW5_-X.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-eR6H6NR7.js → index-uzykuL_R.js} +1 -1
  134. package/src/assets/web-panel/assets/{initDefaultProps-CJ1Bq2L3.js → initDefaultProps-xafkqrW3.js} +1 -1
  135. package/src/assets/web-panel/assets/{motion-rYJN_vzf.js → motion-BrtyQXLk.js} +1 -1
  136. package/src/assets/web-panel/assets/{move-DnDK2gTP.js → move-BunUyg59.js} +1 -1
  137. package/src/assets/web-panel/assets/{mtc-parser-CKnXy7ES.js → mtc-parser-BPwjBeuE.js} +1 -1
  138. package/src/assets/web-panel/assets/{omit-3nRc3hWT.js → omit-BpyqZlNO.js} +1 -1
  139. package/src/assets/web-panel/assets/{pickAttrs-Ddjj3sLu.js → pickAttrs-DDrV0XjE.js} +1 -1
  140. package/src/assets/web-panel/assets/{placementArrow-CbsbphQY.js → placementArrow-1A18Pcl2.js} +1 -1
  141. package/src/assets/web-panel/assets/{responsiveObserve-D9aZ4VDJ.js → responsiveObserve-B8Fnlmso.js} +1 -1
  142. package/src/assets/web-panel/assets/{slide-D4pvdFj0.js → slide-x-Fwb2Zm.js} +1 -1
  143. package/src/assets/web-panel/assets/{statusUtils-C2W-vY-S.js → statusUtils-aAG5yJl_.js} +1 -1
  144. package/src/assets/web-panel/assets/{styleChecker-Cbq5150d.js → styleChecker-BRQSTMRL.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFlexGapSupport-BA2B7R82.js → useFlexGapSupport-D5X_KozY.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFs-CLrwg3xg.js → useFs-DN_YIjP-.js} +1 -1
  147. package/src/assets/web-panel/assets/{usePersonalDataHub-DQeAI5qs.js → usePersonalDataHub-BizxAvjw.js} +1 -1
  148. package/src/assets/web-panel/assets/{vnode-8BDGy-YU.js → vnode-B5oAxN93.js} +1 -1
  149. package/src/assets/web-panel/assets/{zoom-B2SzFprl.js → zoom-DJhvyzKv.js} +1 -1
  150. package/src/assets/web-panel/index.html +1 -1
  151. package/src/commands/eval.js +7 -0
  152. package/src/commands/status.js +55 -0
  153. package/src/commands/team.js +1805 -448
  154. package/src/data/changelog.json +26 -0
  155. package/src/harness/worktree-isolator.js +671 -79
  156. package/src/lazy-dispatch.js +12 -1
  157. package/src/lib/agent-team/task-lease.js +378 -78
  158. package/src/lib/agent-team/team-agent-stream.js +488 -0
  159. package/src/lib/agent-team/team-budget.js +216 -20
  160. package/src/lib/agent-team/team-mailbox.js +389 -15
  161. package/src/lib/agent-team/team-run-state-lock.js +209 -0
  162. package/src/lib/agent-team/team-runner.js +547 -63
  163. package/src/lib/agent-team/team-scope-lock.js +409 -0
  164. package/src/lib/agent-team/team-task-contract.js +277 -0
  165. package/src/lib/agent-team/team-worktree.js +1171 -62
  166. package/src/lib/agent-worktree.js +19 -20
  167. package/src/lib/collaboration-run-store.js +983 -68
  168. package/src/lib/observability/index.js +191 -19
  169. package/src/lib/observability/otlp-exporter.js +1209 -207
  170. package/src/lib/otlp-exporter.js +8 -36
  171. package/src/lib/with-file-lock.js +190 -24
  172. package/src/lib/worktree-isolator.js +3 -0
  173. package/src/lib/worktree-sparse.js +23 -0
  174. package/src/program-base.js +9 -2
  175. package/src/runtime/headless-runner.js +45 -21
  176. package/src/runtime/headless-stream.js +6 -1
  177. package/src/assets/web-panel/assets/ChatBubbleRenderer-CtDb-2Qu.js +0 -1
  178. package/src/assets/web-panel/assets/MarkdownRenderer-BL1IVXm-.js +0 -1
  179. package/src/assets/web-panel/assets/MobileProjects-7IN2hWHg.js +0 -1
  180. package/src/assets/web-panel/assets/Projects-Bg8obRAM.js +0 -1
  181. package/src/assets/web-panel/assets/TimelineRenderer-Mq2juUC3.js +0 -1
  182. package/src/assets/web-panel/assets/devWarning-Cj_36GHq.js +0 -1
  183. package/src/assets/web-panel/assets/index-CezFrAmD.js +0 -1
  184. package/src/assets/web-panel/assets/index-Cxgguez6.js +0 -1
@@ -42,13 +42,34 @@ export class TeamRunner {
42
42
  // Team-wide token/USD/time/task budget (null → unbounded). Consulted before
43
43
  // every claim; folded after every settled task.
44
44
  this.budget = opts.budget || null;
45
+ this.budgetForTask =
46
+ typeof opts.budgetForTask === "function" ? opts.budgetForTask : null;
45
47
  // Directed/broadcast messaging between teammates (null → disabled).
46
48
  this.mailbox = opts.mailbox || null;
49
+ // Optional conservative path ownership. Tasks with overlapping scopes are
50
+ // serialized in a shared workspace; an empty scope means the whole repo.
51
+ this.scopeLock = opts.scopeLock || null;
52
+ this.scopeForTask =
53
+ typeof opts.scopeForTask === "function"
54
+ ? opts.scopeForTask
55
+ : (task) => task?.metadata?.scopePaths || [];
56
+ this.maxScopeScan =
57
+ Number.isSafeInteger(opts.maxScopeScan) && opts.maxScopeScan > 0
58
+ ? opts.maxScopeScan
59
+ : 1024;
47
60
  // Optional TelemetryRecorder: emits a `team.task` span per execution (and
48
61
  // inherits the recorder's default workflow.run_id/name attributes) so
49
62
  // `cc team run --otlp` traces the whole graph. Null → zero cost.
50
63
  this.recorder = opts.recorder || null;
64
+ // Durable state hooks. `beforeTask` runs after lease acquisition but before
65
+ // the executor (so a claim can be persisted before side effects);
66
+ // `afterTask` runs after the authoritative registry settlement.
67
+ this.beforeTask =
68
+ typeof opts.beforeTask === "function" ? opts.beforeTask : null;
69
+ this.afterTask =
70
+ typeof opts.afterTask === "function" ? opts.afterTask : null;
51
71
  this._executions = 0;
72
+ this._reservedExecutions = 0;
52
73
  this._inFlight = 0;
53
74
  this._maxInFlight = 0;
54
75
  // Keys executing in this runner process. An event-loop stall may let a
@@ -57,6 +78,14 @@ export class TeamRunner {
57
78
  this._activeKeys = new Set();
58
79
  this._members = new Map(); // holder → lifecycle record
59
80
  this._budgetStopped = false;
81
+ this._fatalError = null;
82
+ // Idle workers wait on graph progress instead of polling the full graph
83
+ // through setTimeout(0). This matters once a team has many workers but only
84
+ // a narrow DAG frontier.
85
+ this._progressWaiters = new Set();
86
+ this._workerCount = 1;
87
+ this._claimControllers = new Set();
88
+ this._wallTimer = null;
60
89
  }
61
90
 
62
91
  _emit(type, extra) {
@@ -131,27 +160,95 @@ export class TeamRunner {
131
160
  if (typeof this.runTask !== "function") {
132
161
  throw new Error("TeamRunner: opts.runTask is required");
133
162
  }
163
+ const initialScopeStatus = this.scopeLock?.status?.();
164
+ if (initialScopeStatus?.count > 0) {
165
+ const error = new Error(
166
+ "TeamRunner cannot start with orphaned scope ownership",
167
+ );
168
+ error.code = "TEAM_SCOPE_LOCK_ORPHANED";
169
+ throw error;
170
+ }
134
171
  this._emit("run:start", {
135
172
  teammates: this.teammates,
136
173
  tasks: this.registry.list().length,
137
174
  });
138
- if (this.budget) this.budget.start();
175
+ if (this.budget) {
176
+ this.budget.start();
177
+ const maxWallMs = Number(this.budget.maxWallMs);
178
+ if (Number.isFinite(maxWallMs) && maxWallMs > 0) {
179
+ const exhaustWallBudget = () => {
180
+ if (!this._budgetStopped) {
181
+ this._budgetStopped = true;
182
+ this._emit("run:budget-exhausted", {
183
+ reason: "max-wall-ms",
184
+ });
185
+ }
186
+ for (const controller of this._claimControllers) {
187
+ try {
188
+ controller.abort(new Error("Team wall-clock budget exhausted"));
189
+ } catch {
190
+ /* every settlement remains fenced by its lease id */
191
+ }
192
+ }
193
+ this._signalProgress();
194
+ };
195
+ const armWallTimer = () => {
196
+ const elapsedMs = this.budget.status().elapsedMs;
197
+ const remainingMs = maxWallMs - elapsedMs;
198
+ if (remainingMs <= 0) {
199
+ exhaustWallBudget();
200
+ return;
201
+ }
202
+ // Node timers are signed 32-bit. Re-arm long budgets instead of
203
+ // truncating them, and charge only the active time restored from a
204
+ // prior process (not the downtime between runs).
205
+ this._wallTimer = setTimeout(
206
+ armWallTimer,
207
+ Math.min(remainingMs, 2_147_483_647),
208
+ );
209
+ };
210
+ armWallTimer();
211
+ }
212
+ }
139
213
  const workers = [];
140
- for (let i = 0; i < this.teammates; i++) {
141
- const holder = `teammate-${i + 1}`;
214
+ const workerCount = Math.min(
215
+ this.teammates,
216
+ Math.max(1, this.registry.list().length),
217
+ );
218
+ this._workerCount = workerCount;
219
+ const holders = Array.from(
220
+ { length: workerCount },
221
+ (_, index) => `teammate-${index + 1}`,
222
+ );
223
+ this.mailbox?.registerRecipients?.(holders);
224
+ for (const holder of holders) {
142
225
  this._setState(holder, "idle");
143
226
  workers.push(this._worker(holder));
144
227
  }
145
- await Promise.all(workers);
228
+ try {
229
+ await Promise.all(workers);
230
+ } finally {
231
+ if (this._wallTimer) {
232
+ clearTimeout(this._wallTimer);
233
+ this._wallTimer = null;
234
+ }
235
+ }
236
+ if (this._fatalError) throw this._fatalError;
146
237
  const stats = this.registry.stats();
238
+ const done = this.registry.allDone();
147
239
  const summary = {
148
- done: this.registry.allDone(),
240
+ done,
241
+ success: done && stats.completed === stats.total,
149
242
  executions: this._executions,
150
243
  maxConcurrent: this._maxInFlight,
244
+ requestedTeammates: this.teammates,
245
+ activeTeammates: workerCount,
151
246
  budgetStopped: this._budgetStopped,
152
247
  budgetReason: this.budget ? this.budget.reason() : null,
153
248
  members: this.members(),
154
249
  messages: this.mailbox ? this.mailbox.size() : 0,
250
+ mailboxStatus: this.mailbox?.status?.() || null,
251
+ scopeOwnership: this.scopeLock?.status?.() || null,
155
252
  stats,
156
253
  };
157
254
  this._emit("run:end", summary);
@@ -164,11 +261,29 @@ export class TeamRunner {
164
261
  // its dep will finish — so a worker that finds nothing claimable yields and
165
262
  // re-checks while any work is still in flight.
166
263
  for (;;) {
167
- if (this._executions >= this.maxTasks) {
264
+ if (this._fatalError) {
265
+ this._setState(holder, "shutdown", { reason: "fatal-error" });
266
+ return;
267
+ }
268
+ if (this._executions + this._reservedExecutions >= this.maxTasks) {
168
269
  this._emit("run:budget-exhausted", { maxTasks: this.maxTasks });
169
270
  this._setState(holder, "shutdown", { reason: "max-tasks" });
170
271
  return;
171
272
  }
273
+ const budgetStatus = this.budget?.status?.();
274
+ if (
275
+ budgetStatus?.maxTasks != null &&
276
+ Math.max(budgetStatus.tasks, this._executions) +
277
+ this._reservedExecutions >=
278
+ budgetStatus.maxTasks
279
+ ) {
280
+ if (!this._budgetStopped) {
281
+ this._budgetStopped = true;
282
+ this._emit("run:budget-exhausted", { reason: "max-tasks" });
283
+ }
284
+ this._setState(holder, "shutdown", { reason: "max-tasks" });
285
+ return;
286
+ }
172
287
  // Team budget (token/USD/time/task) — stop CLAIMING once any cap is hit so
173
288
  // the team overshoots by at most the tasks already in flight.
174
289
  if (this.budget && this.budget.shouldStop()) {
@@ -184,11 +299,21 @@ export class TeamRunner {
184
299
  if (!key) {
185
300
  // Nothing to claim right now. If peers are still working, wait and
186
301
  // retry (their completions may unblock a dependent). Otherwise we're done.
187
- if (this._inFlight > 0) {
302
+ const heldScopes = this.scopeLock?.status?.().count || 0;
303
+ if (this._inFlight > 0 || this._reservedExecutions > 0) {
188
304
  this._setState(holder, "idle");
189
- await this._tick();
305
+ await this._waitForProgress();
190
306
  continue;
191
307
  }
308
+ if (heldScopes > 0) {
309
+ const error = new Error(
310
+ "TeamRunner found scope ownership without a live local claim",
311
+ );
312
+ error.code = "TEAM_SCOPE_LOCK_ORPHANED";
313
+ this._setFatal(error, { phase: "scope-reconciliation" });
314
+ this._setState(holder, "shutdown", { reason: "fatal-error" });
315
+ return;
316
+ }
192
317
  this._setState(holder, "shutdown", { reason: "no-more-work" });
193
318
  return; // no claimable + nothing in flight → this worker is finished
194
319
  }
@@ -199,12 +324,262 @@ export class TeamRunner {
199
324
  await this._tick();
200
325
  continue;
201
326
  }
202
- await this._execute(holder, key, acq.lease);
327
+ const budgetReservation = this._reserveTaskBudget(
328
+ key,
329
+ this.registry.getTask(key),
330
+ acq.lease,
331
+ );
332
+ if (!budgetReservation.ok) {
333
+ this.registry.release(key, {
334
+ holder,
335
+ leaseId: acq.lease.leaseId,
336
+ });
337
+ if (budgetReservation.temporary) {
338
+ this._setState(holder, "idle");
339
+ await this._waitForProgress();
340
+ continue;
341
+ }
342
+ if (!this._budgetStopped) {
343
+ this._budgetStopped = true;
344
+ this._emit("run:budget-exhausted", {
345
+ reason: budgetReservation.reason,
346
+ });
347
+ }
348
+ this._setState(holder, "shutdown", {
349
+ reason: budgetReservation.reason,
350
+ });
351
+ return;
352
+ }
353
+ this._reservedExecutions++;
354
+ const claim = this._beginClaim(holder, key, acq.lease, budgetReservation);
355
+ if (!this._acquireScope(holder, key)) {
356
+ this._abandonClaim(holder, key, claim);
357
+ if (this._fatalError) {
358
+ this._setState(holder, "shutdown", { reason: "fatal-error" });
359
+ return;
360
+ }
361
+ this._setState(holder, "idle");
362
+ await this._waitForProgress();
363
+ continue;
364
+ }
365
+ if (!(await this._prepareTask(holder, key, claim))) {
366
+ this._abandonClaim(holder, key, claim);
367
+ return;
368
+ }
369
+ if (this._fatalError || claim.lost) {
370
+ if (claim.lost && !this._fatalError) {
371
+ const error = new Error(
372
+ `Lease for task "${key}" was lost while preparing it`,
373
+ );
374
+ error.code = "TEAM_TASK_LEASE_LOST";
375
+ this._setFatal(error, { phase: "before-task", key, holder });
376
+ }
377
+ this._abandonClaim(holder, key, claim);
378
+ this._setState(holder, "shutdown", { reason: "fatal-error" });
379
+ return;
380
+ }
381
+ await this._execute(holder, key, claim);
382
+ }
383
+ }
384
+
385
+ async _prepareTask(holder, key, claim) {
386
+ if (!this.beforeTask) return true;
387
+ try {
388
+ await this.beforeTask({
389
+ key,
390
+ task: this.registry.getTask(key),
391
+ holder,
392
+ lease: this.registry.getTask(key)?.lease || claim.lease,
393
+ });
394
+ return true;
395
+ } catch (error) {
396
+ this._setFatal(error, { phase: "before-task", key, holder });
397
+ this._setState(holder, "shutdown", { reason: "fatal-error" });
398
+ return false;
399
+ }
400
+ }
401
+
402
+ _setFatal(error, extra = {}) {
403
+ if (this._fatalError) return;
404
+ const failure =
405
+ error instanceof Error ? error : new Error(String(error || "team fatal"));
406
+ this._fatalError = failure;
407
+ this._emit("run:fatal", {
408
+ ...extra,
409
+ error: failure.message,
410
+ });
411
+ this._signalProgress();
412
+ }
413
+
414
+ _reserveTaskBudget(key, task, lease) {
415
+ if (!this.budget?.reserve) return { ok: true, id: null };
416
+ const requested = this.budgetForTask?.(task, key) || {};
417
+ if (requested.reserveUsage === false) return { ok: true, id: null };
418
+ const availableWorkers = Math.max(
419
+ 1,
420
+ this._workerCount - this._inFlight - this._reservedExecutions,
421
+ );
422
+ const readyAfterAcquire =
423
+ typeof this.registry.claimableCount === "function"
424
+ ? this.registry.claimableCount()
425
+ : availableWorkers - 1;
426
+ const slots = Math.max(
427
+ 1,
428
+ Math.min(availableWorkers, readyAfterAcquire + 1),
429
+ );
430
+ const reservation = this.budget.reserve(lease.leaseId, {
431
+ maxTokens: requested.maxTokens,
432
+ maxUsd: requested.maxBudgetUsd ?? requested.maxUsd,
433
+ slots,
434
+ });
435
+ if (!reservation.ok) return reservation;
436
+ return {
437
+ ...reservation,
438
+ maxBudgetUsd: reservation.maxUsd,
439
+ };
440
+ }
441
+
442
+ _beginClaim(holder, key, lease, budgetReservation = null) {
443
+ const claim = {
444
+ holder,
445
+ key,
446
+ lease,
447
+ leaseId: lease?.leaseId,
448
+ heartbeat: null,
449
+ lost: false,
450
+ abortController: new AbortController(),
451
+ budgetReservation,
452
+ budgetSettled: false,
453
+ };
454
+ // Fence the task immediately after acquire, before any async durable hook.
455
+ // Otherwise a slow beforeTask can outlive the TTL and a peer can run the
456
+ // same side effect concurrently.
457
+ this._activeKeys.add(key);
458
+ this._claimControllers.add(claim.abortController);
459
+ const effectiveTtl =
460
+ this.ttlMs > 0 ? this.ttlMs : this.registry.defaultTtlMs || 60000;
461
+ const every = Math.max(
462
+ 25,
463
+ Math.floor(this.renewEveryMs > 0 ? this.renewEveryMs : effectiveTtl / 3),
464
+ );
465
+ claim.heartbeat = setInterval(() => {
466
+ const result = this._renewClaim(claim);
467
+ if (!result?.ok) clearInterval(claim.heartbeat);
468
+ }, every);
469
+ if (typeof claim.heartbeat.unref === "function") claim.heartbeat.unref();
470
+ return claim;
471
+ }
472
+
473
+ _renewClaim(claim) {
474
+ if (!claim || claim.lost) {
475
+ return { ok: false, reason: "lease_lost" };
476
+ }
477
+ let result;
478
+ try {
479
+ result = this.registry.renew(claim.key, {
480
+ holder: claim.holder,
481
+ leaseId: claim.leaseId,
482
+ ttlMs: this.ttlMs,
483
+ });
484
+ // A delayed heartbeat may wake just after expiry. Reacquire creates a new
485
+ // fencing token and succeeds only if no peer has taken the task.
486
+ if (!result?.ok && result?.reason === "not_holder_or_expired") {
487
+ result = this.registry.acquire(claim.key, {
488
+ holder: claim.holder,
489
+ ttlMs: this.ttlMs,
490
+ });
491
+ if (result?.ok) {
492
+ claim.lease = result.lease;
493
+ claim.leaseId = result.lease.leaseId;
494
+ }
495
+ } else if (result?.ok) {
496
+ claim.lease = result.lease;
497
+ }
498
+ } catch {
499
+ result = { ok: false, reason: "renew_failed" };
500
+ }
501
+ if (!result?.ok) {
502
+ claim.lost = true;
503
+ try {
504
+ claim.abortController.abort(
505
+ new Error(`Lease for task "${claim.key}" was lost`),
506
+ );
507
+ } catch {
508
+ /* the fencing token still prevents stale settlement */
509
+ }
510
+ this._signalProgress();
511
+ }
512
+ return result;
513
+ }
514
+
515
+ _endClaim(claim) {
516
+ if (!claim) return;
517
+ if (claim.heartbeat) clearInterval(claim.heartbeat);
518
+ this._activeKeys.delete(claim.key);
519
+ this._claimControllers.delete(claim.abortController);
520
+ }
521
+
522
+ _abandonClaim(holder, key, claim) {
523
+ this.registry.release(key, {
524
+ holder,
525
+ leaseId: claim?.leaseId,
526
+ });
527
+ this._releaseScope(holder, key);
528
+ if (claim?.budgetReservation?.id) {
529
+ this.budget?.releaseReservation?.(claim.budgetReservation.id);
530
+ }
531
+ this._endClaim(claim);
532
+ this._reservedExecutions = Math.max(0, this._reservedExecutions - 1);
533
+ this._signalProgress();
534
+ }
535
+
536
+ async _notifySettlement(settlement) {
537
+ if (!this.afterTask) return;
538
+ try {
539
+ await this.afterTask(settlement);
540
+ } catch (error) {
541
+ this._setFatal(error, {
542
+ phase: "after-task",
543
+ key: settlement.key,
544
+ holder: settlement.holder,
545
+ });
203
546
  }
204
547
  }
205
548
 
206
549
  /** Pick a claimable task key for this holder (highest priority first). */
207
550
  _nextFor() {
551
+ // A whole-workspace owner blocks every other scope, so avoid walking a
552
+ // large ready queue only to rediscover the same conflict for each task.
553
+ if (
554
+ this.scopeLock
555
+ ?.status?.()
556
+ ?.locks?.some((candidate) => candidate.workspace === true)
557
+ ) {
558
+ return null;
559
+ }
560
+ if (typeof this.registry.nextClaimable === "function") {
561
+ const excluded = new Set(this._activeKeys);
562
+ for (let scanned = 0; scanned < this.maxScopeScan; scanned++) {
563
+ const key = this.registry.nextClaimable({ excludeKeys: excluded });
564
+ if (!key || !this.scopeLock) return key;
565
+ const task = this.registry.getTask(key);
566
+ const check = this.scopeLock.canAcquire(
567
+ key,
568
+ this.scopeForTask(task, key),
569
+ );
570
+ if (check.ok) return key;
571
+ if (check.code !== "TEAM_SCOPE_LOCK_SCOPE_CONFLICT") {
572
+ const error = new Error(
573
+ check.message || `invalid task scope ownership for "${key}"`,
574
+ );
575
+ error.code = check.code || "TEAM_SCOPE_LOCK_INVALID_SCOPES";
576
+ this._setFatal(error, { phase: "scope-validation", key });
577
+ return null;
578
+ }
579
+ excluded.add(key);
580
+ }
581
+ return null;
582
+ }
208
583
  const claimable = this.registry.claimable();
209
584
  if (claimable.length === 0) return null;
210
585
  // Stable: prefer higher priority, else insertion order (claimable order).
@@ -223,55 +598,82 @@ export class TeamRunner {
223
598
  return best;
224
599
  }
225
600
 
226
- async _execute(holder, key, lease) {
601
+ _acquireScope(holder, key) {
602
+ if (!this.scopeLock) return true;
227
603
  const task = this.registry.getTask(key);
604
+ const result = this.scopeLock.acquire(key, this.scopeForTask(task, key));
605
+ if (!result.ok) {
606
+ if (result.code !== "TEAM_SCOPE_LOCK_SCOPE_CONFLICT") {
607
+ const error = new Error(
608
+ result.message || `invalid task scope ownership for "${key}"`,
609
+ );
610
+ error.code = result.code || "TEAM_SCOPE_LOCK_INVALID_SCOPES";
611
+ this._setFatal(error, {
612
+ phase: "scope-acquire",
613
+ key,
614
+ holder,
615
+ });
616
+ }
617
+ return false;
618
+ }
619
+ this._emit("task:scope-acquired", {
620
+ key,
621
+ holder,
622
+ scopes: result.scopes,
623
+ workspace: result.scopes.length === 0,
624
+ });
625
+ return true;
626
+ }
627
+
628
+ _releaseScope(holder, key) {
629
+ if (!this.scopeLock) return;
630
+ const released = this.scopeLock.release(key);
631
+ if (released?.ok) {
632
+ this._emit("task:scope-released", {
633
+ key,
634
+ holder,
635
+ scopes: released.scopes,
636
+ workspace: released.workspace,
637
+ });
638
+ }
639
+ }
640
+
641
+ async _execute(holder, key, claim) {
642
+ const task = this.registry.getTask(key);
643
+ this._reservedExecutions--;
228
644
  this._executions++;
229
645
  this._inFlight++;
230
- this._activeKeys.add(key);
231
646
  this._maxInFlight = Math.max(this._maxInFlight, this._inFlight);
232
647
  this._setState(holder, "running", { key });
233
648
  this._emit("task:claimed", { key, holder, attempts: task.attempts });
234
- const renew = () => {
235
- const result = this.registry.renew(key, {
236
- holder,
237
- ttlMs: this.ttlMs,
238
- });
239
- // A delayed timer may wake just after expiry. Reacquire is safe and
240
- // optimistic: it succeeds only if no external runner has taken the
241
- // lease; a real takeover remains authoritative and stops the heartbeat.
242
- return !result?.ok && result?.reason === "not_holder_or_expired"
243
- ? this.registry.acquire(key, { holder, ttlMs: this.ttlMs })
244
- : result;
245
- };
246
- // Heartbeat: the built-in executors (shell / agent / worktree) never call
247
- // `renew` themselves, so before this ANY task outliving the lease TTL was
248
- // indistinguishable from a crash — a peer could steal it and the SAME task
249
- // would run twice concurrently. The runner owns the TTL, so it renews here
250
- // for every executor. Stops itself once the lease is lost (renew rejected —
251
- // keeping a lost lease alive would be lying to the registry).
252
- const effectiveTtl =
253
- this.ttlMs > 0 ? this.ttlMs : this.registry.defaultTtlMs || 60000;
254
- const every = Math.max(
255
- 25,
256
- Math.floor(this.renewEveryMs > 0 ? this.renewEveryMs : effectiveTtl / 3),
257
- );
258
- const heartbeat = setInterval(() => {
259
- try {
260
- const r = renew();
261
- if (!r || r.ok !== true) clearInterval(heartbeat);
262
- } catch {
263
- clearInterval(heartbeat);
264
- }
265
- }, every);
266
- // Never hold the process open just for a heartbeat.
267
- if (typeof heartbeat.unref === "function") heartbeat.unref();
649
+ const renew = () => this._renewClaim(claim);
268
650
  // A teammate-scoped messaging handle: post to a peer / broadcast, and read
269
651
  // its own inbox (direct messages + unseen broadcasts).
270
652
  const inbox = this.mailbox ? this.mailbox.drain(holder) : [];
271
- const sendMessage = (to, body, subject = null) =>
272
- this.mailbox
273
- ? this.mailbox.send({ from: holder, to, subject, body })
274
- : null;
653
+ const sendMessage = (to, body, subject = null) => {
654
+ if (!this.mailbox) return null;
655
+ let message;
656
+ try {
657
+ message = this.mailbox.send({ from: holder, to, subject, body });
658
+ } catch (error) {
659
+ this._emit("mailbox:backpressure", {
660
+ holder,
661
+ code: error?.code || "TEAM_MAILBOX_SEND_FAILED",
662
+ status: this.mailbox.status?.() || null,
663
+ });
664
+ throw error;
665
+ }
666
+ const pressure = this.mailbox.pressure?.();
667
+ if (pressure && pressure.level !== "normal") {
668
+ this._emit("mailbox:pressure", {
669
+ holder,
670
+ level: pressure.level,
671
+ ratio: pressure.ratio,
672
+ messages: this.mailbox.size(),
673
+ });
674
+ }
675
+ return message;
676
+ };
275
677
  const startedAt = this._now();
276
678
  const span = this.recorder
277
679
  ? this.recorder.startSpan("team.task", {
@@ -290,8 +692,9 @@ export class TeamRunner {
290
692
  sendMessage,
291
693
  mailbox: this.mailbox,
292
694
  budget: this.budget,
695
+ budgetReservation: claim.budgetReservation,
696
+ signal: claim.abortController.signal,
293
697
  });
294
- const done = this.registry.complete(key, { holder, result });
295
698
  // Fold usage/cost into the team budget regardless — the task DID execute
296
699
  // and consumed resources. `--exec` shell tasks carry no usage → only the
297
700
  // task-count / wall-clock dimensions move.
@@ -299,12 +702,28 @@ export class TeamRunner {
299
702
  this.budget.record(
300
703
  {
301
704
  usage: result?.usage || null,
705
+ usageRecords: result?.usageRecords || null,
302
706
  provider: result?.provider,
303
707
  model: result?.model,
708
+ reservationId: claim.budgetReservation?.id,
304
709
  },
305
710
  this._now(),
306
711
  );
712
+ claim.budgetSettled = true;
713
+ if (this.budget.reason() === "unpriced-usage") {
714
+ const error = new Error(
715
+ "Team USD budget cannot account for unpriced remote usage",
716
+ );
717
+ error.code = "TEAM_BUDGET_UNPRICED_USAGE";
718
+ error.retryable = false;
719
+ throw error;
720
+ }
307
721
  }
722
+ const done = this.registry.complete(key, {
723
+ holder,
724
+ leaseId: claim.leaseId,
725
+ result,
726
+ });
308
727
  if (done.ok) {
309
728
  if (span) span.end();
310
729
  this._setState(holder, "completed-task");
@@ -313,6 +732,12 @@ export class TeamRunner {
313
732
  holder,
314
733
  ms: this._now() - startedAt,
315
734
  });
735
+ await this._notifySettlement({
736
+ key,
737
+ holder,
738
+ status: "completed",
739
+ result,
740
+ });
316
741
  } else {
317
742
  if (span) {
318
743
  span.setAttribute("team.completion_discarded", true);
@@ -330,6 +755,12 @@ export class TeamRunner {
330
755
  reason: done.reason,
331
756
  ms: this._now() - startedAt,
332
757
  });
758
+ await this._notifySettlement({
759
+ key,
760
+ holder,
761
+ status: "completion-discarded",
762
+ reason: done.reason,
763
+ });
333
764
  }
334
765
  } catch (err) {
335
766
  if (span) {
@@ -338,28 +769,81 @@ export class TeamRunner {
338
769
  }
339
770
  // A failed task still consumed a task-count slot (and any wall-clock) — fold
340
771
  // it so a doomed retry loop can't dodge the budget.
341
- if (this.budget) this.budget.record({ usage: null }, this._now());
772
+ if (this.budget && !claim.budgetSettled) {
773
+ this.budget.record(
774
+ {
775
+ usage: err?.usage || null,
776
+ usageRecords: err?.usageRecords || null,
777
+ provider: err?.provider,
778
+ model: err?.model,
779
+ reservationId: claim.budgetReservation?.id,
780
+ },
781
+ this._now(),
782
+ );
783
+ claim.budgetSettled = true;
784
+ }
342
785
  const outcome = this.registry.fail(key, {
343
786
  holder,
787
+ leaseId: claim.leaseId,
344
788
  error: err?.message || String(err),
789
+ retryable: err?.retryable !== false,
345
790
  });
346
- this._setState(holder, "failed-task", {
347
- error: err?.message || String(err),
348
- });
349
- this._emit("task:failed", {
350
- key,
351
- holder,
352
- error: err?.message || String(err),
353
- retry: outcome?.retry === true,
354
- attempts: outcome?.attempts,
355
- });
791
+ if (outcome?.ok) {
792
+ this._setState(holder, "failed-task", {
793
+ error: err?.message || String(err),
794
+ });
795
+ this._emit("task:failed", {
796
+ key,
797
+ holder,
798
+ error: err?.message || String(err),
799
+ retry: outcome.retry === true,
800
+ attempts: outcome.attempts,
801
+ });
802
+ await this._notifySettlement({
803
+ key,
804
+ holder,
805
+ status: outcome.retry === true ? "pending" : "failed",
806
+ error: err?.message || String(err),
807
+ retry: outcome.retry === true,
808
+ attempts: outcome.attempts,
809
+ });
810
+ } else {
811
+ this._emit("task:failure-discarded", {
812
+ key,
813
+ holder,
814
+ reason: outcome?.reason || "lease_lost",
815
+ error: err?.message || String(err),
816
+ ms: this._now() - startedAt,
817
+ });
818
+ await this._notifySettlement({
819
+ key,
820
+ holder,
821
+ status: "failure-discarded",
822
+ reason: outcome?.reason || "lease_lost",
823
+ });
824
+ }
356
825
  } finally {
357
- clearInterval(heartbeat);
358
- this._activeKeys.delete(key);
826
+ if (!claim.budgetSettled && claim.budgetReservation?.id) {
827
+ this.budget?.releaseReservation?.(claim.budgetReservation.id);
828
+ }
829
+ this._releaseScope(holder, key);
830
+ this._endClaim(claim);
359
831
  this._inFlight--;
832
+ this._signalProgress();
360
833
  }
361
834
  }
362
835
 
836
+ _waitForProgress() {
837
+ return new Promise((resolve) => this._progressWaiters.add(resolve));
838
+ }
839
+
840
+ _signalProgress() {
841
+ if (this._progressWaiters.size === 0) return;
842
+ const waiters = Array.from(this._progressWaiters);
843
+ this._progressWaiters.clear();
844
+ for (const resolve of waiters) resolve();
845
+ }
846
+
363
847
  /** Yield to the event loop so peer completions land before re-checking. */
364
848
  _tick() {
365
849
  return new Promise((resolve) => setTimeout(resolve, 0));