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.
- package/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-DTXiO-TB.js → AIOps-CETaO3s8.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BbmDW-Pz.js → ActionButton-CAe7E9TW.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CSfgXJuD.js → Analytics-BGr2mzwO.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-BS0FFLb7.js → AppLayout-BEoY-NcT.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-B19WFPK2.js → Artifacts-DUZivCgt.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CQhZBi7n.js → Audit-C3yBs68k.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-BOLZCpGH.js → BackgroundAgents-C9KwiY3O.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-Dz3R_ePD.js → Backup-BoSWD_C8.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CYfgMpax.js → BaseInput-Dp09GkxL.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-5CymMr1j.js → Chat-5t9d-Ja0.js} +4 -4
- package/src/assets/web-panel/assets/ChatBubbleRenderer-DmVku4GH.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-CDXHVYcu.js → Checkbox-8QJFlkqI.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-uq9Z9oiS.js → Codegen-BJVFQ3lL.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BuW1K_65.js → Col-6hdLD0oB.js} +1 -1
- package/src/assets/web-panel/assets/{Community-Bbg-PvRY.js → Community-B8opo0uY.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-co0pPPTw.js → Compact-Dv8HudiN.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-B-pFAv7z.js → Compliance-DhxuCS5N.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-54ZQS4gC.js → Cowork-Dggy6VIL.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-B1BF2Seq.js → Cron-DSmDqJTT.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-Dey6qQTj.js → Crosschain-7bZq0j5f.js} +1 -1
- package/src/assets/web-panel/assets/{DID-ltERvfAj.js → DID-BWY4kh-H.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-DuWxOcqN.js → Dashboard-bOzPZxlu.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CrZ8ekTo.js → Dropdown-BTFJHqQu.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-fIyHXhUS.js → EmailListRenderer-BC3MOdtT.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BtRyrRPm.js → FamilyGuardDashboard-CFHAdTTY.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-EUC6Wydj.js → Federation-DZ_4aF_n.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BOpka2G-.js → FormItemContext-BZje6UTj.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BXtrrYHn.js → GenericCardRenderer-D7QfCvjN.js} +1 -1
- package/src/assets/web-panel/assets/{Git-8WMb7PiR.js → Git-DuG7qANG.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DNWOeDTV.js → Governance-BDmYAgTh.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-Bne9b_Lb.js → Inference-DSXAfc1Q.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-Cp-6Us9b.js → KnowledgeGraph-DusleniP.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-Ck-uSPAx.js → Logs-CpImmA8p.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-DxjsdAXw.js → MarkdownRenderer-BZ6LJBkk.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-Vdq7brrJ.js → Marketplace-DXS8CXkE.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-ggLnw4hY.js → McpTools-D151UfUI.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-BKIjNVCU.js → Memory-DKDxnoA1.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BElCY25a.js → MobileBridge-BFZ95iKM.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-CDAR-grF.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-bNdrF_Zh.js → Mtc-OGJ95xJs.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-Dw8w1Cqk.js → MtcAudit-BmGTo1XC.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-CfGKvlqc.js → Multisig-XumJRqOa.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-D6OJf7YN.js → NLProgramming-DHhhRZed.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-B3n_LzIS.js → Notes-B9rGxNNr.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-Uu1V_ICM.js → NotificationSettings-DClLbMYV.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-aGyWwWcp.js → OrderTableRenderer-I8OohT7N.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-qjFFmuwK.js → Organization-M6WzaYVa.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-J4ZgnwZz.js → Overflow-Cb3B5KtM.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-YEZF-uVH.js → P2P-BGk57kya.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-yqx7fwqk.js → PdhVaultBrowser-CXcKyPqK.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-DdEjlZ02.js → Permissions-hsfJ-Y0e.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-DbAPYvns.js → PersonalDataHub-BSBii4js.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-Bxlk_ZJU.js → Pipeline-BNw52rey.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-CpE-05tz.js → Privacy-DUpfue4E.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-DWlhcGHT.js → ProjectInit-C68NsP3h.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BnH6BiTQ.js → ProjectSettings-BJ2It9v4.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-CHw2fXDB.js → Projects-D2FHLUa-.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-Bdz_sTyb.js → Providers-EnU4VWwS.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BHh2b3pH.js → QrScannerModal-BRt5-B_I.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-D84M09pe.js → QuickAsk-Jd5L9UL2.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Czc30K6M.js → Recommend-BNDhoVIu.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-q_TU5qxt.js → RemoteSession-BII41Mel.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-CnTce4WM.js → Reputation-D1wQPUNv.js} +1 -1
- package/src/assets/web-panel/assets/{Row-C60UFLay.js → Row-DnjRegul.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BOUFEu9_.js → RssFeed-C_T4I-nF.js} +3 -3
- package/src/assets/web-panel/assets/{Search-D4__blE-.js → Search-C4lrxlLh.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BoOm7_JK.js → Security-ByYT-VzP.js} +4 -4
- package/src/assets/web-panel/assets/{Services-AvzrW6HA.js → Services-BHpyEqB5.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-BZlrHozi.js → Skeleton-C0I23vG-.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CKaMLuk0.js → Skills-DGZXz22K.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DYCNJF8h.js → Sla-DL_urP9A.js} +1 -1
- package/src/assets/web-panel/assets/SpeechSettings-6x-hUNQ_.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-B6tW8dVO.js → SyncSettings-Crk5TCXq.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-oNC_2NVz.js → Tasks-rZGq5PZs.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DvLPvvjL.js → Templates-DT5el8Q8.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BDt9UBn_.js → Tenant-mlgeyMK8.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-ddCUnH-X.js → Terminal-C8HEA-lM.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-CA4sDL47.js → TimelineRenderer-DvlqBayB.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-By03hnVs.js → Tokens-C7lDcNmT.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-Dz2mH-ZU.js → Trigger-CM8nCnIJ.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DShSWHeJ.js → Trust-BQDLUQ9x.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-JN8lP3gt.js → UkeySign-CVqM0OFE.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BgPy4gV3.js → VideoEditing-BvAcQpzT.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CQGEsXw_.js → Wallet-zFVFaAZ2.js} +3 -3
- package/src/assets/web-panel/assets/{WebAuthn-D8xR-ds_.js → WebAuthn-Cc522ums.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-Df61_p1p.js → WorkflowEditor-Bg_wxpDo.js} +1 -1
- package/src/assets/web-panel/assets/{chat-fdPby427.js → chat-jg0QwC8K.js} +1 -1
- package/src/assets/web-panel/assets/{colors-Dimqhufo.js → colors-ALzYz1gO.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DHayKcqZ.js → compact-item-DXHYUoqi.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-hZUhypRH.js → createContext-BsGsBM5m.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-Dy1GJA4S.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-Dw7zC_dV.js → hasIn-BccMLzxA.js} +1 -1
- package/src/assets/web-panel/assets/{index-BIxxvaGN.js → index-08diKcTm.js} +1 -1
- package/src/assets/web-panel/assets/{index-BVHdkz8F.js → index-2VstcImq.js} +17 -17
- package/src/assets/web-panel/assets/index-8zbMbbuf.js +1 -0
- package/src/assets/web-panel/assets/{index-DAjvkQ4K.js → index-BBbbr1YB.js} +1 -1
- package/src/assets/web-panel/assets/{index-DX1DwDFI.js → index-BTTeAiOP.js} +1 -1
- package/src/assets/web-panel/assets/{index-JE6Udz2r.js → index-BcJzphrP.js} +1 -1
- package/src/assets/web-panel/assets/{index-BYdNwhkv.js → index-BdCmJ_bG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTiVrnpR.js → index-BfF0OGKU.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1yMYKi-.js → index-BfV8RDlg.js} +1 -1
- package/src/assets/web-panel/assets/{index-DxoPzPHy.js → index-BvsII10i.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyEhrvpm.js → index-C0AbXsDy.js} +1 -1
- package/src/assets/web-panel/assets/{index-lPjBQJCy.js → index-C3Rf8wIM.js} +1 -1
- package/src/assets/web-panel/assets/{index-CPZ3U5iH.js → index-C3k3ZwkQ.js} +1 -1
- package/src/assets/web-panel/assets/index-C97f71oi.js +1 -0
- package/src/assets/web-panel/assets/{index-DJP7TjWm.js → index-CBVIFX-x.js} +1 -1
- package/src/assets/web-panel/assets/{index-DSlda4-6.js → index-CJg6Wsjc.js} +1 -1
- package/src/assets/web-panel/assets/{index-DzJXkoZs.js → index-CLzPzN75.js} +1 -1
- package/src/assets/web-panel/assets/{index-cEnkyHtX.js → index-CQ1ORdp-.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bhmpp3cm.js → index-CSFESurl.js} +1 -1
- package/src/assets/web-panel/assets/{index-BwnUluxu.js → index-CWLWBgfe.js} +1 -1
- package/src/assets/web-panel/assets/{index-CIOE8tt6.js → index-CahDve8M.js} +1 -1
- package/src/assets/web-panel/assets/{index-CBQK-LEF.js → index-Cg9cOVe9.js} +1 -1
- package/src/assets/web-panel/assets/{index-D7jQEwB6.js → index-Cmg_Npx5.js} +1 -1
- package/src/assets/web-panel/assets/{index-BuMSdwno.js → index-CrZHUsOZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DnwDYBnG.js → index-Cz_s25m3.js} +1 -1
- package/src/assets/web-panel/assets/{index-DVM_KgzF.js → index-D9oV2iRR.js} +1 -1
- package/src/assets/web-panel/assets/{index--DVpUpz3.js → index-DgjQ-kdT.js} +1 -1
- package/src/assets/web-panel/assets/{index-y5LHQ5Ck.js → index-Dmk7dD-T.js} +1 -1
- package/src/assets/web-panel/assets/{index-Urt2R5aE.js → index-Dxqiu9-F.js} +1 -1
- package/src/assets/web-panel/assets/{index-CvKHpaao.js → index-JS2LXdd0.js} +1 -1
- package/src/assets/web-panel/assets/{index-CsCo_A0I.js → index-TeCof1s3.js} +1 -1
- package/src/assets/web-panel/assets/{index-ulvBurtw.js → index-UQXedWQz.js} +1 -1
- package/src/assets/web-panel/assets/{index-BM5BNqp9.js → index-koT8Q86Z.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHJUptSx.js → index-mIQUoUx1.js} +1 -1
- package/src/assets/web-panel/assets/{index-C8XXPz2n.js → index-oWltafBn.js} +1 -1
- package/src/assets/web-panel/assets/{index-DC2R4zvM.js → index-rIy96PeM.js} +1 -1
- package/src/assets/web-panel/assets/{index-j0pxzOu7.js → index-rcwgNMc0.js} +1 -1
- package/src/assets/web-panel/assets/{index-CuaVa8tX.js → index-srqW5_-X.js} +1 -1
- package/src/assets/web-panel/assets/{index-CcpDboUd.js → index-uzykuL_R.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-B4pR3px5.js → initDefaultProps-xafkqrW3.js} +1 -1
- package/src/assets/web-panel/assets/{motion-CE7dWPPs.js → motion-BrtyQXLk.js} +1 -1
- package/src/assets/web-panel/assets/{move-Hw8cUObc.js → move-BunUyg59.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-CvRjE4Fa.js → mtc-parser-BPwjBeuE.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CkFeLVre.js → omit-BpyqZlNO.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-5ZMqmbbb.js → pickAttrs-DDrV0XjE.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BZ4oeZ0q.js → placementArrow-1A18Pcl2.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-lwoiX_Us.js → responsiveObserve-B8Fnlmso.js} +1 -1
- package/src/assets/web-panel/assets/{slide-D_as37l8.js → slide-x-Fwb2Zm.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BsA-4Bs7.js → statusUtils-aAG5yJl_.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BVar4sLQ.js → styleChecker-BRQSTMRL.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-j1IwVpGd.js → useFlexGapSupport-D5X_KozY.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-7Q9bEnUp.js → useFs-DN_YIjP-.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BBwTIRMd.js → usePersonalDataHub-BizxAvjw.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-C7Dk-M8w.js → vnode-B5oAxN93.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-D3UHaTFB.js → zoom-DJhvyzKv.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/team.js +1805 -473
- package/src/data/changelog.json +13 -0
- package/src/harness/worktree-isolator.js +671 -79
- package/src/lib/agent-team/task-lease.js +378 -78
- package/src/lib/agent-team/team-agent-stream.js +488 -0
- package/src/lib/agent-team/team-budget.js +216 -20
- package/src/lib/agent-team/team-mailbox.js +389 -15
- package/src/lib/agent-team/team-run-state-lock.js +209 -0
- package/src/lib/agent-team/team-runner.js +547 -63
- package/src/lib/agent-team/team-scope-lock.js +409 -0
- package/src/lib/agent-team/team-task-contract.js +277 -0
- package/src/lib/agent-team/team-worktree.js +1171 -62
- package/src/lib/agent-worktree.js +19 -20
- package/src/lib/collaboration-run-store.js +983 -68
- package/src/lib/with-file-lock.js +190 -24
- package/src/lib/worktree-isolator.js +3 -0
- package/src/lib/worktree-sparse.js +23 -0
- package/src/runtime/headless-stream.js +6 -1
- package/src/assets/web-panel/assets/ChatBubbleRenderer-B2A_TJtl.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-DpLgovdH.js +0 -1
- package/src/assets/web-panel/assets/SpeechSettings-BvCSU6d2.js +0 -1
- package/src/assets/web-panel/assets/devWarning-ePxoBp92.js +0 -1
- package/src/assets/web-panel/assets/index-BTBDdCoo.js +0 -1
- 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
|
|
82
|
-
dependsOn
|
|
165
|
+
key,
|
|
166
|
+
dependsOn,
|
|
83
167
|
lease: null,
|
|
84
168
|
attempts: 0,
|
|
85
169
|
},
|
|
86
170
|
});
|
|
87
|
-
this._byKey.set(
|
|
88
|
-
|
|
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
|
|
150
|
-
const
|
|
151
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
334
|
+
const renewing = existingValid;
|
|
199
335
|
const lease = {
|
|
200
336
|
holder,
|
|
201
|
-
|
|
337
|
+
leaseId: renewing ? existing.leaseId : this._nextLeaseId(),
|
|
338
|
+
acquiredAt: renewing ? existing.acquiredAt : now,
|
|
202
339
|
expiresAt: now + ttl,
|
|
203
|
-
renewals:
|
|
204
|
-
stolen:
|
|
340
|
+
renewals: renewing ? (existing.renewals || 0) + 1 : 0,
|
|
341
|
+
stolen: !renewing && !!existing,
|
|
205
342
|
};
|
|
206
|
-
|
|
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
|
-
|
|
212
|
-
|
|
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.
|
|
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
|
|
242
|
-
return { ok: false, reason: "
|
|
380
|
+
if (!this._leaseOwned(lease, { holder, leaseId, now })) {
|
|
381
|
+
return { ok: false, reason: "not_holder_or_expired" };
|
|
243
382
|
}
|
|
244
|
-
|
|
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
|
-
|
|
250
|
-
|
|
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.
|
|
402
|
+
if (!this._leaseOwned(lease, { holder, leaseId, now })) {
|
|
262
403
|
return { ok: false, reason: "not_holder_or_expired" };
|
|
263
404
|
}
|
|
264
|
-
|
|
405
|
+
const ok = this._write(task, {
|
|
265
406
|
status: TASK_STATUS.COMPLETED,
|
|
266
407
|
metadata: { ...task.metadata, lease: null, result },
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
|
|
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(
|
|
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
|
|
284
|
-
return { ok: false, reason: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
537
|
+
metadata,
|
|
347
538
|
})
|
|
348
539
|
) {
|
|
349
|
-
|
|
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
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
color.
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|