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.
- package/bin/chainlesschain.js +21 -5
- package/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-Co4ipAKx.js → AIOps-CETaO3s8.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BpsoDrDq.js → ActionButton-CAe7E9TW.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DMUqeBik.js → Analytics-BGr2mzwO.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-C6fD_dXr.js → AppLayout-BEoY-NcT.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-DW0YlQlV.js → Artifacts-DUZivCgt.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-DXtHiiFK.js → Audit-C3yBs68k.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-BgmF62U-.js → BackgroundAgents-C9KwiY3O.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-D8vhVLQC.js → Backup-BoSWD_C8.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-D7nCtY2X.js → BaseInput-Dp09GkxL.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CBZLEV2G.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-DJLkeD4M.js → Checkbox-8QJFlkqI.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-Cs6xO8Fd.js → Codegen-BJVFQ3lL.js} +1 -1
- package/src/assets/web-panel/assets/{Col-Dtx9wxfT.js → Col-6hdLD0oB.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DiigJg4n.js → Community-B8opo0uY.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-B67oMDDL.js → Compact-Dv8HudiN.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-BBDl-Jos.js → Compliance-DhxuCS5N.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Ba98hSsa.js → Cowork-Dggy6VIL.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DBWQZIz7.js → Cron-DSmDqJTT.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-OcrKzxAt.js → Crosschain-7bZq0j5f.js} +1 -1
- package/src/assets/web-panel/assets/{DID-mW2eSbjM.js → DID-BWY4kh-H.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-BmsDPmKJ.js → Dashboard-bOzPZxlu.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CVzmCyRx.js → Dropdown-BTFJHqQu.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-vVgqKzhh.js → EmailListRenderer-BC3MOdtT.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-CWY5t_ZJ.js → FamilyGuardDashboard-CFHAdTTY.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-ZGomhjxr.js → Federation-DZ_4aF_n.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DyaknbLr.js → FormItemContext-BZje6UTj.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-CmIvN1qr.js → GenericCardRenderer-D7QfCvjN.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DQOJIYkG.js → Git-DuG7qANG.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CMiY-EEp.js → Governance-BDmYAgTh.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-D6IXuucf.js → Inference-DSXAfc1Q.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-6jkTL0hV.js → KnowledgeGraph-DusleniP.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-N0jD-349.js → Logs-CpImmA8p.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-BZ6LJBkk.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-WhYuhBxm.js → Marketplace-DXS8CXkE.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DTeO28xy.js → McpTools-D151UfUI.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-MSHzPSE-.js → Memory-DKDxnoA1.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-Cyqs3J3W.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-MWBnezNM.js → Mtc-OGJ95xJs.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-DQu35Nu-.js → MtcAudit-BmGTo1XC.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-CQS_M-DP.js → Multisig-XumJRqOa.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-WD16vQvY.js → NLProgramming-DHhhRZed.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DqsiSwOq.js → Notes-B9rGxNNr.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-D3LoxO-P.js → NotificationSettings-DClLbMYV.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-C5jPoTd6.js → OrderTableRenderer-I8OohT7N.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-48aMBJso.js → Organization-M6WzaYVa.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-j-a2XkhR.js → Overflow-Cb3B5KtM.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BYpBNYnh.js → P2P-BGk57kya.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-CYCsrrLg.js → PdhVaultBrowser-CXcKyPqK.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-ZRXGXDFb.js → Permissions-hsfJ-Y0e.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-6OWUPHyL.js → PersonalDataHub-BSBii4js.js} +5 -5
- package/src/assets/web-panel/assets/{Pipeline-FcjlDK9E.js → Pipeline-BNw52rey.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-vD_6Tldp.js → Privacy-DUpfue4E.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-B_CHOLXp.js → ProjectInit-C68NsP3h.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-rEiqQ2da.js → ProjectSettings-BJ2It9v4.js} +2 -2
- package/src/assets/web-panel/assets/Projects-D2FHLUa-.js +1 -0
- package/src/assets/web-panel/assets/{Providers-Dgtt97B4.js → Providers-EnU4VWwS.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-DaGVylZO.js → QrScannerModal-BRt5-B_I.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-DP2_2x3u.js → QuickAsk-Jd5L9UL2.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-D29yj4ki.js → Recommend-BNDhoVIu.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-Ue2wnBLp.js → RemoteSession-BII41Mel.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-CrxIHqeC.js → Reputation-D1wQPUNv.js} +1 -1
- package/src/assets/web-panel/assets/{Row-BgSzJPCn.js → Row-DnjRegul.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-AwhYOrP9.js → RssFeed-C_T4I-nF.js} +2 -2
- package/src/assets/web-panel/assets/{Search-D2shpEcm.js → Search-C4lrxlLh.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BtKzhTbT.js → Security-ByYT-VzP.js} +4 -4
- package/src/assets/web-panel/assets/{Services-BY0DsUSm.js → Services-BHpyEqB5.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-B3Sb0UI9.js → Skeleton-C0I23vG-.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-Ne5w0bDs.js → Skills-DGZXz22K.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-C8VkaP4T.js → Sla-DL_urP9A.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-BQdIEYP9.js → SpeechSettings-6x-hUNQ_.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CwFafrUg.js → SyncSettings-Crk5TCXq.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-yJuKoDlR.js → Tasks-rZGq5PZs.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DYn9qCmx.js → Templates-DT5el8Q8.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-DkKReHvn.js → Tenant-mlgeyMK8.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-r6bd-owt.js → Terminal-C8HEA-lM.js} +2 -2
- package/src/assets/web-panel/assets/TimelineRenderer-DvlqBayB.js +1 -0
- package/src/assets/web-panel/assets/{Tokens-BnMw7MRi.js → Tokens-C7lDcNmT.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-CTzKp4o9.js → Trigger-CM8nCnIJ.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BBrSZm9U.js → Trust-BQDLUQ9x.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-D9fDvEMq.js → UkeySign-CVqM0OFE.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DUi5ve-0.js → VideoEditing-BvAcQpzT.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-C8Fi1P8_.js → Wallet-zFVFaAZ2.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-s1LppYi2.js → WebAuthn-Cc522ums.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-DJL2C4lj.js → WorkflowEditor-Bg_wxpDo.js} +1 -1
- package/src/assets/web-panel/assets/{chat-D3F2ndOB.js → chat-jg0QwC8K.js} +1 -1
- package/src/assets/web-panel/assets/{colors-B7739-e3.js → colors-ALzYz1gO.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CcJbFTF4.js → compact-item-DXHYUoqi.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CuP_DEFS.js → createContext-BsGsBM5m.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-Dy1GJA4S.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-6YEXfIik.js → hasIn-BccMLzxA.js} +1 -1
- package/src/assets/web-panel/assets/{index-fWgUQZ9d.js → index-08diKcTm.js} +1 -1
- package/src/assets/web-panel/assets/{index-CxradkS6.js → index-2VstcImq.js} +19 -19
- package/src/assets/web-panel/assets/index-8zbMbbuf.js +1 -0
- package/src/assets/web-panel/assets/{index-DDRlBaDt.js → index-BBbbr1YB.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dox44j29.js → index-BTTeAiOP.js} +1 -1
- package/src/assets/web-panel/assets/{index-BYYSLEjn.js → index-BcJzphrP.js} +1 -1
- package/src/assets/web-panel/assets/{index-CRuoRapN.js → index-BdCmJ_bG.js} +1 -1
- package/src/assets/web-panel/assets/{index-BxSqVMGh.js → index-BfF0OGKU.js} +1 -1
- package/src/assets/web-panel/assets/{index-D6Ij_2uw.js → index-BfV8RDlg.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dsy7_x4s.js → index-BvsII10i.js} +1 -1
- package/src/assets/web-panel/assets/{index-BsdSnHQE.js → index-C0AbXsDy.js} +1 -1
- package/src/assets/web-panel/assets/{index-mfWQRSLn.js → index-C3Rf8wIM.js} +1 -1
- package/src/assets/web-panel/assets/{index-BIID4XcP.js → index-C3k3ZwkQ.js} +1 -1
- package/src/assets/web-panel/assets/index-C97f71oi.js +1 -0
- package/src/assets/web-panel/assets/{index-4h3lpWJ3.js → index-CBVIFX-x.js} +1 -1
- package/src/assets/web-panel/assets/{index-DHvxXXNA.js → index-CJg6Wsjc.js} +1 -1
- package/src/assets/web-panel/assets/{index-DjI9MkId.js → index-CLzPzN75.js} +1 -1
- package/src/assets/web-panel/assets/{index-D0zdPyTD.js → index-CQ1ORdp-.js} +1 -1
- package/src/assets/web-panel/assets/{index-D0K_rFC1.js → index-CSFESurl.js} +1 -1
- package/src/assets/web-panel/assets/{index-b9cuvFEv.js → index-CWLWBgfe.js} +1 -1
- package/src/assets/web-panel/assets/{index-Nz-MUHD1.js → index-CahDve8M.js} +1 -1
- package/src/assets/web-panel/assets/{index-DXdnfDVK.js → index-Cg9cOVe9.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bh4hcNWW.js → index-Cmg_Npx5.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bru_kpZl.js → index-CrZHUsOZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-yOZYSNmG.js → index-Cz_s25m3.js} +1 -1
- package/src/assets/web-panel/assets/{index-DlSvpNMf.js → index-D9oV2iRR.js} +1 -1
- package/src/assets/web-panel/assets/{index-LfRlSmqe.js → index-DgjQ-kdT.js} +1 -1
- package/src/assets/web-panel/assets/{index-BOHUo3aM.js → index-Dmk7dD-T.js} +1 -1
- package/src/assets/web-panel/assets/{index-D3haorUt.js → index-Dxqiu9-F.js} +1 -1
- package/src/assets/web-panel/assets/{index-DV11jpeO.js → index-JS2LXdd0.js} +1 -1
- package/src/assets/web-panel/assets/{index-CY0zd_jc.js → index-TeCof1s3.js} +1 -1
- package/src/assets/web-panel/assets/{index-DpEKmjoa.js → index-UQXedWQz.js} +1 -1
- package/src/assets/web-panel/assets/{index-C3pctdCb.js → index-koT8Q86Z.js} +1 -1
- package/src/assets/web-panel/assets/{index-D_75jYWk.js → index-mIQUoUx1.js} +1 -1
- package/src/assets/web-panel/assets/{index-Gxw09e9R.js → index-oWltafBn.js} +1 -1
- package/src/assets/web-panel/assets/{index-BhIKnQAW.js → index-rIy96PeM.js} +1 -1
- package/src/assets/web-panel/assets/{index-C3ZA_h1j.js → index-rcwgNMc0.js} +1 -1
- package/src/assets/web-panel/assets/{index-DGsE7MQz.js → index-srqW5_-X.js} +1 -1
- package/src/assets/web-panel/assets/{index-eR6H6NR7.js → index-uzykuL_R.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-CJ1Bq2L3.js → initDefaultProps-xafkqrW3.js} +1 -1
- package/src/assets/web-panel/assets/{motion-rYJN_vzf.js → motion-BrtyQXLk.js} +1 -1
- package/src/assets/web-panel/assets/{move-DnDK2gTP.js → move-BunUyg59.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-CKnXy7ES.js → mtc-parser-BPwjBeuE.js} +1 -1
- package/src/assets/web-panel/assets/{omit-3nRc3hWT.js → omit-BpyqZlNO.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-Ddjj3sLu.js → pickAttrs-DDrV0XjE.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-CbsbphQY.js → placementArrow-1A18Pcl2.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-D9aZ4VDJ.js → responsiveObserve-B8Fnlmso.js} +1 -1
- package/src/assets/web-panel/assets/{slide-D4pvdFj0.js → slide-x-Fwb2Zm.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-C2W-vY-S.js → statusUtils-aAG5yJl_.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-Cbq5150d.js → styleChecker-BRQSTMRL.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-BA2B7R82.js → useFlexGapSupport-D5X_KozY.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CLrwg3xg.js → useFs-DN_YIjP-.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-DQeAI5qs.js → usePersonalDataHub-BizxAvjw.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-8BDGy-YU.js → vnode-B5oAxN93.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-B2SzFprl.js → zoom-DJhvyzKv.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/eval.js +7 -0
- package/src/commands/status.js +55 -0
- package/src/commands/team.js +1805 -448
- package/src/data/changelog.json +26 -0
- package/src/harness/worktree-isolator.js +671 -79
- package/src/lazy-dispatch.js +12 -1
- 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/observability/index.js +191 -19
- package/src/lib/observability/otlp-exporter.js +1209 -207
- package/src/lib/otlp-exporter.js +8 -36
- 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/program-base.js +9 -2
- package/src/runtime/headless-runner.js +45 -21
- package/src/runtime/headless-stream.js +6 -1
- package/src/assets/web-panel/assets/ChatBubbleRenderer-CtDb-2Qu.js +0 -1
- package/src/assets/web-panel/assets/MarkdownRenderer-BL1IVXm-.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-7IN2hWHg.js +0 -1
- package/src/assets/web-panel/assets/Projects-Bg8obRAM.js +0 -1
- package/src/assets/web-panel/assets/TimelineRenderer-Mq2juUC3.js +0 -1
- package/src/assets/web-panel/assets/devWarning-Cj_36GHq.js +0 -1
- package/src/assets/web-panel/assets/index-CezFrAmD.js +0 -1
- 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)
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
274
|
-
|
|
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
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
358
|
-
|
|
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));
|