chainlesschain 0.162.160 → 0.162.161

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 (179) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-CkRHjW8e.js → AIOps-DMn528q-.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BP3MeNi2.js → ActionButton-D9ZcDCJL.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-CjbjQsOr.js → Analytics-A_K-8PX0.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-i0adH1M8.js → AppLayout-D4pFW3X6.js} +3 -3
  6. package/src/assets/web-panel/assets/{Artifacts-CCCG2GOU.js → Artifacts-Dz46COAd.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-DrsgvwHC.js → Audit-cbMjbyg0.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-DkXNkbZM.js → BackgroundAgents-Dtf88GAj.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-DarTWCex.js → Backup-C5KRxn52.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-DaDzhcto.js → BaseInput-Dd2UK3BN.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-C4EzGw6V.js → Chat-lptKTFdG.js} +6 -6
  12. package/src/assets/web-panel/assets/ChatBubbleRenderer-C2KLtwHz.js +1 -0
  13. package/src/assets/web-panel/assets/{Checkbox-gddNgaQi.js → Checkbox-_yspG6rG.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-BKldFWf9.js → Codegen-BGq_GAha.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-ChR-brDs.js → Col-CePor71g.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-DlHtZX86.js → Community-CLOWhIT7.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-BAhfcBpC.js → Compact-CivJt2vT.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-gIrKkm6m.js → Compliance-Btmo1GGE.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-BYnzLBO3.js → Cowork-Ct35OlG4.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-ByUDIljI.js → Cron-CTc2GmhB.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-DOdNbHFp.js → Crosschain-ZtiBETk1.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-BJHaNWD2.js → DID-BNqMQQXw.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-Drm_tClz.js → Dashboard-BmzZd-9V.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-CA1WLzGN.js → Dropdown-CMVgvP2M.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-Di1SKjNO.js → EmailListRenderer-CcqQFAfT.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-44Ve7ZFe.js → FamilyGuardDashboard-CTqB1_4V.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-CubPwv_N.js → Federation-CiKOOzlb.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-CsUFe5bU.js → FormItemContext-Wk-OT0gr.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-D7xRjPjZ.js → GenericCardRenderer-DbEQT6ue.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-D-Qx9dhO.js → Git-0ZvCPSD9.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-_YMYaU4n.js → Governance-CmVuKkPb.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-J7Yzh-K_.js → Inference-CTWE3Jwg.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-D37iI_Mo.js → KnowledgeGraph-CBEIF00L.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-oMTSYfGY.js → Logs-DLhxDwVG.js} +2 -2
  35. package/src/assets/web-panel/assets/MarkdownRenderer-DxywvhTU.js +1 -0
  36. package/src/assets/web-panel/assets/{Marketplace-DKUqYDaF.js → Marketplace-BgsCBrnc.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-BEv3Gx61.js → McpTools-ClBUGDS2.js} +3 -3
  38. package/src/assets/web-panel/assets/{Memory-CtA8XD2y.js → Memory-CnG_ZnYw.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-ug7c1mMj.js → MobileBridge-BUOAbR1Z.js} +1 -1
  40. package/src/assets/web-panel/assets/MobileProjects-Dnt_9-WB.js +1 -0
  41. package/src/assets/web-panel/assets/{Mtc-WJaMs4i2.js → Mtc-CLjt4kCD.js} +2 -2
  42. package/src/assets/web-panel/assets/{MtcAudit-DXnK_-ru.js → MtcAudit-f24U9Aes.js} +2 -2
  43. package/src/assets/web-panel/assets/{Multisig-C4Ev7726.js → Multisig-GrmGQbSq.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-BMFb9_zk.js → NLProgramming-DADaWn-n.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-Bl93nvBX.js → Notes-xtf5RX6C.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-BN2W5cl6.js → NotificationSettings-Cd27Rgtx.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-Dxc9oN4o.js → OrderTableRenderer-fXRxlDjl.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-CAs48637.js → Organization-joCFlNB0.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-Bhq27Zoc.js → Overflow-B1HoURq0.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-D61-2542.js → P2P-DiwsKbT7.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-C1j4iUxz.js → PdhVaultBrowser-Xe-BsW6S.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-rJ2-fScM.js → Permissions-C23qi8kl.js} +3 -3
  53. package/src/assets/web-panel/assets/{PersonalDataHub-DFIlTok6.js → PersonalDataHub-BS1syX2f.js} +2 -2
  54. package/src/assets/web-panel/assets/{Pipeline-BjIgEWoZ.js → Pipeline-DRYwwDyh.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-BQoH5wcX.js → Privacy-BQKaDHPH.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-CYR_-_Ub.js → ProjectInit-BC6F9xiG.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-nkatCq-9.js → ProjectSettings-ClFPc1Nd.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-SS9uZKCl.js → Projects-Dn-f5aYe.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-BnKM9fsF.js → Providers-CLWuOUNt.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-Cfe82dbz.js → QrScannerModal-DiqULTxb.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-r1RbPufV.js → QuickAsk-9A03P-E_.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-CjCbM2WE.js → Recommend-CJZAnbq_.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-BuMGPH6V.js → RemoteSession-C9A1R77h.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-Bmj3NY1F.js → Reputation-BIZc6Rky.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-BIsNcE_1.js → Row-DfIPNtuG.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-BObmojD2.js → RssFeed-C5znFcQ1.js} +2 -2
  67. package/src/assets/web-panel/assets/{Search-BRmbkQNS.js → Search-DSs2z3OX.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-_rF3DRpY.js → Security-CV6LcrLY.js} +3 -3
  69. package/src/assets/web-panel/assets/{Services-D0If9ZV4.js → Services-cmhoX0Xj.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-o7zIksUg.js → Skeleton-tCsg2po7.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-oUR7zi9u.js → Skills-BK1KUDiy.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-fqKyGi03.js → Sla-yRgyeA82.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-B1wZ_gxx.js → SpeechSettings-c1mo1mlp.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-B3qJWYXq.js → SyncSettings-koIAfuT3.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-BH5_dUfr.js → Tasks-DkkreAaB.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-CCIVpjRP.js → Templates-fy5Ctf9M.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-2GZtTGnB.js → Tenant-Bw70Jf6S.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-BlMkfs_u.js → Terminal-BKh6Rx-4.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-U6hi5OMc.js → TimelineRenderer-Dg54WP0c.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-C-_BerGJ.js → Tokens-BvvD5SxJ.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-DaKzBgN3.js → Trigger-B7Ug9x4e.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-CLvcsco3.js → Trust-BjvuMORY.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-BFn_Y1zZ.js → UkeySign-CZqhkx8p.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-B0V18dLT.js → VideoEditing-ClR50lhX.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-BbmP7Imu.js → Wallet-DlfppYjx.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-Dewj_0kt.js → WebAuthn-BirgVr8B.js} +4 -4
  87. package/src/assets/web-panel/assets/{WorkflowEditor-DOxz3Lwa.js → WorkflowEditor-B8DP_bax.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-B1OIpZnh.js → chat-DA8AUZzL.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-CSGNVdri.js → colors-C38pLGp1.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-8S0vcg0u.js → compact-item-81rtr98o.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-vZyc27TN.js → createContext-D34ixq8t.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-CHDIEOeN.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-DWw3-UhA.js → hasIn-D4bzqTrZ.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-dxocDVD_.js → index-9qoVpPt6.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-xx5M5Rbj.js → index-B0QArPQt.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-DtHWHJnj.js → index-B8mkLM4x.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-DLgkAhEE.js → index-BIP62a6r.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-tr1x96W9.js → index-BP-zkXK8.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-CfEvacz7.js → index-BS3M5H9V.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-D1i_8oop.js → index-BXd2_LmH.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BDHyovnK.js → index-BlxEphl7.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-CLPr2vC_.js → index-BqIhLAbA.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-BQssf0EQ.js → index-BtGxuQrO.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-BuB5P0TR.js → index-BvD7b2IK.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DeCS12Mh.js → index-BvL_3J_n.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BeBv896m.js → index-COe_cOLJ.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-Dc40pX3F.js → index-CP5alL4t.js} +1 -1
  108. package/src/assets/web-panel/assets/index-CY1NURWZ.js +1 -0
  109. package/src/assets/web-panel/assets/{index-B4pP2ycr.js → index-CbhE3j5j.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-DKwl1AkQ.js → index-Cd3KuNK5.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BZnauCqN.js → index-CoVgZw_b.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-BwKrTVV0.js → index-CuS-UPoj.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-Q6oyLnKC.js → index-D0Ih7Hwn.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-CB2MOS_8.js → index-D3zCCZRS.js} +3 -3
  115. package/src/assets/web-panel/assets/{index-BdyokEau.js → index-D6GfvEQG.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DPgY5GLy.js → index-DBFSo1NF.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CFQWZvak.js → index-DFHa1HIg.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-BjC-TrOb.js → index-DFyWKOBD.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-Dg0Izipr.js → index-DMgnRwrK.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-BKEmQQvR.js → index-DUQLXLQM.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-DLAvUtOH.js → index-D_2VYPr1.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-D8qXVfoV.js → index-DakfFZ-k.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-DcfLXAW7.js → index-Ddk0e_Pi.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-BoIPHP4R.js → index-DkHqMnn3.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-CJgKP2cm.js → index-Dmo3dFHb.js} +1 -1
  126. package/src/assets/web-panel/assets/index-DokUYqpq.js +1 -0
  127. package/src/assets/web-panel/assets/{index-C6r27A-o.js → index-DvfCEJG-.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-CbZqw7pg.js → index-E1OJRXs4.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BkWd6YL7.js → index-F76w1NiL.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-t-q3rkVm.js → index-ZRJRItm0.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-CwXBblm-.js → index-fI5sXMLg.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-CI3slBKI.js → index-tKiC8T-4.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-Di5YFId0.js → initDefaultProps-ByyvflIR.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-BVSN0aeL.js → motion-B3FQm2By.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-DHNiaP2l.js → move-BSfns6Ef.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-xJPf3gQD.js → mtc-parser-CTG1Ieri.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-C7qzUdkN.js → omit-DObM_2j6.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-pSpjg3Le.js → pickAttrs-DOfelQ1t.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-cs_jL_UC.js → placementArrow-CHZILds_.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-DkS_Zs4r.js → responsiveObserve-REbucKSx.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-B5O1Kpxo.js → slide-C6EZzD7o.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-BX02GnZp.js → statusUtils-BxkWS-g3.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-EmBYRftd.js → styleChecker-Chklyoq6.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-ZGzN1cU8.js → useFlexGapSupport-CRVwKrrI.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-CfP7M1m7.js → useFs-DV24ik_J.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-CbIPHgNh.js → usePersonalDataHub-C7vIo4Pg.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-7xh0If_O.js → vnode-By-v5GlF.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-DmitvTR2.js → zoom-B1w9Am3r.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/agenda.js +15 -1
  151. package/src/commands/agent.js +7 -0
  152. package/src/commands/background-session.js +16 -2
  153. package/src/data/changelog.json +7 -2
  154. package/src/gateways/ws/background-agent-protocol.js +88 -14
  155. package/src/lib/agent-authority.js +251 -0
  156. package/src/lib/background-agent-phase.js +100 -0
  157. package/src/lib/background-agent-supervisor.js +288 -8
  158. package/src/lib/backpressure-policy.js +105 -0
  159. package/src/lib/capability-negotiation.js +212 -0
  160. package/src/lib/credential-guard.js +11 -0
  161. package/src/lib/credential-proxy.js +182 -0
  162. package/src/lib/event-seq-replay.js +167 -0
  163. package/src/lib/goal-condition-engine.js +369 -0
  164. package/src/lib/headless-manifest.js +13 -0
  165. package/src/lib/ide-context.js +53 -14
  166. package/src/lib/project-instructions.js +98 -1
  167. package/src/lib/remote-path-mapping.js +229 -0
  168. package/src/lib/schedule-planner.js +136 -0
  169. package/src/repl/bg-dashboard.js +13 -12
  170. package/src/runtime/agent-core.js +11 -4
  171. package/src/runtime/headless-stream.js +195 -11
  172. package/src/runtime/system-prompt.js +3 -0
  173. package/src/workers/background-agent-worker.js +37 -2
  174. package/src/assets/web-panel/assets/ChatBubbleRenderer-lkavIWMS.js +0 -1
  175. package/src/assets/web-panel/assets/MarkdownRenderer-BqaPmC0W.js +0 -1
  176. package/src/assets/web-panel/assets/MobileProjects-BpX5g9It.js +0 -1
  177. package/src/assets/web-panel/assets/devWarning-DYEZcY55.js +0 -1
  178. package/src/assets/web-panel/assets/index-DmVFfcNR.js +0 -1
  179. package/src/assets/web-panel/assets/index-x7YyhMUa.js +0 -1
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Outbound backpressure policy — bounds server→client buffering when a
3
+ * consumer drains too slowly, so a stuck WS client can't grow the sender's
4
+ * memory without limit (the classic slow-consumer blowup) and the UI degrades
5
+ * explicitly instead of freezing.
6
+ *
7
+ * Pure + deterministic (no transport, no timers): the caller feeds the
8
+ * transport's current queued-byte count (e.g. `ws.bufferedAmount`) and a frame
9
+ * class; the policy decides send-vs-drop with hysteresis and reports the
10
+ * lag-episode transitions the caller turns into a notice.
11
+ *
12
+ * Designed to compose with EventReplayBuffer (event-seq-replay.js): a dropped
13
+ * *droppable* frame should still be RECORDED (it gets a seq and stays in the
14
+ * bounded replay buffer), just not sent. The seq gap on the next *sent* frame
15
+ * then trips the consumer's SeqGapTracker, which requests replaySince — so the
16
+ * dropped frames are recovered once the consumer catches up (or a truncated
17
+ * replay tells it to full-resync). Critical frames are never dropped.
18
+ */
19
+
20
+ const DEFAULT_HIGH_WATER_BYTES = 4 << 20; // 4 MiB queued → consumer is behind
21
+ const DEFAULT_LOW_WATER_BYTES = 1 << 20; // 1 MiB → considered caught up again
22
+
23
+ /** A frame that must always be delivered (lifecycle, errors, acks). */
24
+ export const CRITICAL = "critical";
25
+ /** A frame safe to drop under pressure and recover via replay/resync. */
26
+ export const DROPPABLE = "droppable";
27
+
28
+ export class BackpressurePolicy {
29
+ constructor({
30
+ highWaterBytes = DEFAULT_HIGH_WATER_BYTES,
31
+ lowWaterBytes = DEFAULT_LOW_WATER_BYTES,
32
+ } = {}) {
33
+ const high = Math.max(
34
+ 1,
35
+ Math.floor(highWaterBytes) || DEFAULT_HIGH_WATER_BYTES,
36
+ );
37
+ // Low water must sit below high water or hysteresis collapses to a flapping
38
+ // single threshold; clamp it to at most high-1.
39
+ const low = Math.min(Math.max(0, Math.floor(lowWaterBytes) || 0), high - 1);
40
+ this._high = high;
41
+ this._low = low;
42
+ this._lagging = false;
43
+ this._dropped = 0; // dropped since the CURRENT lag episode started
44
+ this._droppedTotal = 0; // dropped across the whole connection
45
+ }
46
+
47
+ /** True while the consumer is behind (buffered ≥ high, until it drains ≤ low). */
48
+ get lagging() {
49
+ return this._lagging;
50
+ }
51
+
52
+ /** Frames dropped across the whole connection. */
53
+ get droppedTotal() {
54
+ return this._droppedTotal;
55
+ }
56
+
57
+ /**
58
+ * Decide what to do with one outbound frame.
59
+ * @param {number} bufferedBytes transport's currently-queued bytes
60
+ * (`ws.bufferedAmount`); non-finite → treated as 0 (never lags).
61
+ * @param {"critical"|"droppable"} frameClass
62
+ * @returns {{send:boolean, drop:boolean, lagStarted:boolean, lagEnded:boolean,
63
+ * dropped:number}} `dropped` = count dropped in the episode that just
64
+ * started/ended (for the caller's notice); `send:false` only ever for a
65
+ * droppable frame while lagging.
66
+ */
67
+ evaluate(bufferedBytes, frameClass) {
68
+ const b = Number.isFinite(bufferedBytes) ? bufferedBytes : 0;
69
+ let lagStarted = false;
70
+ let lagEnded = false;
71
+
72
+ if (!this._lagging && b >= this._high) {
73
+ this._lagging = true;
74
+ this._dropped = 0;
75
+ lagStarted = true;
76
+ } else if (this._lagging && b <= this._low) {
77
+ this._lagging = false;
78
+ lagEnded = true;
79
+ }
80
+
81
+ // While lagging, shed droppable frames to let the buffer drain; critical
82
+ // frames (and everything while not lagging) always go out.
83
+ if (this._lagging && frameClass === DROPPABLE) {
84
+ this._dropped += 1;
85
+ this._droppedTotal += 1;
86
+ return {
87
+ send: false,
88
+ drop: true,
89
+ lagStarted,
90
+ lagEnded,
91
+ dropped: this._dropped,
92
+ };
93
+ }
94
+
95
+ // `dropped` carries the episode's running count so a lag-end notice can
96
+ // report how many droppable frames were shed.
97
+ return {
98
+ send: true,
99
+ drop: false,
100
+ lagStarted,
101
+ lagEnded,
102
+ dropped: this._dropped,
103
+ };
104
+ }
105
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Bidirectional capability negotiation with N / N-1 downgrade for Agent
3
+ * Protocol (agent-sdk docs/PROTOCOL.md §1.3).
4
+ *
5
+ * The CLI advertises what it can speak one-directionally (buildAgentCapabilities
6
+ * → `cc agent --capabilities`, and the `system/init` line's protocol_version).
7
+ * That is not negotiation: a client (VS Code / JetBrains panel) had no way to
8
+ * announce what IT understands, and the CLI had no rule to pick a common level
9
+ * or step DOWN when the two disagree. When the protocol later grows a v2 line
10
+ * shape, a v1-only client would silently mis-parse it.
11
+ *
12
+ * This module is the missing algorithm. A client MAY send a `hello` as its
13
+ * first stream-json input line:
14
+ * {"type":"hello","protocol_version":2,"min_protocol_version":1,
15
+ * "features":["event_seq","trace_id"]}
16
+ * The CLI negotiates it against its own offer and echoes the agreed level as a
17
+ * `system/negotiated` line, then honors it for the rest of the run.
18
+ *
19
+ * Rules:
20
+ * - agreedVersion = min(server.max, client.max) — never above what either
21
+ * can parse.
22
+ * - Incompatible when agreedVersion < max(server.min, client.min): the two
23
+ * version ranges don't overlap. ok:false — the caller keeps a safe baseline
24
+ * or errors out; it never speaks a version the peer can't read.
25
+ * - effective features = the server-offered features the client also accepts
26
+ * (intersection), minus any whose minimum protocol version is above the
27
+ * agreed version (an N-only field is dropped for an N-1 session).
28
+ * - No client offer at all → legacy client that predates negotiation. The
29
+ * additive fields are defined "consumers MUST tolerate absence AND
30
+ * presence" (docs/PROTOCOL.md §1.2.1), so we keep FULL behavior unchanged
31
+ * (byte-for-byte) and mark clientAware:false. Negotiation only ever
32
+ * RESTRICTS when a client explicitly narrows the set.
33
+ *
34
+ * Pure: no fs / process / clock. The same algorithm is mirrored in the Java
35
+ * twin (jetbrains-plugin CapabilityNegotiation.java); both read the shared
36
+ * fixture __tests__/fixtures/capability-negotiation-cases.json.
37
+ */
38
+
39
+ /** The current protocol version the CLI speaks (mirror of PROTOCOL_VERSION). */
40
+ export const PROTOCOL_VERSION = 1;
41
+
42
+ /**
43
+ * The oldest protocol version the CLI can still speak end-to-end (the N-1 in
44
+ * "N / N-1"). At v1 there is no older line shape, so min === current; once v2
45
+ * ships this drops to 1 so a v1-only client negotiates a v1 session.
46
+ */
47
+ export const PROTOCOL_MIN_VERSION = 1;
48
+
49
+ /**
50
+ * The wire-protocol features subject to negotiation — the additive per-line
51
+ * fields a client may or may not understand. Runtime capabilities (bare,
52
+ * worktree, mcp, …) are NOT negotiated: they change what the CLI can do, not
53
+ * the shape of a line the client must parse.
54
+ */
55
+ export const PROTOCOL_FEATURES = ["event_seq", "tool_use_id", "trace_id"];
56
+
57
+ /**
58
+ * Minimum protocol version a feature requires. Empty today (every current
59
+ * feature is a v1 additive field). When a v2 field lands, add it here so an
60
+ * N-1 (v1) session drops it automatically.
61
+ * @type {Record<string, number>}
62
+ */
63
+ export const FEATURE_MIN_VERSION = {};
64
+
65
+ function intOr(value, fallback) {
66
+ const n = Number(value);
67
+ return Number.isInteger(n) && n > 0 ? n : fallback;
68
+ }
69
+
70
+ /**
71
+ * Coerce a features declaration into a sorted, de-duplicated array of keys.
72
+ * Accepts an array of strings, or an object whose truthy keys are the features
73
+ * (the nested manifest shape). Anything else → [].
74
+ * @returns {string[]}
75
+ */
76
+ export function normalizeFeatureList(input) {
77
+ let keys;
78
+ if (Array.isArray(input)) {
79
+ keys = input.filter((k) => typeof k === "string" && k);
80
+ } else if (input && typeof input === "object") {
81
+ keys = Object.keys(input).filter((k) => input[k]);
82
+ } else {
83
+ return [];
84
+ }
85
+ return [...new Set(keys)].sort();
86
+ }
87
+
88
+ /**
89
+ * Extract the server's negotiation offer from a `cc agent --capabilities`
90
+ * manifest (buildAgentCapabilities output). Only the negotiable wire features
91
+ * the manifest advertises truthy are offered.
92
+ * @param {object} manifest
93
+ * @returns {{protocolVersion:number, minProtocolVersion:number, features:string[]}}
94
+ */
95
+ export function buildServerOffer(manifest = {}) {
96
+ const protocolVersion = intOr(manifest.protocol_version, PROTOCOL_VERSION);
97
+ const minProtocolVersion = intOr(
98
+ manifest.min_protocol_version,
99
+ Math.min(PROTOCOL_MIN_VERSION, protocolVersion),
100
+ );
101
+ const advertised = normalizeFeatureList(manifest.features);
102
+ const features = PROTOCOL_FEATURES.filter((f) => advertised.includes(f));
103
+ return { protocolVersion, minProtocolVersion, features };
104
+ }
105
+
106
+ /**
107
+ * Negotiate the effective protocol version + feature set between a server offer
108
+ * and a client offer.
109
+ *
110
+ * @param {{protocolVersion:number, minProtocolVersion?:number, features:string[]}} server
111
+ * @param {null|{protocolVersion?:number, minProtocolVersion?:number, features?:string[]}} clientOffer
112
+ * @param {object} [opts]
113
+ * @param {Record<string,number>} [opts.featureMinVersion=FEATURE_MIN_VERSION]
114
+ * @returns {{
115
+ * ok:boolean, agreedVersion:(number|null), features:string[],
116
+ * downgraded:boolean, disabledFeatures:string[], clientAware:boolean,
117
+ * reason:(string|null)
118
+ * }}
119
+ */
120
+ export function negotiateProtocol(server = {}, clientOffer = null, opts = {}) {
121
+ const featureMinVersion = opts.featureMinVersion || FEATURE_MIN_VERSION;
122
+ const serverMax = intOr(server.protocolVersion, PROTOCOL_VERSION);
123
+ const serverMin = intOr(server.minProtocolVersion, serverMax);
124
+ const serverFeatures = normalizeFeatureList(server.features);
125
+
126
+ const versionOk = (f, v) => (featureMinVersion[f] || 0) <= v;
127
+
128
+ // No client offer → legacy peer. Keep full behavior (only version-gate the
129
+ // server's own features against its own max), byte-for-byte unchanged.
130
+ if (clientOffer == null) {
131
+ return {
132
+ ok: true,
133
+ agreedVersion: serverMax,
134
+ features: serverFeatures.filter((f) => versionOk(f, serverMax)),
135
+ downgraded: false,
136
+ disabledFeatures: [],
137
+ clientAware: false,
138
+ reason: null,
139
+ };
140
+ }
141
+
142
+ const clientMax = intOr(clientOffer.protocolVersion, serverMax);
143
+ const clientMin = intOr(clientOffer.minProtocolVersion, clientMax);
144
+ const agreedVersion = Math.min(serverMax, clientMax);
145
+ const floor = Math.max(serverMin, clientMin);
146
+
147
+ if (agreedVersion < floor) {
148
+ return {
149
+ ok: false,
150
+ agreedVersion: null,
151
+ features: [],
152
+ downgraded: true,
153
+ disabledFeatures: [...serverFeatures].sort(),
154
+ clientAware: true,
155
+ reason:
156
+ `no common protocol version (server ${serverMin}-${serverMax}, ` +
157
+ `client ${clientMin}-${clientMax})`,
158
+ };
159
+ }
160
+
161
+ // A client that omits `features` accepts whatever the agreed version offers;
162
+ // a client that sends the array narrows to the intersection.
163
+ const clientFeatures =
164
+ clientOffer.features === undefined
165
+ ? null
166
+ : new Set(normalizeFeatureList(clientOffer.features));
167
+
168
+ const enabled = [];
169
+ const disabled = [];
170
+ for (const f of serverFeatures) {
171
+ const okVersion = versionOk(f, agreedVersion);
172
+ const okClient = clientFeatures == null || clientFeatures.has(f);
173
+ if (okVersion && okClient) enabled.push(f);
174
+ else disabled.push(f);
175
+ }
176
+
177
+ return {
178
+ ok: true,
179
+ agreedVersion,
180
+ features: enabled.sort(),
181
+ downgraded: agreedVersion < serverMax || disabled.length > 0,
182
+ disabledFeatures: disabled.sort(),
183
+ clientAware: true,
184
+ reason: null,
185
+ };
186
+ }
187
+
188
+ /** Feature key → the stream line field it gates. */
189
+ const FEATURE_TO_FIELD = {
190
+ event_seq: "seq",
191
+ trace_id: "trace_id",
192
+ tool_use_id: "tool_use_id",
193
+ };
194
+
195
+ /**
196
+ * Fold a negotiation result into a live field-gate the emitter reads per line:
197
+ * a field stays stamped only if its feature survived negotiation. On an
198
+ * incompatible (ok:false) result nothing is changed (the caller keeps its safe
199
+ * baseline). Mutates `gate` in place and returns it.
200
+ *
201
+ * @param {{features:string[], ok:boolean}} result
202
+ * @param {{seq?:boolean, trace_id?:boolean, tool_use_id?:boolean}} gate
203
+ */
204
+ export function applyNegotiationToGate(result, gate = {}) {
205
+ if (!result || result.ok === false) return gate;
206
+ const enabled = new Set(result.features || []);
207
+ for (const feature of PROTOCOL_FEATURES) {
208
+ const field = FEATURE_TO_FIELD[feature];
209
+ if (field) gate[field] = enabled.has(feature);
210
+ }
211
+ return gate;
212
+ }
@@ -213,6 +213,17 @@ const PRINT_COMMANDS = new Set([
213
213
  const SECRET_VAR_RE =
214
214
  /(?:^|_)(KEY|KEYS|TOKEN|SECRET|SECRETS|PASSWORD|PASSWD|PASSPHRASE|CREDENTIAL|CREDENTIALS|PRIVATE|APIKEY|ACCESSKEY)(?:_|$)/i;
215
215
 
216
+ /**
217
+ * True when an environment-variable NAME looks like it holds a secret
218
+ * (ANTHROPIC_API_KEY, GITHUB_TOKEN, DB_PASSWORD, …). Shared single source of
219
+ * truth so the credential READ guard and the credential PROXY
220
+ * ([[credential-proxy.js]] — which keeps secrets out of subprocess envs)
221
+ * classify identically. `MONKEY`/`KEYBOARD`/`TOKENIZER` do NOT match.
222
+ */
223
+ export function isSecretEnvName(name) {
224
+ return typeof name === "string" && SECRET_VAR_RE.test(name);
225
+ }
226
+
216
227
  // Ways a secret var is referenced inside a shell segment.
217
228
  const SECRET_REF_PATTERNS = [
218
229
  /\$env:([A-Za-z_][A-Za-z0-9_]*)/gi, // PowerShell $env:NAME
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Credential proxy — keep the agent's real, long-lived credentials out of the
3
+ * environment that run_shell / run_code / hook / plugin subprocesses inherit
4
+ * (P0 §"跨平台沙箱与凭据代理").
5
+ *
6
+ * By default a spawned command inherits the WHOLE parent environment — including
7
+ * the agent's ANTHROPIC_API_KEY, cloud keys and tokens. A compromised or
8
+ * careless command can then echo, log, or exfiltrate them. This module replaces
9
+ * credential-named vars with an opaque SENTINEL before the child starts and
10
+ * keeps the real values in a parent-held vault, to be injected only for an
11
+ * approved host (via the egress proxy) — never handed to the child wholesale.
12
+ * The audit log only ever sees redacted values, never a restored secret.
13
+ *
14
+ * Pairs with credential-guard.js: that guard stops the AGENT reading secrets
15
+ * into model context; this stops the SUBPROCESS inheriting them. Both classify
16
+ * secret var names through the same `isSecretEnvName` so they never drift.
17
+ *
18
+ * Opt-in for now (`CC_CREDENTIAL_PROXY=1` or `config.credentialProxy.enabled`)
19
+ * so existing workflows that legitimately read a token from env keep working;
20
+ * default-on is the eventual goal once per-host injection is wired everywhere.
21
+ * Pure + dependency-light so every spawn seam can share it.
22
+ */
23
+
24
+ import { isSecretEnvName } from "./credential-guard.js";
25
+
26
+ /** Opaque replacement a masked credential var carries into the child env. */
27
+ export const CREDENTIAL_SENTINEL_PREFIX = "cc-cred-redacted:";
28
+
29
+ /**
30
+ * Well-known credential vars whose NAME does not obviously match the generic
31
+ * KEY/TOKEN/SECRET/PASSWORD pattern. Most real ones already match; this is a
32
+ * small safety net, extendable per-project via `deny`.
33
+ */
34
+ const EXTRA_CREDENTIAL_NAMES = new Set([
35
+ "AWS_SESSION_TOKEN", // matches TOKEN, kept explicit
36
+ "GOOGLE_APPLICATION_CREDENTIALS", // path to a key file
37
+ "CLOUDSDK_AUTH_ACCESS_TOKEN",
38
+ "DIGITALOCEAN_ACCESS_TOKEN",
39
+ ]);
40
+
41
+ function toSet(value) {
42
+ if (value instanceof Set) return value;
43
+ return new Set(Array.isArray(value) ? value : value ? [value] : []);
44
+ }
45
+
46
+ /**
47
+ * Classify an env var NAME as credential-bearing. `opts.allow` forces
48
+ * pass-through (never masked); `opts.deny` forces masking; otherwise the shared
49
+ * secret-name classifier + the curated extra set decide.
50
+ */
51
+ export function isCredentialEnvName(name, opts = {}) {
52
+ if (typeof name !== "string" || !name) return false;
53
+ if (toSet(opts.allow).has(name)) return false;
54
+ if (toSet(opts.deny).has(name)) return true;
55
+ return isSecretEnvName(name) || EXTRA_CREDENTIAL_NAMES.has(name);
56
+ }
57
+
58
+ /** The sentinel a child sees in place of a credential's real value. */
59
+ export function makeSentinel(name) {
60
+ return CREDENTIAL_SENTINEL_PREFIX + String(name ?? "");
61
+ }
62
+
63
+ /** True when a value is a proxy sentinel (carries no secret). */
64
+ export function isSentinel(value) {
65
+ return (
66
+ typeof value === "string" && value.startsWith(CREDENTIAL_SENTINEL_PREFIX)
67
+ );
68
+ }
69
+
70
+ /**
71
+ * Mask credential-named vars in `env`. Returns a NEW object (never mutates the
72
+ * input):
73
+ * - `env` — credential vars replaced with a sentinel (mode "mask", the
74
+ * default) or removed entirely (mode "deny"); everything else
75
+ * copied through verbatim.
76
+ * - `masked` — sorted list of the var names that were masked/removed.
77
+ * - `vault` — Map<name, realValue> the PARENT keeps for approved injection;
78
+ * the child never receives it.
79
+ * Values that are already sentinels, null, or undefined are left as-is (a
80
+ * sentinel is never double-masked; an absent var is not invented).
81
+ */
82
+ export function maskCredentialEnv(env = {}, opts = {}) {
83
+ const mode = opts.mode === "deny" ? "deny" : "mask";
84
+ const out = {};
85
+ const masked = [];
86
+ const vault = new Map();
87
+ for (const [name, value] of Object.entries(env || {})) {
88
+ if (
89
+ value != null &&
90
+ !isSentinel(value) &&
91
+ isCredentialEnvName(name, opts)
92
+ ) {
93
+ masked.push(name);
94
+ vault.set(name, String(value));
95
+ if (mode === "mask") out[name] = makeSentinel(name);
96
+ // mode "deny": drop the var so the child does not even see a sentinel
97
+ } else {
98
+ out[name] = value;
99
+ }
100
+ }
101
+ masked.sort();
102
+ return { env: out, masked, vault };
103
+ }
104
+
105
+ /** Replace a secret value with a fixed marker — never the real value. */
106
+ export function redactSecretValue(value) {
107
+ if (value == null) return value;
108
+ return String(value) ? "***" : "";
109
+ }
110
+
111
+ /**
112
+ * A log-safe projection of an env for the audit trail: credential values →
113
+ * "***", sentinels kept verbatim (they hold no secret), everything else
114
+ * untouched. NEVER emits a restored/real credential value.
115
+ */
116
+ export function redactEnvForAudit(env = {}, opts = {}) {
117
+ const out = {};
118
+ for (const [name, value] of Object.entries(env || {})) {
119
+ if (isSentinel(value)) out[name] = value;
120
+ else if (isCredentialEnvName(name, opts))
121
+ out[name] = redactSecretValue(value);
122
+ else out[name] = value;
123
+ }
124
+ return out;
125
+ }
126
+
127
+ /**
128
+ * Resolve the real value for a masked credential — but ONLY when the target
129
+ * host is on the approved list. The whole point of the proxy is that the child
130
+ * never gets the raw secret; the parent injects it just-in-time for an approved
131
+ * destination (e.g. the egress proxy adding Authorization for an allowed API
132
+ * host). Returns null (fail closed) for an unknown var, an empty host, or a
133
+ * non-approved host.
134
+ */
135
+ export function resolveApprovedInjection(vault, name, opts = {}) {
136
+ if (!(vault instanceof Map) || !vault.has(name)) return null;
137
+ const host = String(opts.host || "").toLowerCase();
138
+ if (!host) return null;
139
+ const approved = (opts.approvedHosts || []).map((h) =>
140
+ String(h || "").toLowerCase(),
141
+ );
142
+ return approved.includes(host) ? vault.get(name) : null;
143
+ }
144
+
145
+ /**
146
+ * Resolve whether the credential proxy is enabled. Env var wins so a spawn
147
+ * seam without config access can still honor it; otherwise the config flag.
148
+ */
149
+ export function credentialProxyEnabled(config = {}, env = process.env) {
150
+ const raw = env && env.CC_CREDENTIAL_PROXY;
151
+ if (raw != null) {
152
+ const v = String(raw).toLowerCase();
153
+ return v === "1" || v === "true" || v === "on" || v === "yes";
154
+ }
155
+ return config?.credentialProxy?.enabled === true;
156
+ }
157
+
158
+ /**
159
+ * Apply the credential proxy to a child env when enabled; otherwise return the
160
+ * env UNCHANGED (same reference — the default path stays byte-identical). The
161
+ * returned `vault` lets the caller inject approved creds later.
162
+ *
163
+ * @returns {{env:object, masked:string[], vault:Map, enabled:boolean}}
164
+ */
165
+ export function applyCredentialProxy(env, options = {}) {
166
+ const procEnv = options.env || process.env;
167
+ const config = options.config || {};
168
+ if (!credentialProxyEnabled(config, procEnv)) {
169
+ return { env, masked: [], vault: new Map(), enabled: false };
170
+ }
171
+ const allow = new Set([
172
+ ...toSet(options.allow),
173
+ ...(config?.credentialProxy?.allow || []),
174
+ ]);
175
+ const deny = new Set([
176
+ ...toSet(options.deny),
177
+ ...(config?.credentialProxy?.deny || []),
178
+ ]);
179
+ const mode = options.mode || config?.credentialProxy?.mode || "mask";
180
+ const result = maskCredentialEnv(env, { allow, deny, mode });
181
+ return { ...result, enabled: true };
182
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Event-stream gap-detection + replay primitives — the server→client (event)
3
+ * counterpart to the client→server RemoteCommandLedger (harness/
4
+ * remote-command-ledger.js). Both give a reconnecting peer an at-most-once,
5
+ * totally-ordered view over a lossy transport; the ledger orders *commands*,
6
+ * this orders *events*.
7
+ *
8
+ * A producer keeps a bounded buffer of recently emitted, seq-stamped frames
9
+ * (EventReplayBuffer); a reconnecting consumer reports its last-seen seq and
10
+ * gets the tail re-sent, or is told to full-resync when the missed range has
11
+ * already been evicted. A consumer detects holes with SeqGapTracker.
12
+ *
13
+ * Pure + deterministic: no I/O, no timers, no globals. The `seq` it stamps is
14
+ * the same additive protocol-v1 notion used on the stream-json surface
15
+ * (agent-sdk docs/PROTOCOL.md §1.2.1) — 1-based, monotonic, per producer
16
+ * instance; consumers MUST tolerate its absence and MUST NOT require gap-free
17
+ * numbering across a producer restart (a fresh instance rewinds to 1).
18
+ */
19
+
20
+ const DEFAULT_MAX_EVENTS = 512;
21
+ const DEFAULT_MAX_BYTES = 1 << 20; // 1 MiB — bound memory on a chatty session
22
+
23
+ /** Best-effort byte size of a frame (overridable for deterministic tests). */
24
+ function approxBytes(frame) {
25
+ try {
26
+ return Buffer.byteLength(JSON.stringify(frame));
27
+ } catch {
28
+ return 0; // circular / unserializable — don't let sizing throw
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Producer-side bounded replay buffer. `record(frame)` stamps the next `seq`
34
+ * onto a shallow copy, retains it (evicting the oldest beyond count/byte
35
+ * bounds), and returns the stamped frame ready to send. `replaySince(cursor)`
36
+ * returns the frames a consumer with last-seen seq `cursor` still needs.
37
+ */
38
+ export class EventReplayBuffer {
39
+ constructor({
40
+ maxEvents = DEFAULT_MAX_EVENTS,
41
+ maxBytes = DEFAULT_MAX_BYTES,
42
+ sizeOf,
43
+ } = {}) {
44
+ this._max = Math.max(1, Math.floor(maxEvents) || DEFAULT_MAX_EVENTS);
45
+ this._maxBytes = Math.max(0, Math.floor(maxBytes) || 0);
46
+ this._sizeOf = typeof sizeOf === "function" ? sizeOf : approxBytes;
47
+ this._buf = []; // [{ seq, frame, bytes }] oldest → newest
48
+ this._seq = 0;
49
+ this._bytes = 0;
50
+ }
51
+
52
+ /** Highest seq stamped so far (0 before anything is recorded). */
53
+ get latestSeq() {
54
+ return this._seq;
55
+ }
56
+
57
+ /** Oldest seq still retained (== latestSeq when the buffer is empty). */
58
+ get oldestSeq() {
59
+ return this._buf.length ? this._buf[0].seq : this._seq;
60
+ }
61
+
62
+ /** Number of frames currently retained. */
63
+ get size() {
64
+ return this._buf.length;
65
+ }
66
+
67
+ /**
68
+ * Stamp the next seq onto `frame` (shallow copy), retain it, evict overflow.
69
+ * @returns the stamped frame (`{ ...frame, seq }`) to hand straight to send.
70
+ */
71
+ record(frame) {
72
+ const seq = ++this._seq;
73
+ const stamped = { ...frame, seq };
74
+ const bytes = this._sizeOf(stamped) || 0;
75
+ this._buf.push({ seq, frame: stamped, bytes });
76
+ this._bytes += bytes;
77
+ this._evict();
78
+ return stamped;
79
+ }
80
+
81
+ _evict() {
82
+ // Always keep at least the newest frame even if it alone exceeds maxBytes.
83
+ while (
84
+ this._buf.length > this._max ||
85
+ (this._maxBytes > 0 &&
86
+ this._bytes > this._maxBytes &&
87
+ this._buf.length > 1)
88
+ ) {
89
+ const dropped = this._buf.shift();
90
+ this._bytes -= dropped.bytes;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Frames a consumer whose last-seen seq is `cursor` still needs.
96
+ * - `cursor >= latestSeq` → nothing missed → `{ frames: [], truncated: false }`.
97
+ * - `cursor` still within the retained window → the exact tail after it.
98
+ * - `cursor` older than what's retained → the gap was evicted →
99
+ * `truncated: true` (consumer MUST full-resync; the returned frames are
100
+ * only the retained suffix, not the whole hole).
101
+ * @param {number} cursor last seq the consumer acknowledged (default 0 = all)
102
+ */
103
+ replaySince(cursor = 0) {
104
+ const c = Number.isFinite(cursor) ? cursor : 0;
105
+ if (c >= this._seq) {
106
+ return { frames: [], truncated: false, from: c, to: this._seq };
107
+ }
108
+ // The lowest cursor we can honor without a hole is (oldest retained − 1).
109
+ const floor = this._buf.length ? this._buf[0].seq - 1 : this._seq;
110
+ const truncated = c < floor;
111
+ const frames = this._buf.filter((e) => e.seq > c).map((e) => e.frame);
112
+ return { frames, truncated, from: c, to: this._seq };
113
+ }
114
+ }
115
+
116
+ const OK = Object.freeze({ status: "ok" });
117
+
118
+ /**
119
+ * Consumer-side sequence tracker. Feed it each received frame's `seq`; it
120
+ * reports contiguity so the consumer can request a replay on a hole and drop
121
+ * duplicates a reconnect re-sent. One tracker per producer epoch — reset it
122
+ * when the transport hands you a fresh producer instance (e.g. a non-reattach
123
+ * bg-attach reply), since a fresh producer rewinds seq to 1.
124
+ */
125
+ export class SeqGapTracker {
126
+ constructor() {
127
+ this._last = 0;
128
+ this._started = false;
129
+ }
130
+
131
+ /** Last contiguous seq accepted (0 before the first frame). */
132
+ get lastSeq() {
133
+ return this._last;
134
+ }
135
+
136
+ /**
137
+ * @param {number} seq the received frame's seq (absent/NaN on older
138
+ * producers that don't stamp — treated as always-ok, never a gap).
139
+ * @returns {{status:"ok"}|{status:"duplicate"}|{status:"gap",from:number,to:number}}
140
+ * - `ok`: contiguous (or the first frame, or an unstamped frame).
141
+ * - `duplicate`: `seq <= lastSeq` — already seen; drop it.
142
+ * - `gap`: `seq > lastSeq + 1` — frames `(from, to)` were missed; ask the
143
+ * producer for `replaySince(from)`. The tracker adopts `to` as current.
144
+ */
145
+ observe(seq) {
146
+ if (!Number.isFinite(seq)) return OK; // unstamped producer — tolerate
147
+ if (!this._started) {
148
+ this._started = true;
149
+ this._last = seq;
150
+ return OK;
151
+ }
152
+ if (seq === this._last + 1) {
153
+ this._last = seq;
154
+ return OK;
155
+ }
156
+ if (seq <= this._last) return { status: "duplicate" };
157
+ const from = this._last;
158
+ this._last = seq; // adopt new position; caller replays the (from, to) hole
159
+ return { status: "gap", from, to: seq };
160
+ }
161
+
162
+ /** Forget all history (call when attaching to a fresh producer instance). */
163
+ reset() {
164
+ this._last = 0;
165
+ this._started = false;
166
+ }
167
+ }