chainlesschain 0.162.186 → 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 (173) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-DTXiO-TB.js → AIOps-CETaO3s8.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BbmDW-Pz.js → ActionButton-CAe7E9TW.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-CSfgXJuD.js → Analytics-BGr2mzwO.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-BS0FFLb7.js → AppLayout-BEoY-NcT.js} +5 -5
  6. package/src/assets/web-panel/assets/{Artifacts-B19WFPK2.js → Artifacts-DUZivCgt.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-CQhZBi7n.js → Audit-C3yBs68k.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-BOLZCpGH.js → BackgroundAgents-C9KwiY3O.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-Dz3R_ePD.js → Backup-BoSWD_C8.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-CYfgMpax.js → BaseInput-Dp09GkxL.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-5CymMr1j.js → Chat-5t9d-Ja0.js} +4 -4
  12. package/src/assets/web-panel/assets/ChatBubbleRenderer-DmVku4GH.js +1 -0
  13. package/src/assets/web-panel/assets/{Checkbox-CDXHVYcu.js → Checkbox-8QJFlkqI.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-uq9Z9oiS.js → Codegen-BJVFQ3lL.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-BuW1K_65.js → Col-6hdLD0oB.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-Bbg-PvRY.js → Community-B8opo0uY.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-co0pPPTw.js → Compact-Dv8HudiN.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-B-pFAv7z.js → Compliance-DhxuCS5N.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-54ZQS4gC.js → Cowork-Dggy6VIL.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-B1BF2Seq.js → Cron-DSmDqJTT.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-Dey6qQTj.js → Crosschain-7bZq0j5f.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-ltERvfAj.js → DID-BWY4kh-H.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-DuWxOcqN.js → Dashboard-bOzPZxlu.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-CrZ8ekTo.js → Dropdown-BTFJHqQu.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-fIyHXhUS.js → EmailListRenderer-BC3MOdtT.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-BtRyrRPm.js → FamilyGuardDashboard-CFHAdTTY.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-EUC6Wydj.js → Federation-DZ_4aF_n.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-BOpka2G-.js → FormItemContext-BZje6UTj.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-BXtrrYHn.js → GenericCardRenderer-D7QfCvjN.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-8WMb7PiR.js → Git-DuG7qANG.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-DNWOeDTV.js → Governance-BDmYAgTh.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-Bne9b_Lb.js → Inference-DSXAfc1Q.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-Cp-6Us9b.js → KnowledgeGraph-DusleniP.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-Ck-uSPAx.js → Logs-CpImmA8p.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-DxjsdAXw.js → MarkdownRenderer-BZ6LJBkk.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-Vdq7brrJ.js → Marketplace-DXS8CXkE.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-ggLnw4hY.js → McpTools-D151UfUI.js} +5 -5
  38. package/src/assets/web-panel/assets/{Memory-BKIjNVCU.js → Memory-DKDxnoA1.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-BElCY25a.js → MobileBridge-BFZ95iKM.js} +1 -1
  40. package/src/assets/web-panel/assets/MobileProjects-CDAR-grF.js +1 -0
  41. package/src/assets/web-panel/assets/{Mtc-bNdrF_Zh.js → Mtc-OGJ95xJs.js} +4 -4
  42. package/src/assets/web-panel/assets/{MtcAudit-Dw8w1Cqk.js → MtcAudit-BmGTo1XC.js} +2 -2
  43. package/src/assets/web-panel/assets/{Multisig-CfGKvlqc.js → Multisig-XumJRqOa.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-D6OJf7YN.js → NLProgramming-DHhhRZed.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-B3n_LzIS.js → Notes-B9rGxNNr.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-Uu1V_ICM.js → NotificationSettings-DClLbMYV.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-aGyWwWcp.js → OrderTableRenderer-I8OohT7N.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-qjFFmuwK.js → Organization-M6WzaYVa.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-J4ZgnwZz.js → Overflow-Cb3B5KtM.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-YEZF-uVH.js → P2P-BGk57kya.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-yqx7fwqk.js → PdhVaultBrowser-CXcKyPqK.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-DdEjlZ02.js → Permissions-hsfJ-Y0e.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub-DbAPYvns.js → PersonalDataHub-BSBii4js.js} +4 -4
  54. package/src/assets/web-panel/assets/{Pipeline-Bxlk_ZJU.js → Pipeline-BNw52rey.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-CpE-05tz.js → Privacy-DUpfue4E.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-DWlhcGHT.js → ProjectInit-C68NsP3h.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-BnH6BiTQ.js → ProjectSettings-BJ2It9v4.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-CHw2fXDB.js → Projects-D2FHLUa-.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-Bdz_sTyb.js → Providers-EnU4VWwS.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-BHh2b3pH.js → QrScannerModal-BRt5-B_I.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-D84M09pe.js → QuickAsk-Jd5L9UL2.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-Czc30K6M.js → Recommend-BNDhoVIu.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-q_TU5qxt.js → RemoteSession-BII41Mel.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-CnTce4WM.js → Reputation-D1wQPUNv.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-C60UFLay.js → Row-DnjRegul.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-BOUFEu9_.js → RssFeed-C_T4I-nF.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-D4__blE-.js → Search-C4lrxlLh.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-BoOm7_JK.js → Security-ByYT-VzP.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-AvzrW6HA.js → Services-BHpyEqB5.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-BZlrHozi.js → Skeleton-C0I23vG-.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-CKaMLuk0.js → Skills-DGZXz22K.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-DYCNJF8h.js → Sla-DL_urP9A.js} +1 -1
  73. package/src/assets/web-panel/assets/SpeechSettings-6x-hUNQ_.js +1 -0
  74. package/src/assets/web-panel/assets/{SyncSettings-B6tW8dVO.js → SyncSettings-Crk5TCXq.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-oNC_2NVz.js → Tasks-rZGq5PZs.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-DvLPvvjL.js → Templates-DT5el8Q8.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-BDt9UBn_.js → Tenant-mlgeyMK8.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-ddCUnH-X.js → Terminal-C8HEA-lM.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-CA4sDL47.js → TimelineRenderer-DvlqBayB.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-By03hnVs.js → Tokens-C7lDcNmT.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-Dz2mH-ZU.js → Trigger-CM8nCnIJ.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-DShSWHeJ.js → Trust-BQDLUQ9x.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-JN8lP3gt.js → UkeySign-CVqM0OFE.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-BgPy4gV3.js → VideoEditing-BvAcQpzT.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-CQGEsXw_.js → Wallet-zFVFaAZ2.js} +3 -3
  86. package/src/assets/web-panel/assets/{WebAuthn-D8xR-ds_.js → WebAuthn-Cc522ums.js} +4 -4
  87. package/src/assets/web-panel/assets/{WorkflowEditor-Df61_p1p.js → WorkflowEditor-Bg_wxpDo.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-fdPby427.js → chat-jg0QwC8K.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-Dimqhufo.js → colors-ALzYz1gO.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-DHayKcqZ.js → compact-item-DXHYUoqi.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-hZUhypRH.js → createContext-BsGsBM5m.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-Dy1GJA4S.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-Dw7zC_dV.js → hasIn-BccMLzxA.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-BIxxvaGN.js → index-08diKcTm.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-BVHdkz8F.js → index-2VstcImq.js} +17 -17
  96. package/src/assets/web-panel/assets/index-8zbMbbuf.js +1 -0
  97. package/src/assets/web-panel/assets/{index-DAjvkQ4K.js → index-BBbbr1YB.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-DX1DwDFI.js → index-BTTeAiOP.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-JE6Udz2r.js → index-BcJzphrP.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BYdNwhkv.js → index-BdCmJ_bG.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CTiVrnpR.js → index-BfF0OGKU.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-C1yMYKi-.js → index-BfV8RDlg.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DxoPzPHy.js → index-BvsII10i.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DyEhrvpm.js → index-C0AbXsDy.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-lPjBQJCy.js → index-C3Rf8wIM.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-CPZ3U5iH.js → index-C3k3ZwkQ.js} +1 -1
  107. package/src/assets/web-panel/assets/index-C97f71oi.js +1 -0
  108. package/src/assets/web-panel/assets/{index-DJP7TjWm.js → index-CBVIFX-x.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-DSlda4-6.js → index-CJg6Wsjc.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-DzJXkoZs.js → index-CLzPzN75.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-cEnkyHtX.js → index-CQ1ORdp-.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-Bhmpp3cm.js → index-CSFESurl.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-BwnUluxu.js → index-CWLWBgfe.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-CIOE8tt6.js → index-CahDve8M.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-CBQK-LEF.js → index-Cg9cOVe9.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-D7jQEwB6.js → index-Cmg_Npx5.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BuMSdwno.js → index-CrZHUsOZ.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-DnwDYBnG.js → index-Cz_s25m3.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DVM_KgzF.js → index-D9oV2iRR.js} +1 -1
  120. package/src/assets/web-panel/assets/{index--DVpUpz3.js → index-DgjQ-kdT.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-y5LHQ5Ck.js → index-Dmk7dD-T.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-Urt2R5aE.js → index-Dxqiu9-F.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CvKHpaao.js → index-JS2LXdd0.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CsCo_A0I.js → index-TeCof1s3.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-ulvBurtw.js → index-UQXedWQz.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-BM5BNqp9.js → index-koT8Q86Z.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BHJUptSx.js → index-mIQUoUx1.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-C8XXPz2n.js → index-oWltafBn.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-DC2R4zvM.js → index-rIy96PeM.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-j0pxzOu7.js → index-rcwgNMc0.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-CuaVa8tX.js → index-srqW5_-X.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-CcpDboUd.js → index-uzykuL_R.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-B4pR3px5.js → initDefaultProps-xafkqrW3.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-CE7dWPPs.js → motion-BrtyQXLk.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-Hw8cUObc.js → move-BunUyg59.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-CvRjE4Fa.js → mtc-parser-BPwjBeuE.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-CkFeLVre.js → omit-BpyqZlNO.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-5ZMqmbbb.js → pickAttrs-DDrV0XjE.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-BZ4oeZ0q.js → placementArrow-1A18Pcl2.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-lwoiX_Us.js → responsiveObserve-B8Fnlmso.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-D_as37l8.js → slide-x-Fwb2Zm.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-BsA-4Bs7.js → statusUtils-aAG5yJl_.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-BVar4sLQ.js → styleChecker-BRQSTMRL.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-j1IwVpGd.js → useFlexGapSupport-D5X_KozY.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-7Q9bEnUp.js → useFs-DN_YIjP-.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-BBwTIRMd.js → usePersonalDataHub-BizxAvjw.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-C7Dk-M8w.js → vnode-B5oAxN93.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-D3UHaTFB.js → zoom-DJhvyzKv.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/team.js +1805 -473
  151. package/src/data/changelog.json +13 -0
  152. package/src/harness/worktree-isolator.js +671 -79
  153. package/src/lib/agent-team/task-lease.js +378 -78
  154. package/src/lib/agent-team/team-agent-stream.js +488 -0
  155. package/src/lib/agent-team/team-budget.js +216 -20
  156. package/src/lib/agent-team/team-mailbox.js +389 -15
  157. package/src/lib/agent-team/team-run-state-lock.js +209 -0
  158. package/src/lib/agent-team/team-runner.js +547 -63
  159. package/src/lib/agent-team/team-scope-lock.js +409 -0
  160. package/src/lib/agent-team/team-task-contract.js +277 -0
  161. package/src/lib/agent-team/team-worktree.js +1171 -62
  162. package/src/lib/agent-worktree.js +19 -20
  163. package/src/lib/collaboration-run-store.js +983 -68
  164. package/src/lib/with-file-lock.js +190 -24
  165. package/src/lib/worktree-isolator.js +3 -0
  166. package/src/lib/worktree-sparse.js +23 -0
  167. package/src/runtime/headless-stream.js +6 -1
  168. package/src/assets/web-panel/assets/ChatBubbleRenderer-B2A_TJtl.js +0 -1
  169. package/src/assets/web-panel/assets/MobileProjects-DpLgovdH.js +0 -1
  170. package/src/assets/web-panel/assets/SpeechSettings-BvCSU6d2.js +0 -1
  171. package/src/assets/web-panel/assets/devWarning-ePxoBp92.js +0 -1
  172. package/src/assets/web-panel/assets/index-BTBDdCoo.js +0 -1
  173. package/src/assets/web-panel/assets/index-CfOGx8Ns.js +0 -1
@@ -24,6 +24,7 @@
24
24
  */
25
25
 
26
26
  import { SharedTaskList, TASK_STATUS } from "@chainlesschain/session-core";
27
+ import { randomUUID } from "node:crypto";
27
28
 
28
29
  export const DEFAULT_LEASE_TTL_MS = 60000;
29
30
  export const DEFAULT_MAX_ATTEMPTS = 3;
@@ -34,12 +35,27 @@ export class TaskLeaseRegistry {
34
35
  now = () => Date.now(),
35
36
  defaultTtlMs = DEFAULT_LEASE_TTL_MS,
36
37
  maxAttempts = DEFAULT_MAX_ATTEMPTS,
38
+ leaseEpoch = randomUUID(),
37
39
  } = {}) {
38
40
  this._now = typeof now === "function" ? now : () => now;
39
41
  this.defaultTtlMs = defaultTtlMs > 0 ? defaultTtlMs : DEFAULT_LEASE_TTL_MS;
40
42
  this.maxAttempts = maxAttempts > 0 ? maxAttempts : DEFAULT_MAX_ATTEMPTS;
43
+ this._leaseEpoch = String(leaseEpoch || randomUUID());
44
+ this._leaseSequence = 0;
41
45
  this._tasks = new SharedTaskList({ groupId, now: this._now });
42
46
  this._byKey = new Map(); // stable user key → internal SharedTaskList id
47
+ // Large-team scheduling indexes. The original implementation rebuilt and
48
+ // scanned the full task graph for every claim, which turns a 5k-task run
49
+ // into O(tasks² × teammates) work. Keep the dependency graph and ready
50
+ // queues incrementally so the hot claim path only touches ready work.
51
+ this._depsByKey = new Map();
52
+ this._dependentsByKey = new Map();
53
+ this._readyByPriority = new Map([
54
+ ["high", new Set()],
55
+ ["normal", new Set()],
56
+ ["low", new Set()],
57
+ ]);
58
+ this._leasedKeys = new Set();
43
59
  }
44
60
 
45
61
  /**
@@ -72,20 +88,101 @@ export class TaskLeaseRegistry {
72
88
  if (cycle) {
73
89
  return { ok: false, reason: "dependency cycle", cycle };
74
90
  }
91
+ return this._insertTask({
92
+ key: k,
93
+ title,
94
+ dependsOn: deps,
95
+ priority,
96
+ metadata,
97
+ createdBy,
98
+ });
99
+ }
100
+
101
+ /**
102
+ * Atomically validate and add a whole graph in O(V + E). `loadRegistry`
103
+ * uses this path for large plans so adding N tasks does not run N complete
104
+ * graph traversals. No task is inserted when validation fails.
105
+ */
106
+ addTasks(definitions = []) {
107
+ if (!Array.isArray(definitions) || definitions.length === 0) {
108
+ return { ok: false, reason: "tasks required" };
109
+ }
110
+ const normalized = [];
111
+ const seen = new Set(this._byKey.keys());
112
+ for (const source of definitions) {
113
+ const item = source && typeof source === "object" ? source : {};
114
+ const key = item.key || `t_${this._tasks.size() + normalized.length + 1}`;
115
+ if (seen.has(key)) {
116
+ return { ok: false, reason: `duplicate task key "${key}"`, key };
117
+ }
118
+ if (!item.title || typeof item.title !== "string") {
119
+ return { ok: false, reason: "title required", key };
120
+ }
121
+ seen.add(key);
122
+ normalized.push({
123
+ key,
124
+ title: item.title,
125
+ dependsOn: Array.isArray(item.dependsOn)
126
+ ? item.dependsOn.filter(Boolean)
127
+ : [],
128
+ priority: item.priority || "normal",
129
+ metadata: item.metadata || {},
130
+ createdBy: item.createdBy ?? null,
131
+ });
132
+ }
133
+
134
+ const adjacency = new Map(this._depsByKey);
135
+ for (const item of normalized) {
136
+ adjacency.set(item.key, item.dependsOn);
137
+ }
138
+ const cycle = this._findCycle(adjacency);
139
+ if (cycle) {
140
+ return { ok: false, reason: "dependency cycle", cycle };
141
+ }
142
+
143
+ const inserted = normalized.map((item) => this._insertTask(item));
144
+ return {
145
+ ok: true,
146
+ keys: inserted.map((item) => item.key),
147
+ ids: inserted.map((item) => item.id),
148
+ };
149
+ }
150
+
151
+ _insertTask({
152
+ key,
153
+ title,
154
+ dependsOn = [],
155
+ priority = "normal",
156
+ metadata = {},
157
+ createdBy = null,
158
+ }) {
75
159
  const created = this._tasks.add({
76
160
  title,
77
161
  priority,
78
162
  createdBy,
79
163
  metadata: {
80
164
  ...metadata,
81
- key: k,
82
- dependsOn: deps,
165
+ key,
166
+ dependsOn,
83
167
  lease: null,
84
168
  attempts: 0,
85
169
  },
86
170
  });
87
- this._byKey.set(k, created.id);
88
- return { ok: true, key: k, id: created.id };
171
+ this._byKey.set(key, created.id);
172
+ this._depsByKey.set(key, [...dependsOn]);
173
+ if (!this._dependentsByKey.has(key)) {
174
+ this._dependentsByKey.set(key, new Set());
175
+ }
176
+ for (const dependency of dependsOn) {
177
+ let dependents = this._dependentsByKey.get(dependency);
178
+ if (!dependents) {
179
+ dependents = new Set();
180
+ this._dependentsByKey.set(dependency, dependents);
181
+ }
182
+ dependents.add(key);
183
+ }
184
+ this._enqueueIfReady(key);
185
+ return { ok: true, key, id: created.id };
89
186
  }
90
187
 
91
188
  /** Resolve a stable key → the current task object (or null). */
@@ -140,27 +237,60 @@ export class TaskLeaseRegistry {
140
237
  return !!(lease && lease.expiresAt > now);
141
238
  }
142
239
 
240
+ _leaseOwned(lease, { holder, leaseId, now }) {
241
+ return !!(
242
+ this._leaseValid(lease, now) &&
243
+ lease.holder === holder &&
244
+ typeof leaseId === "string" &&
245
+ leaseId.length > 0 &&
246
+ lease.leaseId === leaseId
247
+ );
248
+ }
249
+
250
+ _nextLeaseId() {
251
+ this._leaseSequence += 1;
252
+ return `${this._leaseEpoch}:${this._leaseSequence}`;
253
+ }
254
+
143
255
  /**
144
256
  * Keys that could be leased right now: PENDING (or a task whose lease has
145
257
  * expired) with all dependencies COMPLETED and no currently-valid lease.
146
258
  */
147
259
  claimable({ now = this._now() } = {}) {
260
+ this._requeueExpiredLeases(now);
148
261
  const out = [];
149
- for (const t of this._tasks.list()) {
150
- const v = this._view(t);
151
- if (
152
- v.status === TASK_STATUS.COMPLETED ||
153
- v.status === TASK_STATUS.CANCELLED
154
- ) {
155
- continue;
262
+ for (const priority of ["high", "normal", "low"]) {
263
+ for (const key of this._readyByPriority.get(priority)) {
264
+ out.push(key);
156
265
  }
157
- if (this._leaseValid(v.lease, now)) continue; // held by a live teammate
158
- if (this.unmetDependencies(v.key).length > 0) continue; // blocked by deps
159
- out.push(v.key);
160
266
  }
161
267
  return out;
162
268
  }
163
269
 
270
+ /** O(1) ready-frontier size for fair worker/budget reservation. */
271
+ claimableCount({ now = this._now() } = {}) {
272
+ this._requeueExpiredLeases(now);
273
+ let count = 0;
274
+ for (const queue of this._readyByPriority.values()) count += queue.size;
275
+ return count;
276
+ }
277
+
278
+ /**
279
+ * Return one ready key without materializing the whole ready set. The runner
280
+ * acquires it immediately, which removes it from the queue synchronously.
281
+ */
282
+ nextClaimable({ now = this._now(), excludeKeys = null } = {}) {
283
+ this._requeueExpiredLeases(now);
284
+ const excluded =
285
+ excludeKeys instanceof Set ? excludeKeys : new Set(excludeKeys || []);
286
+ for (const priority of ["high", "normal", "low"]) {
287
+ for (const key of this._readyByPriority.get(priority)) {
288
+ if (!excluded.has(key)) return key;
289
+ }
290
+ }
291
+ return null;
292
+ }
293
+
164
294
  /**
165
295
  * Acquire (or renew, or steal an expired) exclusive lease.
166
296
  * - blocked by unmet deps → { ok:false, reason:"blocked_by_deps", unmet }
@@ -170,7 +300,7 @@ export class TaskLeaseRegistry {
170
300
  * - expired lease held by another → steal (stolen:true)
171
301
  * @returns {{ ok:boolean, reason?:string, lease?:object, unmet?:string[], holder?:string, expiresAt?:number }}
172
302
  */
173
- acquire(key, { holder, ttlMs, now = this._now() } = {}) {
303
+ acquire(key, { holder, leaseId = null, ttlMs, now = this._now() } = {}) {
174
304
  if (!holder) return { ok: false, reason: "holder required" };
175
305
  const id = this._byKey.get(key);
176
306
  const task = id ? this._tasks.get(id) : null;
@@ -186,39 +316,48 @@ export class TaskLeaseRegistry {
186
316
  return { ok: false, reason: "blocked_by_deps", unmet };
187
317
  }
188
318
  const existing = task.metadata?.lease || null;
189
- if (this._leaseValid(existing, now) && existing.holder !== holder) {
190
- return {
191
- ok: false,
192
- reason: "leased",
193
- holder: existing.holder,
194
- expiresAt: existing.expiresAt,
195
- };
319
+ const existingValid = this._leaseValid(existing, now);
320
+ if (existingValid) {
321
+ // Holder labels are intentionally human-readable and may be reused after
322
+ // a restart. They are not authority. Only the unguessable leaseId fences
323
+ // an older executor from renewing or settling a newer lease.
324
+ if (!this._leaseOwned(existing, { holder, leaseId, now })) {
325
+ return {
326
+ ok: false,
327
+ reason: "leased",
328
+ holder: existing.holder,
329
+ expiresAt: existing.expiresAt,
330
+ };
331
+ }
196
332
  }
197
333
  const ttl = ttlMs > 0 ? ttlMs : this.defaultTtlMs;
198
- const sameHolder = existing && existing.holder === holder;
334
+ const renewing = existingValid;
199
335
  const lease = {
200
336
  holder,
201
- acquiredAt: sameHolder ? existing.acquiredAt : now,
337
+ leaseId: renewing ? existing.leaseId : this._nextLeaseId(),
338
+ acquiredAt: renewing ? existing.acquiredAt : now,
202
339
  expiresAt: now + ttl,
203
- renewals: sameHolder ? (existing.renewals || 0) + 1 : 0,
204
- stolen: !!(existing && existing.holder !== holder),
340
+ renewals: renewing ? (existing.renewals || 0) + 1 : 0,
341
+ stolen: !renewing && !!existing,
205
342
  };
206
- return this._write(task, {
343
+ const ok = this._write(task, {
207
344
  status: TASK_STATUS.IN_PROGRESS,
208
345
  assignee: holder,
209
346
  metadata: { ...task.metadata, lease },
210
- })
211
- ? { ok: true, lease }
212
- : { ok: false, reason: "concurrent" };
347
+ });
348
+ if (!ok) return { ok: false, reason: "concurrent" };
349
+ this._removeReady(key);
350
+ this._leasedKeys.add(key);
351
+ return { ok: true, lease };
213
352
  }
214
353
 
215
354
  /** Extend the lease you hold. Fails if you're not the current valid holder. */
216
- renew(key, { holder, ttlMs, now = this._now() } = {}) {
355
+ renew(key, { holder, leaseId, ttlMs, now = this._now() } = {}) {
217
356
  const id = this._byKey.get(key);
218
357
  const task = id ? this._tasks.get(id) : null;
219
358
  if (!task) return { ok: false, reason: "not_found" };
220
359
  const lease = task.metadata?.lease || null;
221
- if (!this._leaseValid(lease, now) || lease.holder !== holder) {
360
+ if (!this._leaseOwned(lease, { holder, leaseId, now })) {
222
361
  return { ok: false, reason: "not_holder_or_expired" };
223
362
  }
224
363
  const ttl = ttlMs > 0 ? ttlMs : this.defaultTtlMs;
@@ -233,40 +372,47 @@ export class TaskLeaseRegistry {
233
372
  }
234
373
 
235
374
  /** Voluntarily give up a lease — the task returns to PENDING for re-claim. */
236
- release(key, { holder, now = this._now() } = {}) {
375
+ release(key, { holder, leaseId, now = this._now() } = {}) {
237
376
  const id = this._byKey.get(key);
238
377
  const task = id ? this._tasks.get(id) : null;
239
378
  if (!task) return { ok: false, reason: "not_found" };
240
379
  const lease = task.metadata?.lease || null;
241
- if (!lease || lease.holder !== holder) {
242
- return { ok: false, reason: "not_holder" };
380
+ if (!this._leaseOwned(lease, { holder, leaseId, now })) {
381
+ return { ok: false, reason: "not_holder_or_expired" };
243
382
  }
244
- return this._write(task, {
383
+ const ok = this._write(task, {
245
384
  status: TASK_STATUS.PENDING,
246
385
  assignee: null,
247
386
  metadata: { ...task.metadata, lease: null },
248
- })
249
- ? { ok: true }
250
- : { ok: false, reason: "concurrent" };
387
+ });
388
+ if (!ok) return { ok: false, reason: "concurrent" };
389
+ this._leasedKeys.delete(key);
390
+ this._enqueueIfReady(key, now);
391
+ return { ok: true };
251
392
  }
252
393
 
253
394
  /** Complete a task you hold the lease on → terminal COMPLETED. */
254
- complete(key, { holder, result = null, now = this._now() } = {}) {
395
+ complete(key, { holder, leaseId, result = null, now = this._now() } = {}) {
255
396
  const id = this._byKey.get(key);
256
397
  const task = id ? this._tasks.get(id) : null;
257
398
  if (!task) return { ok: false, reason: "not_found" };
258
399
  const lease = task.metadata?.lease || null;
259
400
  // Only the valid holder may complete — prevents a stale teammate (whose
260
401
  // lease already expired and was reassigned) from marking another's work done.
261
- if (!this._leaseValid(lease, now) || lease.holder !== holder) {
402
+ if (!this._leaseOwned(lease, { holder, leaseId, now })) {
262
403
  return { ok: false, reason: "not_holder_or_expired" };
263
404
  }
264
- return this._write(task, {
405
+ const ok = this._write(task, {
265
406
  status: TASK_STATUS.COMPLETED,
266
407
  metadata: { ...task.metadata, lease: null, result },
267
- })
268
- ? { ok: true }
269
- : { ok: false, reason: "concurrent" };
408
+ });
409
+ if (!ok) return { ok: false, reason: "concurrent" };
410
+ this._leasedKeys.delete(key);
411
+ this._removeReady(key);
412
+ for (const dependent of this._dependentsByKey.get(key) || []) {
413
+ this._enqueueIfReady(dependent, now);
414
+ }
415
+ return { ok: true };
270
416
  }
271
417
 
272
418
  /**
@@ -275,16 +421,19 @@ export class TaskLeaseRegistry {
275
421
  * doesn't loop forever on a doomed task.
276
422
  * @returns {{ ok:boolean, retry?:boolean, attempts?:number, reason?:string }}
277
423
  */
278
- fail(key, { holder, error = null, now = this._now() } = {}) {
424
+ fail(
425
+ key,
426
+ { holder, leaseId, error = null, retryable = true, now = this._now() } = {},
427
+ ) {
279
428
  const id = this._byKey.get(key);
280
429
  const task = id ? this._tasks.get(id) : null;
281
430
  if (!task) return { ok: false, reason: "not_found" };
282
431
  const lease = task.metadata?.lease || null;
283
- if (!lease || lease.holder !== holder) {
284
- return { ok: false, reason: "not_holder" };
432
+ if (!this._leaseOwned(lease, { holder, leaseId, now })) {
433
+ return { ok: false, reason: "not_holder_or_expired" };
285
434
  }
286
435
  const attempts = (task.metadata?.attempts || 0) + 1;
287
- const willRetry = attempts < this.maxAttempts;
436
+ const willRetry = retryable !== false && attempts < this.maxAttempts;
288
437
  const ok = this._write(task, {
289
438
  status: willRetry ? TASK_STATUS.PENDING : TASK_STATUS.CANCELLED,
290
439
  assignee: null,
@@ -296,6 +445,9 @@ export class TaskLeaseRegistry {
296
445
  },
297
446
  });
298
447
  if (!ok) return { ok: false, reason: "concurrent" };
448
+ this._leasedKeys.delete(key);
449
+ this._removeReady(key);
450
+ if (willRetry) this._enqueueIfReady(key, now);
299
451
  return { ok: true, retry: willRetry, attempts };
300
452
  }
301
453
 
@@ -317,6 +469,8 @@ export class TaskLeaseRegistry {
317
469
  metadata: { ...fresh.metadata, lease: null },
318
470
  })
319
471
  ) {
472
+ this._leasedKeys.delete(fresh.metadata?.key);
473
+ this._enqueueIfReady(fresh.metadata?.key, now);
320
474
  reclaimed.push(fresh.metadata?.key);
321
475
  }
322
476
  }
@@ -334,22 +488,66 @@ export class TaskLeaseRegistry {
334
488
  * Returns the reclaimed keys.
335
489
  */
336
490
  reclaimAll() {
337
- const reclaimed = [];
491
+ return this.reconcileAbandoned().reclaimed;
492
+ }
493
+
494
+ /**
495
+ * Reconcile leases restored from a process that is known to be gone.
496
+ *
497
+ * A caller may opt individual tasks into retry. Everything else is cancelled
498
+ * fail-closed so an unknown external side effect is not silently replayed.
499
+ * The returned lists let the resume authority report which tasks need human
500
+ * adjudication.
501
+ */
502
+ reconcileAbandoned({
503
+ shouldRetry = () => true,
504
+ error = "prior execution outcome requires adjudication",
505
+ } = {}) {
506
+ if (typeof shouldRetry !== "function") {
507
+ throw new TypeError("shouldRetry must be a function");
508
+ }
509
+ const candidates = [];
338
510
  for (const t of this._tasks.list()) {
339
511
  const lease = t.metadata?.lease || null;
340
512
  if (!lease) continue;
341
- const fresh = this._tasks.get(t.id); // re-read for current rev
513
+ candidates.push({
514
+ key: t.metadata?.key,
515
+ retry: shouldRetry(t) === true,
516
+ });
517
+ }
518
+
519
+ const reclaimed = [];
520
+ const adjudicationRequired = [];
521
+ for (const candidate of candidates) {
522
+ const id = this._byKey.get(candidate.key);
523
+ const fresh = id ? this._tasks.get(id) : null;
524
+ if (!fresh?.metadata?.lease) continue;
525
+ const metadata = {
526
+ ...fresh.metadata,
527
+ lease: null,
528
+ };
529
+ if (!candidate.retry) {
530
+ metadata.attempts = (fresh.metadata?.attempts || 0) + 1;
531
+ metadata.lastError = String(error);
532
+ }
342
533
  if (
343
534
  this._write(fresh, {
344
- status: TASK_STATUS.PENDING,
535
+ status: candidate.retry ? TASK_STATUS.PENDING : TASK_STATUS.CANCELLED,
345
536
  assignee: null,
346
- metadata: { ...fresh.metadata, lease: null },
537
+ metadata,
347
538
  })
348
539
  ) {
349
- reclaimed.push(fresh.metadata?.key);
540
+ this._leasedKeys.delete(candidate.key);
541
+ this._removeReady(candidate.key);
542
+ if (candidate.retry) {
543
+ this._enqueueIfReady(candidate.key);
544
+ reclaimed.push(candidate.key);
545
+ } else {
546
+ adjudicationRequired.push(candidate.key);
547
+ }
350
548
  }
351
549
  }
352
- return reclaimed;
550
+ return { reclaimed, adjudicationRequired };
353
551
  }
354
552
 
355
553
  stats({ now = this._now() } = {}) {
@@ -401,6 +599,7 @@ export class TaskLeaseRegistry {
401
599
  });
402
600
  reg._tasks = SharedTaskList.restore(snapshot.tasks, { now: reg._now });
403
601
  reg._byKey = new Map(snapshot?.registry?.byKey || []);
602
+ reg._rebuildIndexes();
404
603
  return reg;
405
604
  }
406
605
 
@@ -427,36 +626,137 @@ export class TaskLeaseRegistry {
427
626
  * null. A self-edge counts as a cycle.
428
627
  */
429
628
  _detectCycleIfAdded(key, deps) {
430
- // Build adjacency from current tasks, then add the candidate.
431
- const adj = new Map();
432
- for (const t of this._tasks.list()) {
433
- adj.set(t.metadata?.key, t.metadata?.dependsOn || []);
434
- }
629
+ const adj = new Map(this._depsByKey);
435
630
  adj.set(key, deps);
631
+ return this._findCycle(adj, [key]);
632
+ }
633
+
634
+ _findCycle(adj, roots = null) {
436
635
  const WHITE = 0,
437
636
  GRAY = 1,
438
637
  BLACK = 2;
439
638
  const color = new Map();
440
- const stack = [];
441
- let found = null;
442
- const visit = (node) => {
443
- color.set(node, GRAY);
444
- stack.push(node);
445
- for (const next of adj.get(node) || []) {
446
- if (!adj.has(next)) continue; // edge to a not-yet-added task: no cycle via it
447
- const c = color.get(next) || WHITE;
448
- if (c === GRAY) {
449
- found = [...stack.slice(stack.indexOf(next)), next];
450
- return true;
639
+ // Iterative DFS avoids overflowing the JS call stack on a legitimate deep
640
+ // graph (for example a 10,000-task migration chain).
641
+ for (const root of roots || adj.keys()) {
642
+ if ((color.get(root) || WHITE) !== WHITE) continue;
643
+ const path = [root];
644
+ const frames = [
645
+ {
646
+ node: root,
647
+ edges: adj.get(root) || [],
648
+ index: 0,
649
+ },
650
+ ];
651
+ color.set(root, GRAY);
652
+ while (frames.length > 0) {
653
+ const frame = frames[frames.length - 1];
654
+ if (frame.index >= frame.edges.length) {
655
+ color.set(frame.node, BLACK);
656
+ frames.pop();
657
+ path.pop();
658
+ continue;
451
659
  }
452
- if (c === WHITE && visit(next)) return true;
660
+ const next = frame.edges[frame.index++];
661
+ if (!adj.has(next)) continue;
662
+ const state = color.get(next) || WHITE;
663
+ if (state === GRAY) {
664
+ return [...path.slice(path.indexOf(next)), next];
665
+ }
666
+ if (state === BLACK) continue;
667
+ color.set(next, GRAY);
668
+ path.push(next);
669
+ frames.push({
670
+ node: next,
671
+ edges: adj.get(next) || [],
672
+ index: 0,
673
+ });
453
674
  }
454
- stack.pop();
455
- color.set(node, BLACK);
456
- return false;
457
- };
458
- // Only need to start from the candidate — a new cycle must pass through it.
459
- if (visit(key)) return found;
675
+ }
460
676
  return null;
461
677
  }
678
+
679
+ _priorityFor(task) {
680
+ return task?.priority === "high"
681
+ ? "high"
682
+ : task?.priority === "low"
683
+ ? "low"
684
+ : "normal";
685
+ }
686
+
687
+ _removeReady(key) {
688
+ for (const queue of this._readyByPriority.values()) queue.delete(key);
689
+ }
690
+
691
+ _dependenciesCompleted(key) {
692
+ for (const dependency of this._depsByKey.get(key) || []) {
693
+ if (this._statusOf(dependency) !== TASK_STATUS.COMPLETED) return false;
694
+ }
695
+ return true;
696
+ }
697
+
698
+ _enqueueIfReady(key, now = this._now()) {
699
+ if (!key) return false;
700
+ const task = this.getTask(key);
701
+ if (
702
+ !task ||
703
+ task.status === TASK_STATUS.COMPLETED ||
704
+ task.status === TASK_STATUS.CANCELLED
705
+ ) {
706
+ this._removeReady(key);
707
+ this._leasedKeys.delete(key);
708
+ return false;
709
+ }
710
+ if (this._leaseValid(task.lease, now)) {
711
+ this._removeReady(key);
712
+ this._leasedKeys.add(key);
713
+ return false;
714
+ }
715
+ if (!this._dependenciesCompleted(key)) {
716
+ this._removeReady(key);
717
+ return false;
718
+ }
719
+ this._leasedKeys.delete(key);
720
+ this._readyByPriority.get(this._priorityFor(task)).add(key);
721
+ return true;
722
+ }
723
+
724
+ _requeueExpiredLeases(now = this._now()) {
725
+ for (const key of Array.from(this._leasedKeys)) {
726
+ const task = this.getTask(key);
727
+ if (!task || !this._leaseValid(task.lease, now)) {
728
+ this._leasedKeys.delete(key);
729
+ this._enqueueIfReady(key, now);
730
+ }
731
+ }
732
+ }
733
+
734
+ _rebuildIndexes() {
735
+ this._depsByKey = new Map();
736
+ this._dependentsByKey = new Map();
737
+ this._readyByPriority = new Map([
738
+ ["high", new Set()],
739
+ ["normal", new Set()],
740
+ ["low", new Set()],
741
+ ]);
742
+ this._leasedKeys = new Set();
743
+ for (const task of this._tasks.list()) {
744
+ const key = task.metadata?.key;
745
+ const dependencies = task.metadata?.dependsOn || [];
746
+ this._depsByKey.set(key, [...dependencies]);
747
+ if (!this._dependentsByKey.has(key)) {
748
+ this._dependentsByKey.set(key, new Set());
749
+ }
750
+ for (const dependency of dependencies) {
751
+ let dependents = this._dependentsByKey.get(dependency);
752
+ if (!dependents) {
753
+ dependents = new Set();
754
+ this._dependentsByKey.set(dependency, dependents);
755
+ }
756
+ dependents.add(key);
757
+ }
758
+ }
759
+ const now = this._now();
760
+ for (const key of this._byKey.keys()) this._enqueueIfReady(key, now);
761
+ }
462
762
  }