onbuzz 4.9.13 → 4.10.0

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 (451) hide show
  1. package/node_modules/glob/README.md +31 -5
  2. package/node_modules/glob/dist/commonjs/glob.d.ts +8 -0
  3. package/node_modules/glob/dist/commonjs/glob.d.ts.map +1 -1
  4. package/node_modules/glob/dist/commonjs/glob.js +2 -1
  5. package/node_modules/glob/dist/commonjs/glob.js.map +1 -1
  6. package/node_modules/glob/dist/commonjs/index.min.js +3 -3
  7. package/node_modules/glob/dist/commonjs/index.min.js.map +4 -4
  8. package/node_modules/glob/dist/commonjs/pattern.d.ts +3 -0
  9. package/node_modules/glob/dist/commonjs/pattern.d.ts.map +1 -1
  10. package/node_modules/glob/dist/commonjs/pattern.js +4 -0
  11. package/node_modules/glob/dist/commonjs/pattern.js.map +1 -1
  12. package/node_modules/glob/dist/esm/glob.d.ts +8 -0
  13. package/node_modules/glob/dist/esm/glob.d.ts.map +1 -1
  14. package/node_modules/glob/dist/esm/glob.js +2 -1
  15. package/node_modules/glob/dist/esm/glob.js.map +1 -1
  16. package/node_modules/glob/dist/esm/index.min.js +3 -3
  17. package/node_modules/glob/dist/esm/index.min.js.map +4 -4
  18. package/node_modules/glob/dist/esm/pattern.d.ts +3 -0
  19. package/node_modules/glob/dist/esm/pattern.d.ts.map +1 -1
  20. package/node_modules/glob/dist/esm/pattern.js +4 -0
  21. package/node_modules/glob/dist/esm/pattern.js.map +1 -1
  22. package/node_modules/{@isaacs → glob/node_modules}/balanced-match/README.md +7 -10
  23. package/node_modules/{@isaacs → glob/node_modules}/balanced-match/package.json +7 -18
  24. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/README.md +3 -6
  25. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.js +6 -4
  26. package/node_modules/glob/node_modules/brace-expansion/dist/commonjs/index.js.map +1 -0
  27. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.js +6 -4
  28. package/node_modules/glob/node_modules/brace-expansion/dist/esm/index.js.map +1 -0
  29. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/package.json +11 -7
  30. package/node_modules/glob/node_modules/minimatch/README.md +76 -1
  31. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts +1 -1
  32. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts.map +1 -1
  33. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.js.map +1 -1
  34. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts +4 -2
  35. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts.map +1 -1
  36. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js +309 -55
  37. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js.map +1 -1
  38. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.d.ts.map +1 -1
  39. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js +2 -4
  40. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js.map +1 -1
  41. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts +1 -1
  42. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts.map +1 -1
  43. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js +4 -4
  44. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js.map +1 -1
  45. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts +81 -1
  46. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts.map +1 -1
  47. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js +232 -134
  48. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js.map +1 -1
  49. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts +1 -1
  50. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts.map +1 -1
  51. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js +8 -8
  52. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js.map +1 -1
  53. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts +1 -1
  54. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts.map +1 -1
  55. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.js.map +1 -1
  56. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts +4 -2
  57. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts.map +1 -1
  58. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js +309 -55
  59. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js.map +1 -1
  60. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.d.ts.map +1 -1
  61. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js +2 -4
  62. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js.map +1 -1
  63. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts +1 -1
  64. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts.map +1 -1
  65. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js +4 -4
  66. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js.map +1 -1
  67. package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts +81 -1
  68. package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts.map +1 -1
  69. package/node_modules/glob/node_modules/minimatch/dist/esm/index.js +232 -134
  70. package/node_modules/glob/node_modules/minimatch/dist/esm/index.js.map +1 -1
  71. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts +1 -1
  72. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts.map +1 -1
  73. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js +8 -8
  74. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js.map +1 -1
  75. package/node_modules/glob/node_modules/minimatch/package.json +17 -11
  76. package/node_modules/glob/package.json +10 -13
  77. package/node_modules/minipass/LICENSE.md +55 -0
  78. package/node_modules/minipass/dist/commonjs/index.d.ts +12 -16
  79. package/node_modules/minipass/dist/commonjs/index.d.ts.map +1 -1
  80. package/node_modules/minipass/dist/commonjs/index.js +13 -3
  81. package/node_modules/minipass/dist/commonjs/index.js.map +1 -1
  82. package/node_modules/minipass/dist/esm/index.d.ts +12 -16
  83. package/node_modules/minipass/dist/esm/index.d.ts.map +1 -1
  84. package/node_modules/minipass/dist/esm/index.js +3 -1
  85. package/node_modules/minipass/dist/esm/index.js.map +1 -1
  86. package/node_modules/minipass/package.json +9 -14
  87. package/node_modules/path-scurry/node_modules/lru-cache/README.md +96 -10
  88. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel-browser.d.ts.map +1 -0
  89. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel-browser.js.map +1 -0
  90. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.d.ts +5 -0
  91. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.js +7 -0
  92. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.d.ts +1400 -0
  93. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.d.ts.map +1 -0
  94. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js +1726 -0
  95. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js.map +1 -0
  96. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js +2 -0
  97. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js.map +7 -0
  98. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.d.ts +12 -0
  99. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.d.ts.map +1 -0
  100. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js +10 -0
  101. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js.map +1 -0
  102. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.cjs.map +1 -0
  103. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.d.cts.map +1 -0
  104. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.d.ts +5 -0
  105. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.js +7 -0
  106. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts +109 -32
  107. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts.map +1 -1
  108. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js +334 -197
  109. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js.map +1 -1
  110. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js +1 -1
  111. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js.map +4 -4
  112. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel-node.d.ts.map +1 -0
  113. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel-node.js.map +1 -0
  114. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.d.ts +5 -0
  115. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.js +9 -0
  116. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.d.ts +1400 -0
  117. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.d.ts.map +1 -0
  118. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js +1726 -0
  119. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js.map +1 -0
  120. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js +2 -0
  121. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js.map +7 -0
  122. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.d.ts +12 -0
  123. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.d.ts.map +1 -0
  124. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js +10 -0
  125. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js.map +1 -0
  126. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.d.ts +12 -0
  127. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.d.ts.map +1 -0
  128. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js +10 -0
  129. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js.map +1 -0
  130. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel-browser.d.ts.map +1 -0
  131. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel-browser.js.map +1 -0
  132. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.d.ts +5 -0
  133. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.js +4 -0
  134. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.d.ts +1400 -0
  135. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.d.ts.map +1 -0
  136. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js +1722 -0
  137. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js.map +1 -0
  138. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js +2 -0
  139. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js.map +7 -0
  140. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.d.ts +12 -0
  141. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.d.ts.map +1 -0
  142. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js +7 -0
  143. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js.map +1 -0
  144. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.d.mts.map +1 -0
  145. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.mjs.map +1 -0
  146. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.d.ts +5 -0
  147. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.js +19 -0
  148. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts +109 -32
  149. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts.map +1 -1
  150. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js +333 -196
  151. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js.map +1 -1
  152. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js +1 -1
  153. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js.map +4 -4
  154. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel-node.d.ts.map +1 -0
  155. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel-node.js.map +1 -0
  156. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.d.ts +5 -0
  157. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.js +6 -0
  158. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.d.ts +1400 -0
  159. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.d.ts.map +1 -0
  160. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js +1722 -0
  161. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js.map +1 -0
  162. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js +2 -0
  163. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js.map +7 -0
  164. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.d.ts +12 -0
  165. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.d.ts.map +1 -0
  166. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js +7 -0
  167. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js.map +1 -0
  168. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.d.ts +12 -0
  169. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.d.ts.map +1 -0
  170. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js +7 -0
  171. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js.map +1 -0
  172. package/node_modules/path-scurry/node_modules/lru-cache/package.json +71 -18
  173. package/node_modules/path-scurry/package.json +8 -24
  174. package/package.json +1 -1
  175. package/scripts/debug-balance-probe.mjs +35 -35
  176. package/scripts/push-image.sh +43 -43
  177. package/scripts/setup-acr.sh +65 -65
  178. package/scripts/verify-optional-deps.js +96 -1
  179. package/src/__tests__/composioCliFlags.test.js +239 -239
  180. package/src/analyzers/CSSAnalyzer.js +298 -297
  181. package/src/analyzers/ConfigValidator.js +691 -690
  182. package/src/analyzers/ESLintAnalyzer.js +320 -320
  183. package/src/analyzers/JavaScriptAnalyzer.js +260 -261
  184. package/src/analyzers/PrettierFormatter.js +246 -247
  185. package/src/analyzers/PythonAnalyzer.js +283 -283
  186. package/src/analyzers/SecurityAnalyzer.js +729 -729
  187. package/src/analyzers/SparrowAnalyzer.js +341 -341
  188. package/src/analyzers/TypeScriptAnalyzer.js +247 -247
  189. package/src/analyzers/__tests__/CSSAnalyzer.test.js +41 -41
  190. package/src/analyzers/__tests__/ConfigValidator.test.js +362 -362
  191. package/src/analyzers/__tests__/JavaScriptAnalyzer.test.js +40 -40
  192. package/src/analyzers/__tests__/PythonAnalyzer.test.js +205 -208
  193. package/src/analyzers/__tests__/SecurityAnalyzer.test.js +303 -303
  194. package/src/analyzers/__tests__/TypeScriptAnalyzer.test.js +187 -187
  195. package/src/analyzers/codeCloneDetector/analyzer.js +344 -344
  196. package/src/analyzers/codeCloneDetector/detector.js +250 -250
  197. package/src/analyzers/codeCloneDetector/index.js +194 -192
  198. package/src/analyzers/codeCloneDetector/parser.js +199 -199
  199. package/src/core/__tests__/agentPool.test.js +866 -866
  200. package/src/core/__tests__/agentPoolAutoResume.test.js +209 -209
  201. package/src/core/__tests__/agentPoolWakeOnMessage.test.js +315 -315
  202. package/src/core/__tests__/agentScheduler.emptyResponseChatStall.test.js +213 -213
  203. package/src/core/__tests__/agentScheduler.errorCategorisation.test.js +246 -246
  204. package/src/core/__tests__/agentScheduler.firstChunkTimeout.test.js +138 -138
  205. package/src/core/__tests__/agentScheduler.modeTransitions.test.js +233 -233
  206. package/src/core/__tests__/agentScheduler.nativePromptPick.test.js +319 -319
  207. package/src/core/__tests__/agentScheduler.taskLifecycleInstruction.test.js +78 -78
  208. package/src/core/__tests__/agentScheduler.visualizer.test.js +258 -258
  209. package/src/core/__tests__/flowCheckpointStore.test.js +140 -140
  210. package/src/core/__tests__/flowEndToEnd.test.js +565 -565
  211. package/src/core/__tests__/flowFieldMapping.test.js +188 -189
  212. package/src/core/__tests__/flowLintClientMirror.test.js +96 -98
  213. package/src/core/__tests__/flowSavePayload.test.js +170 -169
  214. package/src/core/__tests__/flowTemplates.test.js +311 -311
  215. package/src/core/__tests__/flowVersionStore.test.js +123 -123
  216. package/src/core/__tests__/messageProcessor.test.js +669 -669
  217. package/src/core/__tests__/stateManager.test.js +0 -1
  218. package/src/core/agentPool.js +2474 -2475
  219. package/src/core/agentScheduler.js +1 -4
  220. package/src/core/contextManager.js +708 -708
  221. package/src/core/flowExecutor.js +1510 -1510
  222. package/src/core/flowFieldMapping.js +136 -138
  223. package/src/core/messageProcessor.js +953 -954
  224. package/src/core/orchestrator.js +593 -595
  225. package/src/core/stateManager.js +1765 -1752
  226. package/src/index.js +1221 -1221
  227. package/src/interfaces/__tests__/archivedAgentDelete.test.js +207 -207
  228. package/src/interfaces/__tests__/bulkAgentRoute.test.js +361 -361
  229. package/src/interfaces/__tests__/imageServing.test.js +228 -228
  230. package/src/interfaces/__tests__/remoteSessionAuth.test.js +308 -308
  231. package/src/interfaces/__tests__/videoJobsRoutes.test.js +178 -179
  232. package/src/interfaces/__tests__/webServer.marketplace.test.js +629 -629
  233. package/src/interfaces/schedulerRoutes.js +50 -50
  234. package/src/interfaces/terminal/__tests__/smoke/connection.test.js +341 -350
  235. package/src/interfaces/terminal/__tests__/smoke/enhancements.test.js +156 -156
  236. package/src/interfaces/terminal/__tests__/smoke/imports.test.js +325 -330
  237. package/src/interfaces/terminal/__tests__/smoke/tools.test.js +385 -388
  238. package/src/interfaces/terminal/api/session.js +265 -266
  239. package/src/interfaces/terminal/api/websocket.js +496 -497
  240. package/src/interfaces/terminal/components/AgentCreator.js +691 -705
  241. package/src/interfaces/terminal/components/AgentEditor.js +676 -678
  242. package/src/interfaces/terminal/components/AgentSwitcher.js +331 -330
  243. package/src/interfaces/terminal/components/ErrorPanel.js +263 -264
  244. package/src/interfaces/terminal/components/Header.js +28 -28
  245. package/src/interfaces/terminal/components/Layout.js +598 -603
  246. package/src/interfaces/terminal/components/MessageList.js +280 -281
  247. package/src/interfaces/terminal/components/SettingsPanel.js +410 -415
  248. package/src/interfaces/terminal/components/StatusBar.js +2 -0
  249. package/src/interfaces/terminal/index.js +168 -168
  250. package/src/interfaces/terminal/state/useAgentControl.js +496 -496
  251. package/src/interfaces/terminal/state/useAgents.js +537 -537
  252. package/src/interfaces/terminal/state/useMessages.js +629 -630
  253. package/src/interfaces/terminal/state/useTools.js +554 -554
  254. package/src/interfaces/terminal/utils/debugLogger.js +44 -44
  255. package/src/interfaces/terminal/utils/settingsStorage.js +232 -232
  256. package/src/interfaces/webServer.js +7578 -7579
  257. package/src/interfaces/webServer.js.bak +7046 -7046
  258. package/src/modules/fileExplorer/__tests__/zipDownload.test.js +237 -237
  259. package/src/modules/fileExplorer/controller.js +470 -469
  260. package/src/modules/fileExplorer/routes.js +285 -286
  261. package/src/modules/widget/__tests__/isDisabled.test.js +41 -41
  262. package/src/modules/widget/__tests__/routes.test.js +677 -678
  263. package/src/modules/widget/__tests__/runtime.test.js +401 -401
  264. package/src/modules/widget/__tests__/versioning.test.js +309 -309
  265. package/src/modules/widget/__tests__/webComponentRuntime.test.js +565 -565
  266. package/src/modules/widget/__tests__/widgetTool.test.js +316 -316
  267. package/src/modules/widget/routes.js +435 -435
  268. package/src/modules/widget/runtime/bundle.js +640 -640
  269. package/src/modules/widget/runtime/webComponentBundle.js +470 -470
  270. package/src/modules/widget/schema.js +182 -181
  271. package/src/modules/widget/widgetTool.js +1389 -1389
  272. package/src/services/__tests__/agentActivityService.test.js +401 -402
  273. package/src/services/__tests__/benchmarkService.test.js +184 -184
  274. package/src/services/__tests__/contextInjectionService.test.js +246 -246
  275. package/src/services/__tests__/conversationQuery.test.js +721 -723
  276. package/src/services/__tests__/credentialVault.test.js +469 -469
  277. package/src/services/__tests__/discordService.integration.test.js +638 -639
  278. package/src/services/__tests__/flowContextService.test.js +590 -590
  279. package/src/services/__tests__/memoryService.test.js +1 -1
  280. package/src/services/__tests__/messageSource.test.js +380 -380
  281. package/src/services/__tests__/modelRouterNaming.test.js +111 -111
  282. package/src/services/__tests__/projectDetector.test.js +34 -34
  283. package/src/services/__tests__/promptService.test.js +242 -242
  284. package/src/services/__tests__/telegramService.test.js +941 -941
  285. package/src/services/__tests__/tokenCountingService.test.js +48 -48
  286. package/src/services/agentActivityService.js +419 -420
  287. package/src/services/aiService.js +2997 -3001
  288. package/src/services/apiKeyManager.js +359 -359
  289. package/src/services/benchmarkService.js +196 -196
  290. package/src/services/codebaseKnowledgeService.js +2 -2
  291. package/src/services/composioService.js +738 -738
  292. package/src/services/conversationCompactionService.js +1258 -1257
  293. package/src/services/credentialVault.js +685 -685
  294. package/src/services/discordService.js +792 -793
  295. package/src/services/embeddings/__tests__/azureCustomProvider.test.js +232 -232
  296. package/src/services/embeddings/__tests__/embeddingService.test.js +417 -417
  297. package/src/services/embeddings/__tests__/localProvider.test.js +263 -263
  298. package/src/services/embeddings/autoRecall.js +218 -219
  299. package/src/services/embeddings/indexers/__tests__/agentIndexer.test.js +232 -232
  300. package/src/services/embeddings/indexers/__tests__/memoryIndexer.test.js +418 -418
  301. package/src/services/embeddings/indexers/__tests__/reminisceIndexer.test.js +356 -357
  302. package/src/services/embeddings/indexers/__tests__/skillsIndexer.test.js +145 -145
  303. package/src/services/embeddings/indexers/__tests__/taskIndexer.test.js +146 -146
  304. package/src/services/embeddings/indexers/composioIndexer.js +279 -279
  305. package/src/services/embeddings/providerInterface.js +206 -206
  306. package/src/services/embeddings/providers/localProvider.js +11 -7
  307. package/src/services/embeddings/providers/openaiProvider.js +101 -101
  308. package/src/services/embeddings/vectorStore/inMemoryJsonStore.js +356 -356
  309. package/src/services/errorHandler.js +809 -809
  310. package/src/services/flowContextService.js +586 -586
  311. package/src/services/grounding/MockAdapter.js +125 -125
  312. package/src/services/modelRouterService.js +26 -31
  313. package/src/services/modelsService.js +322 -322
  314. package/src/services/ollamaService.js +452 -452
  315. package/src/services/projectDetector.js +403 -404
  316. package/src/services/promptService.js +418 -418
  317. package/src/services/qualityInspector.js +795 -795
  318. package/src/services/scheduleService.js +726 -726
  319. package/src/services/serviceRegistry.js +386 -386
  320. package/src/services/telegrafBot.js +174 -174
  321. package/src/services/telegramService.js +1972 -1972
  322. package/src/services/visualEditorBridge.js +1033 -1033
  323. package/src/services/visualEditorServer.js +1769 -1774
  324. package/src/services/whatsappService.js +667 -668
  325. package/src/tools/__tests__/agentCommunicationTool.findAgent.test.js +226 -226
  326. package/src/tools/__tests__/agentCommunicationTool.test.js +3 -3
  327. package/src/tools/__tests__/agentDelayTool.test.js +342 -342
  328. package/src/tools/__tests__/baseTool.test.js +3 -3
  329. package/src/tools/__tests__/codeMapTool.test.js +915 -915
  330. package/src/tools/__tests__/fileContentReplaceTool.test.js +309 -309
  331. package/src/tools/__tests__/fileTreeTool.test.js +274 -274
  332. package/src/tools/__tests__/filesystemTool.test.js +815 -815
  333. package/src/tools/__tests__/foundryWebSearchTool.test.js +252 -252
  334. package/src/tools/__tests__/imageTool.validator.test.js +194 -194
  335. package/src/tools/__tests__/jobDoneTool.test.js +580 -581
  336. package/src/tools/__tests__/memoryTool.forgetStale.test.js +272 -272
  337. package/src/tools/__tests__/memoryTool.reminisce.test.js +2 -2
  338. package/src/tools/__tests__/memoryTool.reminisceSemanticSearch.test.js +301 -301
  339. package/src/tools/__tests__/memoryTool.semanticSearch.test.js +405 -405
  340. package/src/tools/__tests__/memoryTool.teamPool.test.js +293 -293
  341. package/src/tools/__tests__/memoryTool.test.js +1 -1
  342. package/src/tools/__tests__/seekTool.test.js +282 -282
  343. package/src/tools/__tests__/skillsTool.search.test.js +164 -164
  344. package/src/tools/__tests__/skillsTool.test.js +226 -226
  345. package/src/tools/__tests__/staticAnalysisTool.test.js +509 -509
  346. package/src/tools/__tests__/taskManagerTool.discipline.test.js +137 -137
  347. package/src/tools/__tests__/taskManagerTool.search.test.js +143 -143
  348. package/src/tools/__tests__/taskManagerTool.test.js +866 -866
  349. package/src/tools/__tests__/terminalTool.test.js +448 -448
  350. package/src/tools/__tests__/toolShapeForgiveness.test.js +259 -260
  351. package/src/tools/__tests__/userPromptTool.test.js +297 -297
  352. package/src/tools/__tests__/videoTool.jobs.test.js +147 -147
  353. package/src/tools/__tests__/webTool.e2e.test.js +609 -603
  354. package/src/tools/__tests__/webTool.unit.test.js +195 -195
  355. package/src/tools/__tests__/webTool.visionModel.test.js +75 -75
  356. package/src/tools/agentCommunicationTool.js +8 -10
  357. package/src/tools/agentDelayTool.js +496 -497
  358. package/src/tools/asyncToolManager.js +602 -603
  359. package/src/tools/baseTool.js +12 -11
  360. package/src/tools/cloneDetectionTool.js +576 -581
  361. package/src/tools/codeMapTool.js +0 -6
  362. package/src/tools/composioTool.js +617 -617
  363. package/src/tools/dependencyResolverTool.js +1211 -1212
  364. package/src/tools/desktop/DesktopTool.js +629 -638
  365. package/src/tools/desktop/__tests__/DesktopTool.e2e.test.js +306 -306
  366. package/src/tools/desktop/__tests__/DesktopTool.test.js +507 -507
  367. package/src/tools/desktop/__tests__/osController.test.js +364 -364
  368. package/src/tools/desktop/osController.js +491 -491
  369. package/src/tools/docxTool.js +623 -623
  370. package/src/tools/excelTool.js +636 -636
  371. package/src/tools/fileContentReplaceTool.js +5 -7
  372. package/src/tools/fileSystemTool.js +12 -19
  373. package/src/tools/fileTreeTool.js +840 -840
  374. package/src/tools/foundryWebSearchTool.js +273 -273
  375. package/src/tools/helpTool.js +198 -198
  376. package/src/tools/imageTool.js +1397 -1397
  377. package/src/tools/importAnalyzerTool.js +1056 -1056
  378. package/src/tools/jobDoneTool.js +495 -495
  379. package/src/tools/memoryTool.js +1 -1
  380. package/src/tools/office/pres/__tests__/presSystem.test.js +365 -365
  381. package/src/tools/office/pres/archetypes/agenda.js +61 -61
  382. package/src/tools/office/pres/archetypes/bentoGrid.js +218 -219
  383. package/src/tools/office/pres/archetypes/bigStat.js +140 -142
  384. package/src/tools/office/pres/archetypes/closing.js +70 -70
  385. package/src/tools/office/pres/archetypes/hero.js +70 -70
  386. package/src/tools/office/pres/archetypes/productHero.js +93 -94
  387. package/src/tools/office/pres/archetypes/table.js +73 -74
  388. package/src/tools/office/pres/backgrounds/orb.js +66 -66
  389. package/src/tools/office/pres/components.js +422 -423
  390. package/src/tools/officeTool.js +441 -441
  391. package/src/tools/pdfTool.js +625 -627
  392. package/src/tools/platformControlTool.js +1081 -1081
  393. package/src/tools/seekTool.js +917 -918
  394. package/src/tools/skillsTool.js +1 -1
  395. package/src/tools/staticAnalysisTool.js +2143 -2146
  396. package/src/tools/taskManagerTool.js +3324 -3324
  397. package/src/tools/terminalTool.js +2615 -2618
  398. package/src/tools/videoTool.js +1303 -1303
  399. package/src/tools/visionTool.js +508 -508
  400. package/src/tools/visualEditorTool.js +1289 -1290
  401. package/src/tools/webTool.js +3368 -3368
  402. package/src/tools/whatsappTool.js +464 -464
  403. package/src/types/__tests__/agent.test.js +499 -499
  404. package/src/types/__tests__/contextReference.test.js +606 -606
  405. package/src/types/__tests__/conversation.test.js +555 -555
  406. package/src/types/__tests__/toolCommand.test.js +584 -584
  407. package/src/types/contextReference.js +974 -971
  408. package/src/types/conversation.js +729 -729
  409. package/src/types/toolCommand.js +746 -746
  410. package/src/utilities/__tests__/attachmentValidator.test.js +80 -80
  411. package/src/utilities/__tests__/auditReport.test.js +328 -328
  412. package/src/utilities/__tests__/directoryAccessManager.test.js +388 -388
  413. package/src/utilities/__tests__/jsonRepair.test.js +103 -104
  414. package/src/utilities/__tests__/modeTransitionReasons.test.js +105 -105
  415. package/src/utilities/__tests__/platformUtils.test.js +80 -87
  416. package/src/utilities/__tests__/structuredFileValidator.test.js +261 -263
  417. package/src/utilities/__tests__/toolConstants.test.js +92 -94
  418. package/src/utilities/__tests__/useIsTouchDevice.detect.test.js +114 -114
  419. package/src/utilities/__tests__/webUiUtilSync.test.js +117 -117
  420. package/src/utilities/attachmentValidator.js +284 -288
  421. package/src/utilities/authCache.js.backup-1779570472481 +121 -121
  422. package/src/utilities/browserStealth.js +631 -630
  423. package/src/utilities/configManager.js +616 -617
  424. package/src/utilities/directoryAccessManager.js +564 -565
  425. package/src/utilities/fileProcessor.js +308 -307
  426. package/src/utilities/humanBehavior.js +454 -453
  427. package/src/utilities/logger.js +479 -479
  428. package/src/utilities/structuredFileValidator.js +696 -699
  429. package/src/utilities/tagParser.js +5 -10
  430. package/src/utilities/userDataDir.js +308 -308
  431. package/node_modules/@isaacs/brace-expansion/dist/commonjs/index.js.map +0 -1
  432. package/node_modules/@isaacs/brace-expansion/dist/esm/index.js.map +0 -1
  433. package/node_modules/minipass/LICENSE +0 -15
  434. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/LICENSE.md +0 -0
  435. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.d.ts +0 -0
  436. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.d.ts.map +0 -0
  437. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.js +0 -0
  438. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.js.map +0 -0
  439. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/package.json +0 -0
  440. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.d.ts +0 -0
  441. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.d.ts.map +0 -0
  442. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.js +0 -0
  443. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.js.map +0 -0
  444. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/package.json +0 -0
  445. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/LICENSE +0 -0
  446. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.d.ts +0 -0
  447. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.d.ts.map +0 -0
  448. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/package.json +0 -0
  449. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.d.ts +0 -0
  450. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.d.ts.map +0 -0
  451. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/package.json +0 -0
@@ -1,2476 +1,2475 @@
1
- /**
2
- * AgentPool - Manages the lifecycle, state, and communication of all active agents
3
- *
4
- * Purpose:
5
- * - Agent creation and destruction
6
- * - Agent notification and routing
7
- * - Multi-agent conversation coordination
8
- * - Agent state persistence and recovery
9
- * - Agent activity management
10
- */
11
-
12
- import {
13
- AGENT_TYPES,
14
- AGENT_STATUS,
15
- AGENT_MODES,
16
- MESSAGE_ROLES,
17
- MESSAGE_TYPES,
18
- INTER_AGENT_MESSAGE,
19
- MODEL_FORMAT_VERSIONS,
20
- SYSTEM_DEFAULTS
21
- } from '../utilities/constants.js';
22
- import DirectoryAccessManager from '../utilities/directoryAccessManager.js';
23
- import { getVisualEditorBridge } from '../services/visualEditorBridge.js';
24
-
25
- class AgentPool {
26
- // Stopwords for the _tokenize / _jaccard similarity check used by
27
- // auto-save-as-plan dedup. Tight list only words that appear in
28
- // virtually every English sentence regardless of content, so that
29
- // their presence in both messages doesn't inflate similarity.
30
- static _STOPWORDS = new Set([
31
- 'the', 'and', 'for', 'but', 'are', 'was', 'were',
32
- 'has', 'have', 'had', 'this', 'that', 'with', 'will',
33
- 'you', 'your', 'our', 'their', 'them', 'they',
34
- 'can', 'could', 'should', 'would',
35
- ]);
36
-
37
- constructor(config, logger, stateManager, contextManager, toolsRegistry = null) {
38
- this.config = config;
39
- this.logger = logger;
40
- this.stateManager = stateManager;
41
- this.contextManager = contextManager;
42
- this.toolsRegistry = toolsRegistry;
43
-
44
- // Agent registry - maps agent ID to agent object
45
- this.agents = new Map();
46
-
47
- // Agent directory for discovery
48
- this.agentDirectory = new Map();
49
-
50
- // Paused agents tracking
51
- this.pausedAgents = new Map();
52
-
53
- // Agent notification queue
54
- this.notificationQueue = new Map();
55
-
56
- this.maxAgentsPerProject = config.system?.maxAgentsPerProject || SYSTEM_DEFAULTS.MAX_AGENTS_PER_PROJECT;
57
-
58
- // MessageProcessor reference for triggering responses (set via setMessageProcessor)
59
- this.messageProcessor = null;
60
-
61
- // Initialize directory access manager
62
- this.directoryAccessManager = new DirectoryAccessManager(config, logger);
63
- }
64
-
65
- /**
66
- * Create a new agent with specified configuration
67
- * @param {Object} config - Agent configuration
68
- * @param {string} config.name - Agent name
69
- * @param {string} config.type - Agent type ('user-created', 'system-agent', 'agent-engineer')
70
- * @param {string} config.systemPrompt - Agent's system prompt
71
- * @param {string} config.preferredModel - Preferred LLM model
72
- * @param {Array} config.capabilities - Available tools/capabilities
73
- * @param {Object} config.directoryAccess - Directory access configuration
74
- * @param {string} config.projectDir - Project directory for default access setup
75
- * @returns {Promise<Object>} Created agent object
76
- */
77
- async createAgent(config) {
78
- // Check agent limit
79
- if (this.agents.size >= this.maxAgentsPerProject) {
80
- throw new Error(`Maximum agents per project exceeded (${this.maxAgentsPerProject})`);
81
- }
82
-
83
- const agentId = this._generateAgentId(config.name);
84
- const now = new Date().toISOString();
85
-
86
- // Enhance system prompt with tool descriptions if available
87
- let enhancedSystemPrompt = config.systemPrompt;
88
- if (this.toolsRegistry && config.capabilities && config.capabilities.length > 0) {
89
- try {
90
- enhancedSystemPrompt = this.toolsRegistry.enhanceSystemPrompt(
91
- config.systemPrompt,
92
- config.capabilities,
93
- {
94
- compact: config.compactToolDescriptions || false,
95
- layered: config.layeredToolDescriptions || false,
96
- includeExamples: config.includeToolExamples !== false,
97
- includeUsageGuidelines: config.includeUsageGuidelines !== false,
98
- includeSecurityNotes: config.includeSecurityNotes !== false
99
- }
100
- );
101
-
102
- this.logger?.info(`System prompt enhanced with tool descriptions`, {
103
- agentId,
104
- capabilities: config.capabilities,
105
- originalLength: config.systemPrompt?.length || 0,
106
- enhancedLength: enhancedSystemPrompt?.length || 0
107
- });
108
- } catch (error) {
109
- this.logger?.error(`Failed to enhance system prompt with tools`, {
110
- agentId,
111
- error: error.message,
112
- capabilities: config.capabilities
113
- });
114
- // Fall back to original prompt
115
- enhancedSystemPrompt = config.systemPrompt;
116
- }
117
- }
118
-
119
- // Inject assigned skills index into system prompt
120
- if (config.skills && config.skills.length > 0) {
121
- try {
122
- const { getSkillsService } = await import('../services/skillsService.js');
123
- const skillsService = getSkillsService(this.logger);
124
- await skillsService.initialize();
125
- const summaries = await skillsService.getSkillSummaries(config.skills);
126
- if (summaries.length > 0) {
127
- enhancedSystemPrompt += '\n\n## ASSIGNED SKILLS\n\n';
128
- enhancedSystemPrompt += 'Use the skills tool to browse and load skill content. Use "describe" to see sections, "read-section" to load specific parts.\n\n';
129
- for (const s of summaries) {
130
- const sections = s.sections?.length ? `\n Sections: ${s.sections.map(h => h.replace(/^#+\s*/, '')).join(', ')}` : '';
131
- enhancedSystemPrompt += `- **${s.name}** (${s.lineCount} lines): ${s.description}${sections}\n`;
132
- }
133
- }
134
- } catch (error) {
135
- this.logger?.warn('Failed to inject skills index into system prompt', { error: error.message });
136
- }
137
- }
138
-
139
- // Setup directory access configuration
140
- let directoryAccess;
141
-
142
- console.log('AgentPool DEBUG: createAgent - config.directoryAccess:', config.directoryAccess ? 'EXISTS' : 'NULL/UNDEFINED');
143
- if (config.directoryAccess) {
144
- console.log('AgentPool DEBUG: createAgent - directoryAccess from config:', JSON.stringify(config.directoryAccess, null, 2));
145
- }
146
- console.log('AgentPool DEBUG: createAgent - config.projectDir:', config.projectDir);
147
-
148
- if (config.directoryAccess) {
149
- // Validate provided directory access configuration
150
- const validation = this.directoryAccessManager.validateAccessConfiguration(config.directoryAccess);
151
- console.log('AgentPool DEBUG: createAgent - validation result:', validation);
152
- if (!validation.valid) {
153
- throw new Error(`Invalid directory access configuration: ${validation.errors.join(', ')}`);
154
- }
155
- directoryAccess = config.directoryAccess;
156
- console.log('AgentPool DEBUG: createAgent - Using provided directoryAccess');
157
- } else {
158
- // Create default directory access based on project directory
159
- const projectDir = config.projectDir || process.cwd();
160
- directoryAccess = DirectoryAccessManager.createProjectDefaults(projectDir);
161
- console.log('AgentPool DEBUG: createAgent - Created default directoryAccess for projectDir:', projectDir);
162
- console.log('AgentPool DEBUG: createAgent - Default directoryAccess:', JSON.stringify(directoryAccess, null, 2));
163
- }
164
-
165
- const agent = {
166
- id: agentId,
167
- type: config.type || AGENT_TYPES.USER_CREATED,
168
- name: config.name || `Agent-${Date.now()}`,
169
- systemPrompt: enhancedSystemPrompt,
170
- originalSystemPrompt: config.systemPrompt, // Store original for reference
171
- preferredModel: config.preferredModel,
172
- status: AGENT_STATUS.ACTIVE,
173
- capabilities: config.capabilities || [],
174
- directoryAccess: directoryAccess, // Directory access configuration
175
- // Per-tool configuration keyed by tool id (e.g., 'terminal',
176
- // 'filesystem'). When a tool is instantiated for this agent, the
177
- // object under toolConfig[toolId] is merged into the tool's
178
- // constructor config. Previously tools were constructed from global
179
- // defaults only, so there was no way to set per-agent knobs like
180
- // terminal allowed-commands, filesystem size limits, web stealth
181
- // level, etc. See ToolManager integration in step 2.
182
- toolConfig: (config.toolConfig && typeof config.toolConfig === 'object' && !Array.isArray(config.toolConfig))
183
- ? { ...config.toolConfig }
184
- : {},
185
- conversations: {
186
- full: {
187
- messages: [],
188
- lastUpdated: now
189
- }
190
- },
191
- currentModel: config.preferredModel,
192
- dynamicModelRouting: config.dynamicModelRouting || false,
193
- routingStrategy: config.routingStrategy || '',
194
- skills: config.skills || [],
195
- platformProvided: config.platformProvided !== false, // Default true — all models are platform-provided
196
-
197
- // Agent Mode Configuration
198
- mode: config.mode || AGENT_MODES.CHAT,
199
- currentTask: null, // Current autonomous task being executed
200
- taskStartTime: null,
201
- maxIterations: config.maxIterations || 10, // Safety limit for autonomous loops
202
- iterationCount: 0,
203
- stopRequested: false,
204
- delayEndTime: null, // When agent delay expires (for agentDelay tool)
205
- ttl: null, // Time-to-live: processing cycles remaining (null = no TTL, number = cycles left)
206
-
207
- // Message Queues for scheduler processing
208
- messageQueues: {
209
- toolResults: [], // Tool execution results waiting to be processed
210
- interAgentMessages: [], // Messages from other agents
211
- userMessages: [] // Messages from users
212
- },
213
-
214
- createdAt: now,
215
- lastActivity: now,
216
- pausedUntil: null,
217
- // Used by platformControlTool for ancestry checks. null = created
218
- // via the UI / no parent agent. Set to <agentId> when an agent
219
- // creates another via the platformcontrol tool.
220
- createdBy: typeof config.createdBy === 'string' ? config.createdBy : null,
221
- metadata: config.metadata || {},
222
-
223
- // CRITICAL: Store sessionId for API key resolution
224
- sessionId: config.sessionId,
225
-
226
- // Inter-agent conversation tracking to prevent spam
227
- interAgentTracking: new Map(), // recipientId -> { lastSent, lastReceived, lastType }
228
-
229
- // Task Management System for agent-mode autonomous operation
230
- taskList: {
231
- tasks: [], // Array of task objects
232
- lastUpdated: now
233
- },
234
-
235
- // Incoming messages tracking (for unprocessed messages)
236
- incomingMessages: []
237
- };
238
-
239
- this.logger.info(`Agent created with routing config`, {
240
- agentId,
241
- dynamicModelRouting: agent.dynamicModelRouting,
242
- platformProvided: agent.platformProvided,
243
- preferredModel: agent.preferredModel
244
- });
245
-
246
- // Initialize model-specific conversation with dual storage structure
247
- if (config.preferredModel) {
248
- agent.conversations[config.preferredModel] = {
249
- // Dual storage for compactization support
250
- messages: [], // Original messages - never modified
251
- compactizedMessages: null, // Working copy - null until first compaction
252
-
253
- // Compactization metadata
254
- lastCompactization: null, // Timestamp of last compaction
255
- compactizationCount: 0, // Number of times compacted
256
- compactizationStrategy: null, // 'summarization', 'truncation', 'aggressive'
257
- originalTokenCount: 0, // Token count before last compaction
258
- compactedTokenCount: 0, // Token count after last compaction
259
-
260
- // Backward compatibility
261
- tokenCount: 0, // Current effective token count
262
- lastUpdated: now,
263
- formatVersion: this._getModelFormatVersion(config.preferredModel)
264
- };
265
- }
266
-
267
- // Add to registry and directory
268
- this.agents.set(agentId, agent);
269
- this._updateAgentDirectory(agent);
270
-
271
- // Persist agent state (use wrapper that resolves agent object from ID)
272
- await this.persistAgentState(agentId);
273
-
274
- this.logger.info(`Agent created: ${agentId}`, {
275
- agentId,
276
- name: agent.name,
277
- type: agent.type,
278
- model: agent.preferredModel
279
- });
280
-
281
- return agent;
282
- }
283
-
284
- /**
285
- * Retrieve agent instance by ID
286
- * @param {string} agentId - Agent identifier
287
- * @returns {Promise<Object|null>} Agent object or null if not found
288
- */
289
- async getAgent(agentId, enrichWithSchedulerStatus = false) {
290
- const agent = this.agents.get(agentId);
291
- if (!agent) return null;
292
-
293
- // Optionally enrich with scheduler status for UI
294
- if (enrichWithSchedulerStatus && this.scheduler) {
295
- agent.inScheduler = this.scheduler.isAgentInScheduler(agentId);
296
- }
297
-
298
- return agent;
299
- }
300
-
301
- /**
302
- * Update an existing agent's configuration
303
- * @param {string} agentId - Agent identifier
304
- * @param {Object} updates - Updates to apply to the agent
305
- * @returns {Promise<Object>} Updated agent object
306
- */
307
- async updateAgent(agentId, updates) {
308
- const agent = await this.getAgent(agentId);
309
- if (!agent) {
310
- throw new Error(`Agent not found: ${agentId}`);
311
- }
312
-
313
- this.logger.info(`Updating agent: ${agentId}`, {
314
- updates,
315
- currentName: agent.name
316
- });
317
-
318
- // Validate directory access configuration if being updated
319
- if (updates.directoryAccess) {
320
- const validation = this.directoryAccessManager.validateAccessConfiguration(updates.directoryAccess);
321
- if (!validation.valid) {
322
- throw new Error(`Invalid directory access configuration: ${validation.errors.join(', ')}`);
323
- }
324
-
325
- this.logger.info(`Directory access validation passed for agent: ${agentId}`, {
326
- workingDirectory: updates.directoryAccess.workingDirectory,
327
- readOnlyDirs: updates.directoryAccess.readOnlyDirectories?.length || 0,
328
- writeEnabledDirs: updates.directoryAccess.writeEnabledDirectories?.length || 0
329
- });
330
- }
331
-
332
- // Validate per-tool config if being updated. Must be a plain object
333
- // keyed by tool id; each value is an object of config overrides that
334
- // will be merged into the tool's constructor config at instantiation.
335
- // We don't validate individual tool schemas here that happens at
336
- // tool construction time where the tool knows its own shape.
337
- if (updates.toolConfig !== undefined) {
338
- if (updates.toolConfig === null
339
- || typeof updates.toolConfig !== 'object'
340
- || Array.isArray(updates.toolConfig)) {
341
- throw new Error('Invalid toolConfig: must be a plain object keyed by tool id');
342
- }
343
- for (const [toolId, cfg] of Object.entries(updates.toolConfig)) {
344
- if (cfg !== null && (typeof cfg !== 'object' || Array.isArray(cfg))) {
345
- throw new Error(`Invalid toolConfig.${toolId}: must be an object or null`);
346
- }
347
- }
348
- }
349
-
350
- // If originalSystemPrompt is being updated (user edited the raw prompt), store it
351
- // and use it as the base for enhancement. Otherwise use the existing originalSystemPrompt.
352
- if (updates.originalSystemPrompt !== undefined) {
353
- // User explicitly set a new base prompt — store it
354
- this.logger.info(`Original system prompt updated by user`, {
355
- agentId,
356
- oldLength: (agent.originalSystemPrompt || '').length,
357
- newLength: updates.originalSystemPrompt.length
358
- });
359
- }
360
-
361
- // If capabilities or system prompt are being updated, regenerate the enhanced system prompt
362
- if ((updates.capabilities || updates.originalSystemPrompt !== undefined) && this.toolsRegistry) {
363
- try {
364
- // Priority: new user prompt > existing original prompt > existing system prompt
365
- const baseSystemPrompt = updates.originalSystemPrompt !== undefined
366
- ? updates.originalSystemPrompt
367
- : (agent.originalSystemPrompt || agent.systemPrompt || '');
368
- const capabilities = updates.capabilities || agent.capabilities || [];
369
-
370
- const enhancedSystemPrompt = this.toolsRegistry.enhanceSystemPrompt(
371
- baseSystemPrompt,
372
- capabilities,
373
- {
374
- compact: agent.compactToolDescriptions || false,
375
- includeExamples: agent.includeToolExamples !== false,
376
- includeUsageGuidelines: agent.includeUsageGuidelines !== false,
377
- includeSecurityNotes: agent.includeSecurityNotes !== false
378
- }
379
- );
380
-
381
- updates.systemPrompt = enhancedSystemPrompt;
382
- // Always keep originalSystemPrompt in sync with what the user wrote
383
- if (updates.originalSystemPrompt === undefined) {
384
- updates.originalSystemPrompt = baseSystemPrompt;
385
- }
386
-
387
- this.logger.info(`System prompt regenerated with updated capabilities`, {
388
- agentId,
389
- oldCapabilities: agent.capabilities,
390
- newCapabilities: capabilities,
391
- originalLength: baseSystemPrompt?.length || 0,
392
- enhancedLength: enhancedSystemPrompt?.length || 0
393
- });
394
-
395
- // The scheduler caches per-(agent, model) Responses-API prompts
396
- // built from this agent's `originalSystemPrompt` + capabilities.
397
- // Both inputs just changed, so any cached rebuilds are stale.
398
- // No-op when the scheduler isn't attached (tests / very-early
399
- // boot) or when it predates this method (old binaries during
400
- // a rolling upgrade).
401
- try {
402
- this.scheduler?._invalidateNativePromptCache?.(agentId);
403
- } catch (e) {
404
- this.logger.debug?.('Failed to invalidate native prompt cache', { agentId, error: e.message });
405
- }
406
- } catch (error) {
407
- this.logger.error(`Failed to regenerate system prompt with updated capabilities`, {
408
- agentId,
409
- error: error.message,
410
- capabilities: updates.capabilities
411
- });
412
- // Continue with update even if enhancement fails
413
- }
414
- }
415
-
416
- // Create updated agent object with new values
417
- const updatedAgent = {
418
- ...agent,
419
- ...updates,
420
- id: agentId, // Ensure ID cannot be changed
421
- lastModified: new Date().toISOString(),
422
- lastActivity: new Date().toISOString()
423
- };
424
-
425
- // CRITICAL FIX: When preferredModel changes, also update currentModel
426
- // This ensures the UI immediately reflects the model change
427
- if (updates.preferredModel && updates.preferredModel !== agent.preferredModel) {
428
- const oldModel = agent.preferredModel;
429
- const newModel = updates.preferredModel;
430
-
431
- updatedAgent.currentModel = newModel;
432
-
433
- // CRITICAL FIX: Initialize conversation for new model if it doesn't exist
434
- if (!updatedAgent.conversations[newModel]) {
435
- updatedAgent.conversations[newModel] = this._createEmptyConversation(newModel);
436
- this.logger.info(`Created conversation for new model: ${newModel}`, { agentId });
437
- }
438
-
439
- // Copy conversation history from old model to new model
440
- // This preserves context when switching models
441
- if (oldModel && updatedAgent.conversations[oldModel]) {
442
- const oldConversation = updatedAgent.conversations[oldModel];
443
- const newConversation = updatedAgent.conversations[newModel];
444
-
445
- // Copy messages if new conversation is empty
446
- if (newConversation.messages.length === 0 && oldConversation.messages.length > 0) {
447
- // Copy original messages
448
- newConversation.messages = [...oldConversation.messages];
449
-
450
- // Copy compacted messages if they exist
451
- if (oldConversation.compactizedMessages) {
452
- newConversation.compactizedMessages = [...oldConversation.compactizedMessages];
453
- newConversation.lastCompactization = oldConversation.lastCompactization;
454
- newConversation.compactizationCount = oldConversation.compactizationCount;
455
- newConversation.compactizationStrategy = oldConversation.compactizationStrategy;
456
- newConversation.originalTokenCount = oldConversation.originalTokenCount;
457
- newConversation.compactedTokenCount = oldConversation.compactedTokenCount;
458
- // CRITICAL: Copy the sync watermark too without this, getMessagesForAI
459
- // cannot sync new messages to compactizedMessages after a model switch,
460
- // causing the AI to only see the compacted summary and repeat itself endlessly.
461
- newConversation.originalMessageCountAtCompaction = oldConversation.originalMessageCountAtCompaction;
462
- }
463
-
464
- newConversation.lastUpdated = new Date().toISOString();
465
-
466
- this.logger.info(`Copied conversation history from ${oldModel} to ${newModel}`, {
467
- agentId,
468
- messageCount: newConversation.messages.length,
469
- hasCompacted: !!newConversation.compactizedMessages
470
- });
471
- }
472
- }
473
-
474
- this.logger.info(`Model changed via UI - updating both preferredModel and currentModel`, {
475
- agentId,
476
- oldModel,
477
- newModel,
478
- conversationCopied: oldModel && updatedAgent.conversations[oldModel]?.messages.length > 0
479
- });
480
- }
481
-
482
- // Update agent in registry
483
- this.agents.set(agentId, updatedAgent);
484
-
485
- // Log the actual update for debugging
486
- this.logger.info(`Agent updated in registry with mode: ${updatedAgent.mode}`, {
487
- agentId,
488
- beforeMode: agent.mode,
489
- afterMode: updatedAgent.mode,
490
- allUpdates: Object.keys(updates)
491
- });
492
-
493
- // Update agent directory
494
- this._updateAgentDirectory(updatedAgent);
495
-
496
- // Persist the updated agent state
497
- await this.stateManager.persistAgentState(updatedAgent);
498
-
499
- // Record the mode transition (both directions) into the scheduler's
500
- // per-agent history. This is where UI-toggle and programmatic
501
- // update_agent flips land internal scheduler flips go through
502
- // scheduler._transitionMode directly. Both feed the same ring buffer
503
- // and the same /scheduler visualizer row.
504
- if (updates.mode !== undefined && agent.mode !== updates.mode && this.scheduler?.recordModeTransition) {
505
- this.scheduler.recordModeTransition(agentId, agent.mode, updates.mode, 'user-toggle');
506
- }
507
-
508
- // CRITICAL: If agent was switched to AGENT mode, add it to scheduler
509
- if (updates.mode === AGENT_MODES.AGENT && this.scheduler) {
510
- // CRITICAL FIX: Use the session ID from updates first, then agent's sessionId
511
- // Register session with scheduler for API key resolution
512
- // NOTE: The scheduler now uses AgentActivityService to determine which agents
513
- // should be active, so we just register the session here
514
- const sessionId = updates.sessionId || updatedAgent.sessionId;
515
-
516
- if (!sessionId) {
517
- this.logger.warn(`Agent ${agentId} switching to AGENT mode but has no sessionId - this will cause API key resolution issues`);
518
- }
519
-
520
- this.logger.info(`Registering agent session with scheduler (switched to AGENT mode): ${agentId}`, {
521
- agentName: updatedAgent.name,
522
- sessionId: sessionId,
523
- hasSessionId: !!sessionId
524
- });
525
-
526
- await this.scheduler.addAgent(agentId, {
527
- sessionId: sessionId,
528
- triggeredBy: 'mode-change-to-agent'
529
- });
530
- }
531
-
532
- // If agent was switched from AGENT to CHAT mode, clean up session tracking
533
- // NOTE: The agent will automatically become inactive in the next scheduler cycle
534
- // based on AgentActivityService.shouldAgentBeActive() returning false
535
- if (agent.mode === AGENT_MODES.AGENT && updates.mode === AGENT_MODES.CHAT && this.scheduler) {
536
- this.logger.info(`Agent mode changed to CHAT - will become inactive: ${agentId}`);
537
- this.scheduler.removeAgent(agentId, 'mode-change-to-chat');
538
- }
539
-
540
- this.logger.info(`Agent updated successfully: ${agentId}`, {
541
- newName: updatedAgent.name,
542
- changes: Object.keys(updates)
543
- });
544
-
545
- return updatedAgent;
546
- }
547
-
548
- /**
549
- * Agent notification from Message Processor for inter-agent communication
550
- * @param {string} agentId - Target agent ID
551
- * @param {Object} message - Message object with agent redirect
552
- * @returns {Promise<boolean>} Success status
553
- */
554
- async notifyAgent(agentId, message) {
555
- const agent = await this.getAgent(agentId);
556
- if (!agent) {
557
- this.logger.warn(`Agent notification failed - agent not found: ${agentId}`);
558
- return false;
559
- }
560
-
561
- // Check if agent is paused
562
- if (this._isAgentPaused(agent)) {
563
- this.logger.info(`Agent notification queued - agent is paused: ${agentId}`);
564
- this._queueNotification(agentId, message);
565
- return true;
566
- }
567
-
568
- // Add notification to agent's conversation
569
- const notificationMessage = {
570
- id: `msg-${Date.now()}`,
571
- conversationId: message.conversationId,
572
- agentId: message.from, // sender agent ID
573
- content: message.content,
574
- role: MESSAGE_ROLES.SYSTEM,
575
- timestamp: new Date().toISOString(),
576
- type: MESSAGE_TYPES.AGENT_NOTIFICATION,
577
- fromAgent: message.from,
578
- context: message.context,
579
- urgent: message.urgent || false,
580
- requiresResponse: message.requiresResponse || false
581
- };
582
-
583
- // Add to full conversation
584
- agent.conversations.full.messages.push(notificationMessage);
585
- agent.conversations.full.lastUpdated = new Date().toISOString();
586
-
587
- // Add to current model conversation
588
- if (agent.currentModel && agent.conversations[agent.currentModel]) {
589
- const formattedMessage = this._formatMessageForModel(notificationMessage, agent.currentModel);
590
- agent.conversations[agent.currentModel].messages.push(formattedMessage);
591
- agent.conversations[agent.currentModel].lastUpdated = new Date().toISOString();
592
- }
593
-
594
- // Update agent activity
595
- agent.lastActivity = new Date().toISOString();
596
- await this.persistAgentState(agentId);
597
-
598
- this.logger.info(`Agent notified: ${agentId}`, {
599
- fromAgent: message.from,
600
- urgent: message.urgent,
601
- requiresResponse: message.requiresResponse
602
- });
603
-
604
- return true;
605
- }
606
-
607
- /**
608
- * Get all agents (returns full agent objects)
609
- * @returns {Promise<Array>} List of all agent objects
610
- */
611
- async getAllAgents() {
612
- const agents = Array.from(this.agents.values());
613
-
614
- // Update pause status for all agents
615
- for (const agent of agents) {
616
- this._updateAgentPauseStatus(agent);
617
- }
618
-
619
- return agents;
620
- }
621
-
622
- /**
623
- * List all active agents with their current status
624
- * @returns {Promise<Array>} Array of agent objects
625
- */
626
- async listActiveAgents() {
627
- const agents = Array.from(this.agents.values());
628
-
629
- // Update pause status for all agents
630
- for (const agent of agents) {
631
- this._updateAgentPauseStatus(agent);
632
- }
633
-
634
- return agents.map(agent => ({
635
- id: agent.id,
636
- name: agent.name,
637
- type: agent.type,
638
- status: agent.status,
639
- mode: agent.mode,
640
- systemPrompt: agent.systemPrompt,
641
- originalSystemPrompt: agent.originalSystemPrompt,
642
- preferredModel: agent.preferredModel,
643
- currentModel: agent.currentModel,
644
- dynamicModelRouting: agent.dynamicModelRouting,
645
- routingStrategy: agent.routingStrategy || '',
646
- skills: agent.skills || [],
647
- platformProvided: agent.platformProvided,
648
- capabilities: agent.capabilities,
649
- directoryAccess: agent.directoryAccess,
650
- toolConfig: agent.toolConfig || {},
651
- lastActivity: agent.lastActivity,
652
- isPaused: this._isAgentPaused(agent),
653
- pausedUntil: agent.pausedUntil,
654
- messageCount: agent.conversations.full.messages.length,
655
- createdAt: agent.createdAt,
656
- // First user message snippet for card preview (2 lines max)
657
- firstUserMessage: this._getFirstUserMessageSnippet(agent)
658
- }));
659
- }
660
-
661
- /**
662
- * Persist agent state to storage
663
- * @param {string} agentId - Agent identifier
664
- * @returns {Promise<void>}
665
- */
666
- async persistAgentState(agentId) {
667
- const agent = await this.getAgent(agentId);
668
- if (!agent) {
669
- throw new Error(`Agent not found: ${agentId}`);
670
- }
671
-
672
- await this.stateManager.persistAgentState(agent);
673
- }
674
-
675
- /**
676
- * Rehydrate an agent from persisted state on session boot. NOT the
677
- * pause-toggle (see `resumeAgent(agentId)` below). The two were
678
- * historically both named `resumeAgent`, which made this one dead
679
- * code JS class bodies keep the second definition only, so callers
680
- * passing an object ended up in the id-based handler and silently got
681
- * "Agent not found".
682
- *
683
- * Callers: agentPool.restoreAgent, which is in turn called from
684
- * orchestrator's boot-time state restoration path.
685
- *
686
- * @param {Object} agentData - Persisted agent data
687
- * @returns {Promise<Object>} Restored agent object
688
- */
689
- async rehydrateFromState(agentData) {
690
- const agent = {
691
- ...agentData,
692
- status: agentData.status === 'paused' && this._isPauseExpired(agentData) ? 'active' : agentData.status
693
- };
694
-
695
- // RECOVERY: If agent was paused awaiting user input (e.g., credentials),
696
- // the promise is now lost due to server/UI restart. Resume the agent.
697
- if (agent.awaitingUserInput) {
698
- this.logger.warn(`Agent ${agent.id} was awaiting user input (${agent.awaitingUserInput.type}) - recovering from interrupted state`, {
699
- inputType: agent.awaitingUserInput.type,
700
- siteId: agent.awaitingUserInput.siteId,
701
- startedAt: agent.awaitingUserInput.startedAt
702
- });
703
-
704
- // Clear the awaiting flag and resume agent
705
- delete agent.awaitingUserInput;
706
- agent.status = AGENT_STATUS.ACTIVE;
707
-
708
- // Add a system message to the agent's queue so it knows what happened
709
- if (!agent.messageQueues) {
710
- agent.messageQueues = { toolResults: [], interAgentMessages: [], userMessages: [] };
711
- }
712
- agent.messageQueues.toolResults.push({
713
- id: `recovery-${Date.now()}`,
714
- toolId: 'system-recovery',
715
- status: 'info',
716
- result: {
717
- message: 'Agent was waiting for user input (credentials) when the session was interrupted. The credential request has been cancelled. Please retry the authentication if needed.',
718
- recoveredFrom: 'awaitingUserInput',
719
- timestamp: new Date().toISOString()
720
- },
721
- timestamp: new Date().toISOString()
722
- });
723
- }
724
-
725
- // Validate conversations structure
726
- if (!agent.conversations || !agent.conversations.full) {
727
- agent.conversations = {
728
- full: {
729
- messages: [],
730
- lastUpdated: new Date().toISOString()
731
- }
732
- };
733
- }
734
-
735
- // CRITICAL: Restore interAgentTracking as a Map (it comes as plain object from JSON)
736
- if (!agent.interAgentTracking || !(agent.interAgentTracking instanceof Map)) {
737
- // Convert plain object to Map, or create empty Map
738
- if (agent.interAgentTracking && typeof agent.interAgentTracking === 'object') {
739
- agent.interAgentTracking = new Map(Object.entries(agent.interAgentTracking));
740
- } else {
741
- agent.interAgentTracking = new Map();
742
- }
743
- }
744
-
745
- // Add to registry and directory
746
- this.agents.set(agent.id, agent);
747
- this._updateAgentDirectory(agent);
748
-
749
- // CRITICAL: Migrate conversation structure to ensure new fields exist
750
- // This handles agents persisted before the originalMessageCountAtCompaction fix
751
- await this.migrateConversationStructure(agent.id);
752
-
753
- // Process any queued notifications
754
- await this._processQueuedNotifications(agent.id);
755
-
756
- this.logger.info(`Agent resumed: ${agent.id}`, {
757
- name: agent.name,
758
- status: agent.status,
759
- messageCount: agent.conversations.full.messages.length
760
- });
761
-
762
- return agent;
763
- }
764
-
765
- /**
766
- * Pause agent for specified duration
767
- * @param {string} agentId - Agent identifier
768
- * @param {number|Date} duration - Pause duration in seconds or Date object
769
- * @param {string} reason - Reason for pause
770
- * @returns {Promise<Object>} Pause confirmation
771
- */
772
- async pauseAgent(agentId, duration, reason = 'Agent pause requested') {
773
- const agent = await this.getAgent(agentId);
774
- if (!agent) {
775
- throw new Error(`Agent not found: ${agentId}`);
776
- }
777
-
778
- let pauseUntil;
779
- if (duration instanceof Date) {
780
- pauseUntil = duration;
781
- } else {
782
- // Duration in seconds
783
- const maxPauseDuration = this.config.system?.maxPauseDuration || 300;
784
- const pauseSeconds = Math.min(duration, maxPauseDuration);
785
- pauseUntil = new Date(Date.now() + pauseSeconds * 1000);
786
- }
787
-
788
- agent.status = AGENT_STATUS.PAUSED;
789
- agent.pausedUntil = pauseUntil.toISOString();
790
- agent.lastActivity = new Date().toISOString();
791
-
792
- // Add to paused agents tracking
793
- this.pausedAgents.set(agentId, {
794
- agentId,
795
- pausedAt: new Date().toISOString(),
796
- pausedUntil: pauseUntil.toISOString(),
797
- reason,
798
- originalStatus: AGENT_STATUS.ACTIVE
799
- });
800
-
801
- await this.persistAgentState(agentId);
802
-
803
- this.logger.info(`Agent paused: ${agentId}`, {
804
- pausedUntil: pauseUntil.toISOString(),
805
- reason,
806
- durationSeconds: Math.round((pauseUntil.getTime() - Date.now()) / 1000)
807
- });
808
-
809
- return {
810
- success: true,
811
- agentId,
812
- pausedUntil: pauseUntil.toISOString(),
813
- reason,
814
- message: `Agent paused until ${pauseUntil.toISOString()}`
815
- };
816
- }
817
-
818
- /**
819
- * Resume paused agent
820
- * @param {string} agentId - Agent identifier
821
- * @returns {Promise<Object>} Resume confirmation
822
- */
823
- async resumeAgent(agentId) {
824
- const agent = await this.getAgent(agentId);
825
- if (!agent) {
826
- throw new Error(`Agent not found: ${agentId}`);
827
- }
828
-
829
- if (agent.status !== AGENT_STATUS.PAUSED) {
830
- return {
831
- success: true,
832
- message: `Agent ${agentId} is not paused`
833
- };
834
- }
835
-
836
- agent.status = AGENT_STATUS.ACTIVE;
837
- agent.pausedUntil = null;
838
- agent.lastActivity = new Date().toISOString();
839
-
840
- // Remove from paused agents tracking
841
- this.pausedAgents.delete(agentId);
842
-
843
- // Process any queued notifications
844
- await this._processQueuedNotifications(agentId);
845
-
846
- await this.persistAgentState(agentId);
847
-
848
- this.logger.info(`Agent resumed: ${agentId}`);
849
-
850
- return {
851
- success: true,
852
- agentId,
853
- message: `Agent ${agentId} resumed successfully`
854
- };
855
- }
856
-
857
- /**
858
- * Restore agent from saved state. Thin wrapper around rehydrateFromState
859
- * kept for the public-API name orchestrator + stateManager still use.
860
- * @param {Object} agentState - Saved agent state
861
- * @returns {Promise<Object>} Restored agent
862
- */
863
- async restoreAgent(agentState) {
864
- return await this.rehydrateFromState(agentState);
865
- }
866
-
867
- /**
868
- * Get agent discovery directory
869
- * @returns {Array} Array of agent info for discovery
870
- */
871
- getAgentDirectory() {
872
- return Array.from(this.agentDirectory.values());
873
- }
874
-
875
- /**
876
- * List all active agents
877
- * @returns {Array} Array of active agents
878
- */
879
-
880
- /**
881
- * Delete an agent and clean up its resources.
882
- *
883
- * Best-effort / idempotent: if the agent isn't currently loaded in
884
- * memory we skip the live-state cleanup (attachments, visual editor,
885
- * terminal processes those only exist for loaded agents anyway)
886
- * and go straight to clearing persistent state. This is how the
887
- * bulk-delete UI can remove agents that were unloaded earlier in
888
- * the session or that survived a process restart with no in-memory
889
- * footprint. `stateManager.deleteAgentState` is itself ENOENT-safe,
890
- * so deleting an already-gone agent is a no-op success.
891
- *
892
- * @param {string} agentId - Agent identifier
893
- * @returns {Promise<Object>} Deletion result
894
- */
895
- async deleteAgent(agentId) {
896
- const agent = await this.getAgent(agentId);
897
- const wasLoaded = !!agent;
898
-
899
- if (wasLoaded) {
900
- // Clean up file attachments with reference counting
901
- if (this.fileAttachmentService) {
902
- try {
903
- await this.fileAttachmentService.deleteAgentAttachments(agentId);
904
- this.logger.info(`File attachments cleaned up for agent: ${agentId}`);
905
- } catch (error) {
906
- this.logger.warn(`Failed to clean up file attachments for agent: ${error.message}`, { agentId });
907
- // Continue with agent deletion even if attachment cleanup fails
908
- }
909
- }
910
-
911
- // Clear codebase-knowledge cache for this agent — bytes live in
912
- // the service's in-memory map; without this the per-agent entry
913
- // would persist until the next process restart.
914
- try {
915
- const { getCodebaseKnowledgeService } = await import('../services/codebaseKnowledgeService.js');
916
- getCodebaseKnowledgeService(this.logger).clear(agentId);
917
- } catch (e) {
918
- this.logger.debug?.('codebase-knowledge clear failed on agent delete', { agentId, error: e.message });
919
- }
920
-
921
- // Clean up visual editor instance
922
- try {
923
- const visualEditorBridge = getVisualEditorBridge();
924
- if (visualEditorBridge.hasInstance(agentId)) {
925
- await visualEditorBridge.stopInstance(agentId);
926
- this.logger.info(`Visual editor instance cleaned up for agent: ${agentId}`);
927
- }
928
- } catch (error) {
929
- this.logger.warn(`Failed to clean up visual editor for agent: ${error.message}`, { agentId });
930
- }
931
-
932
- // Kill any running terminal processes for this agent
933
- try {
934
- const terminalTool = this.toolsRegistry?.getTool?.('terminal');
935
- if (terminalTool && typeof terminalTool.cleanupAgent === 'function') {
936
- await terminalTool.cleanupAgent(agentId);
937
- this.logger.info(`Terminal processes cleaned up for agent: ${agentId}`);
938
- }
939
- } catch (error) {
940
- this.logger.warn(`Failed to clean up terminal processes for agent: ${error.message}`, { agentId });
941
- }
942
-
943
- // Clean up agent resources
944
- this.agents.delete(agentId);
945
- this.agentDirectory.delete(agentId);
946
- this.pausedAgents.delete(agentId);
947
- this.notificationQueue.delete(agentId);
948
- }
949
-
950
- // Clean up persistent state runs whether or not the agent was
951
- // loaded. deleteAgentState handles missing files gracefully.
952
- try {
953
- await this.stateManager.deleteAgentState(agentId);
954
- } catch (error) {
955
- this.logger.warn(`Failed to delete agent persistent state: ${error.message}`, { agentId });
956
- }
957
-
958
- this.logger.info(`Agent deleted: ${agentId}`, {
959
- agentName: agent?.name || '(not loaded)',
960
- wasLoaded,
961
- totalAgents: this.agents.size
962
- });
963
-
964
- return {
965
- success: true,
966
- agentId,
967
- remainingAgents: this.agents.size
968
- };
969
- }
970
-
971
- /**
972
- * Unload an agent from server memory without deleting persistent files
973
- * Agent can be reloaded later using the Load Agent feature
974
- * @param {string} agentId - Agent identifier
975
- * @returns {Promise<Object>} Unload result
976
- */
977
- async unloadAgent(agentId) {
978
- const agent = await this.getAgent(agentId);
979
- if (!agent) {
980
- throw new Error(`Agent not found: ${agentId}`);
981
- }
982
-
983
- const agentName = agent.name;
984
-
985
- // Persist current state before unloading (so it can be reloaded later)
986
- try {
987
- await this.persistAgentState(agentId);
988
- this.logger.info(`Agent state persisted before unload: ${agentId}`);
989
- } catch (error) {
990
- this.logger.warn(`Failed to persist agent state before unload: ${error.message}`, { agentId });
991
- }
992
-
993
- // Clean up visual editor instance
994
- try {
995
- const visualEditorBridge = getVisualEditorBridge();
996
- if (visualEditorBridge.hasInstance(agentId)) {
997
- await visualEditorBridge.stopInstance(agentId);
998
- this.logger.info(`Visual editor instance cleaned up for unloaded agent: ${agentId}`);
999
- }
1000
- } catch (error) {
1001
- this.logger.warn(`Failed to clean up visual editor for unloaded agent: ${error.message}`, { agentId });
1002
- }
1003
-
1004
- // Remove from memory only (keep persistent files)
1005
- this.agents.delete(agentId);
1006
- this.agentDirectory.delete(agentId);
1007
- this.pausedAgents.delete(agentId);
1008
- this.notificationQueue.delete(agentId);
1009
-
1010
- // Remove from scheduler if present
1011
- if (this.scheduler) {
1012
- this.scheduler.removeAgent(agentId, 'unloaded');
1013
- }
1014
-
1015
- this.logger.info(`Agent unloaded from memory: ${agentId}`, {
1016
- agentName,
1017
- totalAgents: this.agents.size,
1018
- note: 'Persistent files preserved for future reload'
1019
- });
1020
-
1021
- return {
1022
- success: true,
1023
- agentId,
1024
- agentName,
1025
- remainingAgents: this.agents.size,
1026
- message: `Agent "${agentName}" unloaded. Use Load Agent to reload it.`
1027
- };
1028
- }
1029
-
1030
- /**
1031
- * Clear all conversation history for an agent
1032
- * Resets the agent to a fresh state while keeping configuration
1033
- * @param {string} agentId - Agent identifier
1034
- * @returns {Promise<Object>} Clear result
1035
- */
1036
- async clearConversation(agentId) {
1037
- const agent = await this.getAgent(agentId);
1038
- if (!agent) {
1039
- throw new Error(`Agent not found: ${agentId}`);
1040
- }
1041
-
1042
- const previousMessageCount = agent.conversations?.full?.messages?.length || 0;
1043
-
1044
- // Reset full conversation
1045
- agent.conversations.full = {
1046
- messages: [],
1047
- lastUpdated: new Date().toISOString()
1048
- };
1049
-
1050
- // Reset model-specific conversations
1051
- for (const key of Object.keys(agent.conversations)) {
1052
- if (key !== 'full') {
1053
- agent.conversations[key] = {
1054
- messages: [],
1055
- compactizedMessages: null,
1056
- lastUpdated: new Date().toISOString(),
1057
- compactionState: {
1058
- isCompacted: false,
1059
- lastCompactionTime: null,
1060
- originalMessageCount: 0,
1061
- compactedMessageCount: 0
1062
- }
1063
- };
1064
- }
1065
- }
1066
-
1067
- // Clear message queues
1068
- if (agent.messageQueues) {
1069
- agent.messageQueues = {
1070
- toolResults: [],
1071
- interAgentMessages: [],
1072
- userMessages: []
1073
- };
1074
- }
1075
-
1076
- // Clear task list
1077
- if (agent.taskList) {
1078
- agent.taskList = {
1079
- tasks: [],
1080
- lastUpdated: new Date().toISOString()
1081
- };
1082
- }
1083
-
1084
- agent.currentTask = null;
1085
- agent.taskStartTime = null;
1086
- agent.iterationCount = 0;
1087
-
1088
- // Persist the cleared state
1089
- await this.persistAgentState(agentId);
1090
-
1091
- this.logger.info(`Conversation cleared for agent: ${agentId}`, {
1092
- agentName: agent.name,
1093
- previousMessageCount
1094
- });
1095
-
1096
- return {
1097
- success: true,
1098
- agentId,
1099
- previousMessageCount,
1100
- message: `Cleared ${previousMessageCount} messages`
1101
- };
1102
- }
1103
-
1104
- /**
1105
- * Generate unique agent ID
1106
- * @private
1107
- */
1108
- _generateAgentId(name) {
1109
- const sanitizedName = name.toLowerCase().replace(/[^a-z0-9]/g, '-');
1110
- const timestamp = Date.now();
1111
- return `agent-${sanitizedName}-${timestamp}`;
1112
- }
1113
-
1114
- /**
1115
- * Update agent directory for discovery
1116
- * @private
1117
- */
1118
- _updateAgentDirectory(agent) {
1119
- this.agentDirectory.set(agent.id, {
1120
- id: agent.id,
1121
- name: agent.name,
1122
- type: agent.type,
1123
- capabilities: agent.capabilities,
1124
- status: agent.status,
1125
- description: this._generateAgentDescription(agent)
1126
- });
1127
- }
1128
-
1129
- /**
1130
- * Generate agent description for directory
1131
- * @private
1132
- */
1133
- _generateAgentDescription(agent) {
1134
- let description = `${agent.name} (${agent.type})`;
1135
-
1136
- if (agent.capabilities.length > 0) {
1137
- description += ` - Capabilities: ${agent.capabilities.join(', ')}`;
1138
- }
1139
-
1140
- return description;
1141
- }
1142
-
1143
- /**
1144
- * Check if agent is currently paused
1145
- * @private
1146
- */
1147
- _isAgentPaused(agent) {
1148
- if (agent.status !== AGENT_STATUS.PAUSED || !agent.pausedUntil) {
1149
- return false;
1150
- }
1151
-
1152
- return new Date() < new Date(agent.pausedUntil);
1153
- }
1154
-
1155
- /**
1156
- * Get first user message snippet for card preview
1157
- * @private
1158
- */
1159
- _getFirstUserMessageSnippet(agent) {
1160
- const messages = agent.conversations?.full?.messages;
1161
- if (!messages || messages.length === 0) return null;
1162
-
1163
- // Find first user message include consolidated-input since that's how
1164
- // user messages are stored after queue processing. Skip task-boundary.
1165
- const firstUser = messages.find(m =>
1166
- m.role === 'user' && m.content &&
1167
- m.type !== 'task-boundary'
1168
- );
1169
- if (!firstUser) return null;
1170
-
1171
- // Handle both string and array content formats
1172
- const text = typeof firstUser.content === 'string'
1173
- ? firstUser.content
1174
- : Array.isArray(firstUser.content)
1175
- ? firstUser.content.filter(b => b.type === 'text').map(b => b.text).join('\n')
1176
- : null;
1177
- if (!text) return null;
1178
-
1179
- // Take first 2 non-empty lines, cap at 120 chars
1180
- const lines = text.split('\n').filter(l => l.trim());
1181
- const snippet = lines.slice(0, 2).join('\n');
1182
- return snippet.length > 120 ? snippet.slice(0, 117) + '...' : snippet;
1183
- }
1184
-
1185
- /**
1186
- * Check if pause duration has expired
1187
- * @private
1188
- */
1189
- _isPauseExpired(agent) {
1190
- if (!agent.pausedUntil) return true;
1191
- return new Date() >= new Date(agent.pausedUntil);
1192
- }
1193
-
1194
- /**
1195
- * Update agent pause status
1196
- * @private
1197
- */
1198
- _updateAgentPauseStatus(agent) {
1199
- if (agent.status === AGENT_STATUS.PAUSED && this._isPauseExpired(agent)) {
1200
- agent.status = AGENT_STATUS.ACTIVE;
1201
- agent.pausedUntil = null;
1202
- this.pausedAgents.delete(agent.id);
1203
- }
1204
- }
1205
-
1206
- /**
1207
- * Queue notification for paused agent
1208
- * @private
1209
- */
1210
- _queueNotification(agentId, message) {
1211
- if (!this.notificationQueue.has(agentId)) {
1212
- this.notificationQueue.set(agentId, []);
1213
- }
1214
-
1215
- this.notificationQueue.get(agentId).push({
1216
- ...message,
1217
- queuedAt: new Date().toISOString()
1218
- });
1219
- }
1220
-
1221
- /**
1222
- * Process queued notifications for agent
1223
- * @private
1224
- */
1225
- async _processQueuedNotifications(agentId) {
1226
- const notifications = this.notificationQueue.get(agentId);
1227
- if (!notifications || notifications.length === 0) {
1228
- return;
1229
- }
1230
-
1231
- this.logger.info(`Processing ${notifications.length} queued notifications for agent: ${agentId}`);
1232
-
1233
- for (const notification of notifications) {
1234
- await this.notifyAgent(agentId, notification);
1235
- }
1236
-
1237
- // Clear queue
1238
- this.notificationQueue.delete(agentId);
1239
- }
1240
-
1241
- /**
1242
- * Format message for specific model
1243
- * @private
1244
- */
1245
- _formatMessageForModel(message, targetModel) {
1246
- // This would be implemented with model-specific formatting logic
1247
- // For now, return the message as-is
1248
- return { ...message };
1249
- }
1250
-
1251
- /**
1252
- * Get model format version
1253
- * @private
1254
- */
1255
- _getModelFormatVersion(model) {
1256
- return MODEL_FORMAT_VERSIONS[model] || MODEL_FORMAT_VERSIONS.DEFAULT;
1257
- }
1258
-
1259
- /**
1260
- * Refresh tool descriptions for an existing agent
1261
- * @param {string} agentId - Agent identifier
1262
- * @param {Object} options - Refresh options
1263
- * @returns {Promise<boolean>} Success status
1264
- */
1265
- async refreshAgentToolDescriptions(agentId, options = {}) {
1266
- const agent = await this.getAgent(agentId);
1267
- if (!agent || !this.toolsRegistry) {
1268
- return false;
1269
- }
1270
-
1271
- try {
1272
- // Use original prompt if available, otherwise current prompt
1273
- const basePrompt = agent.originalSystemPrompt || agent.systemPrompt;
1274
-
1275
- // Enhance with current tool capabilities
1276
- const enhancedPrompt = this.toolsRegistry.enhanceSystemPrompt(
1277
- basePrompt,
1278
- agent.capabilities,
1279
- {
1280
- compact: options.compact || false,
1281
- includeExamples: options.includeExamples !== false,
1282
- includeUsageGuidelines: options.includeUsageGuidelines !== false,
1283
- includeSecurityNotes: options.includeSecurityNotes !== false
1284
- }
1285
- );
1286
-
1287
- // Update agent's system prompt
1288
- agent.systemPrompt = enhancedPrompt;
1289
- agent.lastActivity = new Date().toISOString();
1290
-
1291
- // Persist changes
1292
- await this.persistAgentState(agentId);
1293
-
1294
- this.logger?.info(`Agent tool descriptions refreshed: ${agentId}`, {
1295
- capabilities: agent.capabilities,
1296
- promptLength: enhancedPrompt.length
1297
- });
1298
-
1299
- return true;
1300
-
1301
- } catch (error) {
1302
- this.logger?.error(`Failed to refresh tool descriptions for agent: ${agentId}`, {
1303
- error: error.message
1304
- });
1305
- return false;
1306
- }
1307
- }
1308
-
1309
- /**
1310
- * Set or update tools registry for the agent pool
1311
- * @param {ToolsRegistry} toolsRegistry - Tools registry instance
1312
- */
1313
- setToolsRegistry(toolsRegistry) {
1314
- this.toolsRegistry = toolsRegistry;
1315
-
1316
- this.logger?.info('Tools registry updated for agent pool', {
1317
- hasRegistry: !!toolsRegistry
1318
- });
1319
- }
1320
-
1321
- /**
1322
- * Bulk refresh tool descriptions for all agents
1323
- * @param {Object} options - Refresh options
1324
- * @returns {Promise<Object>} Results summary
1325
- */
1326
- async bulkRefreshToolDescriptions(options = {}) {
1327
- const results = {
1328
- total: this.agents.size,
1329
- successful: 0,
1330
- failed: 0,
1331
- skipped: 0
1332
- };
1333
-
1334
- for (const [agentId, agent] of this.agents.entries()) {
1335
- if (!agent.capabilities || agent.capabilities.length === 0) {
1336
- results.skipped++;
1337
- continue;
1338
- }
1339
-
1340
- const success = await this.refreshAgentToolDescriptions(agentId, options);
1341
- if (success) {
1342
- results.successful++;
1343
- } else {
1344
- results.failed++;
1345
- }
1346
- }
1347
-
1348
- this.logger?.info('Bulk tool descriptions refresh completed', results);
1349
- return results;
1350
- }
1351
-
1352
- /**
1353
- * Set MessageProcessor reference for triggering responses
1354
- * @param {MessageProcessor} messageProcessor - MessageProcessor instance
1355
- */
1356
- setMessageProcessor(messageProcessor) {
1357
- this.messageProcessor = messageProcessor;
1358
- }
1359
-
1360
- /**
1361
- * Set AgentScheduler reference for managing agent modes
1362
- * @param {AgentScheduler} scheduler - AgentScheduler instance
1363
- */
1364
- setScheduler(scheduler) {
1365
- this.scheduler = scheduler;
1366
-
1367
- this.logger?.info('AgentScheduler reference set for agent pool', {
1368
- hasScheduler: !!scheduler
1369
- });
1370
- }
1371
-
1372
- /**
1373
- * Set FileAttachmentService reference for cleaning up attachments
1374
- * @param {FileAttachmentService} fileAttachmentService - FileAttachmentService instance
1375
- */
1376
- setFileAttachmentService(fileAttachmentService) {
1377
- this.fileAttachmentService = fileAttachmentService;
1378
-
1379
- this.logger?.info('FileAttachmentService reference set for agent pool', {
1380
- hasService: !!fileAttachmentService
1381
- });
1382
- }
1383
-
1384
- // OLD INTER-AGENT MESSAGE QUEUE SYSTEM REMOVED
1385
- // Now using the new messageQueues system with AgentScheduler
1386
- // Inter-agent messages are queued via addInterAgentMessage() method
1387
-
1388
- /**
1389
- * Wake an agent out of any paused/delayed state because a message has
1390
- * arrived for it. Shared by addUserMessage / addInterAgentMessage /
1391
- * addToolResult every inbound path MUST route through this to avoid
1392
- * the "silent queue into a delayed agent" bug where the recipient never
1393
- * wakes up and the message sits until the scheduler back-off naturally
1394
- * expires.
1395
- *
1396
- * Rationale: delays/pauses are "leave this agent alone" signals set by
1397
- * the scheduler on error back-off or by the agentDelayTool for timed
1398
- * waits. Any caller going to the trouble of actually addressing the
1399
- * agent is an explicit "act now" signal that overrides the wait.
1400
- *
1401
- * @param {Object} agent - Agent object (already fetched)
1402
- * @param {string} reason - Source label for logs (e.g. 'user-message',
1403
- * 'inter-agent-message', 'tool-result')
1404
- * @returns {Promise<{wasPaused:boolean, hadDelay:boolean, hadPausedUntil:boolean}>}
1405
- * @private
1406
- */
1407
- async _wakeAgentForMessage(agent, reason) {
1408
- const info = { wasPaused: false, hadDelay: false, hadPausedUntil: false };
1409
- if (!agent) return info;
1410
-
1411
- // Auto-resume explicitly paused agent.
1412
- if (agent.status === AGENT_STATUS.PAUSED) {
1413
- info.wasPaused = true;
1414
- this.logger.info(`Auto-resuming paused agent ${agent.id} due to ${reason}`);
1415
- await this.resumeAgent(agent.id);
1416
- }
1417
-
1418
- // Clear scheduler-enforced delay (rate-limit back-off, api-key delay,
1419
- // builtin webTool delay, etc.). Only clear if actually in the future;
1420
- // stale past values don't matter but shouldn't trigger a broadcast.
1421
- if (agent.delayEndTime && new Date(agent.delayEndTime).getTime() > Date.now()) {
1422
- info.hadDelay = true;
1423
- agent.delayEndTime = null;
1424
- this.logger.info(`Cleared scheduler delay for agent ${agent.id} — ${reason} takes precedence`);
1425
- }
1426
-
1427
- // Defensive: if pausedUntil is set but status isn't PAUSED (shouldn't
1428
- // happen but protects against state drift), clear it too.
1429
- if (agent.pausedUntil && new Date(agent.pausedUntil).getTime() > Date.now()) {
1430
- info.hadPausedUntil = true;
1431
- agent.pausedUntil = null;
1432
- }
1433
-
1434
- return info;
1435
- }
1436
-
1437
- /**
1438
- * Broadcast a delay-clear to the WS so the UI's delay chip disappears
1439
- * immediately instead of waiting for the next scheduler tick.
1440
- * @private
1441
- */
1442
- async _broadcastWake(agentId, reason) {
1443
- if (!this.scheduler?.broadcastAgentStateUpdate) return;
1444
- try {
1445
- await this.scheduler.broadcastAgentStateUpdate(agentId, reason);
1446
- } catch (err) {
1447
- this.logger.warn(`Failed to broadcast wake for ${agentId}: ${err.message}`);
1448
- }
1449
- }
1450
-
1451
- /**
1452
- * Add message to agent's user message queue
1453
- * @param {string} agentId - Agent ID
1454
- * @param {Object} message - User message to queue
1455
- * @returns {Promise<void>}
1456
- */
1457
- async addUserMessage(agentId, message) {
1458
- const agent = await this.getAgent(agentId);
1459
- if (!agent) {
1460
- throw new Error(`Agent not found: ${agentId}`);
1461
- }
1462
-
1463
- // Any inbound message — user, inter-agent, or tool-result takes
1464
- // precedence over scheduler back-off and manual pauses. See
1465
- // _wakeAgentForMessage for the rationale.
1466
- const wakeInfo = await this._wakeAgentForMessage(agent, 'user-message');
1467
-
1468
- const queuedMessage = {
1469
- ...message,
1470
- id: message.id || `user-msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
1471
- queuedAt: new Date().toISOString(),
1472
- timestamp: message.timestamp || new Date().toISOString()
1473
- };
1474
-
1475
- agent.messageQueues.userMessages.push(queuedMessage);
1476
-
1477
- // Auto-create a task for AGENT mode agents so the scheduler picks them up.
1478
- // Scheduling condition is purely task-based: has pending tasks AND in agent mode.
1479
- if (agent.mode === AGENT_MODES.AGENT) {
1480
- this._autoCreateTaskForMessage(agent, queuedMessage, 'user', 'high');
1481
- }
1482
-
1483
- // ── Auto-save substantive user messages as plan/* memories ───────
1484
- // Observed in production: across 670-message agent sessions the
1485
- // agent NEVER wrote a memory voluntarily. Compaction then summarized
1486
- // away the user's literal asks, the agent paraphrased what was left,
1487
- // and ended up doing work the user never requested. Belt-and-
1488
- // suspenders alongside the OPERATING POSTURE prompt nudge: when a
1489
- // user message looks substantive (long, or contains a numbered/
1490
- // bulleted multi-part ask), the SYSTEM saves it as `plan/<auto>` so
1491
- // the system-prompt auto-injection makes the user's words visible
1492
- // every turn even if the agent itself never thought to save.
1493
- // Best-effort: never block the message-enqueue path.
1494
- this._autoSaveUserMessageAsPlan(agentId, queuedMessage).catch(err => {
1495
- this.logger.debug?.('Auto-save of user message as plan/* failed (continuing)', {
1496
- agentId, error: err?.message,
1497
- });
1498
- });
1499
-
1500
- await this.persistAgentState(agentId);
1501
-
1502
- // If we cleared a delay, surface it on the WS so the delay chip in the
1503
- // chat header disappears without waiting for the next scheduler cycle.
1504
- if (wakeInfo.hadDelay) {
1505
- await this._broadcastWake(agentId, 'user-message-clears-delay');
1506
- }
1507
-
1508
- this.logger.info(`User message queued for agent: ${agentId}`, {
1509
- messageId: queuedMessage.id,
1510
- queueSize: agent.messageQueues.userMessages.length
1511
- });
1512
- }
1513
-
1514
- /**
1515
- * Add message to agent's inter-agent message queue
1516
- * @param {string} agentId - Agent ID
1517
- * @param {Object} message - Inter-agent message to queue
1518
- * @returns {Promise<void>}
1519
- */
1520
- async addInterAgentMessage(agentId, message) {
1521
- const agent = await this.getAgent(agentId);
1522
- if (!agent) {
1523
- throw new Error(`Agent not found: ${agentId}`);
1524
- }
1525
-
1526
- // An inter-agent ping from another agent is an explicit "act now"
1527
- // signal and must override any scheduler back-off or manual pause
1528
- // on the recipient. See _wakeAgentForMessage.
1529
- const wakeInfo = await this._wakeAgentForMessage(agent, 'inter-agent-message');
1530
-
1531
- const queuedMessage = {
1532
- ...message,
1533
- id: message.id || message.messageId || `inter-agent-msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
1534
- queuedAt: new Date().toISOString(),
1535
- timestamp: message.timestamp || new Date().toISOString()
1536
- };
1537
-
1538
- agent.messageQueues.interAgentMessages.push(queuedMessage);
1539
-
1540
- // Auto-create a task for AGENT mode agents so the scheduler picks them up.
1541
- if (agent.mode === AGENT_MODES.AGENT) {
1542
- const senderName = message.senderName || message.sender || 'Unknown Agent';
1543
- this._autoCreateTaskForMessage(agent, queuedMessage, `inter-agent from ${senderName}`, 'medium');
1544
- }
1545
-
1546
- await this.persistAgentState(agentId);
1547
-
1548
- // CRITICAL: Register recipient with scheduler so it has a sessionId for API key resolution.
1549
- // Inter-agent messages carry the sender's sessionId — reuse it for the recipient.
1550
- if (this.scheduler && message.sessionId) {
1551
- await this.scheduler.addAgent(agentId, {
1552
- sessionId: message.sessionId,
1553
- triggeredBy: 'inter-agent-message'
1554
- });
1555
- }
1556
-
1557
- if (wakeInfo.hadDelay) {
1558
- await this._broadcastWake(agentId, 'inter-agent-message-clears-delay');
1559
- }
1560
-
1561
- this.logger.info(`Inter-agent message queued for agent: ${agentId}`, {
1562
- messageId: queuedMessage.id,
1563
- sender: message.sender || message.senderName,
1564
- queueSize: agent.messageQueues.interAgentMessages.length,
1565
- sessionRegistered: !!(this.scheduler && message.sessionId)
1566
- });
1567
- }
1568
-
1569
- /**
1570
- * Add tool result to agent's tool results queue
1571
- * @param {string} agentId - Agent ID
1572
- * @param {Object} toolResult - Tool result to queue
1573
- * @returns {Promise<void>}
1574
- */
1575
- async addToolResult(agentId, toolResult) {
1576
- const agent = await this.getAgent(agentId);
1577
- if (!agent) {
1578
- throw new Error(`Agent not found: ${agentId}`);
1579
- }
1580
-
1581
- // Tool results are "external async work finished continue."
1582
- // If the agent is delayed or paused when a result arrives (e.g. a
1583
- // long image/video render completes while an unrelated rate-limit
1584
- // back-off is still in effect) we wake it so the result can be
1585
- // consumed immediately instead of sitting until the back-off expires.
1586
- const wakeInfo = await this._wakeAgentForMessage(agent, 'tool-result');
1587
-
1588
- const queuedResult = {
1589
- ...toolResult,
1590
- id: toolResult.id || `tool-result-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
1591
- queuedAt: new Date().toISOString(),
1592
- timestamp: toolResult.timestamp || new Date().toISOString()
1593
- };
1594
-
1595
- agent.messageQueues.toolResults.push(queuedResult);
1596
- await this.persistAgentState(agentId);
1597
-
1598
- if (wakeInfo.hadDelay) {
1599
- await this._broadcastWake(agentId, 'tool-result-clears-delay');
1600
- }
1601
-
1602
- this.logger.debug(`Tool result queued for agent: ${agentId}`, {
1603
- toolId: toolResult.toolId,
1604
- status: toolResult.status,
1605
- queueSize: agent.messageQueues.toolResults.length
1606
- });
1607
- }
1608
-
1609
- /**
1610
- * Auto-create a task from an incoming message for AGENT mode agents.
1611
- * This ensures the scheduler (which uses pending tasks as the sole activation
1612
- * condition for AGENT mode) picks up the agent for processing.
1613
- * @param {Object} agent - Agent object
1614
- * @param {Object} message - The queued message
1615
- * @param {string} source - Source label (e.g. 'user', 'inter-agent from AgentX')
1616
- * @param {string} priority - Task priority ('high', 'medium', 'low')
1617
- * @private
1618
- */
1619
- /**
1620
- * Save a substantive user message as a `plan/*` memory automatically.
1621
- *
1622
- * Rationale (Talisman case study, May 2026): agents observed in
1623
- * production never wrote a single memory across hundreds of
1624
- * messages, even when the OPERATING POSTURE prompt explicitly told
1625
- * them to. The user's literal ask then got lost in compaction and
1626
- * the agent went off-course. This system-level safety net puts the
1627
- * user's message into the durable plan/* store which the system
1628
- * prompt auto-injects every turn — without depending on the model
1629
- * making the call.
1630
- *
1631
- * What counts as "substantive":
1632
- * - Content length ≥ 60 chars (~12 words) — short acks/yes-no don't qualify
1633
- * - AND any of:
1634
- * • contains a numbered list ("1.", "2.", "3." )
1635
- * • contains a bullet list (-, *, at line start)
1636
- * • OR is ≥ 120 chars (longer than a one-line ack)
1637
- *
1638
- * What gets saved:
1639
- * - title: `plan/user-<short-slug>-<timestamp>`
1640
- * - description: "auto-saved from user message at <iso>"
1641
- * - content: the verbatim user message
1642
- *
1643
- * The agent can rename, consolidate, or delete these later. They
1644
- * exist as a fail-safe if the agent does its job and saves its
1645
- * own better-named plan, these auto-saves can be cleaned up. If
1646
- * the agent doesn't, at least the user's words survive compaction.
1647
- *
1648
- * @param {string} agentId
1649
- * @param {Object} message - The queued user message
1650
- * @private
1651
- */
1652
- async _autoSaveUserMessageAsPlan(agentId, message) {
1653
- const content = typeof message?.content === 'string' ? message.content : '';
1654
- if (!content) return;
1655
- if (!this._looksSubstantive(content)) return;
1656
-
1657
- // Lazy-load to keep agentPool's load order light. The same import
1658
- // pattern as agentScheduler's plan injection.
1659
- let memoryService;
1660
- try {
1661
- const mod = await import('../services/memoryService.js');
1662
- memoryService = mod.getMemoryService(this.logger);
1663
- await memoryService.initialize();
1664
- } catch (e) {
1665
- this.logger.debug?.('Auto-save plan: memory service unavailable', { error: e.message });
1666
- return;
1667
- }
1668
-
1669
- // ── Deduplication ────────────────────────────────────────────────
1670
- // Users repeat themselves ("I repeat my old message", "did you do
1671
- // it all?" + paste the same thing). Without dedup the auto-saver
1672
- // would create N copies of essentially the same plan. Load
1673
- // existing plan/user-* memories and skip when the new content is
1674
- // ≥70% similar to any of them (Jaccard over normalized word sets).
1675
- let existingPlans = [];
1676
- try {
1677
- const all = await memoryService.loadMemories(agentId);
1678
- existingPlans = (all || []).filter(m =>
1679
- typeof m?.title === 'string' && m.title.startsWith('plan/user-')
1680
- );
1681
- } catch (e) {
1682
- // Treat unreadable store as empty we may still write a fresh entry.
1683
- this.logger.debug?.('Auto-save plan: existing memories unreadable', { agentId, error: e.message });
1684
- }
1685
-
1686
- const newTokens = this._tokenize(content);
1687
- for (const existing of existingPlans) {
1688
- const existingTokens = this._tokenize(existing.content || '');
1689
- const sim = this._jaccard(newTokens, existingTokens);
1690
- const containment = this._overlapCoefficient(newTokens, existingTokens);
1691
- // Jaccard catches near-identical reformulations. Containment
1692
- // catches the "I repeat my old message <same content>" case
1693
- // where the user re-pastes the original plus a preamble. Either
1694
- // signal is enough to suppress the duplicate.
1695
- if (sim >= 0.7 || containment >= 0.85) {
1696
- this.logger.info?.('Auto-save plan: skipping near-duplicate of existing plan', {
1697
- agentId, existingTitle: existing.title,
1698
- jaccard: sim.toFixed(2), containment: containment.toFixed(2),
1699
- });
1700
- return;
1701
- }
1702
- }
1703
-
1704
- // ── Per-agent cap ────────────────────────────────────────────────
1705
- // Bound the total auto-saved plans so an active session doesn't
1706
- // bloat the agent's plan/* namespace indefinitely. Keep the K most
1707
- // recent; delete the oldest auto-saves beyond that.
1708
- const AUTO_PLAN_CAP = 8;
1709
- const existingAutoSaves = existingPlans
1710
- .filter(m => /^plan\/user-/.test(m.title))
1711
- .sort((a, b) => String(a.createdAt || '').localeCompare(String(b.createdAt || '')));
1712
- while (existingAutoSaves.length >= AUTO_PLAN_CAP) {
1713
- const oldest = existingAutoSaves.shift();
1714
- try {
1715
- await memoryService.deleteMemory(agentId, oldest.id);
1716
- this.logger.info?.('Auto-save plan: retired oldest auto-save to keep cap', {
1717
- agentId, retiredTitle: oldest.title, cap: AUTO_PLAN_CAP,
1718
- });
1719
- } catch (e) {
1720
- // Non-fatal if we can't delete the oldest, just skip this entry
1721
- // and proceed with the write. Worst case the plan list grows
1722
- // by one beyond the cap still bounded over time.
1723
- this.logger.debug?.('Auto-save plan: retire-oldest failed', { agentId, error: e.message });
1724
- break;
1725
- }
1726
- }
1727
-
1728
- // ── Write the new memory ─────────────────────────────────────────
1729
- const firstLine = (content.match(/[^\n]+/) || [''])[0].trim();
1730
- const slug = firstLine
1731
- .toLowerCase()
1732
- .replace(/[^a-z0-9]+/g, '-')
1733
- .replace(/^-+|-+$/g, '')
1734
- .slice(0, 40) || 'request';
1735
- const ts = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-');
1736
- const title = `plan/user-${slug}-${ts}`;
1737
-
1738
- try {
1739
- await memoryService.addMemory(agentId, {
1740
- title,
1741
- description: `Auto-saved from user message at ${message.timestamp || new Date().toISOString()}`,
1742
- content,
1743
- });
1744
- this.logger.info?.('Auto-saved user message as plan/* memory', {
1745
- agentId, title, contentLength: content.length,
1746
- });
1747
- } catch (e) {
1748
- this.logger.debug?.('Auto-save plan: write failed', { agentId, title, error: e.message });
1749
- }
1750
- }
1751
-
1752
- /**
1753
- * Tokenize a string into a lowercased word set for similarity checks.
1754
- * Strips punctuation, drops short words (<3 chars), and drops a
1755
- * small stopword set so that common words like "the" / "and" don't
1756
- * inflate similarity scores between otherwise different messages.
1757
- * @private
1758
- */
1759
- _tokenize(s) {
1760
- if (typeof s !== 'string') return new Set();
1761
- return new Set(
1762
- s.toLowerCase()
1763
- .replace(/[^a-z0-9\s]+/g, ' ')
1764
- .split(/\s+/)
1765
- .filter(w => w.length >= 3 && !AgentPool._STOPWORDS.has(w))
1766
- );
1767
- }
1768
-
1769
- /**
1770
- * Jaccard similarity over two word sets.
1771
- * @private
1772
- */
1773
- _jaccard(a, b) {
1774
- if (a.size === 0 && b.size === 0) return 1;
1775
- if (a.size === 0 || b.size === 0) return 0;
1776
- let intersection = 0;
1777
- for (const w of a) if (b.has(w)) intersection += 1;
1778
- return intersection / (a.size + b.size - intersection);
1779
- }
1780
-
1781
- /**
1782
- * Overlap coefficient intersection / size-of-smaller-set.
1783
- * Returns 1.0 when one set is fully contained in the other,
1784
- * regardless of how much the other set adds. Catches the "user
1785
- * re-pastes their request with a preamble" duplicate case where
1786
- * Jaccard would mark the messages as merely similar.
1787
- * @private
1788
- */
1789
- _overlapCoefficient(a, b) {
1790
- if (a.size === 0 || b.size === 0) return 0;
1791
- let intersection = 0;
1792
- for (const w of a) if (b.has(w)) intersection += 1;
1793
- return intersection / Math.min(a.size, b.size);
1794
- }
1795
-
1796
- /**
1797
- * Heuristic does this user message look like a real request worth
1798
- * preserving as a plan/*? Errs on the side of saving more (recall
1799
- * over precision) — a stray auto-save is cheap; a lost user request
1800
- * is catastrophic.
1801
- * @private
1802
- */
1803
- _looksSubstantive(text) {
1804
- if (typeof text !== 'string') return false;
1805
- const t = text.trim();
1806
- if (t.length < 30) return false;
1807
- // Tool-result wrappers and previous-task boundaries are not user voice.
1808
- if (t.startsWith('[Tool Results') || t.startsWith('[Previous Task')) return false;
1809
-
1810
- // ── Pollution filter 1: dominated by questions ────────────────────
1811
- // A message that's mostly questions wants an ANSWER, not a plan.
1812
- // If the majority of non-empty lines end in '?' (or are
1813
- // question-shaped), this is a query, not a request.
1814
- if (this._dominatedByQuestions(t)) return false;
1815
-
1816
- // ── Pollution filter 2: list items are just refs (paths, urls) ───
1817
- // A list of file paths / URLs / commit hashes is the user pointing
1818
- // the agent at things, not a multi-part plan. Save it only if the
1819
- // surrounding prose carries imperative intent — and even then the
1820
- // length gate handles that path.
1821
- const hasList = /^\s*(?:\d+[.)]|[-*•])\s/m.test(t);
1822
- if (hasList && this._listItemsAreJustReferences(t)) return false;
1823
-
1824
- // ── Now apply the structural triggers ────────────────────────────
1825
- // Numbered list "1." / "1)" at a line start. Multi-part intent.
1826
- // Require a minimum total length to avoid "1. yes 2. no" nonsense.
1827
- if (/^\s*\d+[.)]\s/m.test(t) && t.length >= 60) return true;
1828
- // Bullet list at line start. Same strong intent signal + length.
1829
- if (/^\s*[-*•]\s/m.test(t) && t.length >= 60) return true;
1830
- // Free-form prose with no list markers must be substantial AND
1831
- // contain an imperative-like signal (a verb you'd give as an
1832
- // order). Raised from 120 → 150 to skip more pleasantries.
1833
- if (t.length >= 150 && this._hasImperativeSignal(t)) return true;
1834
- return false;
1835
- }
1836
-
1837
- /**
1838
- * Heuristic: is this message mostly questions?
1839
- * @private
1840
- */
1841
- _dominatedByQuestions(t) {
1842
- // Split into non-empty lines.
1843
- const lines = t.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
1844
- if (lines.length === 0) return false;
1845
- // Strip leading list markers so we can look at the line's intent.
1846
- const stripMarker = (l) => l.replace(/^(?:\d+[.)]|[-*•])\s+/, '');
1847
- let questionLines = 0;
1848
- for (const raw of lines) {
1849
- const line = stripMarker(raw);
1850
- // Ends in '?', OR starts with a question word at the line head.
1851
- if (/\?\s*$/.test(line) || /^(?:what|why|how|when|where|who|which|is\b|are\b|do\b|does\b|can\b|could\b|should\b|would\b)\b/i.test(line)) {
1852
- questionLines += 1;
1853
- }
1854
- }
1855
- // Strict-majority rule: more than half of lines are questions.
1856
- return questionLines * 2 > lines.length;
1857
- }
1858
-
1859
- /**
1860
- * Heuristic: are the list items in this message just references
1861
- * (file paths, URLs, commit hashes) with no imperative verb of their own?
1862
- * @private
1863
- */
1864
- _listItemsAreJustReferences(t) {
1865
- const lines = t.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
1866
- const listItems = lines.filter(l => /^(?:\d+[.)]|[-*•])\s/.test(l));
1867
- if (listItems.length === 0) return false;
1868
- let refLikeCount = 0;
1869
- for (const li of listItems) {
1870
- const body = li.replace(/^(?:\d+[.)]|[-*•])\s+/, '').trim();
1871
- // Only treat as a "reference" if the line IS the reference
1872
- // i.e. a path/URL/hash with no surrounding English. A short bug
1873
- // description like "login button does nothing on Safari" still
1874
- // counts as content, not a reference.
1875
- // Path: contains '/' or '\' OR starts with '.' AND has NO spaces
1876
- // URL: starts with http(s)://
1877
- // Hash: 7-40 hex chars only, no spaces
1878
- const isPath = (/[/\\]/.test(body) || /^\./.test(body)) && !/\s/.test(body);
1879
- const isUrl = /^https?:\/\//.test(body) && !/\s/.test(body);
1880
- const isHash = /^[0-9a-f]{7,40}$/i.test(body);
1881
- if (isPath || isUrl || isHash) refLikeCount += 1;
1882
- }
1883
- // Strict-majority of list items are reference-like → ignore.
1884
- return refLikeCount * 2 > listItems.length;
1885
- }
1886
-
1887
- /**
1888
- * Heuristic: does the message contain a verb that signals "do this"?
1889
- * Conservative — favors recall over precision.
1890
- * @private
1891
- */
1892
- _hasImperativeSignal(t) {
1893
- // Word-boundary match against a set of common imperative verbs.
1894
- // Order matters only for readability — we check membership.
1895
- return /\b(?:fix|add|build|implement|create|change|remove|delete|update|refactor|rewrite|migrate|integrate|configure|setup|set\s+up|design|generate|make|write|test|verify|ensure|review|optimize|improve|replace|move|rename|extract|split|merge|deploy|publish|ship|release|debug|investigate|analyze|reproduce|escalate|prioritize|schedule)\b/i.test(t);
1896
- }
1897
-
1898
- _autoCreateTaskForMessage(agent, message, source, priority) {
1899
- if (!agent.taskList) {
1900
- agent.taskList = { tasks: [], lastUpdated: new Date().toISOString() };
1901
- }
1902
-
1903
- const content = message.content || '';
1904
- const titleContent = content.trim().replace(/\n+/g, ' ').replace(/\s+/g, ' ');
1905
- const firstSentence = titleContent.split(/[.!?]/)[0].trim();
1906
- const title = firstSentence.length > 50
1907
- ? firstSentence.substring(0, 47) + '...'
1908
- : firstSentence || 'Process message';
1909
-
1910
- const isInterAgent = source.startsWith('inter-agent');
1911
- const requiresReply = isInterAgent && message.requiresReply === true;
1912
- const taskTitle = isInterAgent
1913
- ? (requiresReply
1914
- ? `Handle and reply to ${source}: ${title}`
1915
- : `Handle ${source}: ${title}`)
1916
- : `Process ${source} request: ${title}`;
1917
- const taskDescription = isInterAgent
1918
- ? (requiresReply
1919
- ? `Handle ${source} message and reply using the agentcommunication tool with action="reply-to-message": "${content.length > 200 ? content.substring(0, 197) + '...' : content}"`
1920
- : `Handle ${source} message: "${content.length > 200 ? content.substring(0, 197) + '...' : content}"`)
1921
- : `Handle ${source} message: "${content.length > 200 ? content.substring(0, 197) + '...' : content}"`;
1922
-
1923
- const task = {
1924
- id: `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
1925
- title: taskTitle,
1926
- description: taskDescription,
1927
- status: 'pending',
1928
- priority,
1929
- createdAt: new Date().toISOString(),
1930
- updatedAt: new Date().toISOString(),
1931
- source: 'auto-created',
1932
- messageId: message.id
1933
- };
1934
-
1935
- agent.taskList.tasks.push(task);
1936
- agent.taskList.lastUpdated = new Date().toISOString();
1937
-
1938
- this.logger.info(`Auto-created task for ${source} message`, {
1939
- agentId: agent.id,
1940
- taskId: task.id,
1941
- title: task.title
1942
- });
1943
- }
1944
-
1945
- /**
1946
- * Clear all message queues for an agent
1947
- * @param {string} agentId - Agent ID
1948
- * @returns {Promise<void>}
1949
- */
1950
- async clearAgentQueues(agentId) {
1951
- const agent = await this.getAgent(agentId);
1952
- if (!agent) {
1953
- throw new Error(`Agent not found: ${agentId}`);
1954
- }
1955
-
1956
- agent.messageQueues.toolResults = [];
1957
- agent.messageQueues.interAgentMessages = [];
1958
- agent.messageQueues.userMessages = [];
1959
-
1960
- await this.persistAgentState(agentId);
1961
-
1962
- this.logger.info(`Message queues cleared for agent: ${agentId}`);
1963
- }
1964
-
1965
- /**
1966
- * Get total queued messages count for an agent
1967
- * @param {string} agentId - Agent ID
1968
- * @returns {Promise<Object>} Queue counts by type
1969
- */
1970
- async getQueueCounts(agentId) {
1971
- const agent = await this.getAgent(agentId);
1972
- if (!agent) {
1973
- return { toolResults: 0, interAgentMessages: 0, userMessages: 0, total: 0 };
1974
- }
1975
-
1976
- const counts = {
1977
- toolResults: agent.messageQueues.toolResults.length,
1978
- interAgentMessages: agent.messageQueues.interAgentMessages.length,
1979
- userMessages: agent.messageQueues.userMessages.length
1980
- };
1981
-
1982
- counts.total = counts.toolResults + counts.interAgentMessages + counts.userMessages;
1983
-
1984
- return counts;
1985
- }
1986
-
1987
- /**
1988
- * Get messages for AI request - returns compacted if available, otherwise original
1989
- * CRITICAL FIX: Ensures compacted messages stay in sync with new messages after compaction
1990
- * This is the primary method that should be used when preparing messages for AI service
1991
- * @param {string} agentId - Agent ID
1992
- * @param {string} modelId - Model ID
1993
- * @returns {Promise<Array>} Messages array to send to AI
1994
- */
1995
- async getMessagesForAI(agentId, modelId) {
1996
- const ENABLE_COMPACT_DEBUG = process.env.COMPACT_DEBUG === 'true';
1997
-
1998
- // Helper: Remove trailing empty messages from array (cleans up malformed conversations)
1999
- const cleanTrailingEmptyMessages = (messages) => {
2000
- if (!messages || messages.length === 0) return messages;
2001
-
2002
- let cleaned = [...messages];
2003
- let removedCount = 0;
2004
-
2005
- // Remove trailing empty messages
2006
- while (cleaned.length > 0) {
2007
- const lastMsg = cleaned[cleaned.length - 1];
2008
- const content = lastMsg?.content;
2009
- const isEmpty = !content || (typeof content === 'string' && !content.trim());
2010
-
2011
- if (isEmpty) {
2012
- cleaned.pop();
2013
- removedCount++;
2014
- } else {
2015
- break;
2016
- }
2017
- }
2018
-
2019
- if (removedCount > 0) {
2020
- this.logger?.warn(`Removed ${removedCount} trailing empty message(s) from conversation`, {
2021
- agentId,
2022
- modelId,
2023
- originalLength: messages.length,
2024
- cleanedLength: cleaned.length
2025
- });
2026
- }
2027
-
2028
- return cleaned;
2029
- };
2030
-
2031
- const agent = await this.getAgent(agentId);
2032
- if (!agent) {
2033
- throw new Error(`Agent not found: ${agentId}`);
2034
- }
2035
-
2036
- const conversation = agent.conversations[modelId];
2037
- if (!conversation) {
2038
- this.logger.warn(`No conversation found for model: ${modelId}`, { agentId });
2039
- return [];
2040
- }
2041
-
2042
- // If no compacted messages exist, return original (cleaned)
2043
- if (!conversation.compactizedMessages) {
2044
- this.logger.debug('Retrieved messages for AI (no compaction)', {
2045
- agentId,
2046
- modelId,
2047
- messageCount: conversation.messages.length
2048
- });
2049
-
2050
- if (ENABLE_COMPACT_DEBUG) {
2051
- console.log('[GET-MESSAGES-FOR-AI]', {
2052
- agentId,
2053
- modelId,
2054
- returnedArray: 'originalMessages',
2055
- messageCount: conversation.messages.length,
2056
- reason: 'No compacted messages exist'
2057
- });
2058
- }
2059
-
2060
- return cleanTrailingEmptyMessages(conversation.messages);
2061
- }
2062
-
2063
- // CRITICAL FIX: Only sync messages added AFTER compaction
2064
- // We track originalMessageCountAtCompaction to know which messages are truly new
2065
- // vs which ones were already included in the compaction (sandwich strategy)
2066
- const compactedLength = conversation.compactizedMessages.length;
2067
- const originalLength = conversation.messages.length;
2068
- // SAFETY: If watermark is null/undefined (bug, migration, or cleared state),
2069
- // fall back to compactedLength NOT originalLength. Using originalLength silently
2070
- // drops all unsynced messages because (originalLength > originalLength) is always false.
2071
- // Using compactedLength ensures any messages beyond what's in the compacted array get synced.
2072
- const originalCountAtCompaction = conversation.originalMessageCountAtCompaction ?? compactedLength;
2073
-
2074
- // Only sync if NEW messages were added after compaction
2075
- // (i.e., current original length > original length when compaction happened)
2076
- if (originalLength > originalCountAtCompaction) {
2077
- // New messages exist that weren't present during compaction
2078
- const newMessageCount = originalLength - originalCountAtCompaction;
2079
- const newMessages = conversation.messages.slice(-newMessageCount);
2080
-
2081
- this.logger.info('Syncing truly new messages after compaction', {
2082
- agentId,
2083
- modelId,
2084
- compactedLength,
2085
- originalLength,
2086
- originalCountAtCompaction,
2087
- newMessageCount,
2088
- newMessageRoles: newMessages.map(m => m.role)
2089
- });
2090
-
2091
- // Append only the truly new messages to compacted array
2092
- conversation.compactizedMessages.push(...newMessages);
2093
-
2094
- // Update the tracking to include newly synced messages
2095
- conversation.originalMessageCountAtCompaction = originalLength;
2096
-
2097
- // Persist the update
2098
- await this.persistAgentState(agentId);
2099
- } else if (originalLength > compactedLength) {
2100
- // Length mismatch but no new messages - this is expected with sandwich compaction
2101
- // The compacted version has fewer messages due to summarization, not missing messages
2102
- this.logger.debug('Compacted messages shorter than original (expected with sandwich compaction)', {
2103
- agentId,
2104
- modelId,
2105
- compactedLength,
2106
- originalLength,
2107
- originalCountAtCompaction,
2108
- note: 'No sync needed - compaction reduces message count'
2109
- });
2110
- }
2111
-
2112
- this.logger.debug('Retrieved messages for AI (compacted + synced)', {
2113
- agentId,
2114
- modelId,
2115
- messageCount: conversation.compactizedMessages.length,
2116
- wasResynced: originalLength > compactedLength
2117
- });
2118
-
2119
- if (ENABLE_COMPACT_DEBUG) {
2120
- console.log('[GET-MESSAGES-FOR-AI]', {
2121
- agentId,
2122
- modelId,
2123
- returnedArray: 'compactizedMessages',
2124
- messageCount: conversation.compactizedMessages.length,
2125
- originalMessageCount: conversation.messages.length,
2126
- wasSynced: originalLength > compactedLength,
2127
- syncedMessageCount: originalLength > compactedLength ? originalLength - compactedLength : 0,
2128
- reason: 'Compacted messages exist, returning compacted version'
2129
- });
2130
- }
2131
-
2132
- return cleanTrailingEmptyMessages(conversation.compactizedMessages);
2133
- }
2134
-
2135
- /**
2136
- * Add message to conversation (stores in original messages array)
2137
- * @param {string} agentId - Agent ID
2138
- * @param {string} modelId - Model ID
2139
- * @param {Object} message - Message object to add
2140
- * @returns {Promise<void>}
2141
- */
2142
- async addMessageToConversation(agentId, modelId, message) {
2143
- const ENABLE_COMPACT_DEBUG = process.env.COMPACT_DEBUG === 'true';
2144
-
2145
- const agent = await this.getAgent(agentId);
2146
- if (!agent) {
2147
- throw new Error(`Agent not found: ${agentId}`);
2148
- }
2149
-
2150
- // Ensure model conversation exists
2151
- if (!agent.conversations[modelId]) {
2152
- agent.conversations[modelId] = this._createEmptyConversation(modelId);
2153
- }
2154
-
2155
- const conversation = agent.conversations[modelId];
2156
-
2157
- // GUARD: Skip empty messages - they should never be added to history
2158
- const messageContent = message.content;
2159
- if (!messageContent || (typeof messageContent === 'string' && !messageContent.trim())) {
2160
- this.logger?.warn(`Skipping empty message for agent ${agentId}`, {
2161
- role: message.role,
2162
- modelId,
2163
- hasContent: !!messageContent
2164
- });
2165
- return; // Don't add empty messages
2166
- }
2167
-
2168
- const originalLengthBefore = conversation.messages.length;
2169
- const compactedLengthBefore = conversation.compactizedMessages?.length || 0;
2170
-
2171
- // Always add to original messages (never modify original)
2172
- conversation.messages.push({
2173
- ...message,
2174
- timestamp: message.timestamp || new Date().toISOString()
2175
- });
2176
-
2177
- // If compacted version exists, also add to it (append new messages after compaction)
2178
- if (conversation.compactizedMessages) {
2179
- conversation.compactizedMessages.push({
2180
- ...message,
2181
- timestamp: message.timestamp || new Date().toISOString()
2182
- });
2183
- }
2184
-
2185
- conversation.lastUpdated = new Date().toISOString();
2186
-
2187
- if (ENABLE_COMPACT_DEBUG) {
2188
- console.log('[ADD-MESSAGE-TO-CONVERSATION]', {
2189
- agentId,
2190
- modelId,
2191
- role: message.role,
2192
- hasCompactedVersion: !!conversation.compactizedMessages,
2193
- originalMessages: {
2194
- before: originalLengthBefore,
2195
- after: conversation.messages.length,
2196
- added: 1
2197
- },
2198
- compactizedMessages: conversation.compactizedMessages ? {
2199
- before: compactedLengthBefore,
2200
- after: conversation.compactizedMessages.length,
2201
- added: 1
2202
- } : null,
2203
- behavior: conversation.compactizedMessages ? 'Added to BOTH arrays' : 'Added to original only'
2204
- });
2205
- }
2206
-
2207
- await this.persistAgentState(agentId);
2208
-
2209
- this.logger.debug('Message added to conversation', {
2210
- agentId,
2211
- modelId,
2212
- role: message.role,
2213
- hasCompacted: !!conversation.compactizedMessages
2214
- });
2215
- }
2216
-
2217
- /**
2218
- * Sync pending messages from conversation.messages to compactizedMessages.
2219
- * The scheduler's addMessageToConversation only pushes to conversation.messages,
2220
- * NOT to compactizedMessages. This method syncs any pending messages that haven't
2221
- * been pushed to compactizedMessages yet.
2222
- *
2223
- * MUST be called before compaction reads compactizedMessages, otherwise compaction
2224
- * will process a stale snapshot and the watermark will mark unsynced messages as
2225
- * "already compacted", permanently losing them.
2226
- *
2227
- * @param {string} agentId - Agent ID
2228
- * @param {string} modelId - Model ID
2229
- * @returns {Promise<{synced: number}>} Number of messages synced
2230
- */
2231
- async syncPendingMessages(agentId, modelId) {
2232
- const agent = await this.getAgent(agentId);
2233
- if (!agent) return { synced: 0 };
2234
-
2235
- const conversation = agent.conversations[modelId];
2236
- if (!conversation || !conversation.compactizedMessages) return { synced: 0 };
2237
-
2238
- const originalLength = conversation.messages.length;
2239
- const compactedLength = conversation.compactizedMessages.length;
2240
- // SAFETY: Use ?? compactedLength instead of || originalLength to prevent silent message loss
2241
- // when watermark is null (see getMessagesForAI for detailed explanation)
2242
- const originalCountAtCompaction = conversation.originalMessageCountAtCompaction ?? compactedLength;
2243
-
2244
- if (originalLength > originalCountAtCompaction) {
2245
- const newCount = originalLength - originalCountAtCompaction;
2246
- const newMessages = conversation.messages.slice(-newCount);
2247
- conversation.compactizedMessages.push(...newMessages);
2248
- conversation.originalMessageCountAtCompaction = originalLength;
2249
-
2250
- this.logger.info('Pre-compaction sync: pushed pending messages to compactizedMessages', {
2251
- agentId,
2252
- modelId,
2253
- synced: newCount,
2254
- newMessageRoles: newMessages.map(m => m.role),
2255
- compactizedMessagesLength: conversation.compactizedMessages.length,
2256
- watermarkWasNull: conversation.originalMessageCountAtCompaction === null
2257
- });
2258
-
2259
- return { synced: newCount };
2260
- }
2261
-
2262
- return { synced: 0 };
2263
- }
2264
-
2265
- /**
2266
- * Update compacted messages after compactization
2267
- * @param {string} agentId - Agent ID
2268
- * @param {string} modelId - Model ID
2269
- * @param {Object} compactionResult - Compaction result with messages and metadata
2270
- * @param {number} [preCompactionMessageCount] - Message count recorded BEFORE compaction started.
2271
- * If provided, used as the watermark instead of current messages.length. This prevents
2272
- * messages added DURING compaction from being silently lost.
2273
- * @returns {Promise<void>}
2274
- */
2275
- async updateCompactedMessages(agentId, modelId, compactionResult, preCompactionMessageCount) {
2276
- const agent = await this.getAgent(agentId);
2277
- if (!agent) {
2278
- throw new Error(`Agent not found: ${agentId}`);
2279
- }
2280
-
2281
- // Ensure model conversation exists (important for model switching scenarios)
2282
- if (!agent.conversations[modelId]) {
2283
- agent.conversations[modelId] = this._createEmptyConversation(modelId);
2284
- this.logger.debug(`Created conversation for model switching: ${modelId}`);
2285
- }
2286
-
2287
- const conversation = agent.conversations[modelId];
2288
-
2289
- // Update compacted messages
2290
- conversation.compactizedMessages = compactionResult.compactedMessages;
2291
-
2292
- // CRITICAL: Use the pre-compaction watermark if provided, NOT current messages.length.
2293
- // If we use current messages.length, any messages added DURING compaction (e.g., user
2294
- // messages arriving via WebSocket while the summarization API call is in flight) would
2295
- // be marked as "already compacted" even though they weren't in the compaction input.
2296
- // Using the pre-compaction count ensures those messages are detected as "new" by
2297
- // getMessagesForAI's sync logic and properly appended to the compacted array.
2298
- conversation.originalMessageCountAtCompaction = preCompactionMessageCount || conversation.messages.length;
2299
-
2300
- // Update metadata
2301
- conversation.lastCompactization = new Date().toISOString();
2302
- conversation.compactizationCount += 1;
2303
- conversation.compactizationStrategy = compactionResult.strategy;
2304
- conversation.originalTokenCount = compactionResult.originalTokenCount;
2305
- conversation.compactedTokenCount = compactionResult.compactedTokenCount;
2306
- conversation.tokenCount = compactionResult.compactedTokenCount;
2307
- conversation.lastUpdated = new Date().toISOString();
2308
-
2309
- await this.persistAgentState(agentId);
2310
-
2311
- this.logger.info('Compacted messages updated', {
2312
- agentId,
2313
- modelId,
2314
- strategy: compactionResult.strategy,
2315
- originalTokens: compactionResult.originalTokenCount,
2316
- compactedTokens: compactionResult.compactedTokenCount,
2317
- reductionPercent: compactionResult.reductionPercent,
2318
- compactizationCount: conversation.compactizationCount
2319
- });
2320
- }
2321
-
2322
- /**
2323
- * Clear compacted messages and revert to original
2324
- * Useful for debugging or if compaction needs to be redone
2325
- * @param {string} agentId - Agent ID
2326
- * @param {string} modelId - Model ID
2327
- * @returns {Promise<void>}
2328
- */
2329
- async clearCompactedMessages(agentId, modelId) {
2330
- const agent = await this.getAgent(agentId);
2331
- if (!agent) {
2332
- throw new Error(`Agent not found: ${agentId}`);
2333
- }
2334
-
2335
- const conversation = agent.conversations[modelId];
2336
- if (!conversation) {
2337
- return;
2338
- }
2339
-
2340
- conversation.compactizedMessages = null;
2341
- conversation.lastCompactization = null;
2342
- conversation.compactizationCount = 0;
2343
- conversation.compactizationStrategy = null;
2344
- conversation.originalTokenCount = 0;
2345
- conversation.compactedTokenCount = 0;
2346
- conversation.tokenCount = 0;
2347
- conversation.originalMessageCountAtCompaction = null;
2348
-
2349
- await this.persistAgentState(agentId);
2350
-
2351
- this.logger.info('Compacted messages cleared', { agentId, modelId });
2352
- }
2353
-
2354
- /**
2355
- * Get compaction metadata for a conversation
2356
- * @param {string} agentId - Agent ID
2357
- * @param {string} modelId - Model ID
2358
- * @returns {Promise<Object|null>} Compaction metadata or null if no compaction
2359
- */
2360
- async getCompactionMetadata(agentId, modelId) {
2361
- const agent = await this.getAgent(agentId);
2362
- if (!agent) {
2363
- return null;
2364
- }
2365
-
2366
- const conversation = agent.conversations[modelId];
2367
- if (!conversation) {
2368
- return null;
2369
- }
2370
-
2371
- // Return metadata whether compacted or not
2372
- const isCompacted = !!conversation.compactizedMessages;
2373
-
2374
- return {
2375
- isCompacted,
2376
- lastCompactization: conversation.lastCompactization || null,
2377
- compactizationCount: conversation.compactizationCount || 0,
2378
- strategy: conversation.compactizationStrategy || null,
2379
- originalTokenCount: conversation.originalTokenCount || 0,
2380
- compactedTokenCount: conversation.compactedTokenCount || 0,
2381
- reductionPercent: conversation.originalTokenCount > 0
2382
- ? ((conversation.originalTokenCount - conversation.compactedTokenCount) / conversation.originalTokenCount) * 100
2383
- : 0,
2384
- originalMessages: conversation.messages || [],
2385
- compactedMessages: conversation.compactizedMessages || null,
2386
- originalMessageCount: conversation.messages?.length || 0,
2387
- compactedMessageCount: conversation.compactizedMessages?.length || 0
2388
- };
2389
- }
2390
-
2391
- /**
2392
- * Migrate existing agent conversations to dual storage structure
2393
- * Ensures backward compatibility with existing agents
2394
- * @param {string} agentId - Agent ID
2395
- * @returns {Promise<boolean>} True if migration was needed and performed
2396
- */
2397
- async migrateConversationStructure(agentId) {
2398
- const agent = await this.getAgent(agentId);
2399
- if (!agent) {
2400
- return false;
2401
- }
2402
-
2403
- let migrated = false;
2404
-
2405
- // Check each conversation for migration needs
2406
- for (const [modelId, conversation] of Object.entries(agent.conversations)) {
2407
- if (modelId === 'full') continue; // Skip full conversation
2408
-
2409
- // Check if conversation needs migration (missing new fields)
2410
- if (conversation.compactizedMessages === undefined) {
2411
- // Add new fields
2412
- conversation.compactizedMessages = null;
2413
- conversation.lastCompactization = null;
2414
- conversation.compactizationCount = 0;
2415
- conversation.compactizationStrategy = null;
2416
- conversation.originalTokenCount = 0;
2417
- conversation.compactedTokenCount = 0;
2418
- conversation.originalMessageCountAtCompaction = null;
2419
-
2420
- migrated = true;
2421
-
2422
- this.logger.info('Migrated conversation structure', {
2423
- agentId,
2424
- modelId,
2425
- messageCount: conversation.messages?.length || 0
2426
- });
2427
- }
2428
-
2429
- // CRITICAL: Migrate existing compacted conversations that don't have the new tracking field
2430
- // This prevents the sync bug from re-adding messages already included in compaction
2431
- if (conversation.compactizedMessages && conversation.originalMessageCountAtCompaction === undefined) {
2432
- // Set to current original length to prevent any sync from running
2433
- // This is safe because any truly new messages would have been added to both arrays
2434
- conversation.originalMessageCountAtCompaction = conversation.messages?.length || 0;
2435
- migrated = true;
2436
-
2437
- this.logger.info('Migrated compaction tracking field', {
2438
- agentId,
2439
- modelId,
2440
- originalMessageCountAtCompaction: conversation.originalMessageCountAtCompaction,
2441
- compactedMessageCount: conversation.compactizedMessages.length
2442
- });
2443
- }
2444
- }
2445
-
2446
- if (migrated) {
2447
- await this.persistAgentState(agentId);
2448
- }
2449
-
2450
- return migrated;
2451
- }
2452
-
2453
- /**
2454
- * Create empty conversation structure with all required fields
2455
- * @private
2456
- * @param {string} modelId - Model ID
2457
- * @returns {Object} Empty conversation structure
2458
- */
2459
- _createEmptyConversation(modelId) {
2460
- return {
2461
- messages: [],
2462
- compactizedMessages: null,
2463
- lastCompactization: null,
2464
- compactizationCount: 0,
2465
- compactizationStrategy: null,
2466
- originalTokenCount: 0,
2467
- compactedTokenCount: 0,
2468
- tokenCount: 0,
2469
- originalMessageCountAtCompaction: null, // Tracks original length at compaction time
2470
- lastUpdated: new Date().toISOString(),
2471
- formatVersion: this._getModelFormatVersion(modelId)
2472
- };
2473
- }
2474
- }
2475
-
1
+ /**
2
+ * AgentPool - Manages the lifecycle, state, and communication of all active agents
3
+ *
4
+ * Purpose:
5
+ * - Agent creation and destruction
6
+ * - Agent notification and routing
7
+ * - Multi-agent conversation coordination
8
+ * - Agent state persistence and recovery
9
+ * - Agent activity management
10
+ */
11
+
12
+ import {
13
+ AGENT_TYPES,
14
+ AGENT_STATUS,
15
+ AGENT_MODES,
16
+ MESSAGE_ROLES,
17
+ MESSAGE_TYPES,
18
+ MODEL_FORMAT_VERSIONS,
19
+ SYSTEM_DEFAULTS
20
+ } from '../utilities/constants.js';
21
+ import DirectoryAccessManager from '../utilities/directoryAccessManager.js';
22
+ import { getVisualEditorBridge } from '../services/visualEditorBridge.js';
23
+
24
+ class AgentPool {
25
+ // Stopwords for the _tokenize / _jaccard similarity check used by
26
+ // auto-save-as-plan dedup. Tight list only words that appear in
27
+ // virtually every English sentence regardless of content, so that
28
+ // their presence in both messages doesn't inflate similarity.
29
+ static _STOPWORDS = new Set([
30
+ 'the', 'and', 'for', 'but', 'are', 'was', 'were',
31
+ 'has', 'have', 'had', 'this', 'that', 'with', 'will',
32
+ 'you', 'your', 'our', 'their', 'them', 'they',
33
+ 'can', 'could', 'should', 'would',
34
+ ]);
35
+
36
+ constructor(config, logger, stateManager, contextManager, toolsRegistry = null) {
37
+ this.config = config;
38
+ this.logger = logger;
39
+ this.stateManager = stateManager;
40
+ this.contextManager = contextManager;
41
+ this.toolsRegistry = toolsRegistry;
42
+
43
+ // Agent registry - maps agent ID to agent object
44
+ this.agents = new Map();
45
+
46
+ // Agent directory for discovery
47
+ this.agentDirectory = new Map();
48
+
49
+ // Paused agents tracking
50
+ this.pausedAgents = new Map();
51
+
52
+ // Agent notification queue
53
+ this.notificationQueue = new Map();
54
+
55
+ this.maxAgentsPerProject = config.system?.maxAgentsPerProject || SYSTEM_DEFAULTS.MAX_AGENTS_PER_PROJECT;
56
+
57
+ // MessageProcessor reference for triggering responses (set via setMessageProcessor)
58
+ this.messageProcessor = null;
59
+
60
+ // Initialize directory access manager
61
+ this.directoryAccessManager = new DirectoryAccessManager(config, logger);
62
+ }
63
+
64
+ /**
65
+ * Create a new agent with specified configuration
66
+ * @param {Object} config - Agent configuration
67
+ * @param {string} config.name - Agent name
68
+ * @param {string} config.type - Agent type ('user-created', 'system-agent', 'agent-engineer')
69
+ * @param {string} config.systemPrompt - Agent's system prompt
70
+ * @param {string} config.preferredModel - Preferred LLM model
71
+ * @param {Array} config.capabilities - Available tools/capabilities
72
+ * @param {Object} config.directoryAccess - Directory access configuration
73
+ * @param {string} config.projectDir - Project directory for default access setup
74
+ * @returns {Promise<Object>} Created agent object
75
+ */
76
+ async createAgent(config) {
77
+ // Check agent limit
78
+ if (this.agents.size >= this.maxAgentsPerProject) {
79
+ throw new Error(`Maximum agents per project exceeded (${this.maxAgentsPerProject})`);
80
+ }
81
+
82
+ const agentId = this._generateAgentId(config.name);
83
+ const now = new Date().toISOString();
84
+
85
+ // Enhance system prompt with tool descriptions if available
86
+ let enhancedSystemPrompt = config.systemPrompt;
87
+ if (this.toolsRegistry && config.capabilities && config.capabilities.length > 0) {
88
+ try {
89
+ enhancedSystemPrompt = this.toolsRegistry.enhanceSystemPrompt(
90
+ config.systemPrompt,
91
+ config.capabilities,
92
+ {
93
+ compact: config.compactToolDescriptions || false,
94
+ layered: config.layeredToolDescriptions || false,
95
+ includeExamples: config.includeToolExamples !== false,
96
+ includeUsageGuidelines: config.includeUsageGuidelines !== false,
97
+ includeSecurityNotes: config.includeSecurityNotes !== false
98
+ }
99
+ );
100
+
101
+ this.logger?.info(`System prompt enhanced with tool descriptions`, {
102
+ agentId,
103
+ capabilities: config.capabilities,
104
+ originalLength: config.systemPrompt?.length || 0,
105
+ enhancedLength: enhancedSystemPrompt?.length || 0
106
+ });
107
+ } catch (error) {
108
+ this.logger?.error(`Failed to enhance system prompt with tools`, {
109
+ agentId,
110
+ error: error.message,
111
+ capabilities: config.capabilities
112
+ });
113
+ // Fall back to original prompt
114
+ enhancedSystemPrompt = config.systemPrompt;
115
+ }
116
+ }
117
+
118
+ // Inject assigned skills index into system prompt
119
+ if (config.skills && config.skills.length > 0) {
120
+ try {
121
+ const { getSkillsService } = await import('../services/skillsService.js');
122
+ const skillsService = getSkillsService(this.logger);
123
+ await skillsService.initialize();
124
+ const summaries = await skillsService.getSkillSummaries(config.skills);
125
+ if (summaries.length > 0) {
126
+ enhancedSystemPrompt += '\n\n## ASSIGNED SKILLS\n\n';
127
+ enhancedSystemPrompt += 'Use the skills tool to browse and load skill content. Use "describe" to see sections, "read-section" to load specific parts.\n\n';
128
+ for (const s of summaries) {
129
+ const sections = s.sections?.length ? `\n Sections: ${s.sections.map(h => h.replace(/^#+\s*/, '')).join(', ')}` : '';
130
+ enhancedSystemPrompt += `- **${s.name}** (${s.lineCount} lines): ${s.description}${sections}\n`;
131
+ }
132
+ }
133
+ } catch (error) {
134
+ this.logger?.warn('Failed to inject skills index into system prompt', { error: error.message });
135
+ }
136
+ }
137
+
138
+ // Setup directory access configuration
139
+ let directoryAccess;
140
+
141
+ console.log('AgentPool DEBUG: createAgent - config.directoryAccess:', config.directoryAccess ? 'EXISTS' : 'NULL/UNDEFINED');
142
+ if (config.directoryAccess) {
143
+ console.log('AgentPool DEBUG: createAgent - directoryAccess from config:', JSON.stringify(config.directoryAccess, null, 2));
144
+ }
145
+ console.log('AgentPool DEBUG: createAgent - config.projectDir:', config.projectDir);
146
+
147
+ if (config.directoryAccess) {
148
+ // Validate provided directory access configuration
149
+ const validation = this.directoryAccessManager.validateAccessConfiguration(config.directoryAccess);
150
+ console.log('AgentPool DEBUG: createAgent - validation result:', validation);
151
+ if (!validation.valid) {
152
+ throw new Error(`Invalid directory access configuration: ${validation.errors.join(', ')}`);
153
+ }
154
+ directoryAccess = config.directoryAccess;
155
+ console.log('AgentPool DEBUG: createAgent - Using provided directoryAccess');
156
+ } else {
157
+ // Create default directory access based on project directory
158
+ const projectDir = config.projectDir || process.cwd();
159
+ directoryAccess = DirectoryAccessManager.createProjectDefaults(projectDir);
160
+ console.log('AgentPool DEBUG: createAgent - Created default directoryAccess for projectDir:', projectDir);
161
+ console.log('AgentPool DEBUG: createAgent - Default directoryAccess:', JSON.stringify(directoryAccess, null, 2));
162
+ }
163
+
164
+ const agent = {
165
+ id: agentId,
166
+ type: config.type || AGENT_TYPES.USER_CREATED,
167
+ name: config.name || `Agent-${Date.now()}`,
168
+ systemPrompt: enhancedSystemPrompt,
169
+ originalSystemPrompt: config.systemPrompt, // Store original for reference
170
+ preferredModel: config.preferredModel,
171
+ status: AGENT_STATUS.ACTIVE,
172
+ capabilities: config.capabilities || [],
173
+ directoryAccess: directoryAccess, // Directory access configuration
174
+ // Per-tool configuration keyed by tool id (e.g., 'terminal',
175
+ // 'filesystem'). When a tool is instantiated for this agent, the
176
+ // object under toolConfig[toolId] is merged into the tool's
177
+ // constructor config. Previously tools were constructed from global
178
+ // defaults only, so there was no way to set per-agent knobs like
179
+ // terminal allowed-commands, filesystem size limits, web stealth
180
+ // level, etc. See ToolManager integration in step 2.
181
+ toolConfig: (config.toolConfig && typeof config.toolConfig === 'object' && !Array.isArray(config.toolConfig))
182
+ ? { ...config.toolConfig }
183
+ : {},
184
+ conversations: {
185
+ full: {
186
+ messages: [],
187
+ lastUpdated: now
188
+ }
189
+ },
190
+ currentModel: config.preferredModel,
191
+ dynamicModelRouting: config.dynamicModelRouting || false,
192
+ routingStrategy: config.routingStrategy || '',
193
+ skills: config.skills || [],
194
+ platformProvided: config.platformProvided !== false, // Default true — all models are platform-provided
195
+
196
+ // Agent Mode Configuration
197
+ mode: config.mode || AGENT_MODES.CHAT,
198
+ currentTask: null, // Current autonomous task being executed
199
+ taskStartTime: null,
200
+ maxIterations: config.maxIterations || 10, // Safety limit for autonomous loops
201
+ iterationCount: 0,
202
+ stopRequested: false,
203
+ delayEndTime: null, // When agent delay expires (for agentDelay tool)
204
+ ttl: null, // Time-to-live: processing cycles remaining (null = no TTL, number = cycles left)
205
+
206
+ // Message Queues for scheduler processing
207
+ messageQueues: {
208
+ toolResults: [], // Tool execution results waiting to be processed
209
+ interAgentMessages: [], // Messages from other agents
210
+ userMessages: [] // Messages from users
211
+ },
212
+
213
+ createdAt: now,
214
+ lastActivity: now,
215
+ pausedUntil: null,
216
+ // Used by platformControlTool for ancestry checks. null = created
217
+ // via the UI / no parent agent. Set to <agentId> when an agent
218
+ // creates another via the platformcontrol tool.
219
+ createdBy: typeof config.createdBy === 'string' ? config.createdBy : null,
220
+ metadata: config.metadata || {},
221
+
222
+ // CRITICAL: Store sessionId for API key resolution
223
+ sessionId: config.sessionId,
224
+
225
+ // Inter-agent conversation tracking to prevent spam
226
+ interAgentTracking: new Map(), // recipientId -> { lastSent, lastReceived, lastType }
227
+
228
+ // Task Management System for agent-mode autonomous operation
229
+ taskList: {
230
+ tasks: [], // Array of task objects
231
+ lastUpdated: now
232
+ },
233
+
234
+ // Incoming messages tracking (for unprocessed messages)
235
+ incomingMessages: []
236
+ };
237
+
238
+ this.logger.info(`Agent created with routing config`, {
239
+ agentId,
240
+ dynamicModelRouting: agent.dynamicModelRouting,
241
+ platformProvided: agent.platformProvided,
242
+ preferredModel: agent.preferredModel
243
+ });
244
+
245
+ // Initialize model-specific conversation with dual storage structure
246
+ if (config.preferredModel) {
247
+ agent.conversations[config.preferredModel] = {
248
+ // Dual storage for compactization support
249
+ messages: [], // Original messages - never modified
250
+ compactizedMessages: null, // Working copy - null until first compaction
251
+
252
+ // Compactization metadata
253
+ lastCompactization: null, // Timestamp of last compaction
254
+ compactizationCount: 0, // Number of times compacted
255
+ compactizationStrategy: null, // 'summarization', 'truncation', 'aggressive'
256
+ originalTokenCount: 0, // Token count before last compaction
257
+ compactedTokenCount: 0, // Token count after last compaction
258
+
259
+ // Backward compatibility
260
+ tokenCount: 0, // Current effective token count
261
+ lastUpdated: now,
262
+ formatVersion: this._getModelFormatVersion(config.preferredModel)
263
+ };
264
+ }
265
+
266
+ // Add to registry and directory
267
+ this.agents.set(agentId, agent);
268
+ this._updateAgentDirectory(agent);
269
+
270
+ // Persist agent state (use wrapper that resolves agent object from ID)
271
+ await this.persistAgentState(agentId);
272
+
273
+ this.logger.info(`Agent created: ${agentId}`, {
274
+ agentId,
275
+ name: agent.name,
276
+ type: agent.type,
277
+ model: agent.preferredModel
278
+ });
279
+
280
+ return agent;
281
+ }
282
+
283
+ /**
284
+ * Retrieve agent instance by ID
285
+ * @param {string} agentId - Agent identifier
286
+ * @returns {Promise<Object|null>} Agent object or null if not found
287
+ */
288
+ async getAgent(agentId, enrichWithSchedulerStatus = false) {
289
+ const agent = this.agents.get(agentId);
290
+ if (!agent) return null;
291
+
292
+ // Optionally enrich with scheduler status for UI
293
+ if (enrichWithSchedulerStatus && this.scheduler) {
294
+ agent.inScheduler = this.scheduler.isAgentInScheduler(agentId);
295
+ }
296
+
297
+ return agent;
298
+ }
299
+
300
+ /**
301
+ * Update an existing agent's configuration
302
+ * @param {string} agentId - Agent identifier
303
+ * @param {Object} updates - Updates to apply to the agent
304
+ * @returns {Promise<Object>} Updated agent object
305
+ */
306
+ async updateAgent(agentId, updates) {
307
+ const agent = await this.getAgent(agentId);
308
+ if (!agent) {
309
+ throw new Error(`Agent not found: ${agentId}`);
310
+ }
311
+
312
+ this.logger.info(`Updating agent: ${agentId}`, {
313
+ updates,
314
+ currentName: agent.name
315
+ });
316
+
317
+ // Validate directory access configuration if being updated
318
+ if (updates.directoryAccess) {
319
+ const validation = this.directoryAccessManager.validateAccessConfiguration(updates.directoryAccess);
320
+ if (!validation.valid) {
321
+ throw new Error(`Invalid directory access configuration: ${validation.errors.join(', ')}`);
322
+ }
323
+
324
+ this.logger.info(`Directory access validation passed for agent: ${agentId}`, {
325
+ workingDirectory: updates.directoryAccess.workingDirectory,
326
+ readOnlyDirs: updates.directoryAccess.readOnlyDirectories?.length || 0,
327
+ writeEnabledDirs: updates.directoryAccess.writeEnabledDirectories?.length || 0
328
+ });
329
+ }
330
+
331
+ // Validate per-tool config if being updated. Must be a plain object
332
+ // keyed by tool id; each value is an object of config overrides that
333
+ // will be merged into the tool's constructor config at instantiation.
334
+ // We don't validate individual tool schemas here — that happens at
335
+ // tool construction time where the tool knows its own shape.
336
+ if (updates.toolConfig !== undefined) {
337
+ if (updates.toolConfig === null
338
+ || typeof updates.toolConfig !== 'object'
339
+ || Array.isArray(updates.toolConfig)) {
340
+ throw new Error('Invalid toolConfig: must be a plain object keyed by tool id');
341
+ }
342
+ for (const [toolId, cfg] of Object.entries(updates.toolConfig)) {
343
+ if (cfg !== null && (typeof cfg !== 'object' || Array.isArray(cfg))) {
344
+ throw new Error(`Invalid toolConfig.${toolId}: must be an object or null`);
345
+ }
346
+ }
347
+ }
348
+
349
+ // If originalSystemPrompt is being updated (user edited the raw prompt), store it
350
+ // and use it as the base for enhancement. Otherwise use the existing originalSystemPrompt.
351
+ if (updates.originalSystemPrompt !== undefined) {
352
+ // User explicitly set a new base prompt — store it
353
+ this.logger.info(`Original system prompt updated by user`, {
354
+ agentId,
355
+ oldLength: (agent.originalSystemPrompt || '').length,
356
+ newLength: updates.originalSystemPrompt.length
357
+ });
358
+ }
359
+
360
+ // If capabilities or system prompt are being updated, regenerate the enhanced system prompt
361
+ if ((updates.capabilities || updates.originalSystemPrompt !== undefined) && this.toolsRegistry) {
362
+ try {
363
+ // Priority: new user prompt > existing original prompt > existing system prompt
364
+ const baseSystemPrompt = updates.originalSystemPrompt !== undefined
365
+ ? updates.originalSystemPrompt
366
+ : (agent.originalSystemPrompt || agent.systemPrompt || '');
367
+ const capabilities = updates.capabilities || agent.capabilities || [];
368
+
369
+ const enhancedSystemPrompt = this.toolsRegistry.enhanceSystemPrompt(
370
+ baseSystemPrompt,
371
+ capabilities,
372
+ {
373
+ compact: agent.compactToolDescriptions || false,
374
+ includeExamples: agent.includeToolExamples !== false,
375
+ includeUsageGuidelines: agent.includeUsageGuidelines !== false,
376
+ includeSecurityNotes: agent.includeSecurityNotes !== false
377
+ }
378
+ );
379
+
380
+ updates.systemPrompt = enhancedSystemPrompt;
381
+ // Always keep originalSystemPrompt in sync with what the user wrote
382
+ if (updates.originalSystemPrompt === undefined) {
383
+ updates.originalSystemPrompt = baseSystemPrompt;
384
+ }
385
+
386
+ this.logger.info(`System prompt regenerated with updated capabilities`, {
387
+ agentId,
388
+ oldCapabilities: agent.capabilities,
389
+ newCapabilities: capabilities,
390
+ originalLength: baseSystemPrompt?.length || 0,
391
+ enhancedLength: enhancedSystemPrompt?.length || 0
392
+ });
393
+
394
+ // The scheduler caches per-(agent, model) Responses-API prompts
395
+ // built from this agent's `originalSystemPrompt` + capabilities.
396
+ // Both inputs just changed, so any cached rebuilds are stale.
397
+ // No-op when the scheduler isn't attached (tests / very-early
398
+ // boot) or when it predates this method (old binaries during
399
+ // a rolling upgrade).
400
+ try {
401
+ this.scheduler?._invalidateNativePromptCache?.(agentId);
402
+ } catch (e) {
403
+ this.logger.debug?.('Failed to invalidate native prompt cache', { agentId, error: e.message });
404
+ }
405
+ } catch (error) {
406
+ this.logger.error(`Failed to regenerate system prompt with updated capabilities`, {
407
+ agentId,
408
+ error: error.message,
409
+ capabilities: updates.capabilities
410
+ });
411
+ // Continue with update even if enhancement fails
412
+ }
413
+ }
414
+
415
+ // Create updated agent object with new values
416
+ const updatedAgent = {
417
+ ...agent,
418
+ ...updates,
419
+ id: agentId, // Ensure ID cannot be changed
420
+ lastModified: new Date().toISOString(),
421
+ lastActivity: new Date().toISOString()
422
+ };
423
+
424
+ // CRITICAL FIX: When preferredModel changes, also update currentModel
425
+ // This ensures the UI immediately reflects the model change
426
+ if (updates.preferredModel && updates.preferredModel !== agent.preferredModel) {
427
+ const oldModel = agent.preferredModel;
428
+ const newModel = updates.preferredModel;
429
+
430
+ updatedAgent.currentModel = newModel;
431
+
432
+ // CRITICAL FIX: Initialize conversation for new model if it doesn't exist
433
+ if (!updatedAgent.conversations[newModel]) {
434
+ updatedAgent.conversations[newModel] = this._createEmptyConversation(newModel);
435
+ this.logger.info(`Created conversation for new model: ${newModel}`, { agentId });
436
+ }
437
+
438
+ // Copy conversation history from old model to new model
439
+ // This preserves context when switching models
440
+ if (oldModel && updatedAgent.conversations[oldModel]) {
441
+ const oldConversation = updatedAgent.conversations[oldModel];
442
+ const newConversation = updatedAgent.conversations[newModel];
443
+
444
+ // Copy messages if new conversation is empty
445
+ if (newConversation.messages.length === 0 && oldConversation.messages.length > 0) {
446
+ // Copy original messages
447
+ newConversation.messages = [...oldConversation.messages];
448
+
449
+ // Copy compacted messages if they exist
450
+ if (oldConversation.compactizedMessages) {
451
+ newConversation.compactizedMessages = [...oldConversation.compactizedMessages];
452
+ newConversation.lastCompactization = oldConversation.lastCompactization;
453
+ newConversation.compactizationCount = oldConversation.compactizationCount;
454
+ newConversation.compactizationStrategy = oldConversation.compactizationStrategy;
455
+ newConversation.originalTokenCount = oldConversation.originalTokenCount;
456
+ newConversation.compactedTokenCount = oldConversation.compactedTokenCount;
457
+ // CRITICAL: Copy the sync watermark too — without this, getMessagesForAI
458
+ // cannot sync new messages to compactizedMessages after a model switch,
459
+ // causing the AI to only see the compacted summary and repeat itself endlessly.
460
+ newConversation.originalMessageCountAtCompaction = oldConversation.originalMessageCountAtCompaction;
461
+ }
462
+
463
+ newConversation.lastUpdated = new Date().toISOString();
464
+
465
+ this.logger.info(`Copied conversation history from ${oldModel} to ${newModel}`, {
466
+ agentId,
467
+ messageCount: newConversation.messages.length,
468
+ hasCompacted: !!newConversation.compactizedMessages
469
+ });
470
+ }
471
+ }
472
+
473
+ this.logger.info(`Model changed via UI - updating both preferredModel and currentModel`, {
474
+ agentId,
475
+ oldModel,
476
+ newModel,
477
+ conversationCopied: oldModel && updatedAgent.conversations[oldModel]?.messages.length > 0
478
+ });
479
+ }
480
+
481
+ // Update agent in registry
482
+ this.agents.set(agentId, updatedAgent);
483
+
484
+ // Log the actual update for debugging
485
+ this.logger.info(`Agent updated in registry with mode: ${updatedAgent.mode}`, {
486
+ agentId,
487
+ beforeMode: agent.mode,
488
+ afterMode: updatedAgent.mode,
489
+ allUpdates: Object.keys(updates)
490
+ });
491
+
492
+ // Update agent directory
493
+ this._updateAgentDirectory(updatedAgent);
494
+
495
+ // Persist the updated agent state
496
+ await this.stateManager.persistAgentState(updatedAgent);
497
+
498
+ // Record the mode transition (both directions) into the scheduler's
499
+ // per-agent history. This is where UI-toggle and programmatic
500
+ // update_agent flips land internal scheduler flips go through
501
+ // scheduler._transitionMode directly. Both feed the same ring buffer
502
+ // and the same /scheduler visualizer row.
503
+ if (updates.mode !== undefined && agent.mode !== updates.mode && this.scheduler?.recordModeTransition) {
504
+ this.scheduler.recordModeTransition(agentId, agent.mode, updates.mode, 'user-toggle');
505
+ }
506
+
507
+ // CRITICAL: If agent was switched to AGENT mode, add it to scheduler
508
+ if (updates.mode === AGENT_MODES.AGENT && this.scheduler) {
509
+ // CRITICAL FIX: Use the session ID from updates first, then agent's sessionId
510
+ // Register session with scheduler for API key resolution
511
+ // NOTE: The scheduler now uses AgentActivityService to determine which agents
512
+ // should be active, so we just register the session here
513
+ const sessionId = updates.sessionId || updatedAgent.sessionId;
514
+
515
+ if (!sessionId) {
516
+ this.logger.warn(`Agent ${agentId} switching to AGENT mode but has no sessionId - this will cause API key resolution issues`);
517
+ }
518
+
519
+ this.logger.info(`Registering agent session with scheduler (switched to AGENT mode): ${agentId}`, {
520
+ agentName: updatedAgent.name,
521
+ sessionId: sessionId,
522
+ hasSessionId: !!sessionId
523
+ });
524
+
525
+ await this.scheduler.addAgent(agentId, {
526
+ sessionId: sessionId,
527
+ triggeredBy: 'mode-change-to-agent'
528
+ });
529
+ }
530
+
531
+ // If agent was switched from AGENT to CHAT mode, clean up session tracking
532
+ // NOTE: The agent will automatically become inactive in the next scheduler cycle
533
+ // based on AgentActivityService.shouldAgentBeActive() returning false
534
+ if (agent.mode === AGENT_MODES.AGENT && updates.mode === AGENT_MODES.CHAT && this.scheduler) {
535
+ this.logger.info(`Agent mode changed to CHAT - will become inactive: ${agentId}`);
536
+ this.scheduler.removeAgent(agentId, 'mode-change-to-chat');
537
+ }
538
+
539
+ this.logger.info(`Agent updated successfully: ${agentId}`, {
540
+ newName: updatedAgent.name,
541
+ changes: Object.keys(updates)
542
+ });
543
+
544
+ return updatedAgent;
545
+ }
546
+
547
+ /**
548
+ * Agent notification from Message Processor for inter-agent communication
549
+ * @param {string} agentId - Target agent ID
550
+ * @param {Object} message - Message object with agent redirect
551
+ * @returns {Promise<boolean>} Success status
552
+ */
553
+ async notifyAgent(agentId, message) {
554
+ const agent = await this.getAgent(agentId);
555
+ if (!agent) {
556
+ this.logger.warn(`Agent notification failed - agent not found: ${agentId}`);
557
+ return false;
558
+ }
559
+
560
+ // Check if agent is paused
561
+ if (this._isAgentPaused(agent)) {
562
+ this.logger.info(`Agent notification queued - agent is paused: ${agentId}`);
563
+ this._queueNotification(agentId, message);
564
+ return true;
565
+ }
566
+
567
+ // Add notification to agent's conversation
568
+ const notificationMessage = {
569
+ id: `msg-${Date.now()}`,
570
+ conversationId: message.conversationId,
571
+ agentId: message.from, // sender agent ID
572
+ content: message.content,
573
+ role: MESSAGE_ROLES.SYSTEM,
574
+ timestamp: new Date().toISOString(),
575
+ type: MESSAGE_TYPES.AGENT_NOTIFICATION,
576
+ fromAgent: message.from,
577
+ context: message.context,
578
+ urgent: message.urgent || false,
579
+ requiresResponse: message.requiresResponse || false
580
+ };
581
+
582
+ // Add to full conversation
583
+ agent.conversations.full.messages.push(notificationMessage);
584
+ agent.conversations.full.lastUpdated = new Date().toISOString();
585
+
586
+ // Add to current model conversation
587
+ if (agent.currentModel && agent.conversations[agent.currentModel]) {
588
+ const formattedMessage = this._formatMessageForModel(notificationMessage, agent.currentModel);
589
+ agent.conversations[agent.currentModel].messages.push(formattedMessage);
590
+ agent.conversations[agent.currentModel].lastUpdated = new Date().toISOString();
591
+ }
592
+
593
+ // Update agent activity
594
+ agent.lastActivity = new Date().toISOString();
595
+ await this.persistAgentState(agentId);
596
+
597
+ this.logger.info(`Agent notified: ${agentId}`, {
598
+ fromAgent: message.from,
599
+ urgent: message.urgent,
600
+ requiresResponse: message.requiresResponse
601
+ });
602
+
603
+ return true;
604
+ }
605
+
606
+ /**
607
+ * Get all agents (returns full agent objects)
608
+ * @returns {Promise<Array>} List of all agent objects
609
+ */
610
+ async getAllAgents() {
611
+ const agents = Array.from(this.agents.values());
612
+
613
+ // Update pause status for all agents
614
+ for (const agent of agents) {
615
+ this._updateAgentPauseStatus(agent);
616
+ }
617
+
618
+ return agents;
619
+ }
620
+
621
+ /**
622
+ * List all active agents with their current status
623
+ * @returns {Promise<Array>} Array of agent objects
624
+ */
625
+ async listActiveAgents() {
626
+ const agents = Array.from(this.agents.values());
627
+
628
+ // Update pause status for all agents
629
+ for (const agent of agents) {
630
+ this._updateAgentPauseStatus(agent);
631
+ }
632
+
633
+ return agents.map(agent => ({
634
+ id: agent.id,
635
+ name: agent.name,
636
+ type: agent.type,
637
+ status: agent.status,
638
+ mode: agent.mode,
639
+ systemPrompt: agent.systemPrompt,
640
+ originalSystemPrompt: agent.originalSystemPrompt,
641
+ preferredModel: agent.preferredModel,
642
+ currentModel: agent.currentModel,
643
+ dynamicModelRouting: agent.dynamicModelRouting,
644
+ routingStrategy: agent.routingStrategy || '',
645
+ skills: agent.skills || [],
646
+ platformProvided: agent.platformProvided,
647
+ capabilities: agent.capabilities,
648
+ directoryAccess: agent.directoryAccess,
649
+ toolConfig: agent.toolConfig || {},
650
+ lastActivity: agent.lastActivity,
651
+ isPaused: this._isAgentPaused(agent),
652
+ pausedUntil: agent.pausedUntil,
653
+ messageCount: agent.conversations.full.messages.length,
654
+ createdAt: agent.createdAt,
655
+ // First user message snippet for card preview (2 lines max)
656
+ firstUserMessage: this._getFirstUserMessageSnippet(agent)
657
+ }));
658
+ }
659
+
660
+ /**
661
+ * Persist agent state to storage
662
+ * @param {string} agentId - Agent identifier
663
+ * @returns {Promise<void>}
664
+ */
665
+ async persistAgentState(agentId) {
666
+ const agent = await this.getAgent(agentId);
667
+ if (!agent) {
668
+ throw new Error(`Agent not found: ${agentId}`);
669
+ }
670
+
671
+ await this.stateManager.persistAgentState(agent);
672
+ }
673
+
674
+ /**
675
+ * Rehydrate an agent from persisted state on session boot. NOT the
676
+ * pause-toggle (see `resumeAgent(agentId)` below). The two were
677
+ * historically both named `resumeAgent`, which made this one dead
678
+ * code JS class bodies keep the second definition only, so callers
679
+ * passing an object ended up in the id-based handler and silently got
680
+ * "Agent not found".
681
+ *
682
+ * Callers: agentPool.restoreAgent, which is in turn called from
683
+ * orchestrator's boot-time state restoration path.
684
+ *
685
+ * @param {Object} agentData - Persisted agent data
686
+ * @returns {Promise<Object>} Restored agent object
687
+ */
688
+ async rehydrateFromState(agentData) {
689
+ const agent = {
690
+ ...agentData,
691
+ status: agentData.status === 'paused' && this._isPauseExpired(agentData) ? 'active' : agentData.status
692
+ };
693
+
694
+ // RECOVERY: If agent was paused awaiting user input (e.g., credentials),
695
+ // the promise is now lost due to server/UI restart. Resume the agent.
696
+ if (agent.awaitingUserInput) {
697
+ this.logger.warn(`Agent ${agent.id} was awaiting user input (${agent.awaitingUserInput.type}) - recovering from interrupted state`, {
698
+ inputType: agent.awaitingUserInput.type,
699
+ siteId: agent.awaitingUserInput.siteId,
700
+ startedAt: agent.awaitingUserInput.startedAt
701
+ });
702
+
703
+ // Clear the awaiting flag and resume agent
704
+ delete agent.awaitingUserInput;
705
+ agent.status = AGENT_STATUS.ACTIVE;
706
+
707
+ // Add a system message to the agent's queue so it knows what happened
708
+ if (!agent.messageQueues) {
709
+ agent.messageQueues = { toolResults: [], interAgentMessages: [], userMessages: [] };
710
+ }
711
+ agent.messageQueues.toolResults.push({
712
+ id: `recovery-${Date.now()}`,
713
+ toolId: 'system-recovery',
714
+ status: 'info',
715
+ result: {
716
+ message: 'Agent was waiting for user input (credentials) when the session was interrupted. The credential request has been cancelled. Please retry the authentication if needed.',
717
+ recoveredFrom: 'awaitingUserInput',
718
+ timestamp: new Date().toISOString()
719
+ },
720
+ timestamp: new Date().toISOString()
721
+ });
722
+ }
723
+
724
+ // Validate conversations structure
725
+ if (!agent.conversations || !agent.conversations.full) {
726
+ agent.conversations = {
727
+ full: {
728
+ messages: [],
729
+ lastUpdated: new Date().toISOString()
730
+ }
731
+ };
732
+ }
733
+
734
+ // CRITICAL: Restore interAgentTracking as a Map (it comes as plain object from JSON)
735
+ if (!agent.interAgentTracking || !(agent.interAgentTracking instanceof Map)) {
736
+ // Convert plain object to Map, or create empty Map
737
+ if (agent.interAgentTracking && typeof agent.interAgentTracking === 'object') {
738
+ agent.interAgentTracking = new Map(Object.entries(agent.interAgentTracking));
739
+ } else {
740
+ agent.interAgentTracking = new Map();
741
+ }
742
+ }
743
+
744
+ // Add to registry and directory
745
+ this.agents.set(agent.id, agent);
746
+ this._updateAgentDirectory(agent);
747
+
748
+ // CRITICAL: Migrate conversation structure to ensure new fields exist
749
+ // This handles agents persisted before the originalMessageCountAtCompaction fix
750
+ await this.migrateConversationStructure(agent.id);
751
+
752
+ // Process any queued notifications
753
+ await this._processQueuedNotifications(agent.id);
754
+
755
+ this.logger.info(`Agent resumed: ${agent.id}`, {
756
+ name: agent.name,
757
+ status: agent.status,
758
+ messageCount: agent.conversations.full.messages.length
759
+ });
760
+
761
+ return agent;
762
+ }
763
+
764
+ /**
765
+ * Pause agent for specified duration
766
+ * @param {string} agentId - Agent identifier
767
+ * @param {number|Date} duration - Pause duration in seconds or Date object
768
+ * @param {string} reason - Reason for pause
769
+ * @returns {Promise<Object>} Pause confirmation
770
+ */
771
+ async pauseAgent(agentId, duration, reason = 'Agent pause requested') {
772
+ const agent = await this.getAgent(agentId);
773
+ if (!agent) {
774
+ throw new Error(`Agent not found: ${agentId}`);
775
+ }
776
+
777
+ let pauseUntil;
778
+ if (duration instanceof Date) {
779
+ pauseUntil = duration;
780
+ } else {
781
+ // Duration in seconds
782
+ const maxPauseDuration = this.config.system?.maxPauseDuration || 300;
783
+ const pauseSeconds = Math.min(duration, maxPauseDuration);
784
+ pauseUntil = new Date(Date.now() + pauseSeconds * 1000);
785
+ }
786
+
787
+ agent.status = AGENT_STATUS.PAUSED;
788
+ agent.pausedUntil = pauseUntil.toISOString();
789
+ agent.lastActivity = new Date().toISOString();
790
+
791
+ // Add to paused agents tracking
792
+ this.pausedAgents.set(agentId, {
793
+ agentId,
794
+ pausedAt: new Date().toISOString(),
795
+ pausedUntil: pauseUntil.toISOString(),
796
+ reason,
797
+ originalStatus: AGENT_STATUS.ACTIVE
798
+ });
799
+
800
+ await this.persistAgentState(agentId);
801
+
802
+ this.logger.info(`Agent paused: ${agentId}`, {
803
+ pausedUntil: pauseUntil.toISOString(),
804
+ reason,
805
+ durationSeconds: Math.round((pauseUntil.getTime() - Date.now()) / 1000)
806
+ });
807
+
808
+ return {
809
+ success: true,
810
+ agentId,
811
+ pausedUntil: pauseUntil.toISOString(),
812
+ reason,
813
+ message: `Agent paused until ${pauseUntil.toISOString()}`
814
+ };
815
+ }
816
+
817
+ /**
818
+ * Resume paused agent
819
+ * @param {string} agentId - Agent identifier
820
+ * @returns {Promise<Object>} Resume confirmation
821
+ */
822
+ async resumeAgent(agentId) {
823
+ const agent = await this.getAgent(agentId);
824
+ if (!agent) {
825
+ throw new Error(`Agent not found: ${agentId}`);
826
+ }
827
+
828
+ if (agent.status !== AGENT_STATUS.PAUSED) {
829
+ return {
830
+ success: true,
831
+ message: `Agent ${agentId} is not paused`
832
+ };
833
+ }
834
+
835
+ agent.status = AGENT_STATUS.ACTIVE;
836
+ agent.pausedUntil = null;
837
+ agent.lastActivity = new Date().toISOString();
838
+
839
+ // Remove from paused agents tracking
840
+ this.pausedAgents.delete(agentId);
841
+
842
+ // Process any queued notifications
843
+ await this._processQueuedNotifications(agentId);
844
+
845
+ await this.persistAgentState(agentId);
846
+
847
+ this.logger.info(`Agent resumed: ${agentId}`);
848
+
849
+ return {
850
+ success: true,
851
+ agentId,
852
+ message: `Agent ${agentId} resumed successfully`
853
+ };
854
+ }
855
+
856
+ /**
857
+ * Restore agent from saved state. Thin wrapper around rehydrateFromState
858
+ * kept for the public-API name orchestrator + stateManager still use.
859
+ * @param {Object} agentState - Saved agent state
860
+ * @returns {Promise<Object>} Restored agent
861
+ */
862
+ async restoreAgent(agentState) {
863
+ return await this.rehydrateFromState(agentState);
864
+ }
865
+
866
+ /**
867
+ * Get agent discovery directory
868
+ * @returns {Array} Array of agent info for discovery
869
+ */
870
+ getAgentDirectory() {
871
+ return Array.from(this.agentDirectory.values());
872
+ }
873
+
874
+ /**
875
+ * List all active agents
876
+ * @returns {Array} Array of active agents
877
+ */
878
+
879
+ /**
880
+ * Delete an agent and clean up its resources.
881
+ *
882
+ * Best-effort / idempotent: if the agent isn't currently loaded in
883
+ * memory we skip the live-state cleanup (attachments, visual editor,
884
+ * terminal processes those only exist for loaded agents anyway)
885
+ * and go straight to clearing persistent state. This is how the
886
+ * bulk-delete UI can remove agents that were unloaded earlier in
887
+ * the session or that survived a process restart with no in-memory
888
+ * footprint. `stateManager.deleteAgentState` is itself ENOENT-safe,
889
+ * so deleting an already-gone agent is a no-op success.
890
+ *
891
+ * @param {string} agentId - Agent identifier
892
+ * @returns {Promise<Object>} Deletion result
893
+ */
894
+ async deleteAgent(agentId) {
895
+ const agent = await this.getAgent(agentId);
896
+ const wasLoaded = !!agent;
897
+
898
+ if (wasLoaded) {
899
+ // Clean up file attachments with reference counting
900
+ if (this.fileAttachmentService) {
901
+ try {
902
+ await this.fileAttachmentService.deleteAgentAttachments(agentId);
903
+ this.logger.info(`File attachments cleaned up for agent: ${agentId}`);
904
+ } catch (error) {
905
+ this.logger.warn(`Failed to clean up file attachments for agent: ${error.message}`, { agentId });
906
+ // Continue with agent deletion even if attachment cleanup fails
907
+ }
908
+ }
909
+
910
+ // Clear codebase-knowledge cache for this agent — bytes live in
911
+ // the service's in-memory map; without this the per-agent entry
912
+ // would persist until the next process restart.
913
+ try {
914
+ const { getCodebaseKnowledgeService } = await import('../services/codebaseKnowledgeService.js');
915
+ getCodebaseKnowledgeService(this.logger).clear(agentId);
916
+ } catch (e) {
917
+ this.logger.debug?.('codebase-knowledge clear failed on agent delete', { agentId, error: e.message });
918
+ }
919
+
920
+ // Clean up visual editor instance
921
+ try {
922
+ const visualEditorBridge = getVisualEditorBridge();
923
+ if (visualEditorBridge.hasInstance(agentId)) {
924
+ await visualEditorBridge.stopInstance(agentId);
925
+ this.logger.info(`Visual editor instance cleaned up for agent: ${agentId}`);
926
+ }
927
+ } catch (error) {
928
+ this.logger.warn(`Failed to clean up visual editor for agent: ${error.message}`, { agentId });
929
+ }
930
+
931
+ // Kill any running terminal processes for this agent
932
+ try {
933
+ const terminalTool = this.toolsRegistry?.getTool?.('terminal');
934
+ if (terminalTool && typeof terminalTool.cleanupAgent === 'function') {
935
+ await terminalTool.cleanupAgent(agentId);
936
+ this.logger.info(`Terminal processes cleaned up for agent: ${agentId}`);
937
+ }
938
+ } catch (error) {
939
+ this.logger.warn(`Failed to clean up terminal processes for agent: ${error.message}`, { agentId });
940
+ }
941
+
942
+ // Clean up agent resources
943
+ this.agents.delete(agentId);
944
+ this.agentDirectory.delete(agentId);
945
+ this.pausedAgents.delete(agentId);
946
+ this.notificationQueue.delete(agentId);
947
+ }
948
+
949
+ // Clean up persistent state — runs whether or not the agent was
950
+ // loaded. deleteAgentState handles missing files gracefully.
951
+ try {
952
+ await this.stateManager.deleteAgentState(agentId);
953
+ } catch (error) {
954
+ this.logger.warn(`Failed to delete agent persistent state: ${error.message}`, { agentId });
955
+ }
956
+
957
+ this.logger.info(`Agent deleted: ${agentId}`, {
958
+ agentName: agent?.name || '(not loaded)',
959
+ wasLoaded,
960
+ totalAgents: this.agents.size
961
+ });
962
+
963
+ return {
964
+ success: true,
965
+ agentId,
966
+ remainingAgents: this.agents.size
967
+ };
968
+ }
969
+
970
+ /**
971
+ * Unload an agent from server memory without deleting persistent files
972
+ * Agent can be reloaded later using the Load Agent feature
973
+ * @param {string} agentId - Agent identifier
974
+ * @returns {Promise<Object>} Unload result
975
+ */
976
+ async unloadAgent(agentId) {
977
+ const agent = await this.getAgent(agentId);
978
+ if (!agent) {
979
+ throw new Error(`Agent not found: ${agentId}`);
980
+ }
981
+
982
+ const agentName = agent.name;
983
+
984
+ // Persist current state before unloading (so it can be reloaded later)
985
+ try {
986
+ await this.persistAgentState(agentId);
987
+ this.logger.info(`Agent state persisted before unload: ${agentId}`);
988
+ } catch (error) {
989
+ this.logger.warn(`Failed to persist agent state before unload: ${error.message}`, { agentId });
990
+ }
991
+
992
+ // Clean up visual editor instance
993
+ try {
994
+ const visualEditorBridge = getVisualEditorBridge();
995
+ if (visualEditorBridge.hasInstance(agentId)) {
996
+ await visualEditorBridge.stopInstance(agentId);
997
+ this.logger.info(`Visual editor instance cleaned up for unloaded agent: ${agentId}`);
998
+ }
999
+ } catch (error) {
1000
+ this.logger.warn(`Failed to clean up visual editor for unloaded agent: ${error.message}`, { agentId });
1001
+ }
1002
+
1003
+ // Remove from memory only (keep persistent files)
1004
+ this.agents.delete(agentId);
1005
+ this.agentDirectory.delete(agentId);
1006
+ this.pausedAgents.delete(agentId);
1007
+ this.notificationQueue.delete(agentId);
1008
+
1009
+ // Remove from scheduler if present
1010
+ if (this.scheduler) {
1011
+ this.scheduler.removeAgent(agentId, 'unloaded');
1012
+ }
1013
+
1014
+ this.logger.info(`Agent unloaded from memory: ${agentId}`, {
1015
+ agentName,
1016
+ totalAgents: this.agents.size,
1017
+ note: 'Persistent files preserved for future reload'
1018
+ });
1019
+
1020
+ return {
1021
+ success: true,
1022
+ agentId,
1023
+ agentName,
1024
+ remainingAgents: this.agents.size,
1025
+ message: `Agent "${agentName}" unloaded. Use Load Agent to reload it.`
1026
+ };
1027
+ }
1028
+
1029
+ /**
1030
+ * Clear all conversation history for an agent
1031
+ * Resets the agent to a fresh state while keeping configuration
1032
+ * @param {string} agentId - Agent identifier
1033
+ * @returns {Promise<Object>} Clear result
1034
+ */
1035
+ async clearConversation(agentId) {
1036
+ const agent = await this.getAgent(agentId);
1037
+ if (!agent) {
1038
+ throw new Error(`Agent not found: ${agentId}`);
1039
+ }
1040
+
1041
+ const previousMessageCount = agent.conversations?.full?.messages?.length || 0;
1042
+
1043
+ // Reset full conversation
1044
+ agent.conversations.full = {
1045
+ messages: [],
1046
+ lastUpdated: new Date().toISOString()
1047
+ };
1048
+
1049
+ // Reset model-specific conversations
1050
+ for (const key of Object.keys(agent.conversations)) {
1051
+ if (key !== 'full') {
1052
+ agent.conversations[key] = {
1053
+ messages: [],
1054
+ compactizedMessages: null,
1055
+ lastUpdated: new Date().toISOString(),
1056
+ compactionState: {
1057
+ isCompacted: false,
1058
+ lastCompactionTime: null,
1059
+ originalMessageCount: 0,
1060
+ compactedMessageCount: 0
1061
+ }
1062
+ };
1063
+ }
1064
+ }
1065
+
1066
+ // Clear message queues
1067
+ if (agent.messageQueues) {
1068
+ agent.messageQueues = {
1069
+ toolResults: [],
1070
+ interAgentMessages: [],
1071
+ userMessages: []
1072
+ };
1073
+ }
1074
+
1075
+ // Clear task list
1076
+ if (agent.taskList) {
1077
+ agent.taskList = {
1078
+ tasks: [],
1079
+ lastUpdated: new Date().toISOString()
1080
+ };
1081
+ }
1082
+
1083
+ agent.currentTask = null;
1084
+ agent.taskStartTime = null;
1085
+ agent.iterationCount = 0;
1086
+
1087
+ // Persist the cleared state
1088
+ await this.persistAgentState(agentId);
1089
+
1090
+ this.logger.info(`Conversation cleared for agent: ${agentId}`, {
1091
+ agentName: agent.name,
1092
+ previousMessageCount
1093
+ });
1094
+
1095
+ return {
1096
+ success: true,
1097
+ agentId,
1098
+ previousMessageCount,
1099
+ message: `Cleared ${previousMessageCount} messages`
1100
+ };
1101
+ }
1102
+
1103
+ /**
1104
+ * Generate unique agent ID
1105
+ * @private
1106
+ */
1107
+ _generateAgentId(name) {
1108
+ const sanitizedName = name.toLowerCase().replace(/[^a-z0-9]/g, '-');
1109
+ const timestamp = Date.now();
1110
+ return `agent-${sanitizedName}-${timestamp}`;
1111
+ }
1112
+
1113
+ /**
1114
+ * Update agent directory for discovery
1115
+ * @private
1116
+ */
1117
+ _updateAgentDirectory(agent) {
1118
+ this.agentDirectory.set(agent.id, {
1119
+ id: agent.id,
1120
+ name: agent.name,
1121
+ type: agent.type,
1122
+ capabilities: agent.capabilities,
1123
+ status: agent.status,
1124
+ description: this._generateAgentDescription(agent)
1125
+ });
1126
+ }
1127
+
1128
+ /**
1129
+ * Generate agent description for directory
1130
+ * @private
1131
+ */
1132
+ _generateAgentDescription(agent) {
1133
+ let description = `${agent.name} (${agent.type})`;
1134
+
1135
+ if (agent.capabilities.length > 0) {
1136
+ description += ` - Capabilities: ${agent.capabilities.join(', ')}`;
1137
+ }
1138
+
1139
+ return description;
1140
+ }
1141
+
1142
+ /**
1143
+ * Check if agent is currently paused
1144
+ * @private
1145
+ */
1146
+ _isAgentPaused(agent) {
1147
+ if (agent.status !== AGENT_STATUS.PAUSED || !agent.pausedUntil) {
1148
+ return false;
1149
+ }
1150
+
1151
+ return new Date() < new Date(agent.pausedUntil);
1152
+ }
1153
+
1154
+ /**
1155
+ * Get first user message snippet for card preview
1156
+ * @private
1157
+ */
1158
+ _getFirstUserMessageSnippet(agent) {
1159
+ const messages = agent.conversations?.full?.messages;
1160
+ if (!messages || messages.length === 0) return null;
1161
+
1162
+ // Find first user message — include consolidated-input since that's how
1163
+ // user messages are stored after queue processing. Skip task-boundary.
1164
+ const firstUser = messages.find(m =>
1165
+ m.role === 'user' && m.content &&
1166
+ m.type !== 'task-boundary'
1167
+ );
1168
+ if (!firstUser) return null;
1169
+
1170
+ // Handle both string and array content formats
1171
+ const text = typeof firstUser.content === 'string'
1172
+ ? firstUser.content
1173
+ : Array.isArray(firstUser.content)
1174
+ ? firstUser.content.filter(b => b.type === 'text').map(b => b.text).join('\n')
1175
+ : null;
1176
+ if (!text) return null;
1177
+
1178
+ // Take first 2 non-empty lines, cap at 120 chars
1179
+ const lines = text.split('\n').filter(l => l.trim());
1180
+ const snippet = lines.slice(0, 2).join('\n');
1181
+ return snippet.length > 120 ? snippet.slice(0, 117) + '...' : snippet;
1182
+ }
1183
+
1184
+ /**
1185
+ * Check if pause duration has expired
1186
+ * @private
1187
+ */
1188
+ _isPauseExpired(agent) {
1189
+ if (!agent.pausedUntil) return true;
1190
+ return new Date() >= new Date(agent.pausedUntil);
1191
+ }
1192
+
1193
+ /**
1194
+ * Update agent pause status
1195
+ * @private
1196
+ */
1197
+ _updateAgentPauseStatus(agent) {
1198
+ if (agent.status === AGENT_STATUS.PAUSED && this._isPauseExpired(agent)) {
1199
+ agent.status = AGENT_STATUS.ACTIVE;
1200
+ agent.pausedUntil = null;
1201
+ this.pausedAgents.delete(agent.id);
1202
+ }
1203
+ }
1204
+
1205
+ /**
1206
+ * Queue notification for paused agent
1207
+ * @private
1208
+ */
1209
+ _queueNotification(agentId, message) {
1210
+ if (!this.notificationQueue.has(agentId)) {
1211
+ this.notificationQueue.set(agentId, []);
1212
+ }
1213
+
1214
+ this.notificationQueue.get(agentId).push({
1215
+ ...message,
1216
+ queuedAt: new Date().toISOString()
1217
+ });
1218
+ }
1219
+
1220
+ /**
1221
+ * Process queued notifications for agent
1222
+ * @private
1223
+ */
1224
+ async _processQueuedNotifications(agentId) {
1225
+ const notifications = this.notificationQueue.get(agentId);
1226
+ if (!notifications || notifications.length === 0) {
1227
+ return;
1228
+ }
1229
+
1230
+ this.logger.info(`Processing ${notifications.length} queued notifications for agent: ${agentId}`);
1231
+
1232
+ for (const notification of notifications) {
1233
+ await this.notifyAgent(agentId, notification);
1234
+ }
1235
+
1236
+ // Clear queue
1237
+ this.notificationQueue.delete(agentId);
1238
+ }
1239
+
1240
+ /**
1241
+ * Format message for specific model
1242
+ * @private
1243
+ */
1244
+ _formatMessageForModel(message) {
1245
+ // This would be implemented with model-specific formatting logic
1246
+ // For now, return the message as-is
1247
+ return { ...message };
1248
+ }
1249
+
1250
+ /**
1251
+ * Get model format version
1252
+ * @private
1253
+ */
1254
+ _getModelFormatVersion(model) {
1255
+ return MODEL_FORMAT_VERSIONS[model] || MODEL_FORMAT_VERSIONS.DEFAULT;
1256
+ }
1257
+
1258
+ /**
1259
+ * Refresh tool descriptions for an existing agent
1260
+ * @param {string} agentId - Agent identifier
1261
+ * @param {Object} options - Refresh options
1262
+ * @returns {Promise<boolean>} Success status
1263
+ */
1264
+ async refreshAgentToolDescriptions(agentId, options = {}) {
1265
+ const agent = await this.getAgent(agentId);
1266
+ if (!agent || !this.toolsRegistry) {
1267
+ return false;
1268
+ }
1269
+
1270
+ try {
1271
+ // Use original prompt if available, otherwise current prompt
1272
+ const basePrompt = agent.originalSystemPrompt || agent.systemPrompt;
1273
+
1274
+ // Enhance with current tool capabilities
1275
+ const enhancedPrompt = this.toolsRegistry.enhanceSystemPrompt(
1276
+ basePrompt,
1277
+ agent.capabilities,
1278
+ {
1279
+ compact: options.compact || false,
1280
+ includeExamples: options.includeExamples !== false,
1281
+ includeUsageGuidelines: options.includeUsageGuidelines !== false,
1282
+ includeSecurityNotes: options.includeSecurityNotes !== false
1283
+ }
1284
+ );
1285
+
1286
+ // Update agent's system prompt
1287
+ agent.systemPrompt = enhancedPrompt;
1288
+ agent.lastActivity = new Date().toISOString();
1289
+
1290
+ // Persist changes
1291
+ await this.persistAgentState(agentId);
1292
+
1293
+ this.logger?.info(`Agent tool descriptions refreshed: ${agentId}`, {
1294
+ capabilities: agent.capabilities,
1295
+ promptLength: enhancedPrompt.length
1296
+ });
1297
+
1298
+ return true;
1299
+
1300
+ } catch (error) {
1301
+ this.logger?.error(`Failed to refresh tool descriptions for agent: ${agentId}`, {
1302
+ error: error.message
1303
+ });
1304
+ return false;
1305
+ }
1306
+ }
1307
+
1308
+ /**
1309
+ * Set or update tools registry for the agent pool
1310
+ * @param {ToolsRegistry} toolsRegistry - Tools registry instance
1311
+ */
1312
+ setToolsRegistry(toolsRegistry) {
1313
+ this.toolsRegistry = toolsRegistry;
1314
+
1315
+ this.logger?.info('Tools registry updated for agent pool', {
1316
+ hasRegistry: !!toolsRegistry
1317
+ });
1318
+ }
1319
+
1320
+ /**
1321
+ * Bulk refresh tool descriptions for all agents
1322
+ * @param {Object} options - Refresh options
1323
+ * @returns {Promise<Object>} Results summary
1324
+ */
1325
+ async bulkRefreshToolDescriptions(options = {}) {
1326
+ const results = {
1327
+ total: this.agents.size,
1328
+ successful: 0,
1329
+ failed: 0,
1330
+ skipped: 0
1331
+ };
1332
+
1333
+ for (const [agentId, agent] of this.agents.entries()) {
1334
+ if (!agent.capabilities || agent.capabilities.length === 0) {
1335
+ results.skipped++;
1336
+ continue;
1337
+ }
1338
+
1339
+ const success = await this.refreshAgentToolDescriptions(agentId, options);
1340
+ if (success) {
1341
+ results.successful++;
1342
+ } else {
1343
+ results.failed++;
1344
+ }
1345
+ }
1346
+
1347
+ this.logger?.info('Bulk tool descriptions refresh completed', results);
1348
+ return results;
1349
+ }
1350
+
1351
+ /**
1352
+ * Set MessageProcessor reference for triggering responses
1353
+ * @param {MessageProcessor} messageProcessor - MessageProcessor instance
1354
+ */
1355
+ setMessageProcessor(messageProcessor) {
1356
+ this.messageProcessor = messageProcessor;
1357
+ }
1358
+
1359
+ /**
1360
+ * Set AgentScheduler reference for managing agent modes
1361
+ * @param {AgentScheduler} scheduler - AgentScheduler instance
1362
+ */
1363
+ setScheduler(scheduler) {
1364
+ this.scheduler = scheduler;
1365
+
1366
+ this.logger?.info('AgentScheduler reference set for agent pool', {
1367
+ hasScheduler: !!scheduler
1368
+ });
1369
+ }
1370
+
1371
+ /**
1372
+ * Set FileAttachmentService reference for cleaning up attachments
1373
+ * @param {FileAttachmentService} fileAttachmentService - FileAttachmentService instance
1374
+ */
1375
+ setFileAttachmentService(fileAttachmentService) {
1376
+ this.fileAttachmentService = fileAttachmentService;
1377
+
1378
+ this.logger?.info('FileAttachmentService reference set for agent pool', {
1379
+ hasService: !!fileAttachmentService
1380
+ });
1381
+ }
1382
+
1383
+ // OLD INTER-AGENT MESSAGE QUEUE SYSTEM REMOVED
1384
+ // Now using the new messageQueues system with AgentScheduler
1385
+ // Inter-agent messages are queued via addInterAgentMessage() method
1386
+
1387
+ /**
1388
+ * Wake an agent out of any paused/delayed state because a message has
1389
+ * arrived for it. Shared by addUserMessage / addInterAgentMessage /
1390
+ * addToolResult every inbound path MUST route through this to avoid
1391
+ * the "silent queue into a delayed agent" bug where the recipient never
1392
+ * wakes up and the message sits until the scheduler back-off naturally
1393
+ * expires.
1394
+ *
1395
+ * Rationale: delays/pauses are "leave this agent alone" signals set by
1396
+ * the scheduler on error back-off or by the agentDelayTool for timed
1397
+ * waits. Any caller going to the trouble of actually addressing the
1398
+ * agent is an explicit "act now" signal that overrides the wait.
1399
+ *
1400
+ * @param {Object} agent - Agent object (already fetched)
1401
+ * @param {string} reason - Source label for logs (e.g. 'user-message',
1402
+ * 'inter-agent-message', 'tool-result')
1403
+ * @returns {Promise<{wasPaused:boolean, hadDelay:boolean, hadPausedUntil:boolean}>}
1404
+ * @private
1405
+ */
1406
+ async _wakeAgentForMessage(agent, reason) {
1407
+ const info = { wasPaused: false, hadDelay: false, hadPausedUntil: false };
1408
+ if (!agent) return info;
1409
+
1410
+ // Auto-resume explicitly paused agent.
1411
+ if (agent.status === AGENT_STATUS.PAUSED) {
1412
+ info.wasPaused = true;
1413
+ this.logger.info(`Auto-resuming paused agent ${agent.id} due to ${reason}`);
1414
+ await this.resumeAgent(agent.id);
1415
+ }
1416
+
1417
+ // Clear scheduler-enforced delay (rate-limit back-off, api-key delay,
1418
+ // builtin webTool delay, etc.). Only clear if actually in the future;
1419
+ // stale past values don't matter but shouldn't trigger a broadcast.
1420
+ if (agent.delayEndTime && new Date(agent.delayEndTime).getTime() > Date.now()) {
1421
+ info.hadDelay = true;
1422
+ agent.delayEndTime = null;
1423
+ this.logger.info(`Cleared scheduler delay for agent ${agent.id} ${reason} takes precedence`);
1424
+ }
1425
+
1426
+ // Defensive: if pausedUntil is set but status isn't PAUSED (shouldn't
1427
+ // happen but protects against state drift), clear it too.
1428
+ if (agent.pausedUntil && new Date(agent.pausedUntil).getTime() > Date.now()) {
1429
+ info.hadPausedUntil = true;
1430
+ agent.pausedUntil = null;
1431
+ }
1432
+
1433
+ return info;
1434
+ }
1435
+
1436
+ /**
1437
+ * Broadcast a delay-clear to the WS so the UI's delay chip disappears
1438
+ * immediately instead of waiting for the next scheduler tick.
1439
+ * @private
1440
+ */
1441
+ async _broadcastWake(agentId, reason) {
1442
+ if (!this.scheduler?.broadcastAgentStateUpdate) return;
1443
+ try {
1444
+ await this.scheduler.broadcastAgentStateUpdate(agentId, reason);
1445
+ } catch (err) {
1446
+ this.logger.warn(`Failed to broadcast wake for ${agentId}: ${err.message}`);
1447
+ }
1448
+ }
1449
+
1450
+ /**
1451
+ * Add message to agent's user message queue
1452
+ * @param {string} agentId - Agent ID
1453
+ * @param {Object} message - User message to queue
1454
+ * @returns {Promise<void>}
1455
+ */
1456
+ async addUserMessage(agentId, message) {
1457
+ const agent = await this.getAgent(agentId);
1458
+ if (!agent) {
1459
+ throw new Error(`Agent not found: ${agentId}`);
1460
+ }
1461
+
1462
+ // Any inbound message — user, inter-agent, or tool-result — takes
1463
+ // precedence over scheduler back-off and manual pauses. See
1464
+ // _wakeAgentForMessage for the rationale.
1465
+ const wakeInfo = await this._wakeAgentForMessage(agent, 'user-message');
1466
+
1467
+ const queuedMessage = {
1468
+ ...message,
1469
+ id: message.id || `user-msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
1470
+ queuedAt: new Date().toISOString(),
1471
+ timestamp: message.timestamp || new Date().toISOString()
1472
+ };
1473
+
1474
+ agent.messageQueues.userMessages.push(queuedMessage);
1475
+
1476
+ // Auto-create a task for AGENT mode agents so the scheduler picks them up.
1477
+ // Scheduling condition is purely task-based: has pending tasks AND in agent mode.
1478
+ if (agent.mode === AGENT_MODES.AGENT) {
1479
+ this._autoCreateTaskForMessage(agent, queuedMessage, 'user', 'high');
1480
+ }
1481
+
1482
+ // ── Auto-save substantive user messages as plan/* memories ───────
1483
+ // Observed in production: across 670-message agent sessions the
1484
+ // agent NEVER wrote a memory voluntarily. Compaction then summarized
1485
+ // away the user's literal asks, the agent paraphrased what was left,
1486
+ // and ended up doing work the user never requested. Belt-and-
1487
+ // suspenders alongside the OPERATING POSTURE prompt nudge: when a
1488
+ // user message looks substantive (long, or contains a numbered/
1489
+ // bulleted multi-part ask), the SYSTEM saves it as `plan/<auto>` so
1490
+ // the system-prompt auto-injection makes the user's words visible
1491
+ // every turn even if the agent itself never thought to save.
1492
+ // Best-effort: never block the message-enqueue path.
1493
+ this._autoSaveUserMessageAsPlan(agentId, queuedMessage).catch(err => {
1494
+ this.logger.debug?.('Auto-save of user message as plan/* failed (continuing)', {
1495
+ agentId, error: err?.message,
1496
+ });
1497
+ });
1498
+
1499
+ await this.persistAgentState(agentId);
1500
+
1501
+ // If we cleared a delay, surface it on the WS so the delay chip in the
1502
+ // chat header disappears without waiting for the next scheduler cycle.
1503
+ if (wakeInfo.hadDelay) {
1504
+ await this._broadcastWake(agentId, 'user-message-clears-delay');
1505
+ }
1506
+
1507
+ this.logger.info(`User message queued for agent: ${agentId}`, {
1508
+ messageId: queuedMessage.id,
1509
+ queueSize: agent.messageQueues.userMessages.length
1510
+ });
1511
+ }
1512
+
1513
+ /**
1514
+ * Add message to agent's inter-agent message queue
1515
+ * @param {string} agentId - Agent ID
1516
+ * @param {Object} message - Inter-agent message to queue
1517
+ * @returns {Promise<void>}
1518
+ */
1519
+ async addInterAgentMessage(agentId, message) {
1520
+ const agent = await this.getAgent(agentId);
1521
+ if (!agent) {
1522
+ throw new Error(`Agent not found: ${agentId}`);
1523
+ }
1524
+
1525
+ // An inter-agent ping from another agent is an explicit "act now"
1526
+ // signal and must override any scheduler back-off or manual pause
1527
+ // on the recipient. See _wakeAgentForMessage.
1528
+ const wakeInfo = await this._wakeAgentForMessage(agent, 'inter-agent-message');
1529
+
1530
+ const queuedMessage = {
1531
+ ...message,
1532
+ id: message.id || message.messageId || `inter-agent-msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
1533
+ queuedAt: new Date().toISOString(),
1534
+ timestamp: message.timestamp || new Date().toISOString()
1535
+ };
1536
+
1537
+ agent.messageQueues.interAgentMessages.push(queuedMessage);
1538
+
1539
+ // Auto-create a task for AGENT mode agents so the scheduler picks them up.
1540
+ if (agent.mode === AGENT_MODES.AGENT) {
1541
+ const senderName = message.senderName || message.sender || 'Unknown Agent';
1542
+ this._autoCreateTaskForMessage(agent, queuedMessage, `inter-agent from ${senderName}`, 'medium');
1543
+ }
1544
+
1545
+ await this.persistAgentState(agentId);
1546
+
1547
+ // CRITICAL: Register recipient with scheduler so it has a sessionId for API key resolution.
1548
+ // Inter-agent messages carry the sender's sessionId reuse it for the recipient.
1549
+ if (this.scheduler && message.sessionId) {
1550
+ await this.scheduler.addAgent(agentId, {
1551
+ sessionId: message.sessionId,
1552
+ triggeredBy: 'inter-agent-message'
1553
+ });
1554
+ }
1555
+
1556
+ if (wakeInfo.hadDelay) {
1557
+ await this._broadcastWake(agentId, 'inter-agent-message-clears-delay');
1558
+ }
1559
+
1560
+ this.logger.info(`Inter-agent message queued for agent: ${agentId}`, {
1561
+ messageId: queuedMessage.id,
1562
+ sender: message.sender || message.senderName,
1563
+ queueSize: agent.messageQueues.interAgentMessages.length,
1564
+ sessionRegistered: !!(this.scheduler && message.sessionId)
1565
+ });
1566
+ }
1567
+
1568
+ /**
1569
+ * Add tool result to agent's tool results queue
1570
+ * @param {string} agentId - Agent ID
1571
+ * @param {Object} toolResult - Tool result to queue
1572
+ * @returns {Promise<void>}
1573
+ */
1574
+ async addToolResult(agentId, toolResult) {
1575
+ const agent = await this.getAgent(agentId);
1576
+ if (!agent) {
1577
+ throw new Error(`Agent not found: ${agentId}`);
1578
+ }
1579
+
1580
+ // Tool results are "external async work finished — continue."
1581
+ // If the agent is delayed or paused when a result arrives (e.g. a
1582
+ // long image/video render completes while an unrelated rate-limit
1583
+ // back-off is still in effect) we wake it so the result can be
1584
+ // consumed immediately instead of sitting until the back-off expires.
1585
+ const wakeInfo = await this._wakeAgentForMessage(agent, 'tool-result');
1586
+
1587
+ const queuedResult = {
1588
+ ...toolResult,
1589
+ id: toolResult.id || `tool-result-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
1590
+ queuedAt: new Date().toISOString(),
1591
+ timestamp: toolResult.timestamp || new Date().toISOString()
1592
+ };
1593
+
1594
+ agent.messageQueues.toolResults.push(queuedResult);
1595
+ await this.persistAgentState(agentId);
1596
+
1597
+ if (wakeInfo.hadDelay) {
1598
+ await this._broadcastWake(agentId, 'tool-result-clears-delay');
1599
+ }
1600
+
1601
+ this.logger.debug(`Tool result queued for agent: ${agentId}`, {
1602
+ toolId: toolResult.toolId,
1603
+ status: toolResult.status,
1604
+ queueSize: agent.messageQueues.toolResults.length
1605
+ });
1606
+ }
1607
+
1608
+ /**
1609
+ * Auto-create a task from an incoming message for AGENT mode agents.
1610
+ * This ensures the scheduler (which uses pending tasks as the sole activation
1611
+ * condition for AGENT mode) picks up the agent for processing.
1612
+ * @param {Object} agent - Agent object
1613
+ * @param {Object} message - The queued message
1614
+ * @param {string} source - Source label (e.g. 'user', 'inter-agent from AgentX')
1615
+ * @param {string} priority - Task priority ('high', 'medium', 'low')
1616
+ * @private
1617
+ */
1618
+ /**
1619
+ * Save a substantive user message as a `plan/*` memory automatically.
1620
+ *
1621
+ * Rationale (Talisman case study, May 2026): agents observed in
1622
+ * production never wrote a single memory across hundreds of
1623
+ * messages, even when the OPERATING POSTURE prompt explicitly told
1624
+ * them to. The user's literal ask then got lost in compaction and
1625
+ * the agent went off-course. This system-level safety net puts the
1626
+ * user's message into the durable plan/* store which the system
1627
+ * prompt auto-injects every turn without depending on the model
1628
+ * making the call.
1629
+ *
1630
+ * What counts as "substantive":
1631
+ * - Content length 60 chars (~12 words) — short acks/yes-no don't qualify
1632
+ * - AND any of:
1633
+ * contains a numbered list ("1.", "2.", "3." …)
1634
+ * • contains a bullet list (-, *, at line start)
1635
+ * • OR is 120 chars (longer than a one-line ack)
1636
+ *
1637
+ * What gets saved:
1638
+ * - title: `plan/user-<short-slug>-<timestamp>`
1639
+ * - description: "auto-saved from user message at <iso>"
1640
+ * - content: the verbatim user message
1641
+ *
1642
+ * The agent can rename, consolidate, or delete these later. They
1643
+ * exist as a fail-safe if the agent does its job and saves its
1644
+ * own better-named plan, these auto-saves can be cleaned up. If
1645
+ * the agent doesn't, at least the user's words survive compaction.
1646
+ *
1647
+ * @param {string} agentId
1648
+ * @param {Object} message - The queued user message
1649
+ * @private
1650
+ */
1651
+ async _autoSaveUserMessageAsPlan(agentId, message) {
1652
+ const content = typeof message?.content === 'string' ? message.content : '';
1653
+ if (!content) return;
1654
+ if (!this._looksSubstantive(content)) return;
1655
+
1656
+ // Lazy-load to keep agentPool's load order light. The same import
1657
+ // pattern as agentScheduler's plan injection.
1658
+ let memoryService;
1659
+ try {
1660
+ const mod = await import('../services/memoryService.js');
1661
+ memoryService = mod.getMemoryService(this.logger);
1662
+ await memoryService.initialize();
1663
+ } catch (e) {
1664
+ this.logger.debug?.('Auto-save plan: memory service unavailable', { error: e.message });
1665
+ return;
1666
+ }
1667
+
1668
+ // ── Deduplication ────────────────────────────────────────────────
1669
+ // Users repeat themselves ("I repeat my old message", "did you do
1670
+ // it all?" + paste the same thing). Without dedup the auto-saver
1671
+ // would create N copies of essentially the same plan. Load
1672
+ // existing plan/user-* memories and skip when the new content is
1673
+ // ≥70% similar to any of them (Jaccard over normalized word sets).
1674
+ let existingPlans = [];
1675
+ try {
1676
+ const all = await memoryService.loadMemories(agentId);
1677
+ existingPlans = (all || []).filter(m =>
1678
+ typeof m?.title === 'string' && m.title.startsWith('plan/user-')
1679
+ );
1680
+ } catch (e) {
1681
+ // Treat unreadable store as empty — we may still write a fresh entry.
1682
+ this.logger.debug?.('Auto-save plan: existing memories unreadable', { agentId, error: e.message });
1683
+ }
1684
+
1685
+ const newTokens = this._tokenize(content);
1686
+ for (const existing of existingPlans) {
1687
+ const existingTokens = this._tokenize(existing.content || '');
1688
+ const sim = this._jaccard(newTokens, existingTokens);
1689
+ const containment = this._overlapCoefficient(newTokens, existingTokens);
1690
+ // Jaccard catches near-identical reformulations. Containment
1691
+ // catches the "I repeat my old message — <same content>" case
1692
+ // where the user re-pastes the original plus a preamble. Either
1693
+ // signal is enough to suppress the duplicate.
1694
+ if (sim >= 0.7 || containment >= 0.85) {
1695
+ this.logger.info?.('Auto-save plan: skipping near-duplicate of existing plan', {
1696
+ agentId, existingTitle: existing.title,
1697
+ jaccard: sim.toFixed(2), containment: containment.toFixed(2),
1698
+ });
1699
+ return;
1700
+ }
1701
+ }
1702
+
1703
+ // ── Per-agent cap ────────────────────────────────────────────────
1704
+ // Bound the total auto-saved plans so an active session doesn't
1705
+ // bloat the agent's plan/* namespace indefinitely. Keep the K most
1706
+ // recent; delete the oldest auto-saves beyond that.
1707
+ const AUTO_PLAN_CAP = 8;
1708
+ const existingAutoSaves = existingPlans
1709
+ .filter(m => /^plan\/user-/.test(m.title))
1710
+ .sort((a, b) => String(a.createdAt || '').localeCompare(String(b.createdAt || '')));
1711
+ while (existingAutoSaves.length >= AUTO_PLAN_CAP) {
1712
+ const oldest = existingAutoSaves.shift();
1713
+ try {
1714
+ await memoryService.deleteMemory(agentId, oldest.id);
1715
+ this.logger.info?.('Auto-save plan: retired oldest auto-save to keep cap', {
1716
+ agentId, retiredTitle: oldest.title, cap: AUTO_PLAN_CAP,
1717
+ });
1718
+ } catch (e) {
1719
+ // Non-fatal if we can't delete the oldest, just skip this entry
1720
+ // and proceed with the write. Worst case the plan list grows
1721
+ // by one beyond the cap still bounded over time.
1722
+ this.logger.debug?.('Auto-save plan: retire-oldest failed', { agentId, error: e.message });
1723
+ break;
1724
+ }
1725
+ }
1726
+
1727
+ // ── Write the new memory ─────────────────────────────────────────
1728
+ const firstLine = (content.match(/[^\n]+/) || [''])[0].trim();
1729
+ const slug = firstLine
1730
+ .toLowerCase()
1731
+ .replace(/[^a-z0-9]+/g, '-')
1732
+ .replace(/^-+|-+$/g, '')
1733
+ .slice(0, 40) || 'request';
1734
+ const ts = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-');
1735
+ const title = `plan/user-${slug}-${ts}`;
1736
+
1737
+ try {
1738
+ await memoryService.addMemory(agentId, {
1739
+ title,
1740
+ description: `Auto-saved from user message at ${message.timestamp || new Date().toISOString()}`,
1741
+ content,
1742
+ });
1743
+ this.logger.info?.('Auto-saved user message as plan/* memory', {
1744
+ agentId, title, contentLength: content.length,
1745
+ });
1746
+ } catch (e) {
1747
+ this.logger.debug?.('Auto-save plan: write failed', { agentId, title, error: e.message });
1748
+ }
1749
+ }
1750
+
1751
+ /**
1752
+ * Tokenize a string into a lowercased word set for similarity checks.
1753
+ * Strips punctuation, drops short words (<3 chars), and drops a
1754
+ * small stopword set so that common words like "the" / "and" don't
1755
+ * inflate similarity scores between otherwise different messages.
1756
+ * @private
1757
+ */
1758
+ _tokenize(s) {
1759
+ if (typeof s !== 'string') return new Set();
1760
+ return new Set(
1761
+ s.toLowerCase()
1762
+ .replace(/[^a-z0-9\s]+/g, ' ')
1763
+ .split(/\s+/)
1764
+ .filter(w => w.length >= 3 && !AgentPool._STOPWORDS.has(w))
1765
+ );
1766
+ }
1767
+
1768
+ /**
1769
+ * Jaccard similarity over two word sets.
1770
+ * @private
1771
+ */
1772
+ _jaccard(a, b) {
1773
+ if (a.size === 0 && b.size === 0) return 1;
1774
+ if (a.size === 0 || b.size === 0) return 0;
1775
+ let intersection = 0;
1776
+ for (const w of a) if (b.has(w)) intersection += 1;
1777
+ return intersection / (a.size + b.size - intersection);
1778
+ }
1779
+
1780
+ /**
1781
+ * Overlap coefficient — intersection / size-of-smaller-set.
1782
+ * Returns 1.0 when one set is fully contained in the other,
1783
+ * regardless of how much the other set adds. Catches the "user
1784
+ * re-pastes their request with a preamble" duplicate case where
1785
+ * Jaccard would mark the messages as merely similar.
1786
+ * @private
1787
+ */
1788
+ _overlapCoefficient(a, b) {
1789
+ if (a.size === 0 || b.size === 0) return 0;
1790
+ let intersection = 0;
1791
+ for (const w of a) if (b.has(w)) intersection += 1;
1792
+ return intersection / Math.min(a.size, b.size);
1793
+ }
1794
+
1795
+ /**
1796
+ * Heuristic — does this user message look like a real request worth
1797
+ * preserving as a plan/*? Errs on the side of saving more (recall
1798
+ * over precision) a stray auto-save is cheap; a lost user request
1799
+ * is catastrophic.
1800
+ * @private
1801
+ */
1802
+ _looksSubstantive(text) {
1803
+ if (typeof text !== 'string') return false;
1804
+ const t = text.trim();
1805
+ if (t.length < 30) return false;
1806
+ // Tool-result wrappers and previous-task boundaries are not user voice.
1807
+ if (t.startsWith('[Tool Results') || t.startsWith('[Previous Task')) return false;
1808
+
1809
+ // ── Pollution filter 1: dominated by questions ────────────────────
1810
+ // A message that's mostly questions wants an ANSWER, not a plan.
1811
+ // If the majority of non-empty lines end in '?' (or are
1812
+ // question-shaped), this is a query, not a request.
1813
+ if (this._dominatedByQuestions(t)) return false;
1814
+
1815
+ // ── Pollution filter 2: list items are just refs (paths, urls) ───
1816
+ // A list of file paths / URLs / commit hashes is the user pointing
1817
+ // the agent at things, not a multi-part plan. Save it only if the
1818
+ // surrounding prose carries imperative intent and even then the
1819
+ // length gate handles that path.
1820
+ const hasList = /^\s*(?:\d+[.)]|[-*•])\s/m.test(t);
1821
+ if (hasList && this._listItemsAreJustReferences(t)) return false;
1822
+
1823
+ // ── Now apply the structural triggers ────────────────────────────
1824
+ // Numbered list "1." / "1)" at a line start. Multi-part intent.
1825
+ // Require a minimum total length to avoid "1. yes 2. no" nonsense.
1826
+ if (/^\s*\d+[.)]\s/m.test(t) && t.length >= 60) return true;
1827
+ // Bullet list at line start. Same strong intent signal + length.
1828
+ if (/^\s*[-*•]\s/m.test(t) && t.length >= 60) return true;
1829
+ // Free-form prose with no list markers must be substantial AND
1830
+ // contain an imperative-like signal (a verb you'd give as an
1831
+ // order). Raised from 120 150 to skip more pleasantries.
1832
+ if (t.length >= 150 && this._hasImperativeSignal(t)) return true;
1833
+ return false;
1834
+ }
1835
+
1836
+ /**
1837
+ * Heuristic: is this message mostly questions?
1838
+ * @private
1839
+ */
1840
+ _dominatedByQuestions(t) {
1841
+ // Split into non-empty lines.
1842
+ const lines = t.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
1843
+ if (lines.length === 0) return false;
1844
+ // Strip leading list markers so we can look at the line's intent.
1845
+ const stripMarker = (l) => l.replace(/^(?:\d+[.)]|[-*•])\s+/, '');
1846
+ let questionLines = 0;
1847
+ for (const raw of lines) {
1848
+ const line = stripMarker(raw);
1849
+ // Ends in '?', OR starts with a question word at the line head.
1850
+ if (/\?\s*$/.test(line) || /^(?:what|why|how|when|where|who|which|is\b|are\b|do\b|does\b|can\b|could\b|should\b|would\b)\b/i.test(line)) {
1851
+ questionLines += 1;
1852
+ }
1853
+ }
1854
+ // Strict-majority rule: more than half of lines are questions.
1855
+ return questionLines * 2 > lines.length;
1856
+ }
1857
+
1858
+ /**
1859
+ * Heuristic: are the list items in this message just references
1860
+ * (file paths, URLs, commit hashes) with no imperative verb of their own?
1861
+ * @private
1862
+ */
1863
+ _listItemsAreJustReferences(t) {
1864
+ const lines = t.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
1865
+ const listItems = lines.filter(l => /^(?:\d+[.)]|[-*•])\s/.test(l));
1866
+ if (listItems.length === 0) return false;
1867
+ let refLikeCount = 0;
1868
+ for (const li of listItems) {
1869
+ const body = li.replace(/^(?:\d+[.)]|[-*•])\s+/, '').trim();
1870
+ // Only treat as a "reference" if the line IS the reference —
1871
+ // i.e. a path/URL/hash with no surrounding English. A short bug
1872
+ // description like "login button does nothing on Safari" still
1873
+ // counts as content, not a reference.
1874
+ // Path: contains '/' or '\' OR starts with '.' AND has NO spaces
1875
+ // URL: starts with http(s)://
1876
+ // Hash: 7-40 hex chars only, no spaces
1877
+ const isPath = (/[/\\]/.test(body) || /^\./.test(body)) && !/\s/.test(body);
1878
+ const isUrl = /^https?:\/\//.test(body) && !/\s/.test(body);
1879
+ const isHash = /^[0-9a-f]{7,40}$/i.test(body);
1880
+ if (isPath || isUrl || isHash) refLikeCount += 1;
1881
+ }
1882
+ // Strict-majority of list items are reference-like → ignore.
1883
+ return refLikeCount * 2 > listItems.length;
1884
+ }
1885
+
1886
+ /**
1887
+ * Heuristic: does the message contain a verb that signals "do this"?
1888
+ * Conservative favors recall over precision.
1889
+ * @private
1890
+ */
1891
+ _hasImperativeSignal(t) {
1892
+ // Word-boundary match against a set of common imperative verbs.
1893
+ // Order matters only for readability we check membership.
1894
+ return /\b(?:fix|add|build|implement|create|change|remove|delete|update|refactor|rewrite|migrate|integrate|configure|setup|set\s+up|design|generate|make|write|test|verify|ensure|review|optimize|improve|replace|move|rename|extract|split|merge|deploy|publish|ship|release|debug|investigate|analyze|reproduce|escalate|prioritize|schedule)\b/i.test(t);
1895
+ }
1896
+
1897
+ _autoCreateTaskForMessage(agent, message, source, priority) {
1898
+ if (!agent.taskList) {
1899
+ agent.taskList = { tasks: [], lastUpdated: new Date().toISOString() };
1900
+ }
1901
+
1902
+ const content = message.content || '';
1903
+ const titleContent = content.trim().replace(/\n+/g, ' ').replace(/\s+/g, ' ');
1904
+ const firstSentence = titleContent.split(/[.!?]/)[0].trim();
1905
+ const title = firstSentence.length > 50
1906
+ ? firstSentence.substring(0, 47) + '...'
1907
+ : firstSentence || 'Process message';
1908
+
1909
+ const isInterAgent = source.startsWith('inter-agent');
1910
+ const requiresReply = isInterAgent && message.requiresReply === true;
1911
+ const taskTitle = isInterAgent
1912
+ ? (requiresReply
1913
+ ? `Handle and reply to ${source}: ${title}`
1914
+ : `Handle ${source}: ${title}`)
1915
+ : `Process ${source} request: ${title}`;
1916
+ const taskDescription = isInterAgent
1917
+ ? (requiresReply
1918
+ ? `Handle ${source} message and reply using the agentcommunication tool with action="reply-to-message": "${content.length > 200 ? content.substring(0, 197) + '...' : content}"`
1919
+ : `Handle ${source} message: "${content.length > 200 ? content.substring(0, 197) + '...' : content}"`)
1920
+ : `Handle ${source} message: "${content.length > 200 ? content.substring(0, 197) + '...' : content}"`;
1921
+
1922
+ const task = {
1923
+ id: `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
1924
+ title: taskTitle,
1925
+ description: taskDescription,
1926
+ status: 'pending',
1927
+ priority,
1928
+ createdAt: new Date().toISOString(),
1929
+ updatedAt: new Date().toISOString(),
1930
+ source: 'auto-created',
1931
+ messageId: message.id
1932
+ };
1933
+
1934
+ agent.taskList.tasks.push(task);
1935
+ agent.taskList.lastUpdated = new Date().toISOString();
1936
+
1937
+ this.logger.info(`Auto-created task for ${source} message`, {
1938
+ agentId: agent.id,
1939
+ taskId: task.id,
1940
+ title: task.title
1941
+ });
1942
+ }
1943
+
1944
+ /**
1945
+ * Clear all message queues for an agent
1946
+ * @param {string} agentId - Agent ID
1947
+ * @returns {Promise<void>}
1948
+ */
1949
+ async clearAgentQueues(agentId) {
1950
+ const agent = await this.getAgent(agentId);
1951
+ if (!agent) {
1952
+ throw new Error(`Agent not found: ${agentId}`);
1953
+ }
1954
+
1955
+ agent.messageQueues.toolResults = [];
1956
+ agent.messageQueues.interAgentMessages = [];
1957
+ agent.messageQueues.userMessages = [];
1958
+
1959
+ await this.persistAgentState(agentId);
1960
+
1961
+ this.logger.info(`Message queues cleared for agent: ${agentId}`);
1962
+ }
1963
+
1964
+ /**
1965
+ * Get total queued messages count for an agent
1966
+ * @param {string} agentId - Agent ID
1967
+ * @returns {Promise<Object>} Queue counts by type
1968
+ */
1969
+ async getQueueCounts(agentId) {
1970
+ const agent = await this.getAgent(agentId);
1971
+ if (!agent) {
1972
+ return { toolResults: 0, interAgentMessages: 0, userMessages: 0, total: 0 };
1973
+ }
1974
+
1975
+ const counts = {
1976
+ toolResults: agent.messageQueues.toolResults.length,
1977
+ interAgentMessages: agent.messageQueues.interAgentMessages.length,
1978
+ userMessages: agent.messageQueues.userMessages.length
1979
+ };
1980
+
1981
+ counts.total = counts.toolResults + counts.interAgentMessages + counts.userMessages;
1982
+
1983
+ return counts;
1984
+ }
1985
+
1986
+ /**
1987
+ * Get messages for AI request - returns compacted if available, otherwise original
1988
+ * CRITICAL FIX: Ensures compacted messages stay in sync with new messages after compaction
1989
+ * This is the primary method that should be used when preparing messages for AI service
1990
+ * @param {string} agentId - Agent ID
1991
+ * @param {string} modelId - Model ID
1992
+ * @returns {Promise<Array>} Messages array to send to AI
1993
+ */
1994
+ async getMessagesForAI(agentId, modelId) {
1995
+ const ENABLE_COMPACT_DEBUG = process.env.COMPACT_DEBUG === 'true';
1996
+
1997
+ // Helper: Remove trailing empty messages from array (cleans up malformed conversations)
1998
+ const cleanTrailingEmptyMessages = (messages) => {
1999
+ if (!messages || messages.length === 0) return messages;
2000
+
2001
+ let cleaned = [...messages];
2002
+ let removedCount = 0;
2003
+
2004
+ // Remove trailing empty messages
2005
+ while (cleaned.length > 0) {
2006
+ const lastMsg = cleaned[cleaned.length - 1];
2007
+ const content = lastMsg?.content;
2008
+ const isEmpty = !content || (typeof content === 'string' && !content.trim());
2009
+
2010
+ if (isEmpty) {
2011
+ cleaned.pop();
2012
+ removedCount++;
2013
+ } else {
2014
+ break;
2015
+ }
2016
+ }
2017
+
2018
+ if (removedCount > 0) {
2019
+ this.logger?.warn(`Removed ${removedCount} trailing empty message(s) from conversation`, {
2020
+ agentId,
2021
+ modelId,
2022
+ originalLength: messages.length,
2023
+ cleanedLength: cleaned.length
2024
+ });
2025
+ }
2026
+
2027
+ return cleaned;
2028
+ };
2029
+
2030
+ const agent = await this.getAgent(agentId);
2031
+ if (!agent) {
2032
+ throw new Error(`Agent not found: ${agentId}`);
2033
+ }
2034
+
2035
+ const conversation = agent.conversations[modelId];
2036
+ if (!conversation) {
2037
+ this.logger.warn(`No conversation found for model: ${modelId}`, { agentId });
2038
+ return [];
2039
+ }
2040
+
2041
+ // If no compacted messages exist, return original (cleaned)
2042
+ if (!conversation.compactizedMessages) {
2043
+ this.logger.debug('Retrieved messages for AI (no compaction)', {
2044
+ agentId,
2045
+ modelId,
2046
+ messageCount: conversation.messages.length
2047
+ });
2048
+
2049
+ if (ENABLE_COMPACT_DEBUG) {
2050
+ console.log('[GET-MESSAGES-FOR-AI]', {
2051
+ agentId,
2052
+ modelId,
2053
+ returnedArray: 'originalMessages',
2054
+ messageCount: conversation.messages.length,
2055
+ reason: 'No compacted messages exist'
2056
+ });
2057
+ }
2058
+
2059
+ return cleanTrailingEmptyMessages(conversation.messages);
2060
+ }
2061
+
2062
+ // CRITICAL FIX: Only sync messages added AFTER compaction
2063
+ // We track originalMessageCountAtCompaction to know which messages are truly new
2064
+ // vs which ones were already included in the compaction (sandwich strategy)
2065
+ const compactedLength = conversation.compactizedMessages.length;
2066
+ const originalLength = conversation.messages.length;
2067
+ // SAFETY: If watermark is null/undefined (bug, migration, or cleared state),
2068
+ // fall back to compactedLength NOT originalLength. Using originalLength silently
2069
+ // drops all unsynced messages because (originalLength > originalLength) is always false.
2070
+ // Using compactedLength ensures any messages beyond what's in the compacted array get synced.
2071
+ const originalCountAtCompaction = conversation.originalMessageCountAtCompaction ?? compactedLength;
2072
+
2073
+ // Only sync if NEW messages were added after compaction
2074
+ // (i.e., current original length > original length when compaction happened)
2075
+ if (originalLength > originalCountAtCompaction) {
2076
+ // New messages exist that weren't present during compaction
2077
+ const newMessageCount = originalLength - originalCountAtCompaction;
2078
+ const newMessages = conversation.messages.slice(-newMessageCount);
2079
+
2080
+ this.logger.info('Syncing truly new messages after compaction', {
2081
+ agentId,
2082
+ modelId,
2083
+ compactedLength,
2084
+ originalLength,
2085
+ originalCountAtCompaction,
2086
+ newMessageCount,
2087
+ newMessageRoles: newMessages.map(m => m.role)
2088
+ });
2089
+
2090
+ // Append only the truly new messages to compacted array
2091
+ conversation.compactizedMessages.push(...newMessages);
2092
+
2093
+ // Update the tracking to include newly synced messages
2094
+ conversation.originalMessageCountAtCompaction = originalLength;
2095
+
2096
+ // Persist the update
2097
+ await this.persistAgentState(agentId);
2098
+ } else if (originalLength > compactedLength) {
2099
+ // Length mismatch but no new messages - this is expected with sandwich compaction
2100
+ // The compacted version has fewer messages due to summarization, not missing messages
2101
+ this.logger.debug('Compacted messages shorter than original (expected with sandwich compaction)', {
2102
+ agentId,
2103
+ modelId,
2104
+ compactedLength,
2105
+ originalLength,
2106
+ originalCountAtCompaction,
2107
+ note: 'No sync needed - compaction reduces message count'
2108
+ });
2109
+ }
2110
+
2111
+ this.logger.debug('Retrieved messages for AI (compacted + synced)', {
2112
+ agentId,
2113
+ modelId,
2114
+ messageCount: conversation.compactizedMessages.length,
2115
+ wasResynced: originalLength > compactedLength
2116
+ });
2117
+
2118
+ if (ENABLE_COMPACT_DEBUG) {
2119
+ console.log('[GET-MESSAGES-FOR-AI]', {
2120
+ agentId,
2121
+ modelId,
2122
+ returnedArray: 'compactizedMessages',
2123
+ messageCount: conversation.compactizedMessages.length,
2124
+ originalMessageCount: conversation.messages.length,
2125
+ wasSynced: originalLength > compactedLength,
2126
+ syncedMessageCount: originalLength > compactedLength ? originalLength - compactedLength : 0,
2127
+ reason: 'Compacted messages exist, returning compacted version'
2128
+ });
2129
+ }
2130
+
2131
+ return cleanTrailingEmptyMessages(conversation.compactizedMessages);
2132
+ }
2133
+
2134
+ /**
2135
+ * Add message to conversation (stores in original messages array)
2136
+ * @param {string} agentId - Agent ID
2137
+ * @param {string} modelId - Model ID
2138
+ * @param {Object} message - Message object to add
2139
+ * @returns {Promise<void>}
2140
+ */
2141
+ async addMessageToConversation(agentId, modelId, message) {
2142
+ const ENABLE_COMPACT_DEBUG = process.env.COMPACT_DEBUG === 'true';
2143
+
2144
+ const agent = await this.getAgent(agentId);
2145
+ if (!agent) {
2146
+ throw new Error(`Agent not found: ${agentId}`);
2147
+ }
2148
+
2149
+ // Ensure model conversation exists
2150
+ if (!agent.conversations[modelId]) {
2151
+ agent.conversations[modelId] = this._createEmptyConversation(modelId);
2152
+ }
2153
+
2154
+ const conversation = agent.conversations[modelId];
2155
+
2156
+ // GUARD: Skip empty messages - they should never be added to history
2157
+ const messageContent = message.content;
2158
+ if (!messageContent || (typeof messageContent === 'string' && !messageContent.trim())) {
2159
+ this.logger?.warn(`Skipping empty message for agent ${agentId}`, {
2160
+ role: message.role,
2161
+ modelId,
2162
+ hasContent: !!messageContent
2163
+ });
2164
+ return; // Don't add empty messages
2165
+ }
2166
+
2167
+ const originalLengthBefore = conversation.messages.length;
2168
+ const compactedLengthBefore = conversation.compactizedMessages?.length || 0;
2169
+
2170
+ // Always add to original messages (never modify original)
2171
+ conversation.messages.push({
2172
+ ...message,
2173
+ timestamp: message.timestamp || new Date().toISOString()
2174
+ });
2175
+
2176
+ // If compacted version exists, also add to it (append new messages after compaction)
2177
+ if (conversation.compactizedMessages) {
2178
+ conversation.compactizedMessages.push({
2179
+ ...message,
2180
+ timestamp: message.timestamp || new Date().toISOString()
2181
+ });
2182
+ }
2183
+
2184
+ conversation.lastUpdated = new Date().toISOString();
2185
+
2186
+ if (ENABLE_COMPACT_DEBUG) {
2187
+ console.log('[ADD-MESSAGE-TO-CONVERSATION]', {
2188
+ agentId,
2189
+ modelId,
2190
+ role: message.role,
2191
+ hasCompactedVersion: !!conversation.compactizedMessages,
2192
+ originalMessages: {
2193
+ before: originalLengthBefore,
2194
+ after: conversation.messages.length,
2195
+ added: 1
2196
+ },
2197
+ compactizedMessages: conversation.compactizedMessages ? {
2198
+ before: compactedLengthBefore,
2199
+ after: conversation.compactizedMessages.length,
2200
+ added: 1
2201
+ } : null,
2202
+ behavior: conversation.compactizedMessages ? 'Added to BOTH arrays' : 'Added to original only'
2203
+ });
2204
+ }
2205
+
2206
+ await this.persistAgentState(agentId);
2207
+
2208
+ this.logger.debug('Message added to conversation', {
2209
+ agentId,
2210
+ modelId,
2211
+ role: message.role,
2212
+ hasCompacted: !!conversation.compactizedMessages
2213
+ });
2214
+ }
2215
+
2216
+ /**
2217
+ * Sync pending messages from conversation.messages to compactizedMessages.
2218
+ * The scheduler's addMessageToConversation only pushes to conversation.messages,
2219
+ * NOT to compactizedMessages. This method syncs any pending messages that haven't
2220
+ * been pushed to compactizedMessages yet.
2221
+ *
2222
+ * MUST be called before compaction reads compactizedMessages, otherwise compaction
2223
+ * will process a stale snapshot and the watermark will mark unsynced messages as
2224
+ * "already compacted", permanently losing them.
2225
+ *
2226
+ * @param {string} agentId - Agent ID
2227
+ * @param {string} modelId - Model ID
2228
+ * @returns {Promise<{synced: number}>} Number of messages synced
2229
+ */
2230
+ async syncPendingMessages(agentId, modelId) {
2231
+ const agent = await this.getAgent(agentId);
2232
+ if (!agent) return { synced: 0 };
2233
+
2234
+ const conversation = agent.conversations[modelId];
2235
+ if (!conversation || !conversation.compactizedMessages) return { synced: 0 };
2236
+
2237
+ const originalLength = conversation.messages.length;
2238
+ const compactedLength = conversation.compactizedMessages.length;
2239
+ // SAFETY: Use ?? compactedLength instead of || originalLength to prevent silent message loss
2240
+ // when watermark is null (see getMessagesForAI for detailed explanation)
2241
+ const originalCountAtCompaction = conversation.originalMessageCountAtCompaction ?? compactedLength;
2242
+
2243
+ if (originalLength > originalCountAtCompaction) {
2244
+ const newCount = originalLength - originalCountAtCompaction;
2245
+ const newMessages = conversation.messages.slice(-newCount);
2246
+ conversation.compactizedMessages.push(...newMessages);
2247
+ conversation.originalMessageCountAtCompaction = originalLength;
2248
+
2249
+ this.logger.info('Pre-compaction sync: pushed pending messages to compactizedMessages', {
2250
+ agentId,
2251
+ modelId,
2252
+ synced: newCount,
2253
+ newMessageRoles: newMessages.map(m => m.role),
2254
+ compactizedMessagesLength: conversation.compactizedMessages.length,
2255
+ watermarkWasNull: conversation.originalMessageCountAtCompaction === null
2256
+ });
2257
+
2258
+ return { synced: newCount };
2259
+ }
2260
+
2261
+ return { synced: 0 };
2262
+ }
2263
+
2264
+ /**
2265
+ * Update compacted messages after compactization
2266
+ * @param {string} agentId - Agent ID
2267
+ * @param {string} modelId - Model ID
2268
+ * @param {Object} compactionResult - Compaction result with messages and metadata
2269
+ * @param {number} [preCompactionMessageCount] - Message count recorded BEFORE compaction started.
2270
+ * If provided, used as the watermark instead of current messages.length. This prevents
2271
+ * messages added DURING compaction from being silently lost.
2272
+ * @returns {Promise<void>}
2273
+ */
2274
+ async updateCompactedMessages(agentId, modelId, compactionResult, preCompactionMessageCount) {
2275
+ const agent = await this.getAgent(agentId);
2276
+ if (!agent) {
2277
+ throw new Error(`Agent not found: ${agentId}`);
2278
+ }
2279
+
2280
+ // Ensure model conversation exists (important for model switching scenarios)
2281
+ if (!agent.conversations[modelId]) {
2282
+ agent.conversations[modelId] = this._createEmptyConversation(modelId);
2283
+ this.logger.debug(`Created conversation for model switching: ${modelId}`);
2284
+ }
2285
+
2286
+ const conversation = agent.conversations[modelId];
2287
+
2288
+ // Update compacted messages
2289
+ conversation.compactizedMessages = compactionResult.compactedMessages;
2290
+
2291
+ // CRITICAL: Use the pre-compaction watermark if provided, NOT current messages.length.
2292
+ // If we use current messages.length, any messages added DURING compaction (e.g., user
2293
+ // messages arriving via WebSocket while the summarization API call is in flight) would
2294
+ // be marked as "already compacted" even though they weren't in the compaction input.
2295
+ // Using the pre-compaction count ensures those messages are detected as "new" by
2296
+ // getMessagesForAI's sync logic and properly appended to the compacted array.
2297
+ conversation.originalMessageCountAtCompaction = preCompactionMessageCount || conversation.messages.length;
2298
+
2299
+ // Update metadata
2300
+ conversation.lastCompactization = new Date().toISOString();
2301
+ conversation.compactizationCount += 1;
2302
+ conversation.compactizationStrategy = compactionResult.strategy;
2303
+ conversation.originalTokenCount = compactionResult.originalTokenCount;
2304
+ conversation.compactedTokenCount = compactionResult.compactedTokenCount;
2305
+ conversation.tokenCount = compactionResult.compactedTokenCount;
2306
+ conversation.lastUpdated = new Date().toISOString();
2307
+
2308
+ await this.persistAgentState(agentId);
2309
+
2310
+ this.logger.info('Compacted messages updated', {
2311
+ agentId,
2312
+ modelId,
2313
+ strategy: compactionResult.strategy,
2314
+ originalTokens: compactionResult.originalTokenCount,
2315
+ compactedTokens: compactionResult.compactedTokenCount,
2316
+ reductionPercent: compactionResult.reductionPercent,
2317
+ compactizationCount: conversation.compactizationCount
2318
+ });
2319
+ }
2320
+
2321
+ /**
2322
+ * Clear compacted messages and revert to original
2323
+ * Useful for debugging or if compaction needs to be redone
2324
+ * @param {string} agentId - Agent ID
2325
+ * @param {string} modelId - Model ID
2326
+ * @returns {Promise<void>}
2327
+ */
2328
+ async clearCompactedMessages(agentId, modelId) {
2329
+ const agent = await this.getAgent(agentId);
2330
+ if (!agent) {
2331
+ throw new Error(`Agent not found: ${agentId}`);
2332
+ }
2333
+
2334
+ const conversation = agent.conversations[modelId];
2335
+ if (!conversation) {
2336
+ return;
2337
+ }
2338
+
2339
+ conversation.compactizedMessages = null;
2340
+ conversation.lastCompactization = null;
2341
+ conversation.compactizationCount = 0;
2342
+ conversation.compactizationStrategy = null;
2343
+ conversation.originalTokenCount = 0;
2344
+ conversation.compactedTokenCount = 0;
2345
+ conversation.tokenCount = 0;
2346
+ conversation.originalMessageCountAtCompaction = null;
2347
+
2348
+ await this.persistAgentState(agentId);
2349
+
2350
+ this.logger.info('Compacted messages cleared', { agentId, modelId });
2351
+ }
2352
+
2353
+ /**
2354
+ * Get compaction metadata for a conversation
2355
+ * @param {string} agentId - Agent ID
2356
+ * @param {string} modelId - Model ID
2357
+ * @returns {Promise<Object|null>} Compaction metadata or null if no compaction
2358
+ */
2359
+ async getCompactionMetadata(agentId, modelId) {
2360
+ const agent = await this.getAgent(agentId);
2361
+ if (!agent) {
2362
+ return null;
2363
+ }
2364
+
2365
+ const conversation = agent.conversations[modelId];
2366
+ if (!conversation) {
2367
+ return null;
2368
+ }
2369
+
2370
+ // Return metadata whether compacted or not
2371
+ const isCompacted = !!conversation.compactizedMessages;
2372
+
2373
+ return {
2374
+ isCompacted,
2375
+ lastCompactization: conversation.lastCompactization || null,
2376
+ compactizationCount: conversation.compactizationCount || 0,
2377
+ strategy: conversation.compactizationStrategy || null,
2378
+ originalTokenCount: conversation.originalTokenCount || 0,
2379
+ compactedTokenCount: conversation.compactedTokenCount || 0,
2380
+ reductionPercent: conversation.originalTokenCount > 0
2381
+ ? ((conversation.originalTokenCount - conversation.compactedTokenCount) / conversation.originalTokenCount) * 100
2382
+ : 0,
2383
+ originalMessages: conversation.messages || [],
2384
+ compactedMessages: conversation.compactizedMessages || null,
2385
+ originalMessageCount: conversation.messages?.length || 0,
2386
+ compactedMessageCount: conversation.compactizedMessages?.length || 0
2387
+ };
2388
+ }
2389
+
2390
+ /**
2391
+ * Migrate existing agent conversations to dual storage structure
2392
+ * Ensures backward compatibility with existing agents
2393
+ * @param {string} agentId - Agent ID
2394
+ * @returns {Promise<boolean>} True if migration was needed and performed
2395
+ */
2396
+ async migrateConversationStructure(agentId) {
2397
+ const agent = await this.getAgent(agentId);
2398
+ if (!agent) {
2399
+ return false;
2400
+ }
2401
+
2402
+ let migrated = false;
2403
+
2404
+ // Check each conversation for migration needs
2405
+ for (const [modelId, conversation] of Object.entries(agent.conversations)) {
2406
+ if (modelId === 'full') continue; // Skip full conversation
2407
+
2408
+ // Check if conversation needs migration (missing new fields)
2409
+ if (conversation.compactizedMessages === undefined) {
2410
+ // Add new fields
2411
+ conversation.compactizedMessages = null;
2412
+ conversation.lastCompactization = null;
2413
+ conversation.compactizationCount = 0;
2414
+ conversation.compactizationStrategy = null;
2415
+ conversation.originalTokenCount = 0;
2416
+ conversation.compactedTokenCount = 0;
2417
+ conversation.originalMessageCountAtCompaction = null;
2418
+
2419
+ migrated = true;
2420
+
2421
+ this.logger.info('Migrated conversation structure', {
2422
+ agentId,
2423
+ modelId,
2424
+ messageCount: conversation.messages?.length || 0
2425
+ });
2426
+ }
2427
+
2428
+ // CRITICAL: Migrate existing compacted conversations that don't have the new tracking field
2429
+ // This prevents the sync bug from re-adding messages already included in compaction
2430
+ if (conversation.compactizedMessages && conversation.originalMessageCountAtCompaction === undefined) {
2431
+ // Set to current original length to prevent any sync from running
2432
+ // This is safe because any truly new messages would have been added to both arrays
2433
+ conversation.originalMessageCountAtCompaction = conversation.messages?.length || 0;
2434
+ migrated = true;
2435
+
2436
+ this.logger.info('Migrated compaction tracking field', {
2437
+ agentId,
2438
+ modelId,
2439
+ originalMessageCountAtCompaction: conversation.originalMessageCountAtCompaction,
2440
+ compactedMessageCount: conversation.compactizedMessages.length
2441
+ });
2442
+ }
2443
+ }
2444
+
2445
+ if (migrated) {
2446
+ await this.persistAgentState(agentId);
2447
+ }
2448
+
2449
+ return migrated;
2450
+ }
2451
+
2452
+ /**
2453
+ * Create empty conversation structure with all required fields
2454
+ * @private
2455
+ * @param {string} modelId - Model ID
2456
+ * @returns {Object} Empty conversation structure
2457
+ */
2458
+ _createEmptyConversation(modelId) {
2459
+ return {
2460
+ messages: [],
2461
+ compactizedMessages: null,
2462
+ lastCompactization: null,
2463
+ compactizationCount: 0,
2464
+ compactizationStrategy: null,
2465
+ originalTokenCount: 0,
2466
+ compactedTokenCount: 0,
2467
+ tokenCount: 0,
2468
+ originalMessageCountAtCompaction: null, // Tracks original length at compaction time
2469
+ lastUpdated: new Date().toISOString(),
2470
+ formatVersion: this._getModelFormatVersion(modelId)
2471
+ };
2472
+ }
2473
+ }
2474
+
2476
2475
  export default AgentPool;