chainlesschain 0.166.4 → 0.166.6

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 (167) hide show
  1. package/README.md +32 -0
  2. package/package.json +2 -1
  3. package/src/assets/web-panel/assets/{AIOps-HcOh0ZbH.js → AIOps-CVnFjTfG.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-DWjDqBhd.js → ActionButton-BxkwuuQC.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-C6FS-y3W.js → Analytics-C2670q6z.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-DCN5cldF.js → AppLayout-CU_rylW6.js} +4 -4
  7. package/src/assets/web-panel/assets/{Artifacts-Crd-KE8P.js → Artifacts-DGJmYqnj.js} +1 -1
  8. package/src/assets/web-panel/assets/{Audit-BexuTQZc.js → Audit-BZOQbINJ.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-Dydkh-b5.js → BackgroundAgents-DghB4QwF.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-BYq21wMP.js → Backup-l0mwPbta.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-CmNZP6yM.js → BaseInput-Bv3WSZOh.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-J6bD0zCl.js → Chat-s88nBdvt.js} +6 -6
  13. package/src/assets/web-panel/assets/{ChatBubbleRenderer-MEoaygnL.js → ChatBubbleRenderer-Z8CBuB1a.js} +1 -1
  14. package/src/assets/web-panel/assets/{Checkbox-Ce-9vNmK.js → Checkbox-B5icasEs.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-zf7-Y-Eh.js → Codegen-CsUFqNud.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col-D-TkY0FD.js → Col-DlBtClaW.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-l300NA-1.js → Community-xRn1IdLB.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-D-9Q_hxc.js → Compact-naIr8a_o.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-DAJ8lLiQ.js → Compliance-Dp9esfFE.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-xmzZ967S.js → Cowork-DQlMYJB-.js} +4 -4
  21. package/src/assets/web-panel/assets/{Cron-CeGqO5W4.js → Cron-Cd0oNLzJ.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-DXN0AE-4.js → Crosschain-BtDIx3XX.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-CtIKASPr.js → DID-BOlpzEwg.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-DqRwT8NX.js → Dashboard-DV15Axk1.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-ddIVljO1.js → Dropdown-COXrewN1.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-CcsFzV4y.js → EmailListRenderer-EjIt0Cm7.js} +1 -1
  27. package/src/assets/web-panel/assets/{FamilyGuardDashboard-PnlAUGcG.js → FamilyGuardDashboard-DJlhwb-R.js} +1 -1
  28. package/src/assets/web-panel/assets/{Federation-BlmLuQrL.js → Federation-BMiFl82k.js} +1 -1
  29. package/src/assets/web-panel/assets/{FormItemContext-dDovBj1Y.js → FormItemContext-BUcDQyxn.js} +1 -1
  30. package/src/assets/web-panel/assets/{GenericCardRenderer-DTvGshFr.js → GenericCardRenderer-C25uFxvK.js} +1 -1
  31. package/src/assets/web-panel/assets/{Git-DuFR2xgA.js → Git-D20uaEUU.js} +2 -2
  32. package/src/assets/web-panel/assets/{Governance-9EaNcJuq.js → Governance-CQxYJjCX.js} +1 -1
  33. package/src/assets/web-panel/assets/{Inference-B-iYWH_r.js → Inference-m-sczpS5.js} +1 -1
  34. package/src/assets/web-panel/assets/{KnowledgeGraph-BGBaKiLX.js → KnowledgeGraph-CBsIlU0S.js} +1 -1
  35. package/src/assets/web-panel/assets/{Logs-G3wmXzfa.js → Logs-D0-5bqeu.js} +2 -2
  36. package/src/assets/web-panel/assets/{MarkdownRenderer-hk0YwjTR.js → MarkdownRenderer-BwH414Ms.js} +1 -1
  37. package/src/assets/web-panel/assets/{Marketplace-BNARBeAv.js → Marketplace-CwDGFu2Q.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-DST2cSSK.js → McpTools-7FxJ3omT.js} +3 -3
  39. package/src/assets/web-panel/assets/{Memory-piwDLsVH.js → Memory-DOVyb59U.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-D-W8zHdm.js → MobileBridge-DZXl-Hje.js} +1 -1
  41. package/src/assets/web-panel/assets/{MobileProjects-CwaYHlVP.js → MobileProjects-DlZ8tRXt.js} +1 -1
  42. package/src/assets/web-panel/assets/{Mtc-C2dxnN7Q.js → Mtc-D7Pjx1s_.js} +5 -5
  43. package/src/assets/web-panel/assets/{MtcAudit-BwPZsV3N.js → MtcAudit-BND7lX8p.js} +2 -2
  44. package/src/assets/web-panel/assets/{Multisig-CUt8w2al.js → Multisig-BknS7B-p.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-DwxErx27.js → NLProgramming-7C2-KPHt.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-B-mXAZ84.js → Notes-DNWdTndN.js} +4 -4
  47. package/src/assets/web-panel/assets/{NotificationSettings-Bd1fBAzd.js → NotificationSettings-DCm4KcJZ.js} +1 -1
  48. package/src/assets/web-panel/assets/{OrderTableRenderer-CCInnavw.js → OrderTableRenderer-DyVwwjJx.js} +1 -1
  49. package/src/assets/web-panel/assets/{Organization-DTTONYZT.js → Organization-Dl2O_qJ9.js} +4 -4
  50. package/src/assets/web-panel/assets/{Overflow-H6lCZ1rE.js → Overflow-BOFCvk9Q.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-DyBZzeYW.js → P2P-DgdVowY1.js} +2 -2
  52. package/src/assets/web-panel/assets/PdhVaultBrowser-BiaZfQIM.js +7 -0
  53. package/src/assets/web-panel/assets/{Permissions-B5nSQwsr.js → Permissions-BHNEUUwa.js} +4 -4
  54. package/src/assets/web-panel/assets/{PersonalDataHub-CACaQtlY.js → PersonalDataHub-CaZbrOdz.js} +4 -4
  55. package/src/assets/web-panel/assets/{Pipeline-5y-DR_Bw.js → Pipeline-DJDnoXmE.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-BMSA3Ehd.js → Privacy-CBQr2eX8.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-VCOj1dTa.js → ProjectInit-Yghqa06z.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-CJaXO_iE.js → ProjectSettings-HBAtJi8U.js} +2 -2
  59. package/src/assets/web-panel/assets/{Projects-C21-MQ3H.js → Projects-DiCGnMjz.js} +1 -1
  60. package/src/assets/web-panel/assets/{Providers-DZ1FNKUR.js → Providers-uJTz77GI.js} +1 -1
  61. package/src/assets/web-panel/assets/{QrScannerModal-6DKc9UBX.js → QrScannerModal-k3M5ouTp.js} +1 -1
  62. package/src/assets/web-panel/assets/{QuickAsk-Dyd0Ckrk.js → QuickAsk-CMji45PH.js} +1 -1
  63. package/src/assets/web-panel/assets/{Recommend-CYW4-GJH.js → Recommend-DyoZYPls.js} +1 -1
  64. package/src/assets/web-panel/assets/{RemoteSession-DLrfPy4F.js → RemoteSession-s1gR38IO.js} +3 -3
  65. package/src/assets/web-panel/assets/{Reputation-DfqPo1FI.js → Reputation-D4hIs4vV.js} +1 -1
  66. package/src/assets/web-panel/assets/{Row-CR89H2x5.js → Row-mzE-ElTg.js} +1 -1
  67. package/src/assets/web-panel/assets/{RssFeed-B9QY7R-F.js → RssFeed-cg7J3hI6.js} +3 -3
  68. package/src/assets/web-panel/assets/{Search-CUDnzu4w.js → Search-6M0TkGSZ.js} +1 -1
  69. package/src/assets/web-panel/assets/{Security-CJ0CtM_Z.js → Security-B1GQ67qh.js} +3 -3
  70. package/src/assets/web-panel/assets/{Services-CyrQ6F5U.js → Services-D_RwU_5S.js} +2 -2
  71. package/src/assets/web-panel/assets/{Skeleton-DcwnKatQ.js → Skeleton-TZjk-dsW.js} +1 -1
  72. package/src/assets/web-panel/assets/{Skills-BhKkYF3_.js → Skills-CG6FcaCV.js} +1 -1
  73. package/src/assets/web-panel/assets/{Sla-CWh-awh7.js → Sla-BI190QDv.js} +1 -1
  74. package/src/assets/web-panel/assets/{SpeechSettings-BaUMNzx7.js → SpeechSettings-BWmxF2Wu.js} +1 -1
  75. package/src/assets/web-panel/assets/{SyncSettings-D1Ijwwxl.js → SyncSettings-CwPiGw6L.js} +2 -2
  76. package/src/assets/web-panel/assets/Tasks-BIXgCeSk.js +1 -0
  77. package/src/assets/web-panel/assets/{Templates-Ct-9_ki3.js → Templates-CQFgGCoK.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tenant-zBkgyilh.js → Tenant-DAelEdkL.js} +1 -1
  79. package/src/assets/web-panel/assets/{Terminal-VVO2Hjfi.js → Terminal-BbqWK6qh.js} +2 -2
  80. package/src/assets/web-panel/assets/{TimelineRenderer-CX9pIkx3.js → TimelineRenderer-cmG3zvY5.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tokens-DHaD6e_m.js → Tokens-C8444NKU.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trigger-CFq9j3WV.js → Trigger-C1Rpnz0I.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trust-DuF7gIYX.js → Trust-u4koPiFD.js} +1 -1
  84. package/src/assets/web-panel/assets/{UkeySign-DbuidXJX.js → UkeySign-oPWKn80Z.js} +1 -1
  85. package/src/assets/web-panel/assets/{VideoEditing-B8IA9_qE.js → VideoEditing-D0CUUY_5.js} +1 -1
  86. package/src/assets/web-panel/assets/{Wallet-DdCglSIK.js → Wallet-fu9pIAbt.js} +4 -4
  87. package/src/assets/web-panel/assets/{WebAuthn-CeteRsA8.js → WebAuthn-Bc5ciuOx.js} +5 -5
  88. package/src/assets/web-panel/assets/{WorkflowEditor-DJhIbG__.js → WorkflowEditor-BCin4_4N.js} +1 -1
  89. package/src/assets/web-panel/assets/{chat-srOi_Wef.js → chat-B2jbM-XN.js} +1 -1
  90. package/src/assets/web-panel/assets/{colors-HpVHsNGK.js → colors-CUVoUQ_2.js} +1 -1
  91. package/src/assets/web-panel/assets/{compact-item-DsLhrqWe.js → compact-item-DgOGpkRH.js} +1 -1
  92. package/src/assets/web-panel/assets/{createContext-aOasIUuE.js → createContext-DIxU6eKI.js} +1 -1
  93. package/src/assets/web-panel/assets/devWarning-DiSlgCC4.js +1 -0
  94. package/src/assets/web-panel/assets/{hasIn-D5BVMimS.js → hasIn-DoKRKMav.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-DQ5j1VKg.js → index-2flsPFIX.js} +1 -1
  96. package/src/assets/web-panel/assets/index-9WIQEHvv.js +1 -0
  97. package/src/assets/web-panel/assets/{index-D1xZMQrN.js → index-B-f6l4Vm.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-q--LtRh9.js → index-B3F_p8NA.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-CQEoGqMG.js → index-B5ySiGYy.js} +1 -1
  100. package/src/assets/web-panel/assets/index-B7mDobAl.js +1 -0
  101. package/src/assets/web-panel/assets/{index-DiXsis7m.js → index-B9mSYovv.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-CUUs12L9.js → index-BCLeNqIL.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-Dpj-rEoV.js → index-BFUprhNd.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-b0Y0hDhX.js → index-BRMEO2Tl.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-CQXp_uNd.js → index-BXb-Smmm.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-P6IMLmLT.js → index-BtSPhEyC.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-Bu6TVdgQ.js → index-BwYofQJg.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-CGVHW0nF.js → index-ByjrulIZ.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-V0gpVTbS.js → index-C846lPGD.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-BGBvkNN3.js → index-C88m9lsi.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-dPFzlII1.js → index-C9TsO9T9.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-Dum_r_oY.js → index-CF80xxPG.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CVp_dHCs.js → index-CLdpHe6w.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-LSz0ZOB8.js → index-CPXdBnGi.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-BNbsjeEc.js → index-CWbL1-N0.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CSxJ4oRd.js → index-CeWSw-z5.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-fZnlPk7H.js → index-Cj-us-D_.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CvhVCHk-.js → index-Co4lDEk-.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DcLBzIiI.js → index-CpvyOt5Z.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-C4tPtHU0.js → index-CqlwIjJx.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-ChGUPEwS.js → index-Cskz--fd.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-BD64r0Tx.js → index-D-ufbAJB.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-BTwDhhvl.js → index-D2yo6ast.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-lFOfvZDn.js → index-DGTZdZOE.js} +3 -3
  125. package/src/assets/web-panel/assets/{index-vQ6jy8Ew.js → index-DHugFl3v.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-DV4y5rUj.js → index-DJMHnbmu.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-D2XRXkZU.js → index-DpTFx8Xx.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-BCp70IlO.js → index-Ds8r3Fzb.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-Ow3SRXRX.js → index-DtyAuGOm.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-B615KD2o.js → index-be8SonCh.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-DRCIPsdm.js → index-eTWPaeBI.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-kYqi6zYp.js → index-eYxUASSG.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-BigtYchO.js → index-i5MB455u.js} +1 -1
  134. package/src/assets/web-panel/assets/{initDefaultProps-1NU9yNpr.js → initDefaultProps-B4chl7iR.js} +1 -1
  135. package/src/assets/web-panel/assets/{motion-BZk2hQ6c.js → motion-DruN9h96.js} +1 -1
  136. package/src/assets/web-panel/assets/{move-BNvublME.js → move-Cl0UtCgU.js} +1 -1
  137. package/src/assets/web-panel/assets/{mtc-parser-DWk5BUoL.js → mtc-parser-FX2ttw70.js} +1 -1
  138. package/src/assets/web-panel/assets/{omit-DztYxX3x.js → omit-BDCneY46.js} +1 -1
  139. package/src/assets/web-panel/assets/{pickAttrs-DURL8mtg.js → pickAttrs-DycsEg8b.js} +1 -1
  140. package/src/assets/web-panel/assets/{placementArrow-B820R56z.js → placementArrow-Bc-dBuMv.js} +1 -1
  141. package/src/assets/web-panel/assets/{responsiveObserve-DUAkGJL1.js → responsiveObserve--Wwaf8lu.js} +1 -1
  142. package/src/assets/web-panel/assets/{slide-uZQeNSo6.js → slide-vDmHQqHH.js} +1 -1
  143. package/src/assets/web-panel/assets/{statusUtils-CGR1wFVq.js → statusUtils-9c52yv3G.js} +1 -1
  144. package/src/assets/web-panel/assets/{styleChecker-BIIPOMsQ.js → styleChecker-BT4BiO6_.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFlexGapSupport-rIB9c0g2.js → useFlexGapSupport-C7XxWPcP.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFs-CrJe1BJi.js → useFs-BHoMqh4i.js} +1 -1
  147. package/src/assets/web-panel/assets/{usePersonalDataHub-DyEoD3Qm.js → usePersonalDataHub-BOLxvsAG.js} +1 -1
  148. package/src/assets/web-panel/assets/{vnode-CjohPxI0.js → vnode-CA7poeeP.js} +1 -1
  149. package/src/assets/web-panel/assets/{zoom-C5POL1_6.js → zoom-B_sAn-co.js} +1 -1
  150. package/src/assets/web-panel/index.html +1 -1
  151. package/src/command-help-index.json +1 -1
  152. package/src/commands/serve.js +62 -9
  153. package/src/data/changelog.json +26 -0
  154. package/src/gateways/ws/ws-server.js +392 -33
  155. package/src/generated/cc-agent-protocol.schema.json +1442 -1
  156. package/src/lib/agent-ipc-bus.js +453 -91
  157. package/src/lib/agent-ipc-flow-control.js +200 -0
  158. package/src/lib/app-server/server.js +3 -1
  159. package/src/lib/app-server/websocket-transport.js +546 -0
  160. package/src/lib/public-ide-capability-manifest.js +4 -0
  161. package/src/runtime/__tests__/coding-agent-events.test.js +36 -0
  162. package/src/runtime/coding-agent-events.cjs +82 -0
  163. package/src/assets/web-panel/assets/PdhVaultBrowser-jj7LhT04.js +0 -7
  164. package/src/assets/web-panel/assets/Tasks-DFqjHi3T.js +0 -1
  165. package/src/assets/web-panel/assets/devWarning-B_acr36n.js +0 -1
  166. package/src/assets/web-panel/assets/index-BnLYpdox.js +0 -1
  167. package/src/assets/web-panel/assets/index-Dzmwsl_v.js +0 -1
@@ -14,6 +14,15 @@ import crypto from "node:crypto";
14
14
  import { EventRuntimeProducer } from "./event-runtime-producer.js";
15
15
  import { EventRuntimeStore } from "./event-runtime-store.js";
16
16
  import executionBroker from "./process-execution-broker/index.js";
17
+ import {
18
+ AGENT_IPC_DEFAULT_LIMITS,
19
+ boundedTimeout,
20
+ createBoundedStdinWriter,
21
+ normalizeAgentIPCLimits,
22
+ overloadError,
23
+ } from "./agent-ipc-flow-control.js";
24
+
25
+ export { AGENT_IPC_DEFAULT_LIMITS };
17
26
 
18
27
  export const _deps = {
19
28
  spawn: executionBroker.spawn.bind(executionBroker),
@@ -34,15 +43,20 @@ export const _deps = {
34
43
  */
35
44
 
36
45
  class AgentIPCBus extends EventEmitter {
37
- constructor({ runtimeStore = null } = {}) {
46
+ constructor({ runtimeStore = null, ...limits } = {}) {
38
47
  super();
48
+ this.limits = normalizeAgentIPCLimits(limits);
39
49
  this._runtimeProducer = runtimeStore
40
50
  ? new EventRuntimeProducer({ store: runtimeStore, emitter: this })
41
51
  : null;
42
52
  /** @type {Map<string, {req: InteractionRequest, resolve: Function, reject: Function, timer?: NodeJS.Timeout}>} */
43
53
  this._pendingRequests = new Map();
54
+ /** @type {Map<string, {agentId: string, method: string, resolve: Function, reject: Function, timer: NodeJS.Timeout}>} */
55
+ this._pendingAgentRequests = new Map();
44
56
  /** @type {Map<string, Function>} resolvers by agentId */
45
57
  this._agentResolvers = new Map();
58
+ /** @type {Map<string, {status: Function, close: Function}>} */
59
+ this._childTransports = new Map();
46
60
  }
47
61
 
48
62
  /**
@@ -51,21 +65,54 @@ class AgentIPCBus extends EventEmitter {
51
65
  * @param {(msg: any) => void} sendToWorker
52
66
  */
53
67
  registerAgent(agentId, sendToWorker) {
68
+ if (
69
+ !this._agentResolvers.has(agentId) &&
70
+ !this._childTransports.has(agentId) &&
71
+ this._activeAgentCount() >= this.limits.maxAgents
72
+ ) {
73
+ throw overloadError(
74
+ "registered_agents",
75
+ this.limits.maxAgents,
76
+ this.limits.overloadRetryAfterMs,
77
+ );
78
+ }
54
79
  this._agentResolvers.set(agentId, sendToWorker);
55
80
  this.emit("agent:registered", { agentId });
56
81
  }
57
82
 
58
83
  unregisterAgent(agentId) {
84
+ const hadAgent = this._agentResolvers.has(agentId);
85
+ const hadTransport = this._childTransports.has(agentId);
86
+ let rejectedPending = false;
59
87
  this._agentResolvers.delete(agentId);
60
88
  // Reject all pending for this agent
61
89
  for (const [reqId, entry] of this._pendingRequests.entries()) {
62
90
  if (entry.req.agentId === agentId) {
63
91
  if (entry.timer) clearTimeout(entry.timer);
64
- entry.reject(new Error(`Agent ${agentId} disconnected while waiting for response`));
92
+ entry.reject(
93
+ new Error(`Agent ${agentId} disconnected while waiting for response`),
94
+ );
65
95
  this._pendingRequests.delete(reqId);
96
+ rejectedPending = true;
97
+ }
98
+ }
99
+ for (const [requestId, entry] of this._pendingAgentRequests.entries()) {
100
+ if (entry.agentId === agentId) {
101
+ clearTimeout(entry.timer);
102
+ entry.reject(
103
+ new Error(
104
+ `Agent ${agentId} disconnected while handling ${entry.method}`,
105
+ ),
106
+ );
107
+ this._pendingAgentRequests.delete(requestId);
108
+ rejectedPending = true;
66
109
  }
67
110
  }
68
- this.emit("agent:unregistered", { agentId });
111
+ this._childTransports.get(agentId)?.close();
112
+ this._childTransports.delete(agentId);
113
+ if (hadAgent || hadTransport || rejectedPending) {
114
+ this.emit("agent:unregistered", { agentId });
115
+ }
69
116
  }
70
117
 
71
118
  isAgentRegistered(agentId) {
@@ -78,8 +125,41 @@ class AgentIPCBus extends EventEmitter {
78
125
  * @param {Partial<InteractionRequest>} req
79
126
  * @returns {Promise<any>}
80
127
  */
81
- requestInteraction(agentId, req) {
128
+ requestInteraction(agentId, req = {}) {
129
+ if (this._pendingRequests.size >= this.limits.maxPendingInteractions) {
130
+ return Promise.reject(
131
+ overloadError(
132
+ "pending_interactions",
133
+ this.limits.maxPendingInteractions,
134
+ this.limits.overloadRetryAfterMs,
135
+ ),
136
+ );
137
+ }
138
+ let pendingForAgent = 0;
139
+ for (const entry of this._pendingRequests.values()) {
140
+ if (entry.req.agentId === agentId) pendingForAgent += 1;
141
+ }
142
+ if (pendingForAgent >= this.limits.maxPendingInteractionsPerAgent) {
143
+ return Promise.reject(
144
+ overloadError(
145
+ "pending_interactions_per_agent",
146
+ this.limits.maxPendingInteractionsPerAgent,
147
+ this.limits.overloadRetryAfterMs,
148
+ ),
149
+ );
150
+ }
151
+
82
152
  const requestId = crypto.randomUUID();
153
+ let timeoutMs;
154
+ try {
155
+ timeoutMs = boundedTimeout(
156
+ req.timeoutMs,
157
+ this.limits.interactionTimeoutMs,
158
+ this.limits.maxInteractionTimeoutMs,
159
+ );
160
+ } catch (error) {
161
+ return Promise.reject(error);
162
+ }
83
163
  const fullReq = {
84
164
  requestId,
85
165
  agentId,
@@ -89,7 +169,7 @@ class AgentIPCBus extends EventEmitter {
89
169
  prompt: req.prompt,
90
170
  choices: req.choices,
91
171
  defaultValue: req.defaultValue,
92
- timeoutMs: req.timeoutMs || 300000, // 5min default
172
+ timeoutMs,
93
173
  metadata: req.metadata || {},
94
174
  };
95
175
 
@@ -99,12 +179,21 @@ class AgentIPCBus extends EventEmitter {
99
179
  if (fullReq.defaultValue !== undefined) {
100
180
  resolve(fullReq.defaultValue);
101
181
  } else {
102
- reject(new Error(`Interaction request ${requestId} timed out after ${fullReq.timeoutMs}ms`));
182
+ reject(
183
+ new Error(
184
+ `Interaction request ${requestId} timed out after ${fullReq.timeoutMs}ms`,
185
+ ),
186
+ );
103
187
  }
104
188
  this.emit("request:timeout", fullReq);
105
189
  }, fullReq.timeoutMs);
106
190
 
107
- this._pendingRequests.set(requestId, { req: fullReq, resolve, reject, timer });
191
+ this._pendingRequests.set(requestId, {
192
+ req: fullReq,
193
+ resolve,
194
+ reject,
195
+ timer,
196
+ });
108
197
  try {
109
198
  this._runtimeProducer?.publish(
110
199
  { type: "interaction_request", request: fullReq },
@@ -158,7 +247,7 @@ class AgentIPCBus extends EventEmitter {
158
247
 
159
248
  /** Get pending interaction requests for UI */
160
249
  getPendingRequests() {
161
- return Array.from(this._pendingRequests.values()).map(e => e.req);
250
+ return Array.from(this._pendingRequests.values()).map((e) => e.req);
162
251
  }
163
252
 
164
253
  /** Number of pending requests */
@@ -166,6 +255,26 @@ class AgentIPCBus extends EventEmitter {
166
255
  return this._pendingRequests.size;
167
256
  }
168
257
 
258
+ _activeAgentCount() {
259
+ return new Set([
260
+ ...this._agentResolvers.keys(),
261
+ ...this._childTransports.keys(),
262
+ ]).size;
263
+ }
264
+
265
+ flowControlStatus() {
266
+ return {
267
+ limits: { ...this.limits },
268
+ activeAgents: this._activeAgentCount(),
269
+ registeredAgents: this._agentResolvers.size,
270
+ pendingInteractions: this._pendingRequests.size,
271
+ pendingAgentRequests: this._pendingAgentRequests.size,
272
+ childTransports: Array.from(this._childTransports.entries()).map(
273
+ ([agentId, transport]) => ({ agentId, ...transport.status() }),
274
+ ),
275
+ };
276
+ }
277
+
169
278
  /**
170
279
  * Spawn a child agent process with standardized JSON-RPC over stdio protocol
171
280
  * Aligned with Claude Code sub-agent spawn specification
@@ -176,7 +285,29 @@ class AgentIPCBus extends EventEmitter {
176
285
  */
177
286
  async spawnAgentProcess(command, args = [], options = {}) {
178
287
  const agentId = options.agentId || crypto.randomUUID();
179
- const heartbeatMs = options.heartbeatMs || 30000;
288
+ if (
289
+ this._childTransports.has(agentId) ||
290
+ this._agentResolvers.has(agentId)
291
+ ) {
292
+ throw new Error(`Agent ${agentId} is already registered or starting`);
293
+ }
294
+ if (this._activeAgentCount() >= this.limits.maxAgents) {
295
+ throw overloadError(
296
+ "active_agents",
297
+ this.limits.maxAgents,
298
+ this.limits.overloadRetryAfterMs,
299
+ );
300
+ }
301
+ const heartbeatMs = boundedTimeout(
302
+ options.heartbeatMs,
303
+ this.limits.agentHeartbeatMs,
304
+ this.limits.maxAgentHeartbeatMs,
305
+ );
306
+ const initTimeoutMs = boundedTimeout(
307
+ options.initTimeoutMs,
308
+ this.limits.agentInitTimeoutMs,
309
+ this.limits.maxAgentInitTimeoutMs,
310
+ );
180
311
 
181
312
  return new Promise((resolve, reject) => {
182
313
  const spawnOptions = options.spawnOptions || {};
@@ -196,79 +327,224 @@ class AgentIPCBus extends EventEmitter {
196
327
  shell: spawnOptions.shell === true,
197
328
  });
198
329
 
199
- let buffer = "";
330
+ let buffer = Buffer.alloc(0);
200
331
  let initialized = false;
201
332
  let heartbeatTimer = null;
333
+ let initTimer = null;
334
+ let protocolFailed = false;
335
+ let writer;
336
+ let stderrDrainListener = null;
337
+
338
+ const clearStderrDrainListener = () => {
339
+ if (!stderrDrainListener) return;
340
+ process.stderr.off("drain", stderrDrainListener);
341
+ stderrDrainListener = null;
342
+ };
343
+
344
+ const terminateForProtocolError = (error, details = {}) => {
345
+ if (protocolFailed) return;
346
+ protocolFailed = true;
347
+ buffer = Buffer.alloc(0);
348
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
349
+ if (initTimer) clearTimeout(initTimer);
350
+ clearStderrDrainListener();
351
+ writer?.close();
352
+ this._childTransports.delete(agentId);
353
+ if (this.isAgentRegistered(agentId)) this.unregisterAgent(agentId);
354
+ this.emit("protocol:error", { agentId, error, ...details });
355
+ if (!initialized) reject(error);
356
+ child.kill();
357
+ };
358
+
359
+ writer = createBoundedStdinWriter({
360
+ child,
361
+ agentId,
362
+ limits: this.limits,
363
+ onOverload: (error) => terminateForProtocolError(error),
364
+ });
365
+ this._childTransports.set(agentId, writer);
366
+
367
+ const handleLine = (lineBuffer) => {
368
+ if (protocolFailed || lineBuffer.length === 0) return;
369
+ const line = lineBuffer.toString("utf8");
370
+ if (!line.trim()) return;
371
+ try {
372
+ const msg = JSON.parse(line);
373
+ this._handleIncomingMessage(agentId, msg, child, writer);
374
+ if (
375
+ msg.jsonrpc === "2.0" &&
376
+ msg.method === "initialize" &&
377
+ Object.prototype.hasOwnProperty.call(msg, "id") &&
378
+ !initialized
379
+ ) {
380
+ this.registerAgent(agentId, (outMsg) => writer.write(outMsg));
381
+ writer.write({
382
+ jsonrpc: "2.0",
383
+ id: msg.id,
384
+ result: {
385
+ protocolVersion: "1.0",
386
+ agentId,
387
+ capabilities: { interaction: true, tools: true },
388
+ },
389
+ });
390
+ initialized = true;
391
+ if (initTimer) clearTimeout(initTimer);
392
+ heartbeatTimer = setInterval(() => {
393
+ if (child.exitCode !== null) {
394
+ clearInterval(heartbeatTimer);
395
+ return;
396
+ }
397
+ try {
398
+ writer.write({ jsonrpc: "2.0", method: "heartbeat" });
399
+ } catch {
400
+ clearInterval(heartbeatTimer);
401
+ }
402
+ }, heartbeatMs);
403
+ resolve({ process: child, agentId });
404
+ }
405
+ } catch (error) {
406
+ if (error?.code === "OVERLOADED") {
407
+ terminateForProtocolError(error);
408
+ return;
409
+ }
410
+ this.emit("protocol:error", {
411
+ agentId,
412
+ error,
413
+ raw: line.slice(0, 4096),
414
+ });
415
+ }
416
+ };
202
417
 
203
418
  // Handle stdout: line-delimited JSON-RPC
204
- child.stdout.on("data", (chunk) => {
205
- buffer += chunk.toString("utf8");
206
- const lines = buffer.split("\n");
207
- buffer = lines.pop() || ""; // Keep incomplete line in buffer
208
-
209
- for (const line of lines) {
210
- if (!line.trim()) continue;
211
- try {
212
- const msg = JSON.parse(line);
213
- this._handleIncomingMessage(agentId, msg, child);
214
- if (msg.method === "initialize" && !initialized) {
215
- initialized = true;
216
- this.registerAgent(agentId, (outMsg) => {
217
- child.stdin.write(JSON.stringify(outMsg) + "\n");
218
- });
219
- // Send initialize response
220
- child.stdin.write(JSON.stringify({
221
- jsonrpc: "2.0",
222
- id: msg.id,
223
- result: {
224
- protocolVersion: "1.0",
225
- agentId,
226
- capabilities: { interaction: true, tools: true },
227
- },
228
- }) + "\n");
229
- // Setup heartbeat
230
- heartbeatTimer = setInterval(() => {
231
- if (child.exitCode !== null) {
232
- clearInterval(heartbeatTimer);
233
- return;
234
- }
235
- child.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "heartbeat" }) + "\n");
236
- }, heartbeatMs);
237
- resolve({ process: child, agentId });
419
+ child.stdout.on("data", (rawChunk) => {
420
+ if (protocolFailed) return;
421
+ const chunk = Buffer.isBuffer(rawChunk)
422
+ ? rawChunk
423
+ : Buffer.from(rawChunk);
424
+ let offset = 0;
425
+ while (offset < chunk.length && !protocolFailed) {
426
+ const newline = chunk.indexOf(0x0a, offset);
427
+ if (newline === -1) {
428
+ const tail = chunk.subarray(offset);
429
+ const nextBytes = buffer.length + tail.length;
430
+ if (nextBytes > this.limits.maxStdoutLineBytes) {
431
+ terminateForProtocolError(
432
+ overloadError(
433
+ "stdout_line_bytes",
434
+ this.limits.maxStdoutLineBytes,
435
+ this.limits.overloadRetryAfterMs,
436
+ ),
437
+ { observedBytes: nextBytes },
438
+ );
439
+ return;
238
440
  }
239
- } catch (e) {
240
- this.emit("protocol:error", { agentId, error: e, raw: line });
441
+ buffer =
442
+ buffer.length === 0
443
+ ? Buffer.from(tail)
444
+ : Buffer.concat([buffer, tail], nextBytes);
445
+ return;
241
446
  }
447
+
448
+ const segment = chunk.subarray(offset, newline);
449
+ const lineBytes = buffer.length + segment.length;
450
+ if (lineBytes > this.limits.maxStdoutLineBytes) {
451
+ terminateForProtocolError(
452
+ overloadError(
453
+ "stdout_line_bytes",
454
+ this.limits.maxStdoutLineBytes,
455
+ this.limits.overloadRetryAfterMs,
456
+ ),
457
+ { observedBytes: lineBytes },
458
+ );
459
+ return;
460
+ }
461
+ const lineBuffer =
462
+ buffer.length === 0
463
+ ? segment
464
+ : Buffer.concat([buffer, segment], lineBytes);
465
+ buffer = Buffer.alloc(0);
466
+ handleLine(lineBuffer);
467
+ offset = newline + 1;
242
468
  }
243
469
  });
470
+ child.stdout.on("error", (error) => {
471
+ terminateForProtocolError(error);
472
+ });
244
473
 
245
- // Handle stderr: pass through to logger
246
- child.stderr.on("data", (chunk) => {
247
- this.emit("agent:stderr", { agentId, data: chunk.toString("utf8") });
474
+ // Always consume stderr so a verbose child cannot deadlock on a full OS
475
+ // pipe before its host attaches diagnostics. Each emitted diagnostic is
476
+ // independently bounded; stderr is never interpreted as protocol data.
477
+ child.stderr?.on?.("data", (rawChunk) => {
478
+ const chunk = Buffer.isBuffer(rawChunk)
479
+ ? rawChunk
480
+ : Buffer.from(rawChunk);
481
+ const retainedBytes = Math.min(
482
+ chunk.length,
483
+ this.limits.maxStderrChunkBytes,
484
+ );
485
+ const data = chunk.subarray(0, retainedBytes).toString("utf8");
486
+ this.emit("agent:stderr", {
487
+ agentId,
488
+ data,
489
+ observedBytes: chunk.length,
490
+ truncated: retainedBytes !== chunk.length,
491
+ });
248
492
  if (options.captureStderr !== false) {
249
- process.stderr.write(`[agent:${agentId}] ${chunk}`);
493
+ const accepted = process.stderr.write(`[agent:${agentId}] ${data}`);
494
+ if (!accepted && !stderrDrainListener) {
495
+ child.stderr.pause?.();
496
+ stderrDrainListener = () => {
497
+ stderrDrainListener = null;
498
+ if (child.exitCode === null) child.stderr.resume?.();
499
+ };
500
+ process.stderr.once("drain", stderrDrainListener);
501
+ }
250
502
  }
251
503
  });
504
+ child.stderr?.on?.("error", (error) => {
505
+ this.emit("agent:stderr-error", { agentId, error });
506
+ });
252
507
 
253
508
  child.on("error", (err) => {
254
509
  if (heartbeatTimer) clearInterval(heartbeatTimer);
510
+ if (initTimer) clearTimeout(initTimer);
511
+ clearStderrDrainListener();
512
+ protocolFailed = true;
513
+ buffer = Buffer.alloc(0);
514
+ if (this.isAgentRegistered(agentId)) {
515
+ this.unregisterAgent(agentId);
516
+ } else {
517
+ writer.close();
518
+ this._childTransports.delete(agentId);
519
+ }
255
520
  if (!initialized) reject(err);
256
521
  this.emit("agent:error", { agentId, error: err });
257
522
  });
258
523
 
259
524
  child.on("exit", (code, signal) => {
260
525
  if (heartbeatTimer) clearInterval(heartbeatTimer);
526
+ if (initTimer) clearTimeout(initTimer);
527
+ clearStderrDrainListener();
528
+ if (!initialized && !protocolFailed) {
529
+ protocolFailed = true;
530
+ reject(
531
+ new Error(
532
+ `Agent ${agentId} exited before initialization (code=${code}, signal=${signal ?? "none"})`,
533
+ ),
534
+ );
535
+ }
261
536
  this.unregisterAgent(agentId);
262
537
  this.emit("agent:exit", { agentId, code, signal });
263
538
  });
264
539
 
265
540
  // Timeout for initialization
266
- setTimeout(() => {
541
+ initTimer = setTimeout(() => {
267
542
  if (!initialized) {
268
- child.kill();
269
- reject(new Error(`Agent ${agentId} initialization timed out`));
543
+ terminateForProtocolError(
544
+ new Error(`Agent ${agentId} initialization timed out`),
545
+ );
270
546
  }
271
- }, options.initTimeoutMs || 10000);
547
+ }, initTimeoutMs);
272
548
  });
273
549
  }
274
550
 
@@ -276,48 +552,87 @@ class AgentIPCBus extends EventEmitter {
276
552
  * Handle incoming JSON-RPC message from child agent
277
553
  * @private
278
554
  */
279
- _handleIncomingMessage(agentId, msg, child) {
555
+ _handleIncomingMessage(agentId, msg, child, writer = null) {
280
556
  if (!msg.jsonrpc || msg.jsonrpc !== "2.0") {
281
- this.emit("protocol:warning", { agentId, msg, reason: "missing jsonrpc version" });
557
+ this.emit("protocol:warning", {
558
+ agentId,
559
+ msg,
560
+ reason: "missing jsonrpc version",
561
+ });
282
562
  return;
283
563
  }
284
564
 
565
+ const hasId = Object.prototype.hasOwnProperty.call(msg, "id");
566
+ const send = (message) => {
567
+ if (writer) return writer.write(message);
568
+ return child.stdin.write(`${JSON.stringify(message)}\n`);
569
+ };
570
+ const safeReply = (message) => {
571
+ try {
572
+ return send(message);
573
+ } catch (error) {
574
+ if (!writer) {
575
+ this.emit("protocol:error", { agentId, error });
576
+ }
577
+ return false;
578
+ }
579
+ };
580
+
285
581
  // Handle requests (method + id)
286
- if (msg.method && msg.id) {
582
+ if (msg.method && hasId) {
287
583
  if (msg.method === "interaction_request") {
288
- this.requestInteraction(agentId, msg.params).then((result) => {
289
- child.stdin.write(JSON.stringify({
290
- jsonrpc: "2.0",
291
- id: msg.id,
292
- result,
293
- }) + "\n");
294
- }).catch((err) => {
295
- child.stdin.write(JSON.stringify({
296
- jsonrpc: "2.0",
297
- id: msg.id,
298
- error: { code: -32000, message: err.message },
299
- }) + "\n");
300
- });
584
+ this.requestInteraction(agentId, msg.params)
585
+ .then((result) => {
586
+ safeReply({ jsonrpc: "2.0", id: msg.id, result });
587
+ })
588
+ .catch((err) => {
589
+ safeReply({
590
+ jsonrpc: "2.0",
591
+ id: msg.id,
592
+ error: {
593
+ code: err.code === "OVERLOADED" ? -32001 : -32000,
594
+ message: err.message,
595
+ ...(err.data ? { data: err.data } : {}),
596
+ },
597
+ });
598
+ });
301
599
  } else if (msg.method === "log") {
302
600
  this.emit("agent:log", { agentId, params: msg.params });
303
601
  // Acknowledge log
304
- child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id: msg.id, result: true }) + "\n");
602
+ send({ jsonrpc: "2.0", id: msg.id, result: true });
305
603
  } else {
306
604
  this.emit("agent:request", { agentId, msg });
307
605
  }
308
606
  }
309
607
  // Handle notifications (method, no id)
310
- else if (msg.method && !msg.id) {
608
+ else if (msg.method && !hasId) {
311
609
  if (msg.method === "heartbeat") {
312
610
  this.emit("agent:heartbeat", { agentId });
313
611
  } else if (msg.method === "progress") {
314
612
  this.emit("agent:progress", { agentId, params: msg.params });
315
613
  } else {
316
- this.emit("agent:notification", { agentId, method: msg.method, params: msg.params });
614
+ this.emit("agent:notification", {
615
+ agentId,
616
+ method: msg.method,
617
+ params: msg.params,
618
+ });
317
619
  }
318
620
  }
319
621
  // Handle responses (id + result/error)
320
- else if (msg.id) {
622
+ else if (hasId) {
623
+ const pending = this._pendingAgentRequests.get(msg.id);
624
+ if (pending && pending.agentId === agentId) {
625
+ clearTimeout(pending.timer);
626
+ this._pendingAgentRequests.delete(msg.id);
627
+ if (msg.error) {
628
+ const error = new Error(msg.error.message);
629
+ error.code = msg.error.code;
630
+ error.data = msg.error.data;
631
+ pending.reject(error);
632
+ } else {
633
+ pending.resolve(msg.result);
634
+ }
635
+ }
321
636
  this.emit("agent:response", { agentId, msg });
322
637
  }
323
638
  }
@@ -330,32 +645,79 @@ class AgentIPCBus extends EventEmitter {
330
645
  * @param {number} [timeoutMs=30000]
331
646
  * @returns {Promise<any>}
332
647
  */
333
- sendRequest(agentId, method, params = {}, timeoutMs = 30000) {
648
+ sendRequest(agentId, method, params = {}, timeoutMs = undefined) {
334
649
  const sendToWorker = this._agentResolvers.get(agentId);
335
650
  if (!sendToWorker) {
336
651
  return Promise.reject(new Error(`Agent ${agentId} not registered`));
337
652
  }
653
+ if (
654
+ this._pendingAgentRequests.size >= this.limits.maxPendingAgentRequests
655
+ ) {
656
+ return Promise.reject(
657
+ overloadError(
658
+ "pending_agent_requests",
659
+ this.limits.maxPendingAgentRequests,
660
+ this.limits.overloadRetryAfterMs,
661
+ ),
662
+ );
663
+ }
664
+ let pendingForAgent = 0;
665
+ for (const entry of this._pendingAgentRequests.values()) {
666
+ if (entry.agentId === agentId) pendingForAgent += 1;
667
+ }
668
+ if (pendingForAgent >= this.limits.maxPendingAgentRequestsPerAgent) {
669
+ return Promise.reject(
670
+ overloadError(
671
+ "pending_agent_requests_per_agent",
672
+ this.limits.maxPendingAgentRequestsPerAgent,
673
+ this.limits.overloadRetryAfterMs,
674
+ ),
675
+ );
676
+ }
677
+
678
+ let effectiveTimeoutMs;
679
+ try {
680
+ effectiveTimeoutMs = boundedTimeout(
681
+ timeoutMs,
682
+ this.limits.agentRequestTimeoutMs,
683
+ this.limits.maxAgentRequestTimeoutMs,
684
+ );
685
+ } catch (error) {
686
+ return Promise.reject(error);
687
+ }
338
688
  const id = crypto.randomUUID();
339
689
  return new Promise((resolve, reject) => {
340
690
  const timer = setTimeout(() => {
341
- this.removeAllListeners(`response:${id}`);
691
+ this._pendingAgentRequests.delete(id);
342
692
  reject(new Error(`Request ${method} to agent ${agentId} timed out`));
343
- }, timeoutMs);
344
-
345
- const onResponse = (respMsg) => {
346
- if (respMsg.msg.id === id) {
347
- clearTimeout(timer);
348
- this.off("agent:response", onResponse);
349
- if (respMsg.msg.error) {
350
- reject(new Error(respMsg.msg.error.message));
351
- } else {
352
- resolve(respMsg.msg.result);
353
- }
354
- }
355
- };
356
- this.on("agent:response", onResponse);
693
+ }, effectiveTimeoutMs);
357
694
 
358
- sendToWorker({ jsonrpc: "2.0", id, method, params });
695
+ this._pendingAgentRequests.set(id, {
696
+ agentId,
697
+ method,
698
+ resolve,
699
+ reject,
700
+ timer,
701
+ });
702
+ try {
703
+ const accepted = sendToWorker({
704
+ jsonrpc: "2.0",
705
+ id,
706
+ method,
707
+ params,
708
+ });
709
+ if (accepted === false) {
710
+ throw overloadError(
711
+ "agent_transport",
712
+ 1,
713
+ this.limits.overloadRetryAfterMs,
714
+ );
715
+ }
716
+ } catch (error) {
717
+ clearTimeout(timer);
718
+ this._pendingAgentRequests.delete(id);
719
+ reject(error);
720
+ }
359
721
  });
360
722
  }
361
723
  }