chainlesschain 0.162.185 → 0.162.187

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/bin/chainlesschain.js +21 -5
  2. package/package.json +1 -1
  3. package/src/assets/web-panel/assets/{AIOps-Co4ipAKx.js → AIOps-CETaO3s8.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-BpsoDrDq.js → ActionButton-CAe7E9TW.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-DMUqeBik.js → Analytics-BGr2mzwO.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-C6fD_dXr.js → AppLayout-BEoY-NcT.js} +5 -5
  7. package/src/assets/web-panel/assets/{Artifacts-DW0YlQlV.js → Artifacts-DUZivCgt.js} +1 -1
  8. package/src/assets/web-panel/assets/{Audit-DXtHiiFK.js → Audit-C3yBs68k.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-BgmF62U-.js → BackgroundAgents-C9KwiY3O.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-D8vhVLQC.js → Backup-BoSWD_C8.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-D7nCtY2X.js → BaseInput-Dp09GkxL.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-CBZLEV2G.js → Chat-5t9d-Ja0.js} +4 -4
  13. package/src/assets/web-panel/assets/ChatBubbleRenderer-DmVku4GH.js +1 -0
  14. package/src/assets/web-panel/assets/{Checkbox-DJLkeD4M.js → Checkbox-8QJFlkqI.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-Cs6xO8Fd.js → Codegen-BJVFQ3lL.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col-Dtx9wxfT.js → Col-6hdLD0oB.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-DiigJg4n.js → Community-B8opo0uY.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-B67oMDDL.js → Compact-Dv8HudiN.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-BBDl-Jos.js → Compliance-DhxuCS5N.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-Ba98hSsa.js → Cowork-Dggy6VIL.js} +3 -3
  21. package/src/assets/web-panel/assets/{Cron-DBWQZIz7.js → Cron-DSmDqJTT.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-OcrKzxAt.js → Crosschain-7bZq0j5f.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-mW2eSbjM.js → DID-BWY4kh-H.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-BmsDPmKJ.js → Dashboard-bOzPZxlu.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-CVzmCyRx.js → Dropdown-BTFJHqQu.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-vVgqKzhh.js → EmailListRenderer-BC3MOdtT.js} +1 -1
  27. package/src/assets/web-panel/assets/{FamilyGuardDashboard-CWY5t_ZJ.js → FamilyGuardDashboard-CFHAdTTY.js} +1 -1
  28. package/src/assets/web-panel/assets/{Federation-ZGomhjxr.js → Federation-DZ_4aF_n.js} +1 -1
  29. package/src/assets/web-panel/assets/{FormItemContext-DyaknbLr.js → FormItemContext-BZje6UTj.js} +1 -1
  30. package/src/assets/web-panel/assets/{GenericCardRenderer-CmIvN1qr.js → GenericCardRenderer-D7QfCvjN.js} +1 -1
  31. package/src/assets/web-panel/assets/{Git-DQOJIYkG.js → Git-DuG7qANG.js} +2 -2
  32. package/src/assets/web-panel/assets/{Governance-CMiY-EEp.js → Governance-BDmYAgTh.js} +1 -1
  33. package/src/assets/web-panel/assets/{Inference-D6IXuucf.js → Inference-DSXAfc1Q.js} +1 -1
  34. package/src/assets/web-panel/assets/{KnowledgeGraph-6jkTL0hV.js → KnowledgeGraph-DusleniP.js} +1 -1
  35. package/src/assets/web-panel/assets/{Logs-N0jD-349.js → Logs-CpImmA8p.js} +2 -2
  36. package/src/assets/web-panel/assets/MarkdownRenderer-BZ6LJBkk.js +1 -0
  37. package/src/assets/web-panel/assets/{Marketplace-WhYuhBxm.js → Marketplace-DXS8CXkE.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-DTeO28xy.js → McpTools-D151UfUI.js} +5 -5
  39. package/src/assets/web-panel/assets/{Memory-MSHzPSE-.js → Memory-DKDxnoA1.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-Cyqs3J3W.js → MobileBridge-BFZ95iKM.js} +1 -1
  41. package/src/assets/web-panel/assets/MobileProjects-CDAR-grF.js +1 -0
  42. package/src/assets/web-panel/assets/{Mtc-MWBnezNM.js → Mtc-OGJ95xJs.js} +5 -5
  43. package/src/assets/web-panel/assets/{MtcAudit-DQu35Nu-.js → MtcAudit-BmGTo1XC.js} +2 -2
  44. package/src/assets/web-panel/assets/{Multisig-CQS_M-DP.js → Multisig-XumJRqOa.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-WD16vQvY.js → NLProgramming-DHhhRZed.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-DqsiSwOq.js → Notes-B9rGxNNr.js} +3 -3
  47. package/src/assets/web-panel/assets/{NotificationSettings-D3LoxO-P.js → NotificationSettings-DClLbMYV.js} +1 -1
  48. package/src/assets/web-panel/assets/{OrderTableRenderer-C5jPoTd6.js → OrderTableRenderer-I8OohT7N.js} +1 -1
  49. package/src/assets/web-panel/assets/{Organization-48aMBJso.js → Organization-M6WzaYVa.js} +4 -4
  50. package/src/assets/web-panel/assets/{Overflow-j-a2XkhR.js → Overflow-Cb3B5KtM.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-BYpBNYnh.js → P2P-BGk57kya.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-CYCsrrLg.js → PdhVaultBrowser-CXcKyPqK.js} +3 -3
  53. package/src/assets/web-panel/assets/{Permissions-ZRXGXDFb.js → Permissions-hsfJ-Y0e.js} +4 -4
  54. package/src/assets/web-panel/assets/{PersonalDataHub-6OWUPHyL.js → PersonalDataHub-BSBii4js.js} +5 -5
  55. package/src/assets/web-panel/assets/{Pipeline-FcjlDK9E.js → Pipeline-BNw52rey.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-vD_6Tldp.js → Privacy-DUpfue4E.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-B_CHOLXp.js → ProjectInit-C68NsP3h.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-rEiqQ2da.js → ProjectSettings-BJ2It9v4.js} +2 -2
  59. package/src/assets/web-panel/assets/Projects-D2FHLUa-.js +1 -0
  60. package/src/assets/web-panel/assets/{Providers-Dgtt97B4.js → Providers-EnU4VWwS.js} +1 -1
  61. package/src/assets/web-panel/assets/{QrScannerModal-DaGVylZO.js → QrScannerModal-BRt5-B_I.js} +1 -1
  62. package/src/assets/web-panel/assets/{QuickAsk-DP2_2x3u.js → QuickAsk-Jd5L9UL2.js} +1 -1
  63. package/src/assets/web-panel/assets/{Recommend-D29yj4ki.js → Recommend-BNDhoVIu.js} +1 -1
  64. package/src/assets/web-panel/assets/{RemoteSession-Ue2wnBLp.js → RemoteSession-BII41Mel.js} +2 -2
  65. package/src/assets/web-panel/assets/{Reputation-CrxIHqeC.js → Reputation-D1wQPUNv.js} +1 -1
  66. package/src/assets/web-panel/assets/{Row-BgSzJPCn.js → Row-DnjRegul.js} +1 -1
  67. package/src/assets/web-panel/assets/{RssFeed-AwhYOrP9.js → RssFeed-C_T4I-nF.js} +2 -2
  68. package/src/assets/web-panel/assets/{Search-D2shpEcm.js → Search-C4lrxlLh.js} +1 -1
  69. package/src/assets/web-panel/assets/{Security-BtKzhTbT.js → Security-ByYT-VzP.js} +4 -4
  70. package/src/assets/web-panel/assets/{Services-BY0DsUSm.js → Services-BHpyEqB5.js} +2 -2
  71. package/src/assets/web-panel/assets/{Skeleton-B3Sb0UI9.js → Skeleton-C0I23vG-.js} +1 -1
  72. package/src/assets/web-panel/assets/{Skills-Ne5w0bDs.js → Skills-DGZXz22K.js} +1 -1
  73. package/src/assets/web-panel/assets/{Sla-C8VkaP4T.js → Sla-DL_urP9A.js} +1 -1
  74. package/src/assets/web-panel/assets/{SpeechSettings-BQdIEYP9.js → SpeechSettings-6x-hUNQ_.js} +1 -1
  75. package/src/assets/web-panel/assets/{SyncSettings-CwFafrUg.js → SyncSettings-Crk5TCXq.js} +2 -2
  76. package/src/assets/web-panel/assets/{Tasks-yJuKoDlR.js → Tasks-rZGq5PZs.js} +1 -1
  77. package/src/assets/web-panel/assets/{Templates-DYn9qCmx.js → Templates-DT5el8Q8.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tenant-DkKReHvn.js → Tenant-mlgeyMK8.js} +1 -1
  79. package/src/assets/web-panel/assets/{Terminal-r6bd-owt.js → Terminal-C8HEA-lM.js} +2 -2
  80. package/src/assets/web-panel/assets/TimelineRenderer-DvlqBayB.js +1 -0
  81. package/src/assets/web-panel/assets/{Tokens-BnMw7MRi.js → Tokens-C7lDcNmT.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trigger-CTzKp4o9.js → Trigger-CM8nCnIJ.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trust-BBrSZm9U.js → Trust-BQDLUQ9x.js} +1 -1
  84. package/src/assets/web-panel/assets/{UkeySign-D9fDvEMq.js → UkeySign-CVqM0OFE.js} +1 -1
  85. package/src/assets/web-panel/assets/{VideoEditing-DUi5ve-0.js → VideoEditing-BvAcQpzT.js} +1 -1
  86. package/src/assets/web-panel/assets/{Wallet-C8Fi1P8_.js → Wallet-zFVFaAZ2.js} +4 -4
  87. package/src/assets/web-panel/assets/{WebAuthn-s1LppYi2.js → WebAuthn-Cc522ums.js} +5 -5
  88. package/src/assets/web-panel/assets/{WorkflowEditor-DJL2C4lj.js → WorkflowEditor-Bg_wxpDo.js} +1 -1
  89. package/src/assets/web-panel/assets/{chat-D3F2ndOB.js → chat-jg0QwC8K.js} +1 -1
  90. package/src/assets/web-panel/assets/{colors-B7739-e3.js → colors-ALzYz1gO.js} +1 -1
  91. package/src/assets/web-panel/assets/{compact-item-CcJbFTF4.js → compact-item-DXHYUoqi.js} +1 -1
  92. package/src/assets/web-panel/assets/{createContext-CuP_DEFS.js → createContext-BsGsBM5m.js} +1 -1
  93. package/src/assets/web-panel/assets/devWarning-Dy1GJA4S.js +1 -0
  94. package/src/assets/web-panel/assets/{hasIn-6YEXfIik.js → hasIn-BccMLzxA.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-fWgUQZ9d.js → index-08diKcTm.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-CxradkS6.js → index-2VstcImq.js} +19 -19
  97. package/src/assets/web-panel/assets/index-8zbMbbuf.js +1 -0
  98. package/src/assets/web-panel/assets/{index-DDRlBaDt.js → index-BBbbr1YB.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-Dox44j29.js → index-BTTeAiOP.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BYYSLEjn.js → index-BcJzphrP.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CRuoRapN.js → index-BdCmJ_bG.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-BxSqVMGh.js → index-BfF0OGKU.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-D6Ij_2uw.js → index-BfV8RDlg.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-Dsy7_x4s.js → index-BvsII10i.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BsdSnHQE.js → index-C0AbXsDy.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-mfWQRSLn.js → index-C3Rf8wIM.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BIID4XcP.js → index-C3k3ZwkQ.js} +1 -1
  108. package/src/assets/web-panel/assets/index-C97f71oi.js +1 -0
  109. package/src/assets/web-panel/assets/{index-4h3lpWJ3.js → index-CBVIFX-x.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-DHvxXXNA.js → index-CJg6Wsjc.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DjI9MkId.js → index-CLzPzN75.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-D0zdPyTD.js → index-CQ1ORdp-.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-D0K_rFC1.js → index-CSFESurl.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-b9cuvFEv.js → index-CWLWBgfe.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-Nz-MUHD1.js → index-CahDve8M.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DXdnfDVK.js → index-Cg9cOVe9.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-Bh4hcNWW.js → index-Cmg_Npx5.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-Bru_kpZl.js → index-CrZHUsOZ.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-yOZYSNmG.js → index-Cz_s25m3.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-DlSvpNMf.js → index-D9oV2iRR.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-LfRlSmqe.js → index-DgjQ-kdT.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-BOHUo3aM.js → index-Dmk7dD-T.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-D3haorUt.js → index-Dxqiu9-F.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-DV11jpeO.js → index-JS2LXdd0.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-CY0zd_jc.js → index-TeCof1s3.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-DpEKmjoa.js → index-UQXedWQz.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-C3pctdCb.js → index-koT8Q86Z.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-D_75jYWk.js → index-mIQUoUx1.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-Gxw09e9R.js → index-oWltafBn.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BhIKnQAW.js → index-rIy96PeM.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-C3ZA_h1j.js → index-rcwgNMc0.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-DGsE7MQz.js → index-srqW5_-X.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-eR6H6NR7.js → index-uzykuL_R.js} +1 -1
  134. package/src/assets/web-panel/assets/{initDefaultProps-CJ1Bq2L3.js → initDefaultProps-xafkqrW3.js} +1 -1
  135. package/src/assets/web-panel/assets/{motion-rYJN_vzf.js → motion-BrtyQXLk.js} +1 -1
  136. package/src/assets/web-panel/assets/{move-DnDK2gTP.js → move-BunUyg59.js} +1 -1
  137. package/src/assets/web-panel/assets/{mtc-parser-CKnXy7ES.js → mtc-parser-BPwjBeuE.js} +1 -1
  138. package/src/assets/web-panel/assets/{omit-3nRc3hWT.js → omit-BpyqZlNO.js} +1 -1
  139. package/src/assets/web-panel/assets/{pickAttrs-Ddjj3sLu.js → pickAttrs-DDrV0XjE.js} +1 -1
  140. package/src/assets/web-panel/assets/{placementArrow-CbsbphQY.js → placementArrow-1A18Pcl2.js} +1 -1
  141. package/src/assets/web-panel/assets/{responsiveObserve-D9aZ4VDJ.js → responsiveObserve-B8Fnlmso.js} +1 -1
  142. package/src/assets/web-panel/assets/{slide-D4pvdFj0.js → slide-x-Fwb2Zm.js} +1 -1
  143. package/src/assets/web-panel/assets/{statusUtils-C2W-vY-S.js → statusUtils-aAG5yJl_.js} +1 -1
  144. package/src/assets/web-panel/assets/{styleChecker-Cbq5150d.js → styleChecker-BRQSTMRL.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFlexGapSupport-BA2B7R82.js → useFlexGapSupport-D5X_KozY.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFs-CLrwg3xg.js → useFs-DN_YIjP-.js} +1 -1
  147. package/src/assets/web-panel/assets/{usePersonalDataHub-DQeAI5qs.js → usePersonalDataHub-BizxAvjw.js} +1 -1
  148. package/src/assets/web-panel/assets/{vnode-8BDGy-YU.js → vnode-B5oAxN93.js} +1 -1
  149. package/src/assets/web-panel/assets/{zoom-B2SzFprl.js → zoom-DJhvyzKv.js} +1 -1
  150. package/src/assets/web-panel/index.html +1 -1
  151. package/src/commands/eval.js +7 -0
  152. package/src/commands/status.js +55 -0
  153. package/src/commands/team.js +1805 -448
  154. package/src/data/changelog.json +26 -0
  155. package/src/harness/worktree-isolator.js +671 -79
  156. package/src/lazy-dispatch.js +12 -1
  157. package/src/lib/agent-team/task-lease.js +378 -78
  158. package/src/lib/agent-team/team-agent-stream.js +488 -0
  159. package/src/lib/agent-team/team-budget.js +216 -20
  160. package/src/lib/agent-team/team-mailbox.js +389 -15
  161. package/src/lib/agent-team/team-run-state-lock.js +209 -0
  162. package/src/lib/agent-team/team-runner.js +547 -63
  163. package/src/lib/agent-team/team-scope-lock.js +409 -0
  164. package/src/lib/agent-team/team-task-contract.js +277 -0
  165. package/src/lib/agent-team/team-worktree.js +1171 -62
  166. package/src/lib/agent-worktree.js +19 -20
  167. package/src/lib/collaboration-run-store.js +983 -68
  168. package/src/lib/observability/index.js +191 -19
  169. package/src/lib/observability/otlp-exporter.js +1209 -207
  170. package/src/lib/otlp-exporter.js +8 -36
  171. package/src/lib/with-file-lock.js +190 -24
  172. package/src/lib/worktree-isolator.js +3 -0
  173. package/src/lib/worktree-sparse.js +23 -0
  174. package/src/program-base.js +9 -2
  175. package/src/runtime/headless-runner.js +45 -21
  176. package/src/runtime/headless-stream.js +6 -1
  177. package/src/assets/web-panel/assets/ChatBubbleRenderer-CtDb-2Qu.js +0 -1
  178. package/src/assets/web-panel/assets/MarkdownRenderer-BL1IVXm-.js +0 -1
  179. package/src/assets/web-panel/assets/MobileProjects-7IN2hWHg.js +0 -1
  180. package/src/assets/web-panel/assets/Projects-Bg8obRAM.js +0 -1
  181. package/src/assets/web-panel/assets/TimelineRenderer-Mq2juUC3.js +0 -1
  182. package/src/assets/web-panel/assets/devWarning-Cj_36GHq.js +0 -1
  183. package/src/assets/web-panel/assets/index-CezFrAmD.js +0 -1
  184. package/src/assets/web-panel/assets/index-Cxgguez6.js +0 -1
@@ -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({ now = () => Date.now() } = {}) {
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
- throw new Error("TeamMailbox.send: `to` recipient (or '*') is required");
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
- const msg = {
35
- id: ++this._seq,
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
- return { ...msg };
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) => ({ ...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) => ({ ...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
- log: this._log.map((m) => ({ ...m })),
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(snap, { now = () => Date.now() } = {}) {
98
- const mb = new TeamMailbox({ now });
99
- mb._log = Array.isArray(snap?.log) ? snap.log.map((m) => ({ ...m })) : [];
100
- mb._seq =
101
- Number(snap?.seq) ||
102
- mb._log.reduce((max, m) => Math.max(max, m.id || 0), 0);
103
- mb._delivered = new Map(snap?.delivered || []);
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
+ }