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
|
@@ -11,16 +11,120 @@
|
|
|
11
11
|
* receive it exactly once. Delivery cursors + the log are in the snapshot, so a
|
|
12
12
|
* resumed session re-delivers only what a teammate hadn't yet drained.
|
|
13
13
|
*
|
|
14
|
+
* The mailbox is bounded by message-count, total-byte, and per-message-byte
|
|
15
|
+
* limits. When an incoming message would cross a limit, messages that have
|
|
16
|
+
* already been delivered to every registered intended recipient are compacted
|
|
17
|
+
* first. Undelivered data is never evicted: if safe compaction is insufficient,
|
|
18
|
+
* `send()` throws a stable coded backpressure error.
|
|
19
|
+
*
|
|
14
20
|
* Pure + deterministic: ids come from a monotonic counter (not a clock) and the
|
|
15
21
|
* clock is injected only for timestamps, so ordering is stable in tests.
|
|
16
22
|
*/
|
|
17
23
|
|
|
24
|
+
import { Buffer } from "node:buffer";
|
|
25
|
+
|
|
26
|
+
export const TEAM_MAILBOX_ERROR_CODES = Object.freeze({
|
|
27
|
+
MESSAGE_INVALID: "TEAM_MAILBOX_MESSAGE_INVALID",
|
|
28
|
+
MESSAGE_TOO_LARGE: "TEAM_MAILBOX_MESSAGE_TOO_LARGE",
|
|
29
|
+
CAPACITY_EXCEEDED: "TEAM_MAILBOX_CAPACITY_EXCEEDED",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const DEFAULT_TEAM_MAILBOX_LIMITS = Object.freeze({
|
|
33
|
+
maxMessageBytes: 64 * 1024,
|
|
34
|
+
maxMessages: 1000,
|
|
35
|
+
maxTotalBytes: 4 * 1024 * 1024,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function positiveInteger(value, fallback) {
|
|
39
|
+
const number = Number(value);
|
|
40
|
+
return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function nonNegativeNumber(value, fallback = 0) {
|
|
44
|
+
const number = Number(value);
|
|
45
|
+
return Number.isFinite(number) && number >= 0 ? number : fallback;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class TeamMailboxError extends Error {
|
|
49
|
+
constructor(code, message, details = {}) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = "TeamMailboxError";
|
|
52
|
+
this.code = code;
|
|
53
|
+
Object.assign(this, details);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createMailboxError(code, message, details = {}) {
|
|
58
|
+
return new TeamMailboxError(code, message, details);
|
|
59
|
+
}
|
|
60
|
+
|
|
18
61
|
export class TeamMailbox {
|
|
19
|
-
constructor({
|
|
62
|
+
constructor({
|
|
63
|
+
now = () => Date.now(),
|
|
64
|
+
maxMessageBytes = DEFAULT_TEAM_MAILBOX_LIMITS.maxMessageBytes,
|
|
65
|
+
maxMessages = DEFAULT_TEAM_MAILBOX_LIMITS.maxMessages,
|
|
66
|
+
maxTotalBytes = DEFAULT_TEAM_MAILBOX_LIMITS.maxTotalBytes,
|
|
67
|
+
recipients = [],
|
|
68
|
+
} = {}) {
|
|
20
69
|
this._now = typeof now === "function" ? now : () => now;
|
|
70
|
+
this._limits = {
|
|
71
|
+
maxMessageBytes: positiveInteger(
|
|
72
|
+
maxMessageBytes,
|
|
73
|
+
DEFAULT_TEAM_MAILBOX_LIMITS.maxMessageBytes,
|
|
74
|
+
),
|
|
75
|
+
maxMessages: positiveInteger(
|
|
76
|
+
maxMessages,
|
|
77
|
+
DEFAULT_TEAM_MAILBOX_LIMITS.maxMessages,
|
|
78
|
+
),
|
|
79
|
+
maxTotalBytes: positiveInteger(
|
|
80
|
+
maxTotalBytes,
|
|
81
|
+
DEFAULT_TEAM_MAILBOX_LIMITS.maxTotalBytes,
|
|
82
|
+
),
|
|
83
|
+
};
|
|
21
84
|
this._log = []; // ordered [{ id, from, to, subject, body, ts }]
|
|
85
|
+
this._entryBytes = new Map(); // id → serialized UTF-8 bytes
|
|
86
|
+
this._totalBytes = 0;
|
|
22
87
|
this._seq = 0; // monotonic message id source
|
|
23
88
|
this._delivered = new Map(); // recipient → highest message id already drained
|
|
89
|
+
this._recipients = new Set(); // authoritative compaction audience
|
|
90
|
+
this._counters = {
|
|
91
|
+
acceptedMessages: 0,
|
|
92
|
+
acceptedBytes: 0,
|
|
93
|
+
rejectedMessages: 0,
|
|
94
|
+
rejectedBytes: 0,
|
|
95
|
+
compactionRuns: 0,
|
|
96
|
+
compactedMessages: 0,
|
|
97
|
+
compactedBytes: 0,
|
|
98
|
+
};
|
|
99
|
+
this.registerRecipients(recipients);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Register the current teammate audience used for safe compaction. A direct
|
|
104
|
+
* message is compactable only after its registered target drains it; a
|
|
105
|
+
* broadcast waits for every registered recipient except its sender.
|
|
106
|
+
*
|
|
107
|
+
* Registration is additive. Newly registered teammates can receive messages
|
|
108
|
+
* that are still retained, but messages safely compacted before they joined
|
|
109
|
+
* are not replayed as history.
|
|
110
|
+
*/
|
|
111
|
+
registerRecipients(recipients = []) {
|
|
112
|
+
const values =
|
|
113
|
+
typeof recipients === "string"
|
|
114
|
+
? [recipients]
|
|
115
|
+
: recipients && typeof recipients[Symbol.iterator] === "function"
|
|
116
|
+
? Array.from(recipients)
|
|
117
|
+
: [];
|
|
118
|
+
for (const recipient of values) {
|
|
119
|
+
if (
|
|
120
|
+
typeof recipient === "string" &&
|
|
121
|
+
recipient.trim().length > 0 &&
|
|
122
|
+
recipient !== "*"
|
|
123
|
+
) {
|
|
124
|
+
this._recipients.add(recipient);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return Array.from(this._recipients);
|
|
24
128
|
}
|
|
25
129
|
|
|
26
130
|
/**
|
|
@@ -29,18 +133,141 @@ export class TeamMailbox {
|
|
|
29
133
|
*/
|
|
30
134
|
send({ from = null, to, subject = null, body = null } = {}) {
|
|
31
135
|
if (!to || typeof to !== "string") {
|
|
32
|
-
|
|
136
|
+
this._recordRejection(0);
|
|
137
|
+
throw createMailboxError(
|
|
138
|
+
TEAM_MAILBOX_ERROR_CODES.MESSAGE_INVALID,
|
|
139
|
+
"TeamMailbox.send: `to` recipient (or '*') is required",
|
|
140
|
+
);
|
|
33
141
|
}
|
|
34
|
-
|
|
35
|
-
id:
|
|
142
|
+
let msg = {
|
|
143
|
+
id: this._seq + 1,
|
|
36
144
|
from,
|
|
37
145
|
to,
|
|
38
146
|
subject,
|
|
39
147
|
body,
|
|
40
148
|
ts: this._now(),
|
|
41
149
|
};
|
|
150
|
+
let messageBytes;
|
|
151
|
+
try {
|
|
152
|
+
const serialized = JSON.stringify(msg);
|
|
153
|
+
messageBytes = Buffer.byteLength(serialized, "utf8");
|
|
154
|
+
// Store a detached JSON value so callers cannot mutate a retained body
|
|
155
|
+
// after admission and silently invalidate the byte accounting.
|
|
156
|
+
msg = JSON.parse(serialized);
|
|
157
|
+
if (
|
|
158
|
+
!["id", "from", "to", "subject", "body", "ts"].every((field) =>
|
|
159
|
+
Object.prototype.hasOwnProperty.call(msg, field),
|
|
160
|
+
)
|
|
161
|
+
) {
|
|
162
|
+
throw new TypeError("message fields must be JSON values");
|
|
163
|
+
}
|
|
164
|
+
} catch (cause) {
|
|
165
|
+
this._counters.rejectedMessages += 1;
|
|
166
|
+
throw createMailboxError(
|
|
167
|
+
TEAM_MAILBOX_ERROR_CODES.MESSAGE_INVALID,
|
|
168
|
+
"TeamMailbox.send: message must be JSON-serializable",
|
|
169
|
+
{ cause },
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
if (messageBytes > this._limits.maxMessageBytes) {
|
|
173
|
+
this._recordRejection(messageBytes);
|
|
174
|
+
throw createMailboxError(
|
|
175
|
+
TEAM_MAILBOX_ERROR_CODES.MESSAGE_TOO_LARGE,
|
|
176
|
+
`TeamMailbox.send: message is ${messageBytes} bytes; limit is ${this._limits.maxMessageBytes}`,
|
|
177
|
+
{
|
|
178
|
+
messageBytes,
|
|
179
|
+
maxMessageBytes: this._limits.maxMessageBytes,
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
if (!this._hasCapacity(messageBytes)) {
|
|
184
|
+
this._compactDelivered();
|
|
185
|
+
}
|
|
186
|
+
if (!this._hasCapacity(messageBytes)) {
|
|
187
|
+
this._recordRejection(messageBytes);
|
|
188
|
+
throw createMailboxError(
|
|
189
|
+
TEAM_MAILBOX_ERROR_CODES.CAPACITY_EXCEEDED,
|
|
190
|
+
"TeamMailbox.send: mailbox capacity exceeded; undelivered messages were preserved",
|
|
191
|
+
{
|
|
192
|
+
messageBytes,
|
|
193
|
+
maxMessages: this._limits.maxMessages,
|
|
194
|
+
maxTotalBytes: this._limits.maxTotalBytes,
|
|
195
|
+
currentMessages: this._log.length,
|
|
196
|
+
currentBytes: this._totalBytes,
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
this._seq = msg.id;
|
|
42
201
|
this._log.push(msg);
|
|
43
|
-
|
|
202
|
+
this._entryBytes.set(msg.id, messageBytes);
|
|
203
|
+
this._totalBytes += messageBytes;
|
|
204
|
+
this._counters.acceptedMessages += 1;
|
|
205
|
+
this._counters.acceptedBytes += messageBytes;
|
|
206
|
+
return this._cloneMessage(msg);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
_serializedBytes(msg) {
|
|
210
|
+
return Buffer.byteLength(JSON.stringify(msg), "utf8");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
_cloneMessage(msg) {
|
|
214
|
+
return JSON.parse(JSON.stringify(msg));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
_recordRejection(messageBytes) {
|
|
218
|
+
this._counters.rejectedMessages += 1;
|
|
219
|
+
this._counters.rejectedBytes += nonNegativeNumber(messageBytes);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
_hasCapacity(messageBytes) {
|
|
223
|
+
return (
|
|
224
|
+
this._log.length + 1 <= this._limits.maxMessages &&
|
|
225
|
+
this._totalBytes + messageBytes <= this._limits.maxTotalBytes
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
_fullyDelivered(msg) {
|
|
230
|
+
// Without an explicitly registered audience, removing a message could make
|
|
231
|
+
// a later-known teammate miss it. Prefer backpressure over silent loss.
|
|
232
|
+
if (this._recipients.size === 0) return false;
|
|
233
|
+
|
|
234
|
+
if (msg.to !== "*") {
|
|
235
|
+
if (!this._recipients.has(msg.to)) return false;
|
|
236
|
+
// Existing delivery semantics intentionally suppress self-messages.
|
|
237
|
+
if (msg.from === msg.to) return true;
|
|
238
|
+
return (this._delivered.get(msg.to) || 0) >= msg.id;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
for (const recipient of this._recipients) {
|
|
242
|
+
if (recipient === msg.from) continue;
|
|
243
|
+
if ((this._delivered.get(recipient) || 0) < msg.id) return false;
|
|
244
|
+
}
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
_compactDelivered() {
|
|
249
|
+
let compactedMessages = 0;
|
|
250
|
+
let compactedBytes = 0;
|
|
251
|
+
const retained = [];
|
|
252
|
+
for (const msg of this._log) {
|
|
253
|
+
if (this._fullyDelivered(msg)) {
|
|
254
|
+
compactedMessages += 1;
|
|
255
|
+
const bytes =
|
|
256
|
+
this._entryBytes.get(msg.id) ?? this._serializedBytes(msg);
|
|
257
|
+
compactedBytes += bytes;
|
|
258
|
+
this._entryBytes.delete(msg.id);
|
|
259
|
+
} else {
|
|
260
|
+
retained.push(msg);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (compactedMessages > 0) {
|
|
264
|
+
this._log = retained;
|
|
265
|
+
this._totalBytes = Math.max(0, this._totalBytes - compactedBytes);
|
|
266
|
+
this._counters.compactionRuns += 1;
|
|
267
|
+
this._counters.compactedMessages += compactedMessages;
|
|
268
|
+
this._counters.compactedBytes += compactedBytes;
|
|
269
|
+
}
|
|
270
|
+
return { messages: compactedMessages, bytes: compactedBytes };
|
|
44
271
|
}
|
|
45
272
|
|
|
46
273
|
_isFor(msg, recipient) {
|
|
@@ -55,7 +282,7 @@ export class TeamMailbox {
|
|
|
55
282
|
(m) =>
|
|
56
283
|
m.id > cursor && m.from !== recipient && this._isFor(m, recipient),
|
|
57
284
|
)
|
|
58
|
-
.map((m) => (
|
|
285
|
+
.map((m) => this._cloneMessage(m));
|
|
59
286
|
}
|
|
60
287
|
|
|
61
288
|
/**
|
|
@@ -79,28 +306,175 @@ export class TeamMailbox {
|
|
|
79
306
|
|
|
80
307
|
/** The full ordered message log (for a status panel / audit). */
|
|
81
308
|
log() {
|
|
82
|
-
return this._log.map((m) => (
|
|
309
|
+
return this._log.map((m) => this._cloneMessage(m));
|
|
83
310
|
}
|
|
84
311
|
|
|
85
312
|
size() {
|
|
86
313
|
return this._log.length;
|
|
87
314
|
}
|
|
88
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Current bounded-queue pressure. `ratio` is the stricter of message-count
|
|
318
|
+
* and byte pressure; `full` means a non-empty next message cannot fit without
|
|
319
|
+
* compaction.
|
|
320
|
+
*/
|
|
321
|
+
pressure() {
|
|
322
|
+
const messageRatio = this._log.length / this._limits.maxMessages;
|
|
323
|
+
const byteRatio = this._totalBytes / this._limits.maxTotalBytes;
|
|
324
|
+
const ratio = Math.max(messageRatio, byteRatio);
|
|
325
|
+
const level =
|
|
326
|
+
ratio >= 1
|
|
327
|
+
? "full"
|
|
328
|
+
: ratio >= 0.95
|
|
329
|
+
? "critical"
|
|
330
|
+
: ratio >= 0.8
|
|
331
|
+
? "high"
|
|
332
|
+
: "normal";
|
|
333
|
+
return {
|
|
334
|
+
ratio,
|
|
335
|
+
messageRatio,
|
|
336
|
+
byteRatio,
|
|
337
|
+
level,
|
|
338
|
+
full:
|
|
339
|
+
this._log.length >= this._limits.maxMessages ||
|
|
340
|
+
this._totalBytes >= this._limits.maxTotalBytes,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Queue usage, configured limits, registered audience, and lifetime counts. */
|
|
345
|
+
status() {
|
|
346
|
+
return {
|
|
347
|
+
messages: this._log.length,
|
|
348
|
+
totalBytes: this._totalBytes,
|
|
349
|
+
limits: { ...this._limits },
|
|
350
|
+
recipients: Array.from(this._recipients),
|
|
351
|
+
registeredRecipients: this._recipients.size,
|
|
352
|
+
pressure: this.pressure(),
|
|
353
|
+
counters: { ...this._counters },
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
89
357
|
snapshot() {
|
|
90
358
|
return {
|
|
91
|
-
|
|
359
|
+
version: 2,
|
|
360
|
+
limits: { ...this._limits },
|
|
361
|
+
log: this._log.map((m) => this._cloneMessage(m)),
|
|
92
362
|
seq: this._seq,
|
|
93
363
|
delivered: Array.from(this._delivered.entries()),
|
|
364
|
+
recipients: Array.from(this._recipients),
|
|
365
|
+
totalBytes: this._totalBytes,
|
|
366
|
+
counters: { ...this._counters },
|
|
94
367
|
};
|
|
95
368
|
}
|
|
96
369
|
|
|
97
|
-
static restore(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
370
|
+
static restore(
|
|
371
|
+
snap,
|
|
372
|
+
{
|
|
373
|
+
now = () => Date.now(),
|
|
374
|
+
maxMessageBytes,
|
|
375
|
+
maxMessages,
|
|
376
|
+
maxTotalBytes,
|
|
377
|
+
} = {},
|
|
378
|
+
) {
|
|
379
|
+
const savedLimits = snap?.limits || {};
|
|
380
|
+
const mb = new TeamMailbox({
|
|
381
|
+
now,
|
|
382
|
+
maxMessageBytes:
|
|
383
|
+
maxMessageBytes ??
|
|
384
|
+
savedLimits.maxMessageBytes ??
|
|
385
|
+
DEFAULT_TEAM_MAILBOX_LIMITS.maxMessageBytes,
|
|
386
|
+
maxMessages:
|
|
387
|
+
maxMessages ??
|
|
388
|
+
savedLimits.maxMessages ??
|
|
389
|
+
DEFAULT_TEAM_MAILBOX_LIMITS.maxMessages,
|
|
390
|
+
maxTotalBytes:
|
|
391
|
+
maxTotalBytes ??
|
|
392
|
+
savedLimits.maxTotalBytes ??
|
|
393
|
+
DEFAULT_TEAM_MAILBOX_LIMITS.maxTotalBytes,
|
|
394
|
+
recipients: snap?.recipients || [],
|
|
395
|
+
});
|
|
396
|
+
mb._log = Array.isArray(snap?.log)
|
|
397
|
+
? snap.log.map((m) => mb._cloneMessage(m))
|
|
398
|
+
: [];
|
|
399
|
+
mb._entryBytes = new Map();
|
|
400
|
+
mb._totalBytes = 0;
|
|
401
|
+
let previousId = 0;
|
|
402
|
+
for (const msg of mb._log) {
|
|
403
|
+
if (
|
|
404
|
+
!msg ||
|
|
405
|
+
typeof msg !== "object" ||
|
|
406
|
+
!Number.isSafeInteger(msg.id) ||
|
|
407
|
+
msg.id <= previousId ||
|
|
408
|
+
typeof msg.to !== "string" ||
|
|
409
|
+
msg.to.length === 0
|
|
410
|
+
) {
|
|
411
|
+
throw createMailboxError(
|
|
412
|
+
TEAM_MAILBOX_ERROR_CODES.MESSAGE_INVALID,
|
|
413
|
+
"TeamMailbox.restore: invalid message log",
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
const bytes = mb._serializedBytes(msg);
|
|
417
|
+
if (bytes > mb._limits.maxMessageBytes) {
|
|
418
|
+
throw createMailboxError(
|
|
419
|
+
TEAM_MAILBOX_ERROR_CODES.MESSAGE_TOO_LARGE,
|
|
420
|
+
"TeamMailbox.restore: retained message exceeds its byte limit",
|
|
421
|
+
{
|
|
422
|
+
messageBytes: bytes,
|
|
423
|
+
maxMessageBytes: mb._limits.maxMessageBytes,
|
|
424
|
+
},
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
mb._entryBytes.set(msg.id, bytes);
|
|
428
|
+
mb._totalBytes += bytes;
|
|
429
|
+
previousId = msg.id;
|
|
430
|
+
}
|
|
431
|
+
if (
|
|
432
|
+
mb._log.length > mb._limits.maxMessages ||
|
|
433
|
+
mb._totalBytes > mb._limits.maxTotalBytes
|
|
434
|
+
) {
|
|
435
|
+
throw createMailboxError(
|
|
436
|
+
TEAM_MAILBOX_ERROR_CODES.CAPACITY_EXCEEDED,
|
|
437
|
+
"TeamMailbox.restore: retained log exceeds mailbox capacity",
|
|
438
|
+
{
|
|
439
|
+
currentMessages: mb._log.length,
|
|
440
|
+
currentBytes: mb._totalBytes,
|
|
441
|
+
maxMessages: mb._limits.maxMessages,
|
|
442
|
+
maxTotalBytes: mb._limits.maxTotalBytes,
|
|
443
|
+
},
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
const savedSeq = Number(snap?.seq);
|
|
447
|
+
mb._seq = Math.max(
|
|
448
|
+
Number.isSafeInteger(savedSeq) && savedSeq >= 0 ? savedSeq : 0,
|
|
449
|
+
previousId,
|
|
450
|
+
);
|
|
451
|
+
mb._delivered = new Map();
|
|
452
|
+
for (const entry of Array.isArray(snap?.delivered) ? snap.delivered : []) {
|
|
453
|
+
const [recipient, cursor] = Array.isArray(entry) ? entry : [];
|
|
454
|
+
if (
|
|
455
|
+
typeof recipient !== "string" ||
|
|
456
|
+
!recipient ||
|
|
457
|
+
!Number.isSafeInteger(cursor) ||
|
|
458
|
+
cursor < 0 ||
|
|
459
|
+
cursor > mb._seq
|
|
460
|
+
) {
|
|
461
|
+
throw createMailboxError(
|
|
462
|
+
TEAM_MAILBOX_ERROR_CODES.MESSAGE_INVALID,
|
|
463
|
+
"TeamMailbox.restore: invalid delivery cursor",
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
mb._delivered.set(recipient, cursor);
|
|
467
|
+
}
|
|
468
|
+
const counters = snap?.counters;
|
|
469
|
+
mb._counters = {
|
|
470
|
+
acceptedMessages: nonNegativeNumber(counters?.acceptedMessages, mb._seq),
|
|
471
|
+
acceptedBytes: nonNegativeNumber(counters?.acceptedBytes, mb._totalBytes),
|
|
472
|
+
rejectedMessages: nonNegativeNumber(counters?.rejectedMessages),
|
|
473
|
+
rejectedBytes: nonNegativeNumber(counters?.rejectedBytes),
|
|
474
|
+
compactionRuns: nonNegativeNumber(counters?.compactionRuns),
|
|
475
|
+
compactedMessages: nonNegativeNumber(counters?.compactedMessages),
|
|
476
|
+
compactedBytes: nonNegativeNumber(counters?.compactedBytes),
|
|
477
|
+
};
|
|
104
478
|
return mb;
|
|
105
479
|
}
|
|
106
480
|
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-lifetime exclusive ownership for a `cc team --state` file.
|
|
3
|
+
*
|
|
4
|
+
* Atomic snapshot writes prevent torn JSON, but they do not stop two CLI
|
|
5
|
+
* processes from restoring the same leases and executing the same side effects.
|
|
6
|
+
* A lock directory is held for the entire async run. A crashed owner's lock is
|
|
7
|
+
* reclaimed only when its recorded PID is no longer alive; corrupt ownership
|
|
8
|
+
* fails closed and requires manual adjudication.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
|
|
15
|
+
export const TEAM_RUN_STATE_LOCK_ERROR = "TEAM_RUN_STATE_LOCK_UNAVAILABLE";
|
|
16
|
+
|
|
17
|
+
export const _deps = {
|
|
18
|
+
isProcessAlive: (pid) => {
|
|
19
|
+
try {
|
|
20
|
+
process.kill(pid, 0);
|
|
21
|
+
return true;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return error?.code === "EPERM";
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
ownerToken: () => randomUUID(),
|
|
27
|
+
writeMarker: (markerPath, owner) =>
|
|
28
|
+
fs.writeFileSync(markerPath, JSON.stringify(owner), {
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
mode: 0o600,
|
|
31
|
+
flag: "wx",
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function lockError(statePath, owner = null) {
|
|
36
|
+
const error = new Error(
|
|
37
|
+
`Team state is already owned by another process: ${statePath}`,
|
|
38
|
+
);
|
|
39
|
+
error.code = TEAM_RUN_STATE_LOCK_ERROR;
|
|
40
|
+
if (owner?.pid) error.ownerPid = owner.pid;
|
|
41
|
+
return error;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readOwner(ownerPath) {
|
|
45
|
+
try {
|
|
46
|
+
const owner = JSON.parse(fs.readFileSync(ownerPath, "utf8"));
|
|
47
|
+
if (
|
|
48
|
+
!owner ||
|
|
49
|
+
typeof owner !== "object" ||
|
|
50
|
+
!Number.isSafeInteger(owner.pid) ||
|
|
51
|
+
owner.pid <= 0 ||
|
|
52
|
+
!Number.isFinite(owner.startedAt) ||
|
|
53
|
+
(owner.token !== undefined &&
|
|
54
|
+
(typeof owner.token !== "string" ||
|
|
55
|
+
!/^[a-zA-Z0-9-]{16,128}$/.test(owner.token)))
|
|
56
|
+
) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return owner;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function sameOwner(left, right) {
|
|
66
|
+
return !!(
|
|
67
|
+
left &&
|
|
68
|
+
right &&
|
|
69
|
+
left.pid === right.pid &&
|
|
70
|
+
left.startedAt === right.startedAt &&
|
|
71
|
+
(left.token || null) === (right.token || null)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function ownerMarker(owner) {
|
|
76
|
+
if (owner?.token) return owner.token;
|
|
77
|
+
return `legacy-${owner.pid}-${Math.trunc(owner.startedAt)}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function removeOwnMarker(markerPath, owner) {
|
|
81
|
+
const marker = readOwner(markerPath);
|
|
82
|
+
if (!sameOwner(marker, owner)) return false;
|
|
83
|
+
try {
|
|
84
|
+
fs.rmSync(markerPath, { force: true });
|
|
85
|
+
return true;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Claim deletion inside the observed lock directory, then re-check the exact
|
|
93
|
+
* owner identity and liveness before removing it.
|
|
94
|
+
*
|
|
95
|
+
* This closes the stale-observation ABA race: if a delayed contender writes its
|
|
96
|
+
* old-owner marker into a replacement lock directory, the owner re-read
|
|
97
|
+
* mismatches and it removes only its own marker, never the replacement lock.
|
|
98
|
+
*/
|
|
99
|
+
function reclaimDeadLock(lockDir, owner) {
|
|
100
|
+
const markerPath = path.join(lockDir, `.reclaim-${ownerMarker(owner)}`);
|
|
101
|
+
try {
|
|
102
|
+
_deps.writeMarker(markerPath, owner);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (error?.code === "ENOENT" || error?.code === "EEXIST") return false;
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
const current = readOwner(path.join(lockDir, "owner.json"));
|
|
108
|
+
if (!sameOwner(current, owner) || _deps.isProcessAlive(owner.pid)) {
|
|
109
|
+
removeOwnMarker(markerPath, owner);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function canonicalStatePath(statePath) {
|
|
117
|
+
const requested = path.resolve(statePath);
|
|
118
|
+
const parent = fs.realpathSync.native(path.dirname(requested));
|
|
119
|
+
const target = path.join(parent, path.basename(requested));
|
|
120
|
+
if (fs.existsSync(target)) {
|
|
121
|
+
const entry = fs.lstatSync(target);
|
|
122
|
+
if (entry.isSymbolicLink() || !entry.isFile()) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Team state must be a regular, non-symlink file: ${target}`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (entry.nlink > 1) {
|
|
128
|
+
throw new Error(`Team state hard links are not allowed: ${target}`);
|
|
129
|
+
}
|
|
130
|
+
return fs.realpathSync.native(target);
|
|
131
|
+
}
|
|
132
|
+
return target;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export class TeamRunStateLock {
|
|
136
|
+
constructor(statePath, lockDir, owner) {
|
|
137
|
+
this.statePath = statePath;
|
|
138
|
+
this.lockDir = lockDir;
|
|
139
|
+
this.owner = owner;
|
|
140
|
+
this._released = false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
static acquire(statePath) {
|
|
144
|
+
if (typeof statePath !== "string" || statePath.trim() === "") {
|
|
145
|
+
throw new TypeError("team state path is required");
|
|
146
|
+
}
|
|
147
|
+
const target = canonicalStatePath(statePath);
|
|
148
|
+
const lockDir = `${target}.run-lock`;
|
|
149
|
+
const ownerPath = path.join(lockDir, "owner.json");
|
|
150
|
+
const owner = {
|
|
151
|
+
pid: process.pid,
|
|
152
|
+
startedAt: Date.now(),
|
|
153
|
+
token: _deps.ownerToken(),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
157
|
+
try {
|
|
158
|
+
fs.mkdirSync(lockDir);
|
|
159
|
+
try {
|
|
160
|
+
fs.writeFileSync(ownerPath, JSON.stringify(owner), {
|
|
161
|
+
encoding: "utf8",
|
|
162
|
+
mode: 0o600,
|
|
163
|
+
flag: "wx",
|
|
164
|
+
});
|
|
165
|
+
} catch (error) {
|
|
166
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
return new TeamRunStateLock(target, lockDir, owner);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (error?.code !== "EEXIST") throw error;
|
|
172
|
+
const incumbent = readOwner(ownerPath);
|
|
173
|
+
if (!incumbent) throw lockError(target);
|
|
174
|
+
if (_deps.isProcessAlive(incumbent.pid)) {
|
|
175
|
+
throw lockError(target, incumbent);
|
|
176
|
+
}
|
|
177
|
+
// Claim deletion against the exact observed dead owner. The exact-token
|
|
178
|
+
// reclaim marker prevents a delayed concurrent reclaimer
|
|
179
|
+
// from deleting a replacement lock created at the original path.
|
|
180
|
+
reclaimDeadLock(lockDir, incumbent);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
throw lockError(target);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
release() {
|
|
187
|
+
if (this._released) return false;
|
|
188
|
+
const current = readOwner(path.join(this.lockDir, "owner.json"));
|
|
189
|
+
if (!sameOwner(current, this.owner)) return false;
|
|
190
|
+
const markerPath = path.join(
|
|
191
|
+
this.lockDir,
|
|
192
|
+
`.release-${ownerMarker(this.owner)}`,
|
|
193
|
+
);
|
|
194
|
+
try {
|
|
195
|
+
_deps.writeMarker(markerPath, this.owner);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (error?.code !== "ENOENT" && error?.code !== "EEXIST") throw error;
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
const confirmed = readOwner(path.join(this.lockDir, "owner.json"));
|
|
201
|
+
if (!sameOwner(confirmed, this.owner)) {
|
|
202
|
+
removeOwnMarker(markerPath, this.owner);
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
this._released = true;
|
|
206
|
+
fs.rmSync(this.lockDir, { recursive: true, force: true });
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
}
|