onbuzz 3.3.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 (506) hide show
  1. package/LICENSE +267 -0
  2. package/README.md +425 -0
  3. package/bin/cli.js +556 -0
  4. package/bin/loxia-terminal-v2.js +162 -0
  5. package/bin/loxia-terminal.js +90 -0
  6. package/bin/start-with-terminal.js +200 -0
  7. package/node_modules/@isaacs/balanced-match/LICENSE.md +23 -0
  8. package/node_modules/@isaacs/balanced-match/README.md +60 -0
  9. package/node_modules/@isaacs/balanced-match/dist/commonjs/index.d.ts +9 -0
  10. package/node_modules/@isaacs/balanced-match/dist/commonjs/index.d.ts.map +1 -0
  11. package/node_modules/@isaacs/balanced-match/dist/commonjs/index.js +59 -0
  12. package/node_modules/@isaacs/balanced-match/dist/commonjs/index.js.map +1 -0
  13. package/node_modules/@isaacs/balanced-match/dist/commonjs/package.json +3 -0
  14. package/node_modules/@isaacs/balanced-match/dist/esm/index.d.ts +9 -0
  15. package/node_modules/@isaacs/balanced-match/dist/esm/index.d.ts.map +1 -0
  16. package/node_modules/@isaacs/balanced-match/dist/esm/index.js +54 -0
  17. package/node_modules/@isaacs/balanced-match/dist/esm/index.js.map +1 -0
  18. package/node_modules/@isaacs/balanced-match/dist/esm/package.json +3 -0
  19. package/node_modules/@isaacs/balanced-match/package.json +79 -0
  20. package/node_modules/@isaacs/brace-expansion/LICENSE +23 -0
  21. package/node_modules/@isaacs/brace-expansion/README.md +97 -0
  22. package/node_modules/@isaacs/brace-expansion/dist/commonjs/index.d.ts +6 -0
  23. package/node_modules/@isaacs/brace-expansion/dist/commonjs/index.d.ts.map +1 -0
  24. package/node_modules/@isaacs/brace-expansion/dist/commonjs/index.js +199 -0
  25. package/node_modules/@isaacs/brace-expansion/dist/commonjs/index.js.map +1 -0
  26. package/node_modules/@isaacs/brace-expansion/dist/commonjs/package.json +3 -0
  27. package/node_modules/@isaacs/brace-expansion/dist/esm/index.d.ts +6 -0
  28. package/node_modules/@isaacs/brace-expansion/dist/esm/index.d.ts.map +1 -0
  29. package/node_modules/@isaacs/brace-expansion/dist/esm/index.js +195 -0
  30. package/node_modules/@isaacs/brace-expansion/dist/esm/index.js.map +1 -0
  31. package/node_modules/@isaacs/brace-expansion/dist/esm/package.json +3 -0
  32. package/node_modules/@isaacs/brace-expansion/package.json +60 -0
  33. package/node_modules/glob/LICENSE.md +63 -0
  34. package/node_modules/glob/README.md +1177 -0
  35. package/node_modules/glob/dist/commonjs/glob.d.ts +388 -0
  36. package/node_modules/glob/dist/commonjs/glob.d.ts.map +1 -0
  37. package/node_modules/glob/dist/commonjs/glob.js +247 -0
  38. package/node_modules/glob/dist/commonjs/glob.js.map +1 -0
  39. package/node_modules/glob/dist/commonjs/has-magic.d.ts +14 -0
  40. package/node_modules/glob/dist/commonjs/has-magic.d.ts.map +1 -0
  41. package/node_modules/glob/dist/commonjs/has-magic.js +27 -0
  42. package/node_modules/glob/dist/commonjs/has-magic.js.map +1 -0
  43. package/node_modules/glob/dist/commonjs/ignore.d.ts +24 -0
  44. package/node_modules/glob/dist/commonjs/ignore.d.ts.map +1 -0
  45. package/node_modules/glob/dist/commonjs/ignore.js +119 -0
  46. package/node_modules/glob/dist/commonjs/ignore.js.map +1 -0
  47. package/node_modules/glob/dist/commonjs/index.d.ts +97 -0
  48. package/node_modules/glob/dist/commonjs/index.d.ts.map +1 -0
  49. package/node_modules/glob/dist/commonjs/index.js +68 -0
  50. package/node_modules/glob/dist/commonjs/index.js.map +1 -0
  51. package/node_modules/glob/dist/commonjs/index.min.js +4 -0
  52. package/node_modules/glob/dist/commonjs/index.min.js.map +7 -0
  53. package/node_modules/glob/dist/commonjs/package.json +3 -0
  54. package/node_modules/glob/dist/commonjs/pattern.d.ts +76 -0
  55. package/node_modules/glob/dist/commonjs/pattern.d.ts.map +1 -0
  56. package/node_modules/glob/dist/commonjs/pattern.js +219 -0
  57. package/node_modules/glob/dist/commonjs/pattern.js.map +1 -0
  58. package/node_modules/glob/dist/commonjs/processor.d.ts +59 -0
  59. package/node_modules/glob/dist/commonjs/processor.d.ts.map +1 -0
  60. package/node_modules/glob/dist/commonjs/processor.js +301 -0
  61. package/node_modules/glob/dist/commonjs/processor.js.map +1 -0
  62. package/node_modules/glob/dist/commonjs/walker.d.ts +97 -0
  63. package/node_modules/glob/dist/commonjs/walker.d.ts.map +1 -0
  64. package/node_modules/glob/dist/commonjs/walker.js +387 -0
  65. package/node_modules/glob/dist/commonjs/walker.js.map +1 -0
  66. package/node_modules/glob/dist/esm/glob.d.ts +388 -0
  67. package/node_modules/glob/dist/esm/glob.d.ts.map +1 -0
  68. package/node_modules/glob/dist/esm/glob.js +243 -0
  69. package/node_modules/glob/dist/esm/glob.js.map +1 -0
  70. package/node_modules/glob/dist/esm/has-magic.d.ts +14 -0
  71. package/node_modules/glob/dist/esm/has-magic.d.ts.map +1 -0
  72. package/node_modules/glob/dist/esm/has-magic.js +23 -0
  73. package/node_modules/glob/dist/esm/has-magic.js.map +1 -0
  74. package/node_modules/glob/dist/esm/ignore.d.ts +24 -0
  75. package/node_modules/glob/dist/esm/ignore.d.ts.map +1 -0
  76. package/node_modules/glob/dist/esm/ignore.js +115 -0
  77. package/node_modules/glob/dist/esm/ignore.js.map +1 -0
  78. package/node_modules/glob/dist/esm/index.d.ts +97 -0
  79. package/node_modules/glob/dist/esm/index.d.ts.map +1 -0
  80. package/node_modules/glob/dist/esm/index.js +55 -0
  81. package/node_modules/glob/dist/esm/index.js.map +1 -0
  82. package/node_modules/glob/dist/esm/index.min.js +4 -0
  83. package/node_modules/glob/dist/esm/index.min.js.map +7 -0
  84. package/node_modules/glob/dist/esm/package.json +3 -0
  85. package/node_modules/glob/dist/esm/pattern.d.ts +76 -0
  86. package/node_modules/glob/dist/esm/pattern.d.ts.map +1 -0
  87. package/node_modules/glob/dist/esm/pattern.js +215 -0
  88. package/node_modules/glob/dist/esm/pattern.js.map +1 -0
  89. package/node_modules/glob/dist/esm/processor.d.ts +59 -0
  90. package/node_modules/glob/dist/esm/processor.d.ts.map +1 -0
  91. package/node_modules/glob/dist/esm/processor.js +294 -0
  92. package/node_modules/glob/dist/esm/processor.js.map +1 -0
  93. package/node_modules/glob/dist/esm/walker.d.ts +97 -0
  94. package/node_modules/glob/dist/esm/walker.d.ts.map +1 -0
  95. package/node_modules/glob/dist/esm/walker.js +381 -0
  96. package/node_modules/glob/dist/esm/walker.js.map +1 -0
  97. package/node_modules/glob/node_modules/minimatch/LICENSE.md +55 -0
  98. package/node_modules/glob/node_modules/minimatch/README.md +453 -0
  99. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts +2 -0
  100. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts.map +1 -0
  101. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.js +14 -0
  102. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.js.map +1 -0
  103. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts +20 -0
  104. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts.map +1 -0
  105. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js +591 -0
  106. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js.map +1 -0
  107. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.d.ts +8 -0
  108. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.d.ts.map +1 -0
  109. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js +152 -0
  110. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js.map +1 -0
  111. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts +15 -0
  112. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts.map +1 -0
  113. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js +30 -0
  114. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js.map +1 -0
  115. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts +94 -0
  116. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts.map +1 -0
  117. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js +1029 -0
  118. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js.map +1 -0
  119. package/node_modules/glob/node_modules/minimatch/dist/commonjs/package.json +3 -0
  120. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts +22 -0
  121. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts.map +1 -0
  122. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js +38 -0
  123. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js.map +1 -0
  124. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts +2 -0
  125. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts.map +1 -0
  126. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.js +10 -0
  127. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.js.map +1 -0
  128. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts +20 -0
  129. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts.map +1 -0
  130. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js +587 -0
  131. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js.map +1 -0
  132. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.d.ts +8 -0
  133. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.d.ts.map +1 -0
  134. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js +148 -0
  135. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js.map +1 -0
  136. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts +15 -0
  137. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts.map +1 -0
  138. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js +26 -0
  139. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js.map +1 -0
  140. package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts +94 -0
  141. package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts.map +1 -0
  142. package/node_modules/glob/node_modules/minimatch/dist/esm/index.js +1016 -0
  143. package/node_modules/glob/node_modules/minimatch/dist/esm/index.js.map +1 -0
  144. package/node_modules/glob/node_modules/minimatch/dist/esm/package.json +3 -0
  145. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts +22 -0
  146. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts.map +1 -0
  147. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js +34 -0
  148. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js.map +1 -0
  149. package/node_modules/glob/node_modules/minimatch/package.json +67 -0
  150. package/node_modules/glob/package.json +101 -0
  151. package/node_modules/minipass/LICENSE +15 -0
  152. package/node_modules/minipass/README.md +825 -0
  153. package/node_modules/minipass/dist/commonjs/index.d.ts +549 -0
  154. package/node_modules/minipass/dist/commonjs/index.d.ts.map +1 -0
  155. package/node_modules/minipass/dist/commonjs/index.js +1028 -0
  156. package/node_modules/minipass/dist/commonjs/index.js.map +1 -0
  157. package/node_modules/minipass/dist/commonjs/package.json +3 -0
  158. package/node_modules/minipass/dist/esm/index.d.ts +549 -0
  159. package/node_modules/minipass/dist/esm/index.d.ts.map +1 -0
  160. package/node_modules/minipass/dist/esm/index.js +1018 -0
  161. package/node_modules/minipass/dist/esm/index.js.map +1 -0
  162. package/node_modules/minipass/dist/esm/package.json +3 -0
  163. package/node_modules/minipass/package.json +82 -0
  164. package/node_modules/package-json-from-dist/LICENSE.md +63 -0
  165. package/node_modules/package-json-from-dist/README.md +110 -0
  166. package/node_modules/package-json-from-dist/dist/commonjs/index.d.ts +89 -0
  167. package/node_modules/package-json-from-dist/dist/commonjs/index.d.ts.map +1 -0
  168. package/node_modules/package-json-from-dist/dist/commonjs/index.js +134 -0
  169. package/node_modules/package-json-from-dist/dist/commonjs/index.js.map +1 -0
  170. package/node_modules/package-json-from-dist/dist/commonjs/package.json +3 -0
  171. package/node_modules/package-json-from-dist/dist/esm/index.d.ts +89 -0
  172. package/node_modules/package-json-from-dist/dist/esm/index.d.ts.map +1 -0
  173. package/node_modules/package-json-from-dist/dist/esm/index.js +129 -0
  174. package/node_modules/package-json-from-dist/dist/esm/index.js.map +1 -0
  175. package/node_modules/package-json-from-dist/dist/esm/package.json +3 -0
  176. package/node_modules/package-json-from-dist/package.json +68 -0
  177. package/node_modules/path-scurry/LICENSE.md +55 -0
  178. package/node_modules/path-scurry/README.md +636 -0
  179. package/node_modules/path-scurry/dist/commonjs/index.d.ts +1115 -0
  180. package/node_modules/path-scurry/dist/commonjs/index.d.ts.map +1 -0
  181. package/node_modules/path-scurry/dist/commonjs/index.js +2018 -0
  182. package/node_modules/path-scurry/dist/commonjs/index.js.map +1 -0
  183. package/node_modules/path-scurry/dist/commonjs/package.json +3 -0
  184. package/node_modules/path-scurry/dist/esm/index.d.ts +1115 -0
  185. package/node_modules/path-scurry/dist/esm/index.d.ts.map +1 -0
  186. package/node_modules/path-scurry/dist/esm/index.js +1983 -0
  187. package/node_modules/path-scurry/dist/esm/index.js.map +1 -0
  188. package/node_modules/path-scurry/dist/esm/package.json +3 -0
  189. package/node_modules/path-scurry/node_modules/lru-cache/LICENSE.md +55 -0
  190. package/node_modules/path-scurry/node_modules/lru-cache/README.md +383 -0
  191. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts +1323 -0
  192. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts.map +1 -0
  193. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js +1589 -0
  194. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js.map +1 -0
  195. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js +2 -0
  196. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js.map +7 -0
  197. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/package.json +3 -0
  198. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts +1323 -0
  199. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts.map +1 -0
  200. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js +1585 -0
  201. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js.map +1 -0
  202. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js +2 -0
  203. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js.map +7 -0
  204. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/package.json +3 -0
  205. package/node_modules/path-scurry/node_modules/lru-cache/package.json +101 -0
  206. package/node_modules/path-scurry/package.json +88 -0
  207. package/node_modules/rimraf/LICENSE.md +55 -0
  208. package/node_modules/rimraf/README.md +226 -0
  209. package/node_modules/rimraf/dist/commonjs/default-tmp.d.ts +3 -0
  210. package/node_modules/rimraf/dist/commonjs/default-tmp.d.ts.map +1 -0
  211. package/node_modules/rimraf/dist/commonjs/default-tmp.js +58 -0
  212. package/node_modules/rimraf/dist/commonjs/default-tmp.js.map +1 -0
  213. package/node_modules/rimraf/dist/commonjs/error.d.ts +6 -0
  214. package/node_modules/rimraf/dist/commonjs/error.d.ts.map +1 -0
  215. package/node_modules/rimraf/dist/commonjs/error.js +10 -0
  216. package/node_modules/rimraf/dist/commonjs/error.js.map +1 -0
  217. package/node_modules/rimraf/dist/commonjs/fix-eperm.d.ts +3 -0
  218. package/node_modules/rimraf/dist/commonjs/fix-eperm.d.ts.map +1 -0
  219. package/node_modules/rimraf/dist/commonjs/fix-eperm.js +38 -0
  220. package/node_modules/rimraf/dist/commonjs/fix-eperm.js.map +1 -0
  221. package/node_modules/rimraf/dist/commonjs/fs.d.ts +15 -0
  222. package/node_modules/rimraf/dist/commonjs/fs.d.ts.map +1 -0
  223. package/node_modules/rimraf/dist/commonjs/fs.js +33 -0
  224. package/node_modules/rimraf/dist/commonjs/fs.js.map +1 -0
  225. package/node_modules/rimraf/dist/commonjs/ignore-enoent.d.ts +3 -0
  226. package/node_modules/rimraf/dist/commonjs/ignore-enoent.d.ts.map +1 -0
  227. package/node_modules/rimraf/dist/commonjs/ignore-enoent.js +24 -0
  228. package/node_modules/rimraf/dist/commonjs/ignore-enoent.js.map +1 -0
  229. package/node_modules/rimraf/dist/commonjs/index.d.ts +50 -0
  230. package/node_modules/rimraf/dist/commonjs/index.d.ts.map +1 -0
  231. package/node_modules/rimraf/dist/commonjs/index.js +78 -0
  232. package/node_modules/rimraf/dist/commonjs/index.js.map +1 -0
  233. package/node_modules/rimraf/dist/commonjs/opt-arg.d.ts +34 -0
  234. package/node_modules/rimraf/dist/commonjs/opt-arg.d.ts.map +1 -0
  235. package/node_modules/rimraf/dist/commonjs/opt-arg.js +53 -0
  236. package/node_modules/rimraf/dist/commonjs/opt-arg.js.map +1 -0
  237. package/node_modules/rimraf/dist/commonjs/package.json +3 -0
  238. package/node_modules/rimraf/dist/commonjs/path-arg.d.ts +4 -0
  239. package/node_modules/rimraf/dist/commonjs/path-arg.d.ts.map +1 -0
  240. package/node_modules/rimraf/dist/commonjs/path-arg.js +48 -0
  241. package/node_modules/rimraf/dist/commonjs/path-arg.js.map +1 -0
  242. package/node_modules/rimraf/dist/commonjs/readdir-or-error.d.ts +3 -0
  243. package/node_modules/rimraf/dist/commonjs/readdir-or-error.d.ts.map +1 -0
  244. package/node_modules/rimraf/dist/commonjs/readdir-or-error.js +19 -0
  245. package/node_modules/rimraf/dist/commonjs/readdir-or-error.js.map +1 -0
  246. package/node_modules/rimraf/dist/commonjs/retry-busy.d.ts +8 -0
  247. package/node_modules/rimraf/dist/commonjs/retry-busy.d.ts.map +1 -0
  248. package/node_modules/rimraf/dist/commonjs/retry-busy.js +65 -0
  249. package/node_modules/rimraf/dist/commonjs/retry-busy.js.map +1 -0
  250. package/node_modules/rimraf/dist/commonjs/rimraf-manual.d.ts +3 -0
  251. package/node_modules/rimraf/dist/commonjs/rimraf-manual.d.ts.map +1 -0
  252. package/node_modules/rimraf/dist/commonjs/rimraf-manual.js +8 -0
  253. package/node_modules/rimraf/dist/commonjs/rimraf-manual.js.map +1 -0
  254. package/node_modules/rimraf/dist/commonjs/rimraf-move-remove.d.ts +4 -0
  255. package/node_modules/rimraf/dist/commonjs/rimraf-move-remove.d.ts.map +1 -0
  256. package/node_modules/rimraf/dist/commonjs/rimraf-move-remove.js +138 -0
  257. package/node_modules/rimraf/dist/commonjs/rimraf-move-remove.js.map +1 -0
  258. package/node_modules/rimraf/dist/commonjs/rimraf-native.d.ts +4 -0
  259. package/node_modules/rimraf/dist/commonjs/rimraf-native.d.ts.map +1 -0
  260. package/node_modules/rimraf/dist/commonjs/rimraf-native.js +24 -0
  261. package/node_modules/rimraf/dist/commonjs/rimraf-native.js.map +1 -0
  262. package/node_modules/rimraf/dist/commonjs/rimraf-posix.d.ts +4 -0
  263. package/node_modules/rimraf/dist/commonjs/rimraf-posix.d.ts.map +1 -0
  264. package/node_modules/rimraf/dist/commonjs/rimraf-posix.js +103 -0
  265. package/node_modules/rimraf/dist/commonjs/rimraf-posix.js.map +1 -0
  266. package/node_modules/rimraf/dist/commonjs/rimraf-windows.d.ts +4 -0
  267. package/node_modules/rimraf/dist/commonjs/rimraf-windows.d.ts.map +1 -0
  268. package/node_modules/rimraf/dist/commonjs/rimraf-windows.js +159 -0
  269. package/node_modules/rimraf/dist/commonjs/rimraf-windows.js.map +1 -0
  270. package/node_modules/rimraf/dist/commonjs/use-native.d.ts +4 -0
  271. package/node_modules/rimraf/dist/commonjs/use-native.d.ts.map +1 -0
  272. package/node_modules/rimraf/dist/commonjs/use-native.js +18 -0
  273. package/node_modules/rimraf/dist/commonjs/use-native.js.map +1 -0
  274. package/node_modules/rimraf/dist/esm/bin.d.mts +3 -0
  275. package/node_modules/rimraf/dist/esm/bin.d.mts.map +1 -0
  276. package/node_modules/rimraf/dist/esm/bin.mjs +250 -0
  277. package/node_modules/rimraf/dist/esm/bin.mjs.map +1 -0
  278. package/node_modules/rimraf/dist/esm/default-tmp.d.ts +3 -0
  279. package/node_modules/rimraf/dist/esm/default-tmp.d.ts.map +1 -0
  280. package/node_modules/rimraf/dist/esm/default-tmp.js +55 -0
  281. package/node_modules/rimraf/dist/esm/default-tmp.js.map +1 -0
  282. package/node_modules/rimraf/dist/esm/error.d.ts +6 -0
  283. package/node_modules/rimraf/dist/esm/error.d.ts.map +1 -0
  284. package/node_modules/rimraf/dist/esm/error.js +5 -0
  285. package/node_modules/rimraf/dist/esm/error.js.map +1 -0
  286. package/node_modules/rimraf/dist/esm/fix-eperm.d.ts +3 -0
  287. package/node_modules/rimraf/dist/esm/fix-eperm.d.ts.map +1 -0
  288. package/node_modules/rimraf/dist/esm/fix-eperm.js +33 -0
  289. package/node_modules/rimraf/dist/esm/fix-eperm.js.map +1 -0
  290. package/node_modules/rimraf/dist/esm/fs.d.ts +15 -0
  291. package/node_modules/rimraf/dist/esm/fs.d.ts.map +1 -0
  292. package/node_modules/rimraf/dist/esm/fs.js +18 -0
  293. package/node_modules/rimraf/dist/esm/fs.js.map +1 -0
  294. package/node_modules/rimraf/dist/esm/ignore-enoent.d.ts +3 -0
  295. package/node_modules/rimraf/dist/esm/ignore-enoent.d.ts.map +1 -0
  296. package/node_modules/rimraf/dist/esm/ignore-enoent.js +19 -0
  297. package/node_modules/rimraf/dist/esm/ignore-enoent.js.map +1 -0
  298. package/node_modules/rimraf/dist/esm/index.d.ts +50 -0
  299. package/node_modules/rimraf/dist/esm/index.d.ts.map +1 -0
  300. package/node_modules/rimraf/dist/esm/index.js +70 -0
  301. package/node_modules/rimraf/dist/esm/index.js.map +1 -0
  302. package/node_modules/rimraf/dist/esm/opt-arg.d.ts +34 -0
  303. package/node_modules/rimraf/dist/esm/opt-arg.d.ts.map +1 -0
  304. package/node_modules/rimraf/dist/esm/opt-arg.js +46 -0
  305. package/node_modules/rimraf/dist/esm/opt-arg.js.map +1 -0
  306. package/node_modules/rimraf/dist/esm/package.json +3 -0
  307. package/node_modules/rimraf/dist/esm/path-arg.d.ts +4 -0
  308. package/node_modules/rimraf/dist/esm/path-arg.d.ts.map +1 -0
  309. package/node_modules/rimraf/dist/esm/path-arg.js +46 -0
  310. package/node_modules/rimraf/dist/esm/path-arg.js.map +1 -0
  311. package/node_modules/rimraf/dist/esm/readdir-or-error.d.ts +3 -0
  312. package/node_modules/rimraf/dist/esm/readdir-or-error.d.ts.map +1 -0
  313. package/node_modules/rimraf/dist/esm/readdir-or-error.js +14 -0
  314. package/node_modules/rimraf/dist/esm/readdir-or-error.js.map +1 -0
  315. package/node_modules/rimraf/dist/esm/retry-busy.d.ts +8 -0
  316. package/node_modules/rimraf/dist/esm/retry-busy.d.ts.map +1 -0
  317. package/node_modules/rimraf/dist/esm/retry-busy.js +60 -0
  318. package/node_modules/rimraf/dist/esm/retry-busy.js.map +1 -0
  319. package/node_modules/rimraf/dist/esm/rimraf-manual.d.ts +3 -0
  320. package/node_modules/rimraf/dist/esm/rimraf-manual.d.ts.map +1 -0
  321. package/node_modules/rimraf/dist/esm/rimraf-manual.js +5 -0
  322. package/node_modules/rimraf/dist/esm/rimraf-manual.js.map +1 -0
  323. package/node_modules/rimraf/dist/esm/rimraf-move-remove.d.ts +4 -0
  324. package/node_modules/rimraf/dist/esm/rimraf-move-remove.d.ts.map +1 -0
  325. package/node_modules/rimraf/dist/esm/rimraf-move-remove.js +133 -0
  326. package/node_modules/rimraf/dist/esm/rimraf-move-remove.js.map +1 -0
  327. package/node_modules/rimraf/dist/esm/rimraf-native.d.ts +4 -0
  328. package/node_modules/rimraf/dist/esm/rimraf-native.d.ts.map +1 -0
  329. package/node_modules/rimraf/dist/esm/rimraf-native.js +19 -0
  330. package/node_modules/rimraf/dist/esm/rimraf-native.js.map +1 -0
  331. package/node_modules/rimraf/dist/esm/rimraf-posix.d.ts +4 -0
  332. package/node_modules/rimraf/dist/esm/rimraf-posix.d.ts.map +1 -0
  333. package/node_modules/rimraf/dist/esm/rimraf-posix.js +98 -0
  334. package/node_modules/rimraf/dist/esm/rimraf-posix.js.map +1 -0
  335. package/node_modules/rimraf/dist/esm/rimraf-windows.d.ts +4 -0
  336. package/node_modules/rimraf/dist/esm/rimraf-windows.d.ts.map +1 -0
  337. package/node_modules/rimraf/dist/esm/rimraf-windows.js +154 -0
  338. package/node_modules/rimraf/dist/esm/rimraf-windows.js.map +1 -0
  339. package/node_modules/rimraf/dist/esm/use-native.d.ts +4 -0
  340. package/node_modules/rimraf/dist/esm/use-native.d.ts.map +1 -0
  341. package/node_modules/rimraf/dist/esm/use-native.js +15 -0
  342. package/node_modules/rimraf/dist/esm/use-native.js.map +1 -0
  343. package/node_modules/rimraf/package.json +92 -0
  344. package/package.json +152 -0
  345. package/scripts/install-scanners.js +258 -0
  346. package/scripts/watchdog.js +147 -0
  347. package/src/analyzers/CSSAnalyzer.js +297 -0
  348. package/src/analyzers/ConfigValidator.js +690 -0
  349. package/src/analyzers/ESLintAnalyzer.js +320 -0
  350. package/src/analyzers/JavaScriptAnalyzer.js +261 -0
  351. package/src/analyzers/PrettierFormatter.js +247 -0
  352. package/src/analyzers/PythonAnalyzer.js +283 -0
  353. package/src/analyzers/SecurityAnalyzer.js +729 -0
  354. package/src/analyzers/SparrowAnalyzer.js +341 -0
  355. package/src/analyzers/TypeScriptAnalyzer.js +247 -0
  356. package/src/analyzers/codeCloneDetector/analyzer.js +344 -0
  357. package/src/analyzers/codeCloneDetector/detector.js +250 -0
  358. package/src/analyzers/codeCloneDetector/index.js +192 -0
  359. package/src/analyzers/codeCloneDetector/parser.js +199 -0
  360. package/src/analyzers/codeCloneDetector/reporter.js +148 -0
  361. package/src/analyzers/codeCloneDetector/scanner.js +88 -0
  362. package/src/core/agentPool.js +1957 -0
  363. package/src/core/agentScheduler.js +3212 -0
  364. package/src/core/contextManager.js +709 -0
  365. package/src/core/flowExecutor.js +928 -0
  366. package/src/core/messageProcessor.js +808 -0
  367. package/src/core/orchestrator.js +584 -0
  368. package/src/core/stateManager.js +1500 -0
  369. package/src/index.js +972 -0
  370. package/src/interfaces/cli.js +553 -0
  371. package/src/interfaces/terminal/__tests__/smoke/advancedFeatures.test.js +208 -0
  372. package/src/interfaces/terminal/__tests__/smoke/agentControl.test.js +236 -0
  373. package/src/interfaces/terminal/__tests__/smoke/agents.test.js +138 -0
  374. package/src/interfaces/terminal/__tests__/smoke/components.test.js +137 -0
  375. package/src/interfaces/terminal/__tests__/smoke/connection.test.js +350 -0
  376. package/src/interfaces/terminal/__tests__/smoke/enhancements.test.js +156 -0
  377. package/src/interfaces/terminal/__tests__/smoke/imports.test.js +332 -0
  378. package/src/interfaces/terminal/__tests__/smoke/messages.test.js +256 -0
  379. package/src/interfaces/terminal/__tests__/smoke/tools.test.js +388 -0
  380. package/src/interfaces/terminal/api/apiClient.js +299 -0
  381. package/src/interfaces/terminal/api/messageRouter.js +262 -0
  382. package/src/interfaces/terminal/api/session.js +266 -0
  383. package/src/interfaces/terminal/api/websocket.js +497 -0
  384. package/src/interfaces/terminal/components/AgentCreator.js +705 -0
  385. package/src/interfaces/terminal/components/AgentEditor.js +678 -0
  386. package/src/interfaces/terminal/components/AgentSwitcher.js +330 -0
  387. package/src/interfaces/terminal/components/ErrorBoundary.js +92 -0
  388. package/src/interfaces/terminal/components/ErrorPanel.js +264 -0
  389. package/src/interfaces/terminal/components/Header.js +28 -0
  390. package/src/interfaces/terminal/components/HelpPanel.js +231 -0
  391. package/src/interfaces/terminal/components/InputBox.js +118 -0
  392. package/src/interfaces/terminal/components/Layout.js +603 -0
  393. package/src/interfaces/terminal/components/LoadingSpinner.js +71 -0
  394. package/src/interfaces/terminal/components/MessageList.js +281 -0
  395. package/src/interfaces/terminal/components/MultilineTextInput.js +251 -0
  396. package/src/interfaces/terminal/components/SearchPanel.js +265 -0
  397. package/src/interfaces/terminal/components/SettingsPanel.js +415 -0
  398. package/src/interfaces/terminal/components/StatusBar.js +65 -0
  399. package/src/interfaces/terminal/components/TextInput.js +127 -0
  400. package/src/interfaces/terminal/config/agentEditorConstants.js +227 -0
  401. package/src/interfaces/terminal/config/constants.js +393 -0
  402. package/src/interfaces/terminal/index.js +168 -0
  403. package/src/interfaces/terminal/state/useAgentControl.js +496 -0
  404. package/src/interfaces/terminal/state/useAgents.js +537 -0
  405. package/src/interfaces/terminal/state/useConnection.js +444 -0
  406. package/src/interfaces/terminal/state/useMessages.js +630 -0
  407. package/src/interfaces/terminal/state/useTools.js +554 -0
  408. package/src/interfaces/terminal/utils/debugLogger.js +44 -0
  409. package/src/interfaces/terminal/utils/settingsStorage.js +232 -0
  410. package/src/interfaces/terminal/utils/theme.js +85 -0
  411. package/src/interfaces/webServer.js +5457 -0
  412. package/src/modules/fileExplorer/controller.js +413 -0
  413. package/src/modules/fileExplorer/index.js +37 -0
  414. package/src/modules/fileExplorer/middleware.js +92 -0
  415. package/src/modules/fileExplorer/routes.js +158 -0
  416. package/src/modules/fileExplorer/types.js +44 -0
  417. package/src/services/agentActivityService.js +399 -0
  418. package/src/services/aiService.js +2618 -0
  419. package/src/services/apiKeyManager.js +334 -0
  420. package/src/services/benchmarkService.js +196 -0
  421. package/src/services/budgetService.js +565 -0
  422. package/src/services/contextInjectionService.js +268 -0
  423. package/src/services/conversationCompactionService.js +1103 -0
  424. package/src/services/credentialVault.js +685 -0
  425. package/src/services/errorHandler.js +810 -0
  426. package/src/services/fileAttachmentService.js +547 -0
  427. package/src/services/flowContextService.js +189 -0
  428. package/src/services/memoryService.js +521 -0
  429. package/src/services/modelRouterService.js +365 -0
  430. package/src/services/modelsService.js +323 -0
  431. package/src/services/ollamaService.js +452 -0
  432. package/src/services/portRegistry.js +336 -0
  433. package/src/services/portTracker.js +223 -0
  434. package/src/services/projectDetector.js +404 -0
  435. package/src/services/promptService.js +372 -0
  436. package/src/services/qualityInspector.js +796 -0
  437. package/src/services/scheduleService.js +725 -0
  438. package/src/services/serviceRegistry.js +386 -0
  439. package/src/services/skillsService.js +486 -0
  440. package/src/services/telegramService.js +920 -0
  441. package/src/services/tokenCountingService.js +316 -0
  442. package/src/services/visualEditorBridge.js +1033 -0
  443. package/src/services/visualEditorServer.js +1727 -0
  444. package/src/services/whatsappService.js +663 -0
  445. package/src/tools/__tests__/webTool.e2e.test.js +569 -0
  446. package/src/tools/__tests__/webTool.unit.test.js +195 -0
  447. package/src/tools/agentCommunicationTool.js +1343 -0
  448. package/src/tools/agentDelayTool.js +498 -0
  449. package/src/tools/asyncToolManager.js +604 -0
  450. package/src/tools/baseTool.js +887 -0
  451. package/src/tools/browserTool.js +897 -0
  452. package/src/tools/cloneDetectionTool.js +581 -0
  453. package/src/tools/codeMapTool.js +857 -0
  454. package/src/tools/dependencyResolverTool.js +1212 -0
  455. package/src/tools/docxTool.js +623 -0
  456. package/src/tools/excelTool.js +636 -0
  457. package/src/tools/fileContentReplaceTool.js +840 -0
  458. package/src/tools/fileTreeTool.js +833 -0
  459. package/src/tools/filesystemTool.js +1217 -0
  460. package/src/tools/helpTool.js +198 -0
  461. package/src/tools/imageTool.js +1034 -0
  462. package/src/tools/importAnalyzerTool.js +1056 -0
  463. package/src/tools/jobDoneTool.js +388 -0
  464. package/src/tools/memoryTool.js +554 -0
  465. package/src/tools/pdfTool.js +627 -0
  466. package/src/tools/seekTool.js +883 -0
  467. package/src/tools/skillsTool.js +276 -0
  468. package/src/tools/staticAnalysisTool.js +2146 -0
  469. package/src/tools/taskManagerTool.js +2836 -0
  470. package/src/tools/terminalTool.js +2486 -0
  471. package/src/tools/userPromptTool.js +474 -0
  472. package/src/tools/videoTool.js +1139 -0
  473. package/src/tools/visionTool.js +507 -0
  474. package/src/tools/visualEditorTool.js +1175 -0
  475. package/src/tools/webTool.js +3114 -0
  476. package/src/tools/whatsappTool.js +457 -0
  477. package/src/types/agent.js +519 -0
  478. package/src/types/contextReference.js +972 -0
  479. package/src/types/conversation.js +730 -0
  480. package/src/types/toolCommand.js +747 -0
  481. package/src/utilities/attachmentValidator.js +288 -0
  482. package/src/utilities/browserStealth.js +630 -0
  483. package/src/utilities/configManager.js +618 -0
  484. package/src/utilities/constants.js +870 -0
  485. package/src/utilities/directoryAccessManager.js +566 -0
  486. package/src/utilities/fileProcessor.js +307 -0
  487. package/src/utilities/humanBehavior.js +453 -0
  488. package/src/utilities/jsonRepair.js +242 -0
  489. package/src/utilities/logger.js +436 -0
  490. package/src/utilities/platformUtils.js +255 -0
  491. package/src/utilities/platformUtils.test.js +98 -0
  492. package/src/utilities/stealthConstants.js +377 -0
  493. package/src/utilities/structuredFileValidator.js +699 -0
  494. package/src/utilities/tagParser.js +878 -0
  495. package/src/utilities/toolConstants.js +415 -0
  496. package/src/utilities/userDataDir.js +300 -0
  497. package/web-ui/build/brands/autopilot/favicon.svg +1 -0
  498. package/web-ui/build/brands/autopilot/logo.webp +0 -0
  499. package/web-ui/build/brands/onbuzz/favicon.svg +1 -0
  500. package/web-ui/build/brands/onbuzz/logo-text.webp +0 -0
  501. package/web-ui/build/brands/onbuzz/logo.webp +0 -0
  502. package/web-ui/build/index.html +15 -0
  503. package/web-ui/build/logo.png +0 -0
  504. package/web-ui/build/logo2.png +0 -0
  505. package/web-ui/build/static/index-SmQFfvBs.js +746 -0
  506. package/web-ui/build/static/index-V2ySwjHp.css +1 -0
@@ -0,0 +1,3212 @@
1
+ /**
2
+ * AgentScheduler - Manages cooperative execution of multiple agents
3
+ *
4
+ * Architecture:
5
+ * - Uses centralized AgentActivityService to determine which agents should be active
6
+ * - Iterates over active agents in round-robin fashion
7
+ * - For each agent, processes queued messages (toolResults, interAgentMessages, userMessages) in arrival order
8
+ * - Sends conversation history to AI service for completion
9
+ * - Handles agent mode differences (CHAT vs AGENT)
10
+ * - Respects agent delays set by agentDelay tool
11
+ *
12
+ * Key Change: Instead of managing add/remove events scattered across codebase,
13
+ * the scheduler now queries AgentActivityService.getActiveAgents() each cycle
14
+ * to determine which agents should be processed.
15
+ */
16
+
17
+ import {
18
+ AGENT_MODES,
19
+ MESSAGE_ROLES,
20
+ COMPACTION_CONFIG,
21
+ COMPACTION_STATUS,
22
+ COMPACTION_STRATEGIES,
23
+ SCHEDULER_CONFIG,
24
+ AGENT_ACTIVITY_STATUS,
25
+ TASK_STATUS,
26
+ TASK_PRIORITY_ORDER
27
+ } from '../utilities/constants.js';
28
+ import ContextInjectionService from '../services/contextInjectionService.js';
29
+ import FlowContextService from '../services/flowContextService.js';
30
+ import TokenCountingService from '../services/tokenCountingService.js';
31
+ import ConversationCompactionService from '../services/conversationCompactionService.js';
32
+ import {
33
+ shouldAgentBeActive,
34
+ getActiveAgents,
35
+ shouldSkipIteration
36
+ } from '../services/agentActivityService.js';
37
+
38
+ class AgentScheduler {
39
+ constructor(agentPool, messageProcessor, aiService, logger, webSocketManager = null, modelRouterService = null, modelsService = null) {
40
+ this.agentPool = agentPool;
41
+ this.messageProcessor = messageProcessor;
42
+ this.aiService = aiService;
43
+ this.logger = logger;
44
+ this.webSocketManager = webSocketManager;
45
+ this.modelRouterService = modelRouterService;
46
+ this.modelsService = modelsService;
47
+
48
+ // Initialize ContextInjectionService for file attachments
49
+ this.contextInjectionService = new ContextInjectionService({}, logger);
50
+
51
+ // Initialize FlowContextService for flow execution context
52
+ this.flowContextService = new FlowContextService({}, logger);
53
+
54
+ // Initialize compactization services
55
+ this.tokenCountingService = new TokenCountingService(logger, modelsService);
56
+ this.compactionService = new ConversationCompactionService(
57
+ this.tokenCountingService,
58
+ aiService,
59
+ logger
60
+ );
61
+ // Inject modelsService for runtime compaction model validation
62
+ if (modelsService) {
63
+ this.compactionService.setModelsService(modelsService);
64
+ }
65
+
66
+ // Compactization state tracking
67
+ this.compactionInProgress = new Map(); // Map of agentId to compaction status
68
+
69
+ // Scheduler state
70
+ this.isRunning = false;
71
+ this.agentSessionMap = new Map(); // Map of agentId to sessionId (for API key resolution)
72
+ this.scheduleInterval = null;
73
+
74
+ // Repetition detection - sliding window of recent state hashes per agent
75
+ // Structure: Map<agentId, Array<{hash, timestamp}>>
76
+ this.stateHashHistory = new Map();
77
+
78
+ // Per-agent processing locks — prevent the same agent from being processed
79
+ // concurrently across overlapping cycles. No global lock needed; each cycle
80
+ // skips agents that are already in-flight from a previous cycle.
81
+ this.agentProcessingLocks = new Map(); // Map of agentId to processing state
82
+
83
+ // Round-robin fairness: tracks last cycle's launch order so agents that
84
+ // were skipped due to concurrency cap get priority in the next cycle.
85
+ this._lastLaunchedAgentIds = new Set();
86
+
87
+ // Token limit error retry tracking - tracks failed attempts per agent
88
+ // Structure: Map<agentId, { attempts: number, lastError: string, timestamp: Date }>
89
+ this.tokenLimitRetryTracker = new Map();
90
+ this.MAX_TOKEN_LIMIT_RETRIES = 2;
91
+
92
+ // Consecutive messages without tool usage tracking (AGENT mode only)
93
+ // Structure: Map<agentId, number> - count of consecutive messages without tools
94
+ this.consecutiveNoToolMessages = new Map();
95
+
96
+ // Configuration from constants (no magic numbers)
97
+ this.iterationDelayMs = SCHEDULER_CONFIG.ITERATION_DELAY_MS;
98
+ this.maxIterationsPerCycle = SCHEDULER_CONFIG.MAX_ITERATIONS_PER_CYCLE;
99
+ }
100
+
101
+ /**
102
+ * Start the agent scheduler
103
+ */
104
+ start() {
105
+ if (this.isRunning) {
106
+ this.logger.info('Agent scheduler is already running');
107
+ return;
108
+ }
109
+
110
+ this.isRunning = true;
111
+ this.logger.info('Starting Agent Scheduler with centralized activity service');
112
+
113
+ // Start the main scheduler loop
114
+ // No need to initialize agents - the processing cycle queries active agents each iteration
115
+ this.scheduleInterval = setInterval(() => {
116
+ this.processingCycle().catch(error => {
117
+ this.logger.error('Scheduler processing cycle failed:', error);
118
+ });
119
+ }, this.iterationDelayMs);
120
+ }
121
+
122
+ /**
123
+ * Register session ID for an agent (used for API key resolution)
124
+ * This is called when a message is sent to an agent to associate the session
125
+ * @param {string} agentId - Agent ID
126
+ * @param {string} sessionId - Session ID for API key resolution
127
+ */
128
+ registerAgentSession(agentId, sessionId) {
129
+ if (agentId && sessionId) {
130
+ this.agentSessionMap.set(agentId, sessionId);
131
+ this.logger.debug(`Registered session for agent: ${agentId}`, { sessionId });
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Get session ID for an agent
137
+ * @param {string} agentId - Agent ID
138
+ * @returns {string|undefined} Session ID or undefined
139
+ */
140
+ getAgentSession(agentId) {
141
+ return this.agentSessionMap.get(agentId);
142
+ }
143
+
144
+ /**
145
+ * Stop the agent scheduler
146
+ */
147
+ stop() {
148
+ if (!this.isRunning) return;
149
+
150
+ this.isRunning = false;
151
+ if (this.scheduleInterval) {
152
+ clearInterval(this.scheduleInterval);
153
+ this.scheduleInterval = null;
154
+ }
155
+
156
+ this.agentSessionMap.clear();
157
+ this.compactionInProgress.clear();
158
+ this.stateHashHistory.clear();
159
+ this.agentProcessingLocks.clear();
160
+ this.consecutiveNoToolMessages.clear();
161
+
162
+ // Cleanup services
163
+ if (this.tokenCountingService && this.tokenCountingService.cleanup) {
164
+ this.tokenCountingService.cleanup();
165
+ }
166
+
167
+ this.logger.info('Agent Scheduler stopped');
168
+ }
169
+
170
+ /**
171
+ * Register an agent's session and ensure scheduler is running
172
+ *
173
+ * NOTE: This method is kept for backward compatibility. The actual decision
174
+ * of whether an agent should be processed is now made by AgentActivityService.
175
+ * This method now only:
176
+ * 1. Registers the session ID for API key resolution
177
+ * 2. Ensures the scheduler is running
178
+ *
179
+ * @param {string} agentId - Agent ID
180
+ * @param {Object} context - Context containing sessionId
181
+ */
182
+ async addAgent(agentId, context = {}) {
183
+ // Register session ID for API key resolution
184
+ if (context.sessionId) {
185
+ this.registerAgentSession(agentId, context.sessionId);
186
+ } else {
187
+ this.logger.warn(`Agent ${agentId} registered without sessionId - API key resolution may fail`, {
188
+ triggeredBy: context.triggeredBy
189
+ });
190
+ }
191
+
192
+ // Clear hash history when user sends a new message
193
+ // This provides a "fresh start" after user intervention, preventing false loop detection
194
+ if (context.triggeredBy === 'user-message') {
195
+ this.clearHashHistory(agentId);
196
+ this.logger.debug(`Hash history cleared for agent ${agentId} due to user message`);
197
+
198
+ // Also reset consecutive no-tool counter on user message (fresh interaction)
199
+ if (this.consecutiveNoToolMessages.has(agentId)) {
200
+ this.consecutiveNoToolMessages.set(agentId, 0);
201
+ this.logger.debug(`Consecutive no-tool counter reset for agent ${agentId} due to user message`);
202
+ }
203
+ }
204
+
205
+ // Initialize hash history for this agent if not exists
206
+ if (!this.stateHashHistory.has(agentId)) {
207
+ this.stateHashHistory.set(agentId, []);
208
+ }
209
+
210
+ this.logger.debug(`Agent session registered: ${agentId}`, {
211
+ sessionId: context.sessionId || 'NO_SESSION_ID',
212
+ triggeredBy: context.triggeredBy || 'unknown'
213
+ });
214
+
215
+ // Start scheduler if not running
216
+ if (!this.isRunning) {
217
+ this.start();
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Clean up session tracking for an agent
223
+ *
224
+ * NOTE: This method is kept for backward compatibility and cleanup.
225
+ * The actual decision of whether to stop processing an agent is now
226
+ * made by AgentActivityService - agents are not "removed" from scheduler,
227
+ * they simply become inactive based on their state.
228
+ *
229
+ * @param {string} agentId - Agent ID
230
+ * @param {string} reason - Reason for cleanup (for logging)
231
+ */
232
+ removeAgent(agentId, reason = 'completed') {
233
+ // Clean up session mapping
234
+ if (this.agentSessionMap.has(agentId)) {
235
+ this.agentSessionMap.delete(agentId);
236
+ }
237
+
238
+ // Clean up state hash history for this agent
239
+ if (this.stateHashHistory.has(agentId)) {
240
+ this.stateHashHistory.delete(agentId);
241
+ }
242
+
243
+ // Clean up processing lock
244
+ if (this.agentProcessingLocks.has(agentId)) {
245
+ this.agentProcessingLocks.delete(agentId);
246
+ }
247
+
248
+ // Clean up consecutive no-tool counter
249
+ if (this.consecutiveNoToolMessages.has(agentId)) {
250
+ this.consecutiveNoToolMessages.delete(agentId);
251
+ }
252
+
253
+ this.logger.debug(`Agent session cleaned up: ${agentId}`, { reason });
254
+ }
255
+
256
+ /**
257
+ * Check if agent should currently be active in scheduler
258
+ * Uses the centralized AgentActivityService for the decision
259
+ * @param {string} agentId - Agent ID to check
260
+ * @returns {Promise<boolean>} True if agent should be active
261
+ */
262
+ async isAgentInScheduler(agentId) {
263
+ const agent = await this.agentPool.getAgent(agentId);
264
+ if (!agent) return false;
265
+
266
+ const result = shouldAgentBeActive(agent);
267
+ return result.active;
268
+ }
269
+
270
+ /**
271
+ * Stop autonomous execution for a specific agent
272
+ * This sets the agent state so that shouldAgentBeActive() returns false
273
+ * @param {string} agentId - Agent ID to stop
274
+ * @returns {Promise<Object>} Result with agent state
275
+ */
276
+ async stopAgentExecution(agentId) {
277
+ try {
278
+ const agent = await this.agentPool.getAgent(agentId);
279
+ if (!agent) {
280
+ return {
281
+ success: false,
282
+ error: 'Agent not found'
283
+ };
284
+ }
285
+
286
+ // Set stopRequested flag FIRST - this signals any concurrent processing to stop
287
+ // This is checked by shouldAgentBeActive() and processAgent()
288
+ agent.stopRequested = true;
289
+
290
+ // CRITICAL: Abort any active streaming request to Azure backend
291
+ // This immediately stops the HTTP connection and prevents further chunk processing
292
+ if (this.aiService && this.aiService.abortRequest) {
293
+ const aborted = this.aiService.abortRequest(agentId);
294
+ if (aborted) {
295
+ this.logger.info(`Aborted active request for agent: ${agentId}`);
296
+ }
297
+ }
298
+
299
+ // Set agent mode back to CHAT - this will cause shouldAgentBeActive to return false
300
+ agent.mode = AGENT_MODES.CHAT;
301
+
302
+ // Clear any delays
303
+ agent.delayEndTime = null;
304
+
305
+ // Clear stop request flag after mode is set (it has been honored)
306
+ agent.stopRequested = false;
307
+
308
+ // Persist the change
309
+ await this.agentPool.persistAgentState(agentId);
310
+
311
+ // Clean up session tracking
312
+ this.removeAgent(agentId, 'stopped-by-user');
313
+
314
+ // Get session ID for broadcast
315
+ const sessionId = this.getAgentSession(agentId) || agent.sessionId;
316
+
317
+ // Broadcast stop event if we have a valid session ID
318
+ if (sessionId && this.webSocketManager && this.webSocketManager.broadcastToSession) {
319
+ this.webSocketManager.broadcastToSession(sessionId, {
320
+ type: 'execution_stopped',
321
+ data: {
322
+ agentId,
323
+ type: 'execution_stopped',
324
+ mode: AGENT_MODES.CHAT, // Include mode change for UI toggle
325
+ timestamp: new Date().toISOString()
326
+ }
327
+ });
328
+ }
329
+
330
+ this.logger.info(`Agent execution stopped: ${agentId}`, {
331
+ mode: agent.mode
332
+ });
333
+
334
+ return {
335
+ success: true,
336
+ agent: {
337
+ id: agent.id,
338
+ name: agent.name,
339
+ mode: agent.mode
340
+ }
341
+ };
342
+
343
+ } catch (error) {
344
+ this.logger.error(`Failed to stop agent execution: ${agentId}`, {
345
+ error: error.message
346
+ });
347
+
348
+ return {
349
+ success: false,
350
+ error: error.message
351
+ };
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Main processing cycle - queries active agents and processes them
357
+ *
358
+ * KEY CHANGE: Instead of iterating over a managed activeAgents Map,
359
+ * we now query AgentActivityService.getActiveAgents() each cycle.
360
+ * This is the centralized approach that eliminates scattered add/remove logic.
361
+ *
362
+ * @private
363
+ */
364
+ async processingCycle() {
365
+ // Get all agents from pool and determine which should be active
366
+ const allAgents = await this.agentPool.getAllAgents();
367
+ const activeAgentResults = getActiveAgents(allAgents);
368
+
369
+ if (activeAgentResults.length === 0) {
370
+ return; // No agents to process
371
+ }
372
+
373
+ // Concurrency enforcement: count how many agents are already in-flight
374
+ // from previous cycles, and only launch new ones up to the global cap.
375
+ const maxConcurrent = SCHEDULER_CONFIG.MAX_CONCURRENT_AGENTS || 3;
376
+ const currentlyInFlight = this.agentProcessingLocks.size;
377
+
378
+ // Filter out agents already being processed
379
+ const unlockedAgents = activeAgentResults.filter(r => {
380
+ if (this.agentProcessingLocks.get(r.agentId)) {
381
+ this.logger.debug(`Agent ${r.agentId} still processing from previous cycle, skipping`);
382
+ return false;
383
+ }
384
+ return true;
385
+ });
386
+
387
+ if (unlockedAgents.length === 0) {
388
+ return; // All active agents already being processed
389
+ }
390
+
391
+ // Round-robin fairness: agents that launched in a recent cycle go to the back
392
+ // so that waiting agents get priority for available slots.
393
+ unlockedAgents.sort((a, b) => {
394
+ const aRan = this._lastLaunchedAgentIds.has(a.agentId) ? 1 : 0;
395
+ const bRan = this._lastLaunchedAgentIds.has(b.agentId) ? 1 : 0;
396
+ return aRan - bRan; // agents that didn't run recently come first
397
+ });
398
+
399
+ // Cap new launches so total in-flight never exceeds MAX_CONCURRENT_AGENTS
400
+ const slotsAvailable = Math.max(0, maxConcurrent - currentlyInFlight);
401
+ const agentsToLaunch = unlockedAgents.slice(0, slotsAvailable);
402
+
403
+ if (agentsToLaunch.length === 0) {
404
+ this.logger.debug(`Concurrency cap reached: ${currentlyInFlight}/${maxConcurrent} agents in-flight, ${unlockedAgents.length} waiting`);
405
+ return;
406
+ }
407
+
408
+ this.logger.debug(`Processing cycle: launching ${agentsToLaunch.length} agents (${currentlyInFlight} in-flight, ${maxConcurrent} max)`, {
409
+ agents: agentsToLaunch.map(r => ({ id: r.agentId, reason: r.reason })),
410
+ waiting: unlockedAgents.length - agentsToLaunch.length
411
+ });
412
+
413
+ // Track which agents we're launching for round-robin fairness
414
+ this._lastLaunchedAgentIds = new Set(agentsToLaunch.map(r => r.agentId));
415
+
416
+ // Fire-and-forget: launch processing without awaiting.
417
+ // Each agent is protected by its own lock in processAgent().
418
+ // Next cycle (1s later) will pick up remaining agents when slots free up.
419
+ this.processAgentsInParallel(agentsToLaunch).catch(error => {
420
+ this.logger.error('Parallel agent processing failed:', error);
421
+ });
422
+ }
423
+
424
+ /**
425
+ * Process multiple agents in parallel.
426
+ * Concurrency is capped at the cycle level (processingCycle slices to
427
+ * MAX_CONCURRENT_AGENTS), so this method simply launches all given agents
428
+ * concurrently via Promise.all.
429
+ *
430
+ * @param {Array} activeAgentResults - Array of { agent, agentId, reason }
431
+ * @private
432
+ */
433
+ async processAgentsInParallel(activeAgentResults) {
434
+ this.logger.debug(`Launching ${activeAgentResults.length} agents in parallel`, {
435
+ agentIds: activeAgentResults.map(r => r.agentId)
436
+ });
437
+
438
+ await Promise.all(
439
+ activeAgentResults.map(async ({ agentId, reason }) => {
440
+ try {
441
+ await this.processAgent(agentId);
442
+ } catch (error) {
443
+ this.logger.error(`Agent processing failed: ${agentId}`, {
444
+ error: error.message,
445
+ stack: error.stack,
446
+ activationReason: reason
447
+ });
448
+ // Clean up on error - don't leave stale locks
449
+ this.agentProcessingLocks.delete(agentId);
450
+ }
451
+ })
452
+ );
453
+ }
454
+
455
+ /**
456
+ * Process a single agent - handle queues and get AI response
457
+ *
458
+ * NOTE: This method no longer returns whether the agent should continue.
459
+ * The decision is made by AgentActivityService at the start of each cycle.
460
+ *
461
+ * @param {string} agentId - Agent ID to process
462
+ * @private
463
+ */
464
+ async processAgent(agentId) {
465
+ // Check if this agent is already being processed
466
+ if (this.agentProcessingLocks.get(agentId)) {
467
+ this.logger.debug(`Agent ${agentId} is already being processed, skipping`);
468
+ return;
469
+ }
470
+
471
+ const agent = await this.agentPool.getAgent(agentId);
472
+ if (!agent) {
473
+ return; // Agent no longer exists
474
+ }
475
+
476
+ // Set processing lock (prevents concurrent processing of the same agent)
477
+ this.agentProcessingLocks.set(agentId, true);
478
+
479
+ try {
480
+ // Use centralized service to check if we should skip this iteration
481
+ const skipCheck = shouldSkipIteration(agent);
482
+ if (skipCheck.skip) {
483
+ this.logger.debug(`Agent ${agentId} skipping iteration: ${skipCheck.reason}`);
484
+ return;
485
+ }
486
+
487
+ // Generate current state hash for repetition detection
488
+ const currentStateHash = this.generateAgentStateHash(agent);
489
+
490
+ // Check for repetitive loop using sliding window
491
+ const loopDetection = this.detectRepetitiveLoop(agentId, currentStateHash);
492
+ if (loopDetection.isLoop) {
493
+ this.logger.warn(`Agent ${agentId} detected in repetitive loop - terminating`, {
494
+ stateHash: currentStateHash,
495
+ occurrences: loopDetection.occurrences,
496
+ windowSize: SCHEDULER_CONFIG.STATE_HASH_WINDOW_SIZE,
497
+ threshold: SCHEDULER_CONFIG.REPETITION_THRESHOLD
498
+ });
499
+
500
+ // Notify user about the loop and stop the agent
501
+ await this.handleRepetitiveLoop(agentId, loopDetection);
502
+ return;
503
+ }
504
+
505
+ // Check if this exact state was just processed (immediate duplicate)
506
+ if (loopDetection.isImmediateDuplicate) {
507
+ this.logger.debug(`Agent ${agentId} state unchanged from last iteration, skipping`, {
508
+ stateHash: currentStateHash,
509
+ agentMode: agent.mode
510
+ });
511
+ return; // Skip - nothing new to process
512
+ }
513
+
514
+ // Get message queue status
515
+ const queues = agent.messageQueues || {};
516
+ const totalMessages = (queues.toolResults?.length || 0) +
517
+ (queues.interAgentMessages?.length || 0) +
518
+ (queues.userMessages?.length || 0);
519
+
520
+ // Log if user messages are present (highest priority)
521
+ if (queues.userMessages?.length > 0) {
522
+ this.logger.info(`User message detected for agent ${agentId} - will be prioritized`, {
523
+ userMessageCount: queues.userMessages.length,
524
+ agentMode: agent.mode
525
+ });
526
+ }
527
+
528
+ // Check if stop was requested - exit early if so
529
+ if (agent.stopRequested) {
530
+ this.logger.info(`Agent ${agentId} stop requested - aborting processing`);
531
+ return;
532
+ }
533
+
534
+ // Track whether we actually got a new AI response this cycle
535
+ let gotNewAIResponse = false;
536
+
537
+ // Process based on whether there are messages or agent needs autonomous processing
538
+ if (totalMessages === 0 && agent.mode === AGENT_MODES.AGENT) {
539
+ // AGENT mode with no messages - check for task-based work
540
+ await this.autoCreateInitialTaskIfNeeded(agentId);
541
+ gotNewAIResponse = await this.processAgentAutonomously(agentId);
542
+ } else if (totalMessages > 0) {
543
+ // Has messages to process
544
+ const processedMessages = await this.processAgentQueues(agentId);
545
+
546
+ if (processedMessages > 0 || agent.mode === AGENT_MODES.AGENT) {
547
+ // Get AI response after processing queued messages
548
+ const aiResponse = await this.getAgentAIResponse(agentId);
549
+
550
+ if (aiResponse) {
551
+ // Process AI response and execute any tools
552
+ await this.processAIResponse(agentId, aiResponse);
553
+ gotNewAIResponse = true;
554
+
555
+ // Clear token limit retry tracker on successful AI response
556
+ this.clearTokenLimitRetryTracker(agentId);
557
+ } else {
558
+ this.logger.warn(`No AI response for agent ${agentId}`);
559
+ }
560
+ }
561
+ }
562
+ // CHAT mode with no messages: do nothing - activity service will mark as inactive
563
+
564
+ // Only record state hash when we actually got a new AI response
565
+ // This prevents false loop detection when agent is idle
566
+ if (gotNewAIResponse) {
567
+ this.recordStateHash(agentId, currentStateHash);
568
+ }
569
+
570
+ // Decrement TTL (Time-to-Live) if set
571
+ // TTL gives agents extra processing cycles after clearing tasks
572
+ const agentForTtl = await this.agentPool.getAgent(agentId);
573
+ if (agentForTtl && agentForTtl.ttl !== null && agentForTtl.ttl !== undefined && agentForTtl.ttl > 0) {
574
+ agentForTtl.ttl--;
575
+ if (agentForTtl.ttl <= 0) {
576
+ agentForTtl.ttl = null; // Clear expired TTL
577
+ }
578
+ await this.agentPool.persistAgentState(agentId);
579
+ this.logger.debug(`TTL decremented for agent ${agentId}`, { newTtl: agentForTtl.ttl });
580
+ }
581
+
582
+ } finally {
583
+ // Always clear processing lock, even on errors
584
+ this.agentProcessingLocks.delete(agentId);
585
+ }
586
+ }
587
+
588
+ /**
589
+ * Process agent's message queues with consolidated single-message approach
590
+ * @param {string} agentId - Agent ID
591
+ * @returns {Promise<number>} Number of messages processed
592
+ * @private
593
+ */
594
+ async processAgentQueues(agentId) {
595
+ const agent = await this.agentPool.getAgent(agentId);
596
+ if (!agent) return 0;
597
+
598
+ const queues = agent.messageQueues;
599
+
600
+ // Task boundary: if agent just completed work (jobdone) and has stale tool
601
+ // results from the previous task alongside new messages, drain the stale
602
+ // results into conversation history as a separate boundary entry so they
603
+ // don't get mixed into the new task's consolidated input.
604
+ const hasNewMessages = (queues.userMessages?.length > 0) || (queues.interAgentMessages?.length > 0);
605
+ if (agent.autonomousWorkComplete && queues.toolResults?.length > 0 && hasNewMessages) {
606
+ const staleCount = queues.toolResults.length;
607
+ let boundaryContent = '[Previous Task — Final Tool Results]\n';
608
+ queues.toolResults.forEach(msg => {
609
+ boundaryContent += `${this.formatToolResult(msg)}\n`;
610
+ });
611
+ boundaryContent += '\n--- Previous task completed. New task follows. ---';
612
+
613
+ await this.addMessageToConversation(agentId, {
614
+ id: `task-boundary-${Date.now()}`,
615
+ role: MESSAGE_ROLES.USER,
616
+ content: boundaryContent.trim(),
617
+ timestamp: new Date().toISOString(),
618
+ type: 'task-boundary'
619
+ }, false);
620
+
621
+ queues.toolResults.length = 0;
622
+ agent.autonomousWorkComplete = false;
623
+ this.logger.info(`Task boundary: drained ${staleCount} stale tool results for agent ${agentId}`);
624
+ } else if (agent.autonomousWorkComplete && hasNewMessages) {
625
+ // No stale tool results but new messages arrived — just reset the flag
626
+ agent.autonomousWorkComplete = false;
627
+ }
628
+
629
+ // Collect all messages with timestamps for proper ordering
630
+ const allMessages = [
631
+ ...queues.toolResults.map(msg => ({ ...msg, queueType: 'toolResults' })),
632
+ ...queues.interAgentMessages.map(msg => ({ ...msg, queueType: 'interAgentMessages' })),
633
+ ...queues.userMessages.map(msg => ({ ...msg, queueType: 'userMessages' }))
634
+ ];
635
+
636
+ if (allMessages.length === 0) return 0;
637
+
638
+ // Sort by arrival time (timestamp)
639
+ allMessages.sort((a, b) => new Date(a.timestamp || a.queuedAt || 0) - new Date(b.timestamp || b.queuedAt || 0));
640
+
641
+ // CRITICAL FIX: Consolidate all messages into single AI request
642
+ let consolidatedContent = '';
643
+ const hasUserMessages = allMessages.some(m => m.queueType === 'userMessages');
644
+ const hasInterAgentMessages = allMessages.some(m => m.queueType === 'interAgentMessages');
645
+ const hasToolResults = allMessages.some(m => m.queueType === 'toolResults');
646
+
647
+ // Add user messages first (highest priority)
648
+ const userMessages = allMessages.filter(m => m.queueType === 'userMessages');
649
+ if (userMessages.length > 0) {
650
+ userMessages.forEach(msg => {
651
+ if (consolidatedContent) consolidatedContent += '\n\n';
652
+ consolidatedContent += msg.content;
653
+ });
654
+ }
655
+
656
+ // Add inter-agent messages as context (not as separate system messages)
657
+ const interAgentMessages = allMessages.filter(m => m.queueType === 'interAgentMessages');
658
+ if (interAgentMessages.length > 0) {
659
+ if (consolidatedContent) consolidatedContent += '\n\n';
660
+ consolidatedContent += '[Agent Messages]\n';
661
+ interAgentMessages.forEach(msg => {
662
+ const senderName = msg.senderName || msg.sender || 'Unknown Agent';
663
+ consolidatedContent += `${senderName}: ${msg.content}\n`;
664
+ });
665
+ }
666
+
667
+ // Add tool results as context, grouped by the AI turn that triggered them
668
+ const toolResults = allMessages.filter(m => m.queueType === 'toolResults');
669
+ if (toolResults.length > 0) {
670
+ if (consolidatedContent) consolidatedContent += '\n\n';
671
+
672
+ // Group results by responseTurnId (the AI message that triggered them)
673
+ const turnGroups = new Map();
674
+ for (const msg of toolResults) {
675
+ const turnKey = msg.responseTurnId || 'unknown';
676
+ if (!turnGroups.has(turnKey)) turnGroups.set(turnKey, []);
677
+ turnGroups.get(turnKey).push(msg);
678
+ }
679
+
680
+ // Manifest: tell agent what to expect
681
+ const turnCount = turnGroups.size;
682
+ const toolCount = toolResults.length;
683
+ const toolIds = [...new Set(toolResults.map(m => m.toolId).filter(Boolean))];
684
+ consolidatedContent += `[Tool Results — ${toolCount} result${toolCount > 1 ? 's' : ''} from ${turnCount} tool batch${turnCount > 1 ? 'es' : ''}: ${toolIds.join(', ')}]\n`;
685
+
686
+ if (turnCount === 1) {
687
+ // Single batch — flat list (no sub-headers needed)
688
+ toolResults.forEach(msg => {
689
+ consolidatedContent += `${this.formatToolResult(msg)}\n`;
690
+ });
691
+ } else {
692
+ // Multiple batches — group with labeled sub-headers
693
+ let batchIndex = 1;
694
+ for (const [, group] of turnGroups) {
695
+ consolidatedContent += `\n--- Batch ${batchIndex} of ${turnCount} ---\n`;
696
+ group.forEach(msg => {
697
+ consolidatedContent += `${this.formatToolResult(msg)}\n`;
698
+ });
699
+ batchIndex++;
700
+ }
701
+ }
702
+ }
703
+
704
+ // Add processing instructions only if needed
705
+ if (hasInterAgentMessages) {
706
+ consolidatedContent += '\nNote: Use the agentcommunication tool if you need to respond to other agents.';
707
+ }
708
+
709
+ // PHASE 2: Auto-create tasks for incoming messages
710
+ await this.autoCreateTasksForMessages(agentId, userMessages, interAgentMessages);
711
+
712
+ // Create single consolidated user message for AI processing
713
+ const consolidatedMessage = {
714
+ id: `consolidated-${Date.now()}`,
715
+ role: MESSAGE_ROLES.USER,
716
+ content: consolidatedContent.trim(),
717
+ timestamp: new Date().toISOString(),
718
+ type: 'consolidated-input',
719
+ originalMessageCount: allMessages.length
720
+ };
721
+
722
+ // Add to conversation history (don't broadcast - this is internal)
723
+ await this.addMessageToConversation(agentId, consolidatedMessage, false);
724
+
725
+ // CRITICAL: Update conversation tracking when inter-agent messages are processed
726
+ if (agent && interAgentMessages.length > 0) {
727
+ // Ensure interAgentTracking is a Map (defensive - may be plain object from JSON)
728
+ if (!agent.interAgentTracking || !(agent.interAgentTracking instanceof Map)) {
729
+ if (agent.interAgentTracking && typeof agent.interAgentTracking === 'object') {
730
+ agent.interAgentTracking = new Map(Object.entries(agent.interAgentTracking));
731
+ } else {
732
+ agent.interAgentTracking = new Map();
733
+ }
734
+ }
735
+
736
+ for (const msg of interAgentMessages) {
737
+ if (msg.sender) {
738
+ // Mark that this agent received a message from the sender
739
+ if (!agent.interAgentTracking.has(msg.sender)) {
740
+ agent.interAgentTracking.set(msg.sender, {
741
+ lastSent: null,
742
+ lastReceived: null,
743
+ lastType: null
744
+ });
745
+ }
746
+
747
+ const tracking = agent.interAgentTracking.get(msg.sender);
748
+ tracking.lastReceived = Date.now();
749
+ tracking.lastType = 'received';
750
+ }
751
+ }
752
+ // Persist updated tracking
753
+ await this.agentPool.persistAgentState(agentId);
754
+ }
755
+
756
+ // Clear all processed queues
757
+ queues.toolResults.length = 0;
758
+ queues.interAgentMessages.length = 0;
759
+ queues.userMessages.length = 0;
760
+
761
+ // Persist updated agent state
762
+ await this.agentPool.persistAgentState(agentId);
763
+
764
+ this.logger.debug(`Consolidated ${allMessages.length} queued messages for agent ${agentId}`);
765
+ return allMessages.length;
766
+ }
767
+
768
+ /**
769
+ * Add message to agent's conversation history with proper formatting
770
+ * @param {string} agentId - Agent ID
771
+ * @param {Object} message - Message to add
772
+ * @param {boolean} broadcast - Whether to broadcast message to UI (default true)
773
+ * @private
774
+ */
775
+ async addMessageToConversation(agentId, message, broadcast = true) {
776
+ const agent = await this.agentPool.getAgent(agentId);
777
+ if (!agent) return;
778
+
779
+ // Format message based on queue type
780
+ let formattedMessage;
781
+
782
+ switch (message.queueType) {
783
+ case 'toolResults': // Tool results
784
+ formattedMessage = {
785
+ ...message,
786
+ role: 'tool',
787
+ content: this.formatToolResult(message)
788
+ };
789
+ break;
790
+
791
+ case 'interAgentMessages': // Inter-agent messages
792
+ formattedMessage = {
793
+ ...message,
794
+ role: MESSAGE_ROLES.SYSTEM,
795
+ content: `Message from ${message.senderName || message.sender}: ${message.content}`
796
+ };
797
+ break;
798
+
799
+ case 'userMessages': // User messages
800
+ formattedMessage = {
801
+ ...message,
802
+ role: MESSAGE_ROLES.USER
803
+ };
804
+ break;
805
+
806
+ default:
807
+ formattedMessage = message;
808
+ }
809
+
810
+ // Add timestamp if not present
811
+ if (!formattedMessage.timestamp) {
812
+ formattedMessage.timestamp = new Date().toISOString();
813
+ }
814
+
815
+ // GUARD: Skip empty messages - they should never be added to history
816
+ const messageContent = formattedMessage.content;
817
+ if (!messageContent || (typeof messageContent === 'string' && !messageContent.trim())) {
818
+ this.logger.warn(`Skipping empty message for agent ${agentId}`, {
819
+ role: formattedMessage.role,
820
+ queueType: message.queueType,
821
+ hasContent: !!messageContent
822
+ });
823
+ return; // Don't add empty messages
824
+ }
825
+
826
+ // Add to conversation history
827
+ agent.conversations.full.messages.push(formattedMessage);
828
+ agent.conversations.full.lastUpdated = new Date().toISOString();
829
+
830
+ // Add to current model conversation if exists
831
+ if (agent.currentModel && agent.conversations[agent.currentModel]) {
832
+ agent.conversations[agent.currentModel].messages.push(formattedMessage);
833
+ agent.conversations[agent.currentModel].lastUpdated = new Date().toISOString();
834
+ }
835
+
836
+ // FIX: Only broadcast user-visible messages to UI (not internal system prompts)
837
+ if (broadcast && this.shouldBroadcastMessage(formattedMessage)) {
838
+ this.broadcastMessageUpdate(agentId, formattedMessage);
839
+ }
840
+ }
841
+
842
+ /**
843
+ * Process agent autonomously (for AGENT mode with no queued messages)
844
+ * @param {string} agentId - Agent ID
845
+ * @private
846
+ */
847
+ async processAgentAutonomously(agentId) {
848
+ // Auto-mark the highest priority pending task as in-progress
849
+ await this.autoProgressHighestPriorityTask(agentId);
850
+
851
+ // Get AI response without new messages
852
+ const aiResponse = await this.getAgentAIResponse(agentId);
853
+
854
+ if (!aiResponse) {
855
+ return false; // No response - activity service will determine if we should continue
856
+ }
857
+
858
+ // Process AI response and execute tools
859
+ await this.processAIResponse(agentId, aiResponse);
860
+
861
+ // Clear token limit retry tracker on successful AI response
862
+ this.clearTokenLimitRetryTracker(agentId);
863
+
864
+ return true; // We got a new AI response
865
+ }
866
+
867
+ /**
868
+ * Check if compaction is needed and perform it
869
+ * @param {string} agentId - Agent ID
870
+ * @param {string} targetModel - Target model for AI request
871
+ * @param {string} sessionId - Session ID for context
872
+ * @returns {Promise<Object>} Result with shouldContinue flag
873
+ * @private
874
+ */
875
+ async checkAndPerformCompaction(agentId, targetModel, sessionId) {
876
+ const ENABLE_COMPACT_DEBUG = process.env.COMPACT_DEBUG === 'true';
877
+
878
+ if (ENABLE_COMPACT_DEBUG) {
879
+ console.log('[COMPACT-CHECK-START]', {
880
+ agentId,
881
+ targetModel,
882
+ sessionId,
883
+ timestamp: new Date().toISOString()
884
+ });
885
+ }
886
+
887
+ try {
888
+ const agent = await this.agentPool.getAgent(agentId);
889
+ if (!agent) {
890
+ if (ENABLE_COMPACT_DEBUG) {
891
+ console.log('[COMPACT-ERROR]', { agentId, reason: 'Agent not found' });
892
+ }
893
+ return { shouldContinue: false, error: 'Agent not found' };
894
+ }
895
+
896
+ // For model switching, check the current model's conversation, not the target model's
897
+ // This handles scenarios where we're switching from a larger context to a smaller one
898
+ let modelToCheck = agent.currentModel && agent.currentModel !== targetModel
899
+ ? agent.currentModel
900
+ : targetModel;
901
+
902
+ // DEFENSIVE: If modelToCheck is undefined, try to find a valid conversation key
903
+ if (!modelToCheck) {
904
+ this.logger.warn(`Agent ${agentId} has no currentModel or targetModel set, attempting to use available conversation`, {
905
+ agentId,
906
+ currentModel: agent.currentModel,
907
+ targetModel,
908
+ preferredModel: agent.preferredModel,
909
+ availableConversations: Object.keys(agent.conversations || {})
910
+ });
911
+
912
+ // Notify user via WebSocket
913
+ this.broadcastCompactionEvent(agentId, sessionId, {
914
+ status: 'warning',
915
+ message: 'Agent model configuration issue detected - using fallback',
916
+ details: `Agent has no currentModel set. Using fallback model for compaction check.`,
917
+ agentName: agent.name
918
+ });
919
+
920
+ // Try preferredModel first
921
+ if (agent.preferredModel && agent.conversations[agent.preferredModel]) {
922
+ modelToCheck = agent.preferredModel;
923
+ this.logger.info(`Using preferredModel as fallback for compaction check: ${modelToCheck}`);
924
+ } else {
925
+ // Find any non-'full' conversation key
926
+ const conversationKeys = Object.keys(agent.conversations || {}).filter(key => key !== 'full');
927
+ if (conversationKeys.length > 0) {
928
+ modelToCheck = conversationKeys[0];
929
+ this.logger.warn(`Using first available conversation key for compaction check: ${modelToCheck}`);
930
+ } else {
931
+ this.logger.error(`No valid conversation found for agent ${agentId}, skipping compaction`);
932
+
933
+ // Notify user of critical error
934
+ this.broadcastCompactionEvent(agentId, sessionId, {
935
+ status: 'error',
936
+ message: 'No valid conversation found for compaction',
937
+ details: `Agent ${agent.name} has no valid conversation data. Compaction skipped.`,
938
+ agentName: agent.name
939
+ });
940
+
941
+ return { shouldContinue: true, error: 'No valid conversation found' };
942
+ }
943
+ }
944
+ }
945
+
946
+ // Get conversation metadata
947
+ let metadata = await this.agentPool.getCompactionMetadata(agentId, modelToCheck);
948
+
949
+ if (ENABLE_COMPACT_DEBUG) {
950
+ console.log('[COMPACT-METADATA]', {
951
+ agentId,
952
+ modelToCheck,
953
+ hasMetadata: !!metadata,
954
+ isCompacted: metadata?.isCompacted,
955
+ originalMessageCount: metadata?.originalMessages?.length || 0,
956
+ compactedMessageCount: metadata?.compactedMessages?.length || 0,
957
+ lastCompactization: metadata?.lastCompactization || 'never',
958
+ compactizationCount: metadata?.compactizationCount || 0,
959
+ originalTokenCount: metadata?.originalTokenCount || 0,
960
+ compactedTokenCount: metadata?.compactedTokenCount || 0
961
+ });
962
+ }
963
+
964
+ // If no conversation exists for this model yet, return early
965
+ if (!metadata || (!metadata.originalMessages && !metadata.compactedMessages)) {
966
+ this.logger.debug(`Compaction skipped: no conversation metadata for agent ${agentId}, model ${modelToCheck}`);
967
+ if (ENABLE_COMPACT_DEBUG) {
968
+ console.log('[COMPACT-SKIPPED]', { agentId, reason: 'No conversation metadata' });
969
+ }
970
+ return { shouldContinue: true }; // No conversation to compact
971
+ }
972
+
973
+ // CRITICAL: Sync pending messages BEFORE token counting.
974
+ // Without this, the token count uses stale compactedMessages that miss new messages
975
+ // (tool results, user inputs) added since the last getMessagesForAI call.
976
+ // This was causing severe underestimation (e.g., 50K estimated vs 212K actual).
977
+ if (metadata.isCompacted) {
978
+ const earlySyncResult = await this.agentPool.syncPendingMessages(agentId, modelToCheck);
979
+ if (earlySyncResult.synced > 0) {
980
+ this.logger.info(`Pre-check sync: ${earlySyncResult.synced} pending messages synced for accurate token count`, {
981
+ agentId,
982
+ modelToCheck
983
+ });
984
+ // Re-fetch metadata to get updated compactedMessages
985
+ const updatedMetadata = await this.agentPool.getCompactionMetadata(agentId, modelToCheck);
986
+ metadata = updatedMetadata;
987
+ }
988
+ }
989
+
990
+ // Determine which messages to use for token counting
991
+ let messages = metadata.isCompacted
992
+ ? metadata.compactedMessages
993
+ : metadata.originalMessages;
994
+
995
+ if (ENABLE_COMPACT_DEBUG) {
996
+ console.log('[COMPACT-MESSAGES-SELECTED]', {
997
+ agentId,
998
+ selectedArray: metadata.isCompacted ? 'compactedMessages' : 'originalMessages',
999
+ messageCount: messages?.length || 0,
1000
+ reason: metadata.isCompacted ? 'Compaction exists, using compacted version' : 'No compaction yet, using original'
1001
+ });
1002
+ }
1003
+
1004
+ // Check if any messages are oversized — if so, always allow compaction
1005
+ // because the splitting logic inside compactConversation will create enough messages
1006
+ const hasOversizedMessages = messages && messages.some(m => {
1007
+ const content = typeof m.content === 'string' ? m.content : '';
1008
+ return content.length > COMPACTION_CONFIG.OVERSIZED_MESSAGE_THRESHOLD;
1009
+ });
1010
+
1011
+ if (!hasOversizedMessages && (!messages || messages.length < COMPACTION_CONFIG.MIN_MESSAGES_FOR_COMPACTION)) {
1012
+ this.logger.debug(`Compaction skipped: too few messages (${messages?.length || 0}) for agent ${agentId}`);
1013
+ if (ENABLE_COMPACT_DEBUG) {
1014
+ console.log('[COMPACT-SKIPPED]', { agentId, reason: 'Too few messages', messageCount: messages?.length || 0, minRequired: COMPACTION_CONFIG.MIN_MESSAGES_FOR_COMPACTION });
1015
+ }
1016
+ return { shouldContinue: true }; // Conversation too short to compact
1017
+ }
1018
+
1019
+ // Count current tokens using AI response metadata (system prompt included)
1020
+ const currentTokens = this.tokenCountingService.getConversationTokenCount(
1021
+ messages,
1022
+ targetModel,
1023
+ agent.systemPrompt
1024
+ );
1025
+
1026
+ // Get model specifications
1027
+ const contextWindow = this.tokenCountingService.getModelContextWindow(targetModel);
1028
+ const maxOutputTokens = this.tokenCountingService.getModelMaxOutputTokens(targetModel);
1029
+
1030
+ if (ENABLE_COMPACT_DEBUG) {
1031
+ console.log('[COMPACT-TOKEN-COUNT]', {
1032
+ agentId,
1033
+ currentTokens,
1034
+ maxOutputTokens,
1035
+ contextWindow,
1036
+ model: targetModel,
1037
+ countingMode: 'response-data-based'
1038
+ });
1039
+ }
1040
+
1041
+ // Check if compaction is needed
1042
+ const threshold = agent.compactionThreshold || COMPACTION_CONFIG.DEFAULT_THRESHOLD;
1043
+ const shouldCompact = this.tokenCountingService.shouldTriggerCompaction(
1044
+ currentTokens,
1045
+ maxOutputTokens,
1046
+ contextWindow,
1047
+ threshold
1048
+ );
1049
+
1050
+ if (ENABLE_COMPACT_DEBUG) {
1051
+ const requiredTokens = currentTokens + maxOutputTokens;
1052
+ const thresholdTokens = threshold * contextWindow;
1053
+ console.log('[COMPACT-TRIGGER-CHECK]', {
1054
+ agentId,
1055
+ currentTokens,
1056
+ maxOutputTokens,
1057
+ requiredTokens,
1058
+ contextWindow,
1059
+ threshold,
1060
+ thresholdTokens,
1061
+ shouldCompact,
1062
+ formula: `${currentTokens} + ${maxOutputTokens} = ${requiredTokens} ${shouldCompact ? '>=' : '<'} ${thresholdTokens} (${threshold * 100}% of ${contextWindow})`,
1063
+ decision: shouldCompact ? 'TRIGGER COMPACTION' : 'SKIP - below threshold'
1064
+ });
1065
+ }
1066
+
1067
+ if (!shouldCompact) {
1068
+ if (ENABLE_COMPACT_DEBUG) {
1069
+ console.log('[COMPACT-SKIPPED]', { agentId, reason: 'Below threshold', utilizationPct: ((currentTokens + maxOutputTokens) / contextWindow * 100).toFixed(1) });
1070
+ }
1071
+ return { shouldContinue: true }; // No compaction needed
1072
+ }
1073
+
1074
+ this.logger.info(`Compaction triggered for agent ${agentId}`, {
1075
+ currentTokens,
1076
+ contextWindow,
1077
+ threshold: `${(threshold * 100).toFixed(0)}%`,
1078
+ utilization: `${((currentTokens + maxOutputTokens) / contextWindow * 100).toFixed(1)}%`,
1079
+ targetModel
1080
+ });
1081
+
1082
+ if (ENABLE_COMPACT_DEBUG) {
1083
+ console.log('[COMPACT-TRIGGERED]', {
1084
+ agentId,
1085
+ reason: 'Threshold exceeded',
1086
+ currentTokens,
1087
+ maxOutputTokens,
1088
+ requiredTokens: currentTokens + maxOutputTokens,
1089
+ contextWindow,
1090
+ threshold,
1091
+ utilizationPct: ((currentTokens + maxOutputTokens) / contextWindow * 100).toFixed(1)
1092
+ });
1093
+ }
1094
+
1095
+ // Mark compaction in progress
1096
+ this.compactionInProgress.set(agentId, COMPACTION_STATUS.STARTING);
1097
+
1098
+ // Broadcast compaction started event
1099
+ this.broadcastCompactionEvent(agentId, sessionId, {
1100
+ status: COMPACTION_STATUS.STARTING,
1101
+ currentTokens,
1102
+ targetTokens: this.tokenCountingService.calculateTargetTokenCount(contextWindow),
1103
+ contextWindow,
1104
+ model: targetModel
1105
+ });
1106
+
1107
+ // Always use summarization — multi-pass is handled inside compaction service
1108
+ const currentModel = agent.currentModel;
1109
+ const isModelSwitch = currentModel && currentModel !== targetModel;
1110
+
1111
+ // Update status to in-progress
1112
+ this.compactionInProgress.set(agentId, COMPACTION_STATUS.IN_PROGRESS);
1113
+ this.broadcastCompactionEvent(agentId, sessionId, {
1114
+ status: COMPACTION_STATUS.IN_PROGRESS,
1115
+ strategy: COMPACTION_STRATEGIES.SUMMARIZATION,
1116
+ messageCount: messages.length
1117
+ });
1118
+
1119
+ // Final sync before compaction: catch any messages that arrived AFTER the early pre-check
1120
+ // sync but BEFORE compaction starts (e.g., tool results completing during threshold decision).
1121
+ if (metadata.isCompacted) {
1122
+ const syncResult = await this.agentPool.syncPendingMessages(agentId, modelToCheck);
1123
+ if (syncResult.synced > 0) {
1124
+ this.logger.info(`Pre-compaction sync: ${syncResult.synced} additional messages synced`, {
1125
+ agentId,
1126
+ modelToCheck
1127
+ });
1128
+ const updatedMetadata = await this.agentPool.getCompactionMetadata(agentId, modelToCheck);
1129
+ messages = updatedMetadata.compactedMessages;
1130
+ }
1131
+ }
1132
+
1133
+ // Record message count BEFORE compaction starts.
1134
+ // This is the watermark: messages at indices < this count are considered "already compacted".
1135
+ // Messages added DURING compaction (e.g., user messages via WebSocket while the
1136
+ // summarization API call is in flight) will be at indices >= this count, and will be
1137
+ // detected as "new" by getMessagesForAI's sync logic after compaction completes.
1138
+ const preCompactionMessageCount = agent.conversations[targetModel]?.messages?.length || 0;
1139
+
1140
+ // Gather compacted conversations for model-switch scenarios
1141
+ const compactedConversations = isModelSwitch
1142
+ ? this._gatherCompactedConversations(agent)
1143
+ : null;
1144
+
1145
+ // Single call — multi-pass retry is now inside the compaction service
1146
+ const compactionResult = await this.compactionService.compactConversation(
1147
+ messages,
1148
+ currentModel || targetModel,
1149
+ targetModel,
1150
+ {
1151
+ targetTokenCount: this.tokenCountingService.calculateTargetTokenCount(contextWindow),
1152
+ sessionId,
1153
+ compactedConversations,
1154
+ onAllModelsExhausted: (errorInfo) => {
1155
+ this.broadcastCompactionEvent(agentId, sessionId, {
1156
+ type: 'compaction_models_exhausted',
1157
+ status: 'warning',
1158
+ message: errorInfo.message,
1159
+ modelsAttempted: errorInfo.models,
1160
+ error: errorInfo.error
1161
+ });
1162
+ }
1163
+ }
1164
+ );
1165
+
1166
+ // Update AgentPool with compacted messages — pass pre-compaction watermark
1167
+ await this.agentPool.updateCompactedMessages(agentId, targetModel, compactionResult, preCompactionMessageCount);
1168
+
1169
+ if (ENABLE_COMPACT_DEBUG) {
1170
+ console.log('[COMPACT-COMPLETED]', {
1171
+ agentId,
1172
+ strategy: compactionResult.strategy,
1173
+ originalMessageCount: compactionResult.originalMessages?.length || 0,
1174
+ compactedMessageCount: compactionResult.compactedMessages?.length || 0,
1175
+ originalTokens: compactionResult.originalTokenCount,
1176
+ compactedTokens: compactionResult.compactedTokenCount,
1177
+ reductionPercent: compactionResult.reductionPercent.toFixed(1),
1178
+ executionTimeMs: compactionResult.executionTime,
1179
+ model: targetModel
1180
+ });
1181
+ }
1182
+
1183
+ this.logger.info(`Compaction completed for agent ${agentId}`, {
1184
+ strategy: compactionResult.strategy,
1185
+ originalTokens: compactionResult.originalTokenCount,
1186
+ compactedTokens: compactionResult.compactedTokenCount,
1187
+ reduction: `${compactionResult.reductionPercent.toFixed(1)}%`,
1188
+ executionTime: `${compactionResult.executionTime}ms`
1189
+ });
1190
+
1191
+ // Update status to completed
1192
+ this.compactionInProgress.delete(agentId);
1193
+ this.broadcastCompactionEvent(agentId, sessionId, {
1194
+ status: COMPACTION_STATUS.COMPLETED,
1195
+ originalTokens: compactionResult.originalTokenCount,
1196
+ compactedTokens: compactionResult.compactedTokenCount,
1197
+ reductionPercent: compactionResult.reductionPercent,
1198
+ strategy: compactionResult.strategy,
1199
+ executionTime: compactionResult.executionTime
1200
+ });
1201
+
1202
+ return { shouldContinue: true, compactionPerformed: true };
1203
+
1204
+ } catch (error) {
1205
+ this.logger.error(`Compaction failed for agent ${agentId}`, {
1206
+ error: error.message,
1207
+ stack: error.stack
1208
+ });
1209
+
1210
+ // Update status to failed
1211
+ this.compactionInProgress.delete(agentId);
1212
+ this.broadcastCompactionEvent(agentId, sessionId, {
1213
+ status: COMPACTION_STATUS.FAILED,
1214
+ error: error.message
1215
+ });
1216
+
1217
+ // Don't block AI request on compaction failure
1218
+ return { shouldContinue: true, error: error.message };
1219
+ }
1220
+ }
1221
+
1222
+ /**
1223
+ * Gather compacted conversations from all model conversations for an agent.
1224
+ * Used during model switching to find the best existing conversation.
1225
+ * @param {Object} agent - Agent object with conversations
1226
+ * @returns {Map|null} Map of modelId → compactedMessages, or null
1227
+ * @private
1228
+ */
1229
+ _gatherCompactedConversations(agent) {
1230
+ if (!agent.conversations) return null;
1231
+
1232
+ const result = new Map();
1233
+ for (const [modelId, conv] of Object.entries(agent.conversations)) {
1234
+ if (modelId === 'full') continue;
1235
+ // Use compactizedMessages (correct field name) if available, otherwise messages
1236
+ const msgs = conv.compactizedMessages || conv.messages;
1237
+ if (Array.isArray(msgs) && msgs.length > 0) {
1238
+ result.set(modelId, msgs);
1239
+ }
1240
+ }
1241
+
1242
+ return result.size > 0 ? result : null;
1243
+ }
1244
+
1245
+ /**
1246
+ * Broadcast compaction event to UI
1247
+ * @param {string} agentId - Agent ID
1248
+ * @param {string} sessionId - Session ID
1249
+ * @param {Object} data - Event data
1250
+ * @private
1251
+ */
1252
+ broadcastCompactionEvent(agentId, sessionId, data) {
1253
+ if (!this.webSocketManager || !this.webSocketManager.broadcastToSession) {
1254
+ return;
1255
+ }
1256
+
1257
+ this.webSocketManager.broadcastToSession(sessionId, {
1258
+ type: 'compaction_event',
1259
+ data: {
1260
+ agentId,
1261
+ timestamp: new Date().toISOString(),
1262
+ ...data
1263
+ }
1264
+ });
1265
+ }
1266
+
1267
+ /**
1268
+ * Get AI response for agent with proper error handling
1269
+ * @param {string} agentId - Agent ID
1270
+ * @returns {Promise<Object|null>} AI response or null if failed
1271
+ * @private
1272
+ */
1273
+ async getAgentAIResponse(agentId) {
1274
+ try {
1275
+ const agent = await this.agentPool.getAgent(agentId);
1276
+ if (!agent) return null;
1277
+
1278
+ const conversationHistory = agent.conversations?.full?.messages || [];
1279
+
1280
+ // Get the session ID from the session map or agent's stored sessionId
1281
+ const sessionId = this.getAgentSession(agentId) || agent.sessionId;
1282
+
1283
+ if (!sessionId) {
1284
+ this.logger.error(`Agent ${agentId} has no session ID - API key resolution will fail`, {
1285
+ agentName: agent.name,
1286
+ agentSessionId: agent.sessionId,
1287
+ sessionMapHas: this.agentSessionMap.has(agentId)
1288
+ });
1289
+ // Return null to avoid making requests that will fail
1290
+ return null;
1291
+ }
1292
+
1293
+ // DYNAMIC ROUTING: Check if agent has dynamic model routing enabled
1294
+ let targetModel = agent.currentModel;
1295
+
1296
+ // DEFENSIVE: Ensure targetModel is set, fallback to preferredModel if currentModel is undefined
1297
+ if (!targetModel) {
1298
+ this.logger.warn(`Agent ${agentId} has no currentModel set, using preferredModel as fallback`, {
1299
+ agentId,
1300
+ preferredModel: agent.preferredModel,
1301
+ availableConversations: Object.keys(agent.conversations || {})
1302
+ });
1303
+
1304
+ // Notify user via WebSocket
1305
+ if (this.webSocketManager && sessionId) {
1306
+ this.webSocketManager.broadcastToSession(sessionId, {
1307
+ type: 'agent_warning',
1308
+ agentId,
1309
+ agentName: agent.name,
1310
+ message: 'Agent model configuration restored',
1311
+ details: `Agent "${agent.name}" had no currentModel set. Automatically restored to ${agent.preferredModel || 'default model'}.`,
1312
+ severity: 'warning',
1313
+ timestamp: new Date().toISOString()
1314
+ });
1315
+ }
1316
+
1317
+ targetModel = agent.preferredModel;
1318
+
1319
+ // Update agent's currentModel to preferredModel for future iterations
1320
+ if (targetModel) {
1321
+ agent.currentModel = targetModel;
1322
+ await this.agentPool.persistAgentState(agentId);
1323
+ this.logger.info(`Set agent currentModel to ${targetModel}`);
1324
+ } else {
1325
+ this.logger.error(`Agent ${agentId} has no preferredModel or currentModel, cannot continue`);
1326
+
1327
+ // Notify user of critical error
1328
+ if (this.webSocketManager && sessionId) {
1329
+ this.webSocketManager.broadcastToSession(sessionId, {
1330
+ type: 'agent_error',
1331
+ agentId,
1332
+ agentName: agent.name,
1333
+ message: 'Agent model configuration error',
1334
+ details: `Agent "${agent.name}" has no valid model configuration. Cannot process messages.`,
1335
+ severity: 'error',
1336
+ timestamp: new Date().toISOString()
1337
+ });
1338
+ }
1339
+
1340
+ return null;
1341
+ }
1342
+ }
1343
+
1344
+ if (agent.dynamicModelRouting && this.modelRouterService) {
1345
+ try {
1346
+ // Get the last user message for routing decision
1347
+ const lastUserMessage = [...conversationHistory].reverse().find(m => m.role === 'user');
1348
+
1349
+ if (lastUserMessage) {
1350
+ // Get available models from ModelsService
1351
+ let availableModels = [];
1352
+ if (this.modelsService) {
1353
+ try {
1354
+ this.logger.debug('ModelsService type check', {
1355
+ hasModelsService: !!this.modelsService,
1356
+ type: typeof this.modelsService,
1357
+ methods: Object.getOwnPropertyNames(Object.getPrototypeOf(this.modelsService))
1358
+ });
1359
+
1360
+ // Try to get models, if empty/stale, fetch with current sessionId
1361
+ let allModels = this.modelsService.getModels();
1362
+
1363
+ // Check if models need refresh (safer check)
1364
+ const needsRefresh = !this.modelsService.lastFetched ||
1365
+ (this.modelsService.lastFetched &&
1366
+ (Date.now() - new Date(this.modelsService.lastFetched).getTime()) > (5 * 60 * 1000));
1367
+
1368
+ if (!allModels || allModels.length === 0 || needsRefresh) {
1369
+ this.logger.info('Models list empty or stale, fetching from backend with sessionId');
1370
+ await this.modelsService.fetchModels({ sessionId });
1371
+ allModels = this.modelsService.getModels();
1372
+ }
1373
+
1374
+ // Filter to only include models that are available (not router models)
1375
+ availableModels = allModels
1376
+ .filter(model => model.id !== 'autopilot-model-router' && model.name !== 'autopilot-model-router');
1377
+
1378
+ this.logger.debug(`Available models for routing: ${availableModels.map(m => m.id || m.name).join(', ')}`);
1379
+ } catch (error) {
1380
+ this.logger.warn(`Failed to get available models for routing: ${error.message}`);
1381
+ }
1382
+ }
1383
+
1384
+ const routingResult = await this.modelRouterService.routeMessage(
1385
+ lastUserMessage.content,
1386
+ conversationHistory.slice(-5), // Last 5 messages for context
1387
+ agent.currentModel,
1388
+ availableModels, // Pass actual available models
1389
+ { agentId, sessionId, routingStrategy: agent.routingStrategy }
1390
+ );
1391
+
1392
+ this.logger.info('Routing result analysis', {
1393
+ selectedModel: routingResult.selectedModel,
1394
+ currentModel: agent.currentModel,
1395
+ areEqual: routingResult.selectedModel === agent.currentModel,
1396
+ willSwitch: routingResult.selectedModel && routingResult.selectedModel !== agent.currentModel
1397
+ });
1398
+
1399
+ if (routingResult.selectedModel && routingResult.selectedModel !== agent.currentModel) {
1400
+ this.logger.info(`Dynamic routing: switching from ${agent.currentModel} to ${routingResult.selectedModel}`, {
1401
+ agentId,
1402
+ reason: routingResult.reasoning
1403
+ });
1404
+
1405
+ targetModel = routingResult.selectedModel;
1406
+
1407
+ // Update agent's current model
1408
+ agent.currentModel = targetModel;
1409
+ await this.agentPool.persistAgentState(agentId);
1410
+
1411
+ this.logger.info(`Model updated: targetModel=${targetModel}, agent.currentModel=${agent.currentModel}`);
1412
+ } else {
1413
+ this.logger.info('No model switch needed', {
1414
+ selectedModel: routingResult.selectedModel,
1415
+ currentModel: agent.currentModel,
1416
+ hasSelectedModel: !!routingResult.selectedModel
1417
+ });
1418
+ }
1419
+ }
1420
+ } catch (routingError) {
1421
+ this.logger.warn(`Dynamic routing failed, using current model: ${routingError.message}`);
1422
+ // Fall back to current model on routing failure
1423
+ }
1424
+ }
1425
+
1426
+ this.logger.info(`About to send message to model: ${targetModel}`, {
1427
+ agentId,
1428
+ targetModel,
1429
+ originalModel: agent.currentModel
1430
+ });
1431
+
1432
+ // PHASE 4: Check and perform compaction if needed BEFORE sending to AI
1433
+ const compactionResult = await this.checkAndPerformCompaction(agentId, targetModel, sessionId);
1434
+
1435
+ if (!compactionResult.shouldContinue) {
1436
+ this.logger.warn(`Compaction check returned shouldContinue=false for agent ${agentId}`);
1437
+ return null;
1438
+ }
1439
+
1440
+ if (compactionResult.compactionPerformed) {
1441
+ this.logger.info(`Compaction performed for agent ${agentId}, proceeding with AI request`);
1442
+ }
1443
+
1444
+ // After compaction, retrieve messages from AgentPool (will use compacted if available)
1445
+ const messagesToSend = await this.agentPool.getMessagesForAI(agentId, targetModel);
1446
+
1447
+ // Inject TaskManager instructions for AGENT mode
1448
+ let enhancedSystemPrompt = agent.systemPrompt;
1449
+ if (agent.mode === AGENT_MODES.AGENT) {
1450
+ const taskManagerInstruction = "\n\nIMPORTANT: You are in AGENT mode. The use of TaskManager tool is mandatory. Always create and update tasks to track your work progress. Update the user about task-list status periodically. Use the jobdone tool when your main task is complete. While in agent mode: no thank-you's, no compliments, no rhetorical questions, no self-commentary - stay focused on executing tasks. Only ask questions through dedicated tools designed for user interaction (if available).";
1451
+ enhancedSystemPrompt = (agent.systemPrompt || '') + taskManagerInstruction;
1452
+
1453
+ // Note: Consecutive no-tool reminders are now sent as tool results (see _processAIResponse)
1454
+ }
1455
+
1456
+ // Inject dynamic file attachment context
1457
+ try {
1458
+ const fileAttachmentContext = await this.contextInjectionService.buildDynamicContext(agentId);
1459
+ if (fileAttachmentContext) {
1460
+ enhancedSystemPrompt = (enhancedSystemPrompt || '') + fileAttachmentContext;
1461
+ this.logger.debug(`Injected file attachment context for agent ${agentId}`, {
1462
+ contextLength: fileAttachmentContext.length
1463
+ });
1464
+ }
1465
+ } catch (error) {
1466
+ this.logger.warn(`Failed to inject file attachment context for agent ${agentId}`, {
1467
+ error: error.message
1468
+ });
1469
+ // Continue without file attachments if service fails
1470
+ }
1471
+
1472
+ // Inject system environment constraints (reserved ports, process safety)
1473
+ const systemConstraints = this.contextInjectionService.buildSystemConstraints();
1474
+ if (systemConstraints) {
1475
+ enhancedSystemPrompt = (enhancedSystemPrompt || '') + systemConstraints;
1476
+ }
1477
+
1478
+ // Inject flow execution context if this is part of a flow
1479
+ try {
1480
+ const lastUserMsg = [...conversationHistory].reverse().find(m => m.role === 'user');
1481
+ if (lastUserMsg?.isFlowExecution && lastUserMsg?.flowMetadata) {
1482
+ const flowContext = this.flowContextService.buildFlowAgentContext(
1483
+ lastUserMsg.flowMetadata,
1484
+ lastUserMsg.previousAgentData
1485
+ );
1486
+
1487
+ if (flowContext) {
1488
+ enhancedSystemPrompt = (enhancedSystemPrompt || '') + flowContext;
1489
+ this.logger.info(`Injected flow execution context for agent ${agentId}`, {
1490
+ flowName: lastUserMsg.flowMetadata.flowName,
1491
+ nodePosition: `${lastUserMsg.flowMetadata.nodePosition}/${lastUserMsg.flowMetadata.totalNodes}`,
1492
+ hasPreviousAgent: !!lastUserMsg.previousAgentData
1493
+ });
1494
+ }
1495
+ }
1496
+ } catch (error) {
1497
+ this.logger.warn(`Failed to inject flow execution context for agent ${agentId}`, {
1498
+ error: error.message
1499
+ });
1500
+ // Continue without flow context if service fails
1501
+ }
1502
+
1503
+ // Check if streaming is enabled - consider both agent config and user message preference
1504
+ // Get the last user message to check for streaming preference
1505
+ const lastUserMsg = [...conversationHistory].reverse().find(m => m.role === 'user');
1506
+ const userStreamingPref = lastUserMsg?.streamingEnabled;
1507
+ // Use user preference if explicitly set, otherwise use agent config
1508
+ const streamingEnabled = userStreamingPref !== undefined
1509
+ ? userStreamingPref !== false
1510
+ : agent.streamingEnabled !== false; // Default to true
1511
+
1512
+ if (streamingEnabled && this.aiService.sendMessageStream) {
1513
+ // Build flow context if this is part of a flow execution
1514
+ const flowContext = lastUserMsg?.isFlowExecution ? {
1515
+ flowRunId: lastUserMsg.flowRunId,
1516
+ flowNodeId: lastUserMsg.flowNodeId
1517
+ } : null;
1518
+
1519
+ // Use streaming response
1520
+ return await this._getStreamingResponse(
1521
+ agentId,
1522
+ targetModel,
1523
+ messagesToSend,
1524
+ enhancedSystemPrompt,
1525
+ sessionId,
1526
+ agent.platformProvided,
1527
+ flowContext
1528
+ );
1529
+ }
1530
+
1531
+ // Non-streaming fallback
1532
+ const response = await this.aiService.sendMessage(
1533
+ targetModel,
1534
+ messagesToSend,
1535
+ {
1536
+ agentId: agentId,
1537
+ systemPrompt: enhancedSystemPrompt,
1538
+ sessionId: sessionId,
1539
+ platformProvided: agent.platformProvided
1540
+ }
1541
+ );
1542
+
1543
+ return response;
1544
+
1545
+ } catch (error) {
1546
+ this.logger.error(`AI response failed for agent ${agentId}:`, error);
1547
+
1548
+ // Handle different types of AI service failures
1549
+ await this.handleAIServiceFailure(agentId, error);
1550
+
1551
+ return null;
1552
+ }
1553
+ }
1554
+
1555
+ /**
1556
+ * Get AI response using streaming with WebSocket broadcast
1557
+ * @param {string} agentId - Agent ID
1558
+ * @param {string} targetModel - Model to use
1559
+ * @param {Array} messagesToSend - Messages to send
1560
+ * @param {string} systemPrompt - System prompt
1561
+ * @param {string} sessionId - Session ID for WebSocket
1562
+ * @param {boolean} platformProvided - Whether using platform keys
1563
+ * @param {Object} flowContext - Optional flow execution context
1564
+ * @returns {Promise<Object>} Response object
1565
+ * @private
1566
+ */
1567
+ async _getStreamingResponse(agentId, targetModel, messagesToSend, systemPrompt, sessionId, platformProvided, flowContext = null) {
1568
+ // Generate a unique message ID for this streaming response
1569
+ const streamMessageId = `stream-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1570
+
1571
+ // Flow progress tracking
1572
+ let flowProgress = null;
1573
+ if (flowContext) {
1574
+ flowProgress = {
1575
+ charactersStreamed: 0,
1576
+ chunkCount: 0,
1577
+ lastBroadcast: Date.now(),
1578
+ flowRunId: flowContext.flowRunId,
1579
+ flowNodeId: flowContext.flowNodeId
1580
+ };
1581
+ }
1582
+
1583
+ // Broadcast stream start event
1584
+ this._broadcastStreamEvent(sessionId, {
1585
+ type: 'stream_start',
1586
+ agentId,
1587
+ messageId: streamMessageId,
1588
+ model: targetModel,
1589
+ timestamp: new Date().toISOString(),
1590
+ // Include flow context if present
1591
+ ...(flowContext && {
1592
+ flowRunId: flowContext.flowRunId,
1593
+ flowNodeId: flowContext.flowNodeId
1594
+ })
1595
+ });
1596
+
1597
+ try {
1598
+ const response = await this.aiService.sendMessageStream(
1599
+ targetModel,
1600
+ messagesToSend,
1601
+ {
1602
+ agentId: agentId,
1603
+ systemPrompt: systemPrompt,
1604
+ sessionId: sessionId,
1605
+ platformProvided: platformProvided,
1606
+ onChunk: (chunk) => {
1607
+ // Update flow progress if in flow execution
1608
+ if (flowProgress) {
1609
+ flowProgress.charactersStreamed += chunk.length;
1610
+ flowProgress.chunkCount++;
1611
+
1612
+ // Broadcast flow progress every 500ms or 50 chunks
1613
+ const now = Date.now();
1614
+ if (now - flowProgress.lastBroadcast > 500 || flowProgress.chunkCount % 50 === 0) {
1615
+ this._broadcastFlowProgress(sessionId, agentId, flowProgress);
1616
+ flowProgress.lastBroadcast = now;
1617
+ }
1618
+ }
1619
+
1620
+ // Broadcast each chunk to the UI
1621
+ this._broadcastStreamEvent(sessionId, {
1622
+ type: 'stream_chunk',
1623
+ agentId,
1624
+ messageId: streamMessageId,
1625
+ content: chunk,
1626
+ timestamp: new Date().toISOString(),
1627
+ // Include flow context if present
1628
+ ...(flowContext && {
1629
+ flowRunId: flowContext.flowRunId,
1630
+ flowNodeId: flowContext.flowNodeId
1631
+ })
1632
+ });
1633
+ },
1634
+ onDone: (result) => {
1635
+ // Final flow progress broadcast
1636
+ if (flowProgress) {
1637
+ this._broadcastFlowProgress(sessionId, agentId, flowProgress, true);
1638
+ }
1639
+
1640
+ // Broadcast stream completion
1641
+ this._broadcastStreamEvent(sessionId, {
1642
+ type: 'stream_complete',
1643
+ agentId,
1644
+ messageId: streamMessageId,
1645
+ content: result.content,
1646
+ usage: result.usage,
1647
+ model: result.model || targetModel,
1648
+ finishReason: result.finishReason,
1649
+ timestamp: new Date().toISOString(),
1650
+ // Include flow context if present
1651
+ ...(flowContext && {
1652
+ flowRunId: flowContext.flowRunId,
1653
+ flowNodeId: flowContext.flowNodeId
1654
+ })
1655
+ });
1656
+ },
1657
+ onError: (error) => {
1658
+ // Check if this is a model-related error that should show suggestions
1659
+ const isModelError = this.aiService?.isModelRelatedError?.(error);
1660
+ const modelSuggestions = isModelError && this.aiService?.getModelSuggestions?.(targetModel, error);
1661
+
1662
+ // Broadcast stream error with model suggestions if applicable
1663
+ this._broadcastStreamEvent(sessionId, {
1664
+ type: isModelError ? 'model_error' : 'stream_error',
1665
+ agentId,
1666
+ messageId: streamMessageId,
1667
+ error: error.message,
1668
+ model: targetModel,
1669
+ timestamp: new Date().toISOString(),
1670
+ // Include model suggestions for model-related errors
1671
+ ...(modelSuggestions && { modelSuggestions }),
1672
+ // Include flow context if present
1673
+ ...(flowContext && {
1674
+ flowRunId: flowContext.flowRunId,
1675
+ flowNodeId: flowContext.flowNodeId
1676
+ })
1677
+ });
1678
+ }
1679
+ }
1680
+ );
1681
+
1682
+ this.logger.info(`Streaming response completed for agent ${agentId}`, {
1683
+ contentLength: response.content?.length || 0,
1684
+ model: response.model,
1685
+ ...(flowProgress && { flowCharsStreamed: flowProgress.charactersStreamed })
1686
+ });
1687
+
1688
+ return response;
1689
+
1690
+ } catch (error) {
1691
+ // Check if this is a model-related error that should show suggestions
1692
+ const isModelError = this.aiService?.isModelRelatedError?.(error);
1693
+ const modelSuggestions = isModelError && this.aiService?.getModelSuggestions?.(targetModel, error);
1694
+
1695
+ // Broadcast error event with model suggestions if applicable
1696
+ this._broadcastStreamEvent(sessionId, {
1697
+ type: isModelError ? 'model_error' : 'stream_error',
1698
+ agentId,
1699
+ messageId: streamMessageId,
1700
+ error: error.message,
1701
+ model: targetModel,
1702
+ timestamp: new Date().toISOString(),
1703
+ // Include model suggestions for model-related errors
1704
+ ...(modelSuggestions && { modelSuggestions }),
1705
+ // Include flow context if present
1706
+ ...(flowContext && {
1707
+ flowRunId: flowContext.flowRunId,
1708
+ flowNodeId: flowContext.flowNodeId
1709
+ })
1710
+ });
1711
+
1712
+ throw error;
1713
+ }
1714
+ }
1715
+
1716
+ /**
1717
+ * Broadcast flow node progress event
1718
+ * @param {string} sessionId - Session ID
1719
+ * @param {string} agentId - Agent ID
1720
+ * @param {Object} progress - Progress data
1721
+ * @param {boolean} isFinal - Whether this is the final progress update
1722
+ * @private
1723
+ */
1724
+ _broadcastFlowProgress(sessionId, agentId, progress, isFinal = false) {
1725
+ if (!this.webSocketManager) return;
1726
+
1727
+ try {
1728
+ this.webSocketManager.broadcastToSession(sessionId, {
1729
+ type: 'flow_update',
1730
+ data: {
1731
+ type: 'flow_node_progress',
1732
+ runId: progress.flowRunId,
1733
+ nodeId: progress.flowNodeId,
1734
+ agentId,
1735
+ charactersStreamed: progress.charactersStreamed,
1736
+ chunkCount: progress.chunkCount,
1737
+ isFinal,
1738
+ timestamp: new Date().toISOString()
1739
+ },
1740
+ timestamp: new Date().toISOString()
1741
+ });
1742
+ } catch (error) {
1743
+ this.logger.warn('Failed to broadcast flow progress', { error: error.message });
1744
+ }
1745
+ }
1746
+
1747
+ /**
1748
+ * Broadcast streaming event via WebSocket
1749
+ * @param {string} sessionId - Session ID
1750
+ * @param {Object} eventData - Event data to broadcast
1751
+ * @private
1752
+ */
1753
+ _broadcastStreamEvent(sessionId, eventData) {
1754
+ if (this.webSocketManager && this.webSocketManager.broadcastToSession) {
1755
+ this.webSocketManager.broadcastToSession(sessionId, {
1756
+ type: eventData.type,
1757
+ data: eventData
1758
+ });
1759
+ }
1760
+ }
1761
+
1762
+ /**
1763
+ * Handle AI service failures with appropriate recovery strategies
1764
+ * @param {string} agentId - Agent ID that failed
1765
+ * @param {Error} error - The error that occurred
1766
+ * @private
1767
+ */
1768
+ async handleAIServiceFailure(agentId, error) {
1769
+ const agent = await this.agentPool.getAgent(agentId);
1770
+ if (!agent) return;
1771
+
1772
+ const sessionId = this.getAgentSession(agentId) || agent.sessionId || 'scheduler-session';
1773
+
1774
+ // PRIORITY: Handle timeout errors that should return to chat mode
1775
+ if (error.shouldReturnToChat || error.isTimeout) {
1776
+ this.logger.warn(`Agent ${agentId} returning to chat mode due to timeout`, {
1777
+ errorMessage: error.message,
1778
+ status: error.status
1779
+ });
1780
+
1781
+ // Switch agent to CHAT mode - no delay, user can retry when ready
1782
+ agent.mode = AGENT_MODES.CHAT;
1783
+ await this.agentPool.persistAgentState(agentId);
1784
+
1785
+ // Broadcast timeout notification to UI (toast + console log only, no chat message)
1786
+ if (this.webSocketManager && this.webSocketManager.broadcastToSession) {
1787
+ this.webSocketManager.broadcastToSession(sessionId, {
1788
+ type: 'agent_timeout',
1789
+ data: {
1790
+ agentId: agentId,
1791
+ agentName: agent.name,
1792
+ action: 'returned_to_chat',
1793
+ timestamp: new Date().toISOString()
1794
+ }
1795
+ });
1796
+ }
1797
+
1798
+ return; // Don't proceed with other error handling
1799
+ }
1800
+
1801
+ // Determine failure type and response
1802
+ const errorMessage = error.message?.toLowerCase() || '';
1803
+
1804
+ if (errorMessage.includes('api key') || errorMessage.includes('authentication')) {
1805
+ // API key issues - pause agent to prevent infinite retries
1806
+ this.logger.warn(`Agent ${agentId} paused due to API key issue`);
1807
+
1808
+ agent.delayEndTime = new Date(Date.now() + SCHEDULER_CONFIG.API_KEY_ERROR_DELAY_MS).toISOString();
1809
+ await this.agentPool.persistAgentState(agentId);
1810
+
1811
+ // Add error message to agent's queue
1812
+ await this.agentPool.addToolResult(agentId, {
1813
+ toolId: 'system-error',
1814
+ status: 'failed',
1815
+ error: 'API key authentication failed. Please check your API key configuration in Settings.',
1816
+ timestamp: new Date().toISOString()
1817
+ });
1818
+
1819
+ } else if (errorMessage.includes('rate limit') || errorMessage.includes('too many requests')) {
1820
+ // Rate limit - delay agent
1821
+ this.logger.warn(`Agent ${agentId} delayed due to rate limiting`);
1822
+
1823
+ agent.delayEndTime = new Date(Date.now() + SCHEDULER_CONFIG.RATE_LIMIT_DELAY_MS).toISOString();
1824
+ await this.agentPool.persistAgentState(agentId);
1825
+
1826
+ } else if (errorMessage.includes('network') || errorMessage.includes('connection')) {
1827
+ // Network issues (non-timeout) - shorter delay and retry
1828
+ this.logger.warn(`Agent ${agentId} delayed due to network issues`);
1829
+
1830
+ agent.delayEndTime = new Date(Date.now() + SCHEDULER_CONFIG.NETWORK_ERROR_DELAY_MS).toISOString();
1831
+ await this.agentPool.persistAgentState(agentId);
1832
+
1833
+ } else if (this.isTokenLimitError(errorMessage)) {
1834
+ // Token/context limit error - trigger emergency compaction and retry
1835
+ await this.handleTokenLimitError(agentId, agent, error);
1836
+ return; // Don't add error message or broadcast - will retry after compaction
1837
+
1838
+ } else {
1839
+ // Unknown error - pause agent and notify
1840
+ this.logger.error(`Agent ${agentId} paused due to unknown AI service error: ${error.message}`);
1841
+
1842
+ agent.delayEndTime = new Date(Date.now() + SCHEDULER_CONFIG.UNKNOWN_ERROR_DELAY_MS).toISOString();
1843
+ await this.agentPool.persistAgentState(agentId);
1844
+
1845
+ // Add error message to agent's queue
1846
+ await this.agentPool.addToolResult(agentId, {
1847
+ toolId: 'system-error',
1848
+ status: 'failed',
1849
+ error: `AI service error: ${error.message}. Agent temporarily paused.`,
1850
+ timestamp: new Date().toISOString()
1851
+ });
1852
+ }
1853
+
1854
+ // Broadcast error to UI
1855
+ if (this.webSocketManager && this.webSocketManager.broadcastToSession) {
1856
+ const sessionId = this.getAgentSession(agentId) || agent.sessionId || 'scheduler-session';
1857
+
1858
+ // FIX: Wrap payload in 'data' field to match UI expectations
1859
+ this.webSocketManager.broadcastToSession(sessionId, {
1860
+ type: 'agent_error',
1861
+ data: {
1862
+ agentId: agentId,
1863
+ error: error.message,
1864
+ recovery: 'Agent temporarily paused for recovery',
1865
+ timestamp: new Date().toISOString()
1866
+ }
1867
+ });
1868
+ }
1869
+ }
1870
+
1871
+ /**
1872
+ * Check if error message indicates a token/context limit error
1873
+ * @param {string} errorMessage - Lowercased error message
1874
+ * @returns {boolean}
1875
+ * @private
1876
+ */
1877
+ isTokenLimitError(errorMessage) {
1878
+ const tokenLimitPatterns = [
1879
+ 'prompt is too long',
1880
+ 'tokens',
1881
+ 'context length',
1882
+ 'context window',
1883
+ 'maximum context',
1884
+ 'token limit',
1885
+ 'max_tokens',
1886
+ 'context_length_exceeded',
1887
+ 'maximum.*exceeded'
1888
+ ];
1889
+
1890
+ return tokenLimitPatterns.some(pattern => {
1891
+ if (pattern.includes('.*')) {
1892
+ return new RegExp(pattern).test(errorMessage);
1893
+ }
1894
+ return errorMessage.includes(pattern);
1895
+ });
1896
+ }
1897
+
1898
+ /**
1899
+ * Handle token limit errors with emergency compaction and retry
1900
+ * @param {string} agentId - Agent ID
1901
+ * @param {Object} agent - Agent object
1902
+ * @param {Error} error - The token limit error
1903
+ * @private
1904
+ */
1905
+ async handleTokenLimitError(agentId, agent, error) {
1906
+ const sessionId = this.getAgentSession(agentId) || agent.sessionId || 'scheduler-session';
1907
+
1908
+ // Get or initialize retry tracker for this agent
1909
+ let tracker = this.tokenLimitRetryTracker.get(agentId);
1910
+ if (!tracker) {
1911
+ tracker = { attempts: 0, lastError: '', timestamp: new Date() };
1912
+ this.tokenLimitRetryTracker.set(agentId, tracker);
1913
+ }
1914
+
1915
+ // Increment retry count
1916
+ tracker.attempts++;
1917
+ tracker.lastError = error.message;
1918
+ tracker.timestamp = new Date();
1919
+
1920
+ this.logger.warn(`Token limit error for agent ${agentId} (attempt ${tracker.attempts}/${this.MAX_TOKEN_LIMIT_RETRIES})`, {
1921
+ error: error.message,
1922
+ agentName: agent.name
1923
+ });
1924
+
1925
+ // Check if we've exceeded max retries
1926
+ if (tracker.attempts > this.MAX_TOKEN_LIMIT_RETRIES) {
1927
+ this.logger.error(`Agent ${agentId} exceeded max token limit retries, pausing agent`, {
1928
+ attempts: tracker.attempts,
1929
+ lastError: error.message
1930
+ });
1931
+
1932
+ // Clear the retry tracker
1933
+ this.tokenLimitRetryTracker.delete(agentId);
1934
+
1935
+ // Now show the error to the user
1936
+ agent.delayEndTime = new Date(Date.now() + SCHEDULER_CONFIG.UNKNOWN_ERROR_DELAY_MS).toISOString();
1937
+ await this.agentPool.persistAgentState(agentId);
1938
+
1939
+ // Broadcast error to UI
1940
+ if (this.webSocketManager && this.webSocketManager.broadcastToSession) {
1941
+ this.webSocketManager.broadcastToSession(sessionId, {
1942
+ type: 'agent_error',
1943
+ data: {
1944
+ agentId: agentId,
1945
+ error: `Context limit exceeded after ${this.MAX_TOKEN_LIMIT_RETRIES} compaction attempts. The conversation may be too large. Consider starting a new conversation.`,
1946
+ recovery: 'Agent temporarily paused',
1947
+ timestamp: new Date().toISOString()
1948
+ }
1949
+ });
1950
+ }
1951
+
1952
+ // Add a user-friendly error message to the queue
1953
+ await this.agentPool.addToolResult(agentId, {
1954
+ toolId: 'system-error',
1955
+ status: 'failed',
1956
+ error: `Context limit exceeded after automatic compaction. The conversation is too large for the current model. Please consider clearing the conversation or switching to a model with larger context window.`,
1957
+ timestamp: new Date().toISOString()
1958
+ });
1959
+
1960
+ return;
1961
+ }
1962
+
1963
+ // Determine compaction strategy based on attempt number
1964
+ // Attempt 1: Regular compaction (multi-pass summarization)
1965
+ // Attempt 2: Emergency compaction (summarization with lower min messages)
1966
+ const useEmergencyCompaction = tracker.attempts >= 2;
1967
+ const strategyName = useEmergencyCompaction ? 'emergency' : 'regular';
1968
+
1969
+ this.logger.info(`Triggering ${strategyName} compaction for agent ${agentId} due to token limit error`, {
1970
+ attempt: tracker.attempts,
1971
+ maxAttempts: this.MAX_TOKEN_LIMIT_RETRIES,
1972
+ strategy: strategyName
1973
+ });
1974
+
1975
+ // Notify UI that compaction is happening
1976
+ this.broadcastCompactionEvent(agentId, sessionId, {
1977
+ status: COMPACTION_STATUS.STARTING,
1978
+ message: useEmergencyCompaction
1979
+ ? 'Emergency compaction triggered'
1980
+ : 'Compaction triggered due to context limit',
1981
+ emergency: useEmergencyCompaction,
1982
+ retryAttempt: tracker.attempts,
1983
+ timestamp: new Date().toISOString()
1984
+ });
1985
+
1986
+ try {
1987
+ const targetModel = agent.currentModel || agent.preferredModel;
1988
+ let compactionResult;
1989
+
1990
+ if (useEmergencyCompaction) {
1991
+ // Second attempt: emergency compaction with lower minimum messages
1992
+ compactionResult = await this.performEmergencyCompaction(agentId, targetModel, sessionId);
1993
+ } else {
1994
+ // First attempt: regular multi-pass compaction
1995
+ compactionResult = await this.checkAndPerformCompaction(agentId, targetModel, sessionId);
1996
+ // Convert the result format to match emergency compaction output
1997
+ if (compactionResult.compactionPerformed) {
1998
+ compactionResult = {
1999
+ success: true,
2000
+ ...compactionResult
2001
+ };
2002
+ } else if (compactionResult.shouldContinue) {
2003
+ compactionResult = { success: true, skipped: true };
2004
+ } else {
2005
+ compactionResult = { success: false, error: compactionResult.error };
2006
+ }
2007
+ }
2008
+
2009
+ if (compactionResult.success) {
2010
+ if (compactionResult.skipped) {
2011
+ this.logger.info(`Regular compaction skipped for agent ${agentId} (not needed or too few messages)`);
2012
+ } else {
2013
+ this.logger.info(`${strategyName} compaction successful for agent ${agentId}`, {
2014
+ reductionPercent: compactionResult.reductionPercent,
2015
+ originalTokens: compactionResult.originalTokenCount,
2016
+ compactedTokens: compactionResult.compactedTokenCount
2017
+ });
2018
+ }
2019
+
2020
+ // Broadcast successful compaction
2021
+ this.broadcastCompactionEvent(agentId, sessionId, {
2022
+ status: COMPACTION_STATUS.COMPLETED,
2023
+ originalTokens: compactionResult.originalTokenCount,
2024
+ compactedTokens: compactionResult.compactedTokenCount,
2025
+ reductionPercent: compactionResult.reductionPercent,
2026
+ strategy: useEmergencyCompaction ? 'emergency_aggressive' : 'regular',
2027
+ emergency: useEmergencyCompaction,
2028
+ message: compactionResult.skipped
2029
+ ? 'Compaction check complete'
2030
+ : `Compaction complete. Reduced by ${compactionResult.reductionPercent?.toFixed(1) || 0}%`,
2031
+ timestamp: new Date().toISOString()
2032
+ });
2033
+
2034
+ // Don't add any delay - the scheduler will naturally retry on next cycle
2035
+ this.logger.info(`Agent ${agentId} ready for retry after ${strategyName} compaction`);
2036
+
2037
+ } else {
2038
+ this.logger.error(`${strategyName} compaction failed for agent ${agentId}`, {
2039
+ error: compactionResult.error
2040
+ });
2041
+
2042
+ // Broadcast failed compaction
2043
+ this.broadcastCompactionEvent(agentId, sessionId, {
2044
+ status: COMPACTION_STATUS.FAILED,
2045
+ error: compactionResult.error || `${strategyName} compaction failed`,
2046
+ emergency: useEmergencyCompaction,
2047
+ timestamp: new Date().toISOString()
2048
+ });
2049
+
2050
+ // Will retry on next attempt until max retries reached
2051
+ }
2052
+
2053
+ } catch (compactionError) {
2054
+ this.logger.error(`${strategyName} compaction threw error for agent ${agentId}`, {
2055
+ error: compactionError.message
2056
+ });
2057
+
2058
+ this.broadcastCompactionEvent(agentId, sessionId, {
2059
+ status: COMPACTION_STATUS.FAILED,
2060
+ error: compactionError.message,
2061
+ emergency: useEmergencyCompaction,
2062
+ timestamp: new Date().toISOString()
2063
+ });
2064
+ }
2065
+ }
2066
+
2067
+ /**
2068
+ * Perform emergency compaction with more aggressive settings
2069
+ * @param {string} agentId - Agent ID
2070
+ * @param {string} targetModel - Target model
2071
+ * @param {string} sessionId - Session ID
2072
+ * @returns {Promise<Object>} Compaction result
2073
+ * @private
2074
+ */
2075
+ async performEmergencyCompaction(agentId, targetModel, sessionId) {
2076
+ try {
2077
+ const agent = await this.agentPool.getAgent(agentId);
2078
+ if (!agent) {
2079
+ return { success: false, error: 'Agent not found' };
2080
+ }
2081
+
2082
+ // Get the current conversation
2083
+ const modelConversation = agent.conversations[targetModel];
2084
+ if (!modelConversation) {
2085
+ return { success: false, error: 'No conversation found for model' };
2086
+ }
2087
+
2088
+ // CRITICAL: Sync pending messages before reading compactizedMessages.
2089
+ // The scheduler's addMessageToConversation only pushes to conversation.messages.
2090
+ if (modelConversation.compactizedMessages) {
2091
+ const originalLength = modelConversation.messages.length;
2092
+ const originalCount = modelConversation.originalMessageCountAtCompaction || originalLength;
2093
+ if (originalLength > originalCount) {
2094
+ const newCount = originalLength - originalCount;
2095
+ const newMessages = modelConversation.messages.slice(-newCount);
2096
+ modelConversation.compactizedMessages.push(...newMessages);
2097
+ modelConversation.originalMessageCountAtCompaction = originalLength;
2098
+ this.logger.info(`Emergency compaction: pre-synced ${newCount} pending messages`, { agentId });
2099
+ }
2100
+ }
2101
+
2102
+ // Get the messages to compact — use compactizedMessages (correct field name)
2103
+ const messages = modelConversation.compactizedMessages || modelConversation.messages;
2104
+
2105
+ // Allow compaction even with few messages if any are oversized
2106
+ // (splitting inside compactConversation will create enough messages)
2107
+ const hasOversized = messages && messages.some(m => {
2108
+ const content = typeof m.content === 'string' ? m.content : '';
2109
+ return content.length > COMPACTION_CONFIG.OVERSIZED_MESSAGE_THRESHOLD;
2110
+ });
2111
+
2112
+ if (!hasOversized && (!messages || messages.length < 5)) {
2113
+ return { success: false, error: 'Not enough messages to compact' };
2114
+ }
2115
+
2116
+ // Record watermark BEFORE compaction starts
2117
+ const preCompactionMessageCount = modelConversation.messages.length;
2118
+
2119
+ const contextWindow = this.tokenCountingService.getModelContextWindow(targetModel);
2120
+
2121
+ // Use aggressive settings - aim for 50% of context window instead of normal threshold
2122
+ const targetTokens = Math.floor(contextWindow * 0.5);
2123
+
2124
+ this.logger.info(`Emergency compaction: targeting ${targetTokens} tokens (50% of ${contextWindow})`, {
2125
+ agentId,
2126
+ messageCount: messages.length,
2127
+ targetModel
2128
+ });
2129
+
2130
+ // Call compaction service with summarization (multi-pass handles reduction)
2131
+ const compactionResult = await this.compactionService.compactConversation(
2132
+ messages,
2133
+ targetModel,
2134
+ targetModel,
2135
+ {
2136
+ sessionId,
2137
+ agentId,
2138
+ emergency: true,
2139
+ onAllModelsExhausted: (errorInfo) => {
2140
+ this.broadcastCompactionEvent(agentId, sessionId, {
2141
+ type: 'compaction_models_exhausted',
2142
+ status: 'warning',
2143
+ message: errorInfo.message,
2144
+ modelsAttempted: errorInfo.models,
2145
+ error: errorInfo.error
2146
+ });
2147
+ }
2148
+ }
2149
+ );
2150
+
2151
+ // Compaction service returns compactedMessages directly (no success flag)
2152
+ // Check for compactedMessages array with length > 0 and not skipped
2153
+ const compactionSucceeded = compactionResult.compactedMessages?.length > 0 && !compactionResult.skipped;
2154
+
2155
+ if (compactionSucceeded) {
2156
+ // Update the agent's conversation — use correct field name: compactizedMessages
2157
+ modelConversation.compactizedMessages = compactionResult.compactedMessages;
2158
+ modelConversation.originalMessageCountAtCompaction = preCompactionMessageCount;
2159
+ modelConversation.lastCompactization = new Date().toISOString();
2160
+ modelConversation.compactizationCount = (modelConversation.compactizationCount || 0) + 1;
2161
+ modelConversation.compactedTokenCount = compactionResult.compactedTokenCount;
2162
+ modelConversation.originalTokenCount = compactionResult.originalTokenCount;
2163
+
2164
+ // Persist the changes
2165
+ await this.agentPool.persistAgentState(agentId);
2166
+
2167
+ return {
2168
+ success: true,
2169
+ originalTokenCount: compactionResult.originalTokenCount,
2170
+ compactedTokenCount: compactionResult.compactedTokenCount,
2171
+ reductionPercent: compactionResult.reductionPercent
2172
+ };
2173
+ }
2174
+
2175
+ return { success: false, error: compactionResult.error || 'Compaction returned no messages' };
2176
+
2177
+ } catch (error) {
2178
+ this.logger.error(`Emergency compaction error for agent ${agentId}`, {
2179
+ error: error.message,
2180
+ stack: error.stack
2181
+ });
2182
+ return { success: false, error: error.message };
2183
+ }
2184
+ }
2185
+
2186
+ /**
2187
+ * Clear token limit retry tracker for an agent (call after successful AI response)
2188
+ * @param {string} agentId - Agent ID
2189
+ * @private
2190
+ */
2191
+ clearTokenLimitRetryTracker(agentId) {
2192
+ if (this.tokenLimitRetryTracker.has(agentId)) {
2193
+ this.tokenLimitRetryTracker.delete(agentId);
2194
+ this.logger.debug(`Cleared token limit retry tracker for agent ${agentId}`);
2195
+ }
2196
+ }
2197
+
2198
+ /**
2199
+ * Process AI response and execute any tools
2200
+ * @param {string} agentId - Agent ID
2201
+ * @param {Object} aiResponse - AI service response
2202
+ * @private
2203
+ */
2204
+ async processAIResponse(agentId, aiResponse) {
2205
+ // Get the session ID from the session map
2206
+ const agent = await this.agentPool.getAgent(agentId);
2207
+ const sessionId = this.getAgentSession(agentId) || agent?.sessionId || 'scheduler-session';
2208
+
2209
+ // Safety check: agent must exist
2210
+ if (!agent) {
2211
+ this.logger.warn(`Cannot process AI response - agent ${agentId} not found`);
2212
+ return;
2213
+ }
2214
+
2215
+ // Check if response contains tool calls
2216
+ const hasTools = this._hasToolCalls(aiResponse.content);
2217
+
2218
+ // Track consecutive messages without tools (AGENT mode only)
2219
+ if (agent && agent.mode === AGENT_MODES.AGENT && SCHEDULER_CONFIG.CONSECUTIVE_NO_TOOL_ENABLED) {
2220
+ if (!hasTools) {
2221
+ // Increment consecutive no-tool counter
2222
+ const currentCount = this.consecutiveNoToolMessages.get(agentId) || 0;
2223
+ const newCount = currentCount + 1;
2224
+ this.consecutiveNoToolMessages.set(agentId, newCount);
2225
+ this.logger.warn(`[NO-TOOL-TRACKER] Agent ${agentId}: ${newCount} consecutive messages without tools (threshold: ${SCHEDULER_CONFIG.CONSECUTIVE_NO_TOOL_THRESHOLD})`);
2226
+
2227
+ // If threshold exceeded, queue a tool result reminder (more noticeable than system prompt)
2228
+ if (newCount >= SCHEDULER_CONFIG.CONSECUTIVE_NO_TOOL_THRESHOLD) {
2229
+ const toolResultReminder = {
2230
+ id: `no-tool-reminder-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
2231
+ toolId: 'system_reminder',
2232
+ status: 'warning',
2233
+ result: `[NO-TOOL WARNING] You have sent ${newCount} consecutive messages without using any tools. You MUST now either: (1) Update your task list using the TaskManager tool, or (2) Call the jobdone tool if all tasks are complete. Do not respond with text only - take action with a tool.`,
2234
+ timestamp: new Date().toISOString(),
2235
+ queuedAt: new Date().toISOString(),
2236
+ isSystemGenerated: true
2237
+ };
2238
+
2239
+ // Queue as tool result for next processing cycle
2240
+ agent.messageQueues.toolResults.push(toolResultReminder);
2241
+
2242
+ this.logger.warn(`[NO-TOOL-TRACKER] Agent ${agentId}: *** QUEUED NO-TOOL REMINDER AS TOOL RESULT ***`);
2243
+ }
2244
+ } else {
2245
+ this.logger.warn(`[NO-TOOL-TRACKER] Agent ${agentId}: Response HAS tools, counter will reset in _executeToolsAsync`);
2246
+ }
2247
+ // Note: Counter is reset in _executeToolsAsync when tools ARE executed
2248
+ }
2249
+
2250
+ // Normalize token usage field names (backend may send input_tokens/output_tokens
2251
+ // instead of prompt_tokens/completion_tokens depending on provider)
2252
+ let normalizedTokenUsage = null;
2253
+ if (aiResponse.tokenUsage) {
2254
+ normalizedTokenUsage = {
2255
+ prompt_tokens: aiResponse.tokenUsage.prompt_tokens || aiResponse.tokenUsage.input_tokens || 0,
2256
+ completion_tokens: aiResponse.tokenUsage.completion_tokens || aiResponse.tokenUsage.output_tokens || 0,
2257
+ total_tokens: aiResponse.tokenUsage.total_tokens || 0
2258
+ };
2259
+ }
2260
+
2261
+ // Create response message
2262
+ const responseMessage = {
2263
+ id: `ai-response-${Date.now()}`,
2264
+ agentId: agentId,
2265
+ role: MESSAGE_ROLES.ASSISTANT,
2266
+ content: aiResponse.content,
2267
+ timestamp: new Date().toISOString(),
2268
+ model: aiResponse.model,
2269
+ tokenUsage: normalizedTokenUsage,
2270
+ sessionId: sessionId,
2271
+ // Mark if tools will be executed (UI can show loading indicator)
2272
+ pendingToolExecution: hasTools
2273
+ };
2274
+
2275
+ await this.addMessageToConversation(agentId, responseMessage, false);
2276
+
2277
+ // IMMEDIATELY broadcast the AI response to UI (don't wait for tool execution)
2278
+ if (this.shouldBroadcastMessage(responseMessage)) {
2279
+ const updatedAgent = await this.agentPool.getAgent(agentId);
2280
+ this.broadcastMessageUpdate(agentId, responseMessage, {
2281
+ agentCurrentModel: updatedAgent?.currentModel
2282
+ });
2283
+ }
2284
+
2285
+ // Execute tools ASYNCHRONOUSLY - don't block the response
2286
+ this._executeToolsAsync(agentId, aiResponse.content, sessionId, responseMessage.id);
2287
+ }
2288
+
2289
+ /**
2290
+ * Check if AI response contains tool calls
2291
+ * @param {string} content - AI response content
2292
+ * @returns {boolean} Whether content has tool calls
2293
+ * @private
2294
+ */
2295
+ _hasToolCalls(content) {
2296
+ if (!content) return false;
2297
+ // Check for JSON code block tool format (primary format used by tagParser)
2298
+ if (content.includes('```json')) {
2299
+ // Quick check for toolId in JSON block
2300
+ const jsonBlockPattern = /```json\s*\{[\s\S]*?"toolId"\s*:/;
2301
+ if (jsonBlockPattern.test(content)) return true;
2302
+ }
2303
+ // Also check for legacy patterns
2304
+ return content.includes('<tool>') ||
2305
+ content.includes('<function_call>') ||
2306
+ content.includes('```tool') ||
2307
+ /<\w+_tool>/i.test(content);
2308
+ }
2309
+
2310
+ /**
2311
+ * Execute tools asynchronously and stream results to UI
2312
+ * @param {string} agentId - Agent ID
2313
+ * @param {string} content - AI response content
2314
+ * @param {string} sessionId - Session ID
2315
+ * @param {string} responseMessageId - Original response message ID for correlation
2316
+ * @private
2317
+ */
2318
+ async _executeToolsAsync(agentId, content, sessionId, responseMessageId) {
2319
+ // Check tools for builtinDelay via registry and apply the maximum delay
2320
+ try {
2321
+ const extractedTools = await this.messageProcessor.extractToolCommands(content);
2322
+ const toolsRegistry = this.messageProcessor.toolsRegistry;
2323
+
2324
+ // Find the maximum builtinDelay among all tools being executed
2325
+ let maxDelay = 0;
2326
+ for (const cmd of extractedTools) {
2327
+ const tool = toolsRegistry?.getTool(cmd.toolId);
2328
+ if (tool?.builtinDelay > maxDelay) {
2329
+ maxDelay = tool.builtinDelay;
2330
+ }
2331
+ }
2332
+
2333
+ if (maxDelay > 0) {
2334
+ const agent = await this.agentPool.getAgent(agentId);
2335
+ if (agent) {
2336
+ agent.delayEndTime = new Date(Date.now() + maxDelay).toISOString();
2337
+ await this.agentPool.persistAgentState(agentId);
2338
+ this.logger.debug(`Agent ${agentId} - applying ${maxDelay}ms builtin delay for tool execution`);
2339
+ }
2340
+ }
2341
+ } catch (extractError) {
2342
+ this.logger.warn(`Agent ${agentId} - failed to check tool delays:`, extractError.message);
2343
+ }
2344
+
2345
+ try {
2346
+ const toolResults = await this.messageProcessor.extractAndExecuteTools(
2347
+ content,
2348
+ agentId,
2349
+ { sessionId: sessionId }
2350
+ );
2351
+
2352
+ // Queue tool results in T queue for next iteration
2353
+ if (toolResults.length > 0) {
2354
+ // Reset consecutive no-tool counter since tools were executed
2355
+ if (this.consecutiveNoToolMessages.has(agentId)) {
2356
+ this.logger.debug(`Agent ${agentId} used tools - resetting consecutive no-tool counter`);
2357
+ this.consecutiveNoToolMessages.set(agentId, 0);
2358
+ }
2359
+
2360
+ const agent = await this.agentPool.getAgent(agentId);
2361
+ if (agent) {
2362
+ const toolExecutions = [];
2363
+ const fullToolResults = [];
2364
+
2365
+ for (const result of toolResults) {
2366
+ const toolResultEntry = {
2367
+ id: `tool-result-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
2368
+ toolId: result.toolId,
2369
+ status: result.status,
2370
+ result: result.result,
2371
+ error: result.error,
2372
+ executionTime: result.executionTime,
2373
+ timestamp: new Date().toISOString(),
2374
+ queuedAt: new Date().toISOString(),
2375
+ responseTurnId: responseMessageId // Track which AI turn triggered this result
2376
+ };
2377
+
2378
+ agent.messageQueues.toolResults.push(toolResultEntry);
2379
+
2380
+ toolExecutions.push({
2381
+ toolId: result.toolId,
2382
+ status: result.status,
2383
+ error: result.error,
2384
+ executionTime: result.executionTime
2385
+ });
2386
+
2387
+ fullToolResults.push({
2388
+ id: toolResultEntry.id,
2389
+ toolId: result.toolId,
2390
+ status: result.status,
2391
+ result: result.result,
2392
+ error: result.error,
2393
+ executionTime: result.executionTime,
2394
+ timestamp: toolResultEntry.timestamp
2395
+ });
2396
+ }
2397
+
2398
+ // Attach toolResults and toolExecutions to the original assistant message
2399
+ // so they persist in conversation history and are available when loading old conversations
2400
+ this._attachToolResultsToMessage(agent, responseMessageId, toolExecutions, fullToolResults);
2401
+
2402
+ await this.agentPool.persistAgentState(agentId);
2403
+
2404
+ // Broadcast tool execution completion to UI
2405
+ this._broadcastToolResults(agentId, sessionId, responseMessageId, toolExecutions, fullToolResults);
2406
+ }
2407
+ } else {
2408
+ // No tools to execute - broadcast completion with empty results
2409
+ this._broadcastToolResults(agentId, sessionId, responseMessageId, [], []);
2410
+ }
2411
+
2412
+ } catch (error) {
2413
+ this.logger.error(`Tool execution failed for agent ${agentId}:`, error);
2414
+ // Broadcast error to UI
2415
+ this._broadcastToolResults(agentId, sessionId, responseMessageId, [], [], error.message);
2416
+ }
2417
+ }
2418
+
2419
+ /**
2420
+ * Broadcast tool execution results to UI
2421
+ * @param {string} agentId - Agent ID
2422
+ * @param {string} sessionId - Session ID
2423
+ * @param {string} responseMessageId - Original response message ID
2424
+ * @param {Array} toolExecutions - Summary of tool executions
2425
+ * @param {Array} toolResults - Full tool results
2426
+ * @param {string} error - Error message if execution failed
2427
+ * @private
2428
+ */
2429
+ _broadcastToolResults(agentId, sessionId, responseMessageId, toolExecutions, toolResults, error = null) {
2430
+ if (this.webSocketManager && this.webSocketManager.broadcastToSession) {
2431
+ this.webSocketManager.broadcastToSession(sessionId, {
2432
+ type: 'tool_execution_complete',
2433
+ data: {
2434
+ agentId: agentId,
2435
+ responseMessageId: responseMessageId,
2436
+ toolExecutions: toolExecutions,
2437
+ toolResults: toolResults,
2438
+ error: error,
2439
+ timestamp: new Date().toISOString()
2440
+ }
2441
+ });
2442
+
2443
+ this.logger.debug('Broadcast tool execution results', {
2444
+ agentId,
2445
+ sessionId,
2446
+ toolCount: toolExecutions.length,
2447
+ hasError: !!error
2448
+ });
2449
+ }
2450
+ }
2451
+
2452
+ /**
2453
+ * Attach tool execution results to the original assistant message in conversation history.
2454
+ * This ensures results persist and are available when loading old conversations,
2455
+ * rather than only being available via the transient WebSocket event.
2456
+ * @param {Object} agent - Agent object with conversations
2457
+ * @param {string} responseMessageId - ID of the assistant message that triggered tool execution
2458
+ * @param {Array} toolExecutions - Summary of tool executions (toolId, status, error, executionTime)
2459
+ * @param {Array} toolResults - Full tool results with result data
2460
+ * @private
2461
+ */
2462
+ _attachToolResultsToMessage(agent, responseMessageId, toolExecutions, toolResults) {
2463
+ if (!agent || !responseMessageId) return;
2464
+
2465
+ try {
2466
+ // Update in full conversation history
2467
+ if (agent.conversations?.full?.messages) {
2468
+ const fullMsg = agent.conversations.full.messages.find(m => m.id === responseMessageId);
2469
+ if (fullMsg) {
2470
+ fullMsg.toolExecutions = toolExecutions;
2471
+ fullMsg.toolResults = toolResults;
2472
+ fullMsg.pendingToolExecution = false;
2473
+ fullMsg.hasToolExecutions = true;
2474
+ }
2475
+ }
2476
+
2477
+ // Also update in all model-specific conversations
2478
+ for (const [key, conv] of Object.entries(agent.conversations || {})) {
2479
+ if (key === 'full' || !conv?.messages) continue;
2480
+ const modelMsg = conv.messages.find(m => m.id === responseMessageId);
2481
+ if (modelMsg) {
2482
+ modelMsg.toolExecutions = toolExecutions;
2483
+ modelMsg.toolResults = toolResults;
2484
+ modelMsg.pendingToolExecution = false;
2485
+ modelMsg.hasToolExecutions = true;
2486
+ }
2487
+ // Also check compactizedMessages if conversation was compacted
2488
+ if (conv.compactizedMessages) {
2489
+ const compactMsg = conv.compactizedMessages.find(m => m.id === responseMessageId);
2490
+ if (compactMsg) {
2491
+ compactMsg.toolExecutions = toolExecutions;
2492
+ compactMsg.toolResults = toolResults;
2493
+ compactMsg.pendingToolExecution = false;
2494
+ compactMsg.hasToolExecutions = true;
2495
+ }
2496
+ }
2497
+ }
2498
+ } catch (err) {
2499
+ this.logger.warn(`Failed to attach tool results to message ${responseMessageId}:`, err.message);
2500
+ }
2501
+ }
2502
+
2503
+ /**
2504
+ * Format tool result for conversation
2505
+ * @param {Object} toolResult - Tool result message
2506
+ * @returns {string} Formatted content
2507
+ * @private
2508
+ */
2509
+ formatToolResult(toolResult) {
2510
+ const toolLabel = toolResult.toolId ? `[${toolResult.toolId}] ` : '';
2511
+ if (toolResult.status === 'completed') {
2512
+ if (typeof toolResult.result === 'object') {
2513
+ return `${toolLabel}${JSON.stringify(toolResult.result, null, 2)}`;
2514
+ }
2515
+ return `${toolLabel}${String(toolResult.result || 'Tool executed successfully')}`;
2516
+ } else if (toolResult.status === 'failed') {
2517
+ return `${toolLabel}Tool execution failed: ${toolResult.error || 'Unknown error'}`;
2518
+ } else if (toolResult.result) {
2519
+ // Warning or other status with a result message (e.g. no-tool reminders)
2520
+ return `${toolLabel}${String(toolResult.result)}`;
2521
+ }
2522
+ return `${toolLabel}Tool status: ${toolResult.status}`;
2523
+ }
2524
+
2525
+ /**
2526
+ * Check if message should be broadcast to UI
2527
+ * @param {Object} message - Message to check
2528
+ * @returns {boolean} Whether to broadcast
2529
+ * @private
2530
+ */
2531
+ shouldBroadcastMessage(message) {
2532
+ // Don't broadcast internal scheduler prompts
2533
+ if (message.type === 'scheduler-prompt') {
2534
+ return false;
2535
+ }
2536
+
2537
+ // Don't broadcast consolidated-input messages (internal AI context with tool results)
2538
+ if (message.type === 'consolidated-input') {
2539
+ return false;
2540
+ }
2541
+
2542
+ // Don't broadcast pure system instructions (but allow system messages from inter-agent communication)
2543
+ if (message.role === MESSAGE_ROLES.SYSTEM && !message.queueType) {
2544
+ return false;
2545
+ }
2546
+
2547
+ // Broadcast all other messages (user, assistant, tool, inter-agent)
2548
+ return true;
2549
+ }
2550
+
2551
+ /**
2552
+ * Broadcast message update to UI
2553
+ * @param {string} agentId - Agent ID
2554
+ * @param {Object} message - Message that was added
2555
+ * @param {Object} agentInfo - Additional agent information for UI sync
2556
+ * @private
2557
+ */
2558
+ broadcastMessageUpdate(agentId, message, agentInfo = {}) {
2559
+ if (this.webSocketManager && this.webSocketManager.broadcastToSession) {
2560
+ // Get the session ID from session map, message, or fallback
2561
+ let sessionId = this.getAgentSession(agentId);
2562
+
2563
+ // Try to get sessionId from message if not in session map
2564
+ if (!sessionId && message.sessionId) {
2565
+ sessionId = message.sessionId;
2566
+ }
2567
+
2568
+ // Final fallback
2569
+ if (!sessionId) {
2570
+ sessionId = 'scheduler-session';
2571
+ }
2572
+
2573
+ this.logger.debug('Broadcasting message to session', {
2574
+ agentId: agentId,
2575
+ sessionId: sessionId,
2576
+ messageRole: message.role,
2577
+ messageType: message.type
2578
+ });
2579
+
2580
+ this.webSocketManager.broadcastToSession(sessionId, {
2581
+ type: 'message_added',
2582
+ data: {
2583
+ agentId: agentId,
2584
+ message: message,
2585
+ timestamp: new Date().toISOString(),
2586
+ agentCurrentModel: agentInfo.agentCurrentModel
2587
+ }
2588
+ });
2589
+ }
2590
+ }
2591
+
2592
+ /**
2593
+ * Auto-create initial task if agent just switched to AGENT mode
2594
+ * @param {string} agentId - Agent ID
2595
+ * @private
2596
+ */
2597
+ async autoCreateInitialTaskIfNeeded(agentId) {
2598
+ try {
2599
+ const agent = await this.agentPool.getAgent(agentId);
2600
+ if (!agent || agent.mode !== AGENT_MODES.AGENT) {
2601
+ return;
2602
+ }
2603
+
2604
+ // Ensure taskList exists
2605
+ if (!agent.taskList) {
2606
+ agent.taskList = {
2607
+ tasks: [],
2608
+ lastUpdated: new Date().toISOString()
2609
+ };
2610
+ }
2611
+
2612
+ // Check if we already have tasks
2613
+ if (agent.taskList.tasks && agent.taskList.tasks.length > 0) {
2614
+ return; // Already has tasks
2615
+ }
2616
+
2617
+ // If agent just completed work via jobdone, don't recreate a task
2618
+ // from old conversation history — wait for a genuinely new message.
2619
+ if (agent.autonomousWorkComplete) {
2620
+ return;
2621
+ }
2622
+
2623
+ // Look for the last user message in conversation history
2624
+ const conversations = agent.conversations?.full?.messages || [];
2625
+ const lastUserMessage = [...conversations].reverse().find(m => m.role === MESSAGE_ROLES.USER);
2626
+
2627
+ if (lastUserMessage) {
2628
+ const taskTitle = `Process initial request: ${this.extractTaskTitle(lastUserMessage.content)}`;
2629
+ const taskDescription = `Handle user request: "${this.truncateContent(lastUserMessage.content, 200)}"`;
2630
+
2631
+ const task = {
2632
+ id: `task-initial-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
2633
+ title: taskTitle,
2634
+ description: taskDescription,
2635
+ status: 'pending',
2636
+ priority: 'high',
2637
+ createdAt: new Date().toISOString(),
2638
+ updatedAt: new Date().toISOString(),
2639
+ source: 'auto-created-initial',
2640
+ messageId: lastUserMessage.id
2641
+ };
2642
+
2643
+ agent.taskList.tasks.push(task);
2644
+ agent.taskList.lastUpdated = new Date().toISOString();
2645
+
2646
+ await this.agentPool.persistAgentState(agentId);
2647
+
2648
+ this.logger.info(`Auto-created initial task for agent ${agentId}`, {
2649
+ taskId: task.id,
2650
+ title: task.title,
2651
+ agentName: agent.name
2652
+ });
2653
+ }
2654
+ } catch (error) {
2655
+ this.logger.error(`Failed to auto-create initial task for agent ${agentId}`, {
2656
+ error: error.message
2657
+ });
2658
+ }
2659
+ }
2660
+
2661
+ /**
2662
+ * Auto-create tasks for incoming messages (Phase 2)
2663
+ * @param {string} agentId - Agent ID
2664
+ * @param {Array} userMessages - User messages to process
2665
+ * @param {Array} interAgentMessages - Inter-agent messages to process
2666
+ * @private
2667
+ */
2668
+ async autoCreateTasksForMessages(agentId, userMessages, interAgentMessages) {
2669
+ try {
2670
+ const agent = await this.agentPool.getAgent(agentId);
2671
+ if (!agent || agent.mode !== AGENT_MODES.AGENT) {
2672
+ return; // Only auto-create tasks for AGENT mode agents
2673
+ }
2674
+
2675
+ // Ensure taskList exists
2676
+ if (!agent.taskList) {
2677
+ agent.taskList = {
2678
+ tasks: [],
2679
+ lastUpdated: new Date().toISOString()
2680
+ };
2681
+ }
2682
+
2683
+ // Create tasks for user messages
2684
+ for (const msg of userMessages) {
2685
+ const taskTitle = `Process user request: ${this.extractTaskTitle(msg.content)}`;
2686
+ const taskDescription = `Handle user message: "${this.truncateContent(msg.content, 200)}"`;
2687
+
2688
+ // Check if similar task already exists
2689
+ const existingTask = agent.taskList.tasks.find(task =>
2690
+ task.status === 'pending' &&
2691
+ task.title.includes('Process user request') &&
2692
+ this.calculateContentSimilarity(task.description, taskDescription) > 0.7
2693
+ );
2694
+
2695
+ if (!existingTask) {
2696
+ const task = {
2697
+ id: `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
2698
+ title: taskTitle,
2699
+ description: taskDescription,
2700
+ status: 'pending',
2701
+ priority: 'high', // User messages get high priority
2702
+ createdAt: new Date().toISOString(),
2703
+ updatedAt: new Date().toISOString(),
2704
+ source: 'auto-created',
2705
+ messageId: msg.id
2706
+ };
2707
+
2708
+ agent.taskList.tasks.push(task);
2709
+
2710
+ this.logger?.info(`Auto-created task for user message`, {
2711
+ agentId,
2712
+ taskId: task.id,
2713
+ title: task.title
2714
+ });
2715
+ }
2716
+ }
2717
+
2718
+ // Create tasks for inter-agent messages
2719
+ for (const msg of interAgentMessages) {
2720
+ const senderName = msg.senderName || msg.sender || 'Unknown Agent';
2721
+ const taskTitle = `Respond to ${senderName}: ${this.extractTaskTitle(msg.content)}`;
2722
+ const taskDescription = `Handle message from ${senderName}: "${this.truncateContent(msg.content, 200)}"`;
2723
+
2724
+ // Check if similar task already exists
2725
+ const existingTask = agent.taskList.tasks.find(task =>
2726
+ task.status === 'pending' &&
2727
+ task.title.includes(`Respond to ${senderName}`) &&
2728
+ this.calculateContentSimilarity(task.description, taskDescription) > 0.7
2729
+ );
2730
+
2731
+ if (!existingTask) {
2732
+ const task = {
2733
+ id: `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
2734
+ title: taskTitle,
2735
+ description: taskDescription,
2736
+ status: 'pending',
2737
+ priority: 'medium', // Inter-agent messages get medium priority
2738
+ createdAt: new Date().toISOString(),
2739
+ updatedAt: new Date().toISOString(),
2740
+ source: 'auto-created',
2741
+ messageId: msg.id,
2742
+ senderAgent: msg.sender
2743
+ };
2744
+
2745
+ agent.taskList.tasks.push(task);
2746
+
2747
+ this.logger?.info(`Auto-created task for inter-agent message`, {
2748
+ agentId,
2749
+ taskId: task.id,
2750
+ title: task.title,
2751
+ sender: senderName
2752
+ });
2753
+ }
2754
+ }
2755
+
2756
+ // Update task list timestamp
2757
+ if (userMessages.length > 0 || interAgentMessages.length > 0) {
2758
+ agent.taskList.lastUpdated = new Date().toISOString();
2759
+ await this.agentPool.persistAgentState(agentId);
2760
+ }
2761
+
2762
+ } catch (error) {
2763
+ this.logger?.error(`Failed to auto-create tasks for agent ${agentId}`, {
2764
+ error: error.message,
2765
+ userMessageCount: userMessages.length,
2766
+ interAgentMessageCount: interAgentMessages.length
2767
+ });
2768
+ }
2769
+ }
2770
+
2771
+ /**
2772
+ * Extract a concise title from message content
2773
+ * @param {string} content - Message content
2774
+ * @returns {string} Extracted title
2775
+ * @private
2776
+ */
2777
+ extractTaskTitle(content) {
2778
+ // Extract first meaningful sentence or phrase, max 50 chars
2779
+ const cleaned = content.trim().replace(/\n+/g, ' ').replace(/\s+/g, ' ');
2780
+ const firstSentence = cleaned.split(/[.!?]/)[0].trim();
2781
+
2782
+ if (firstSentence.length > 50) {
2783
+ return firstSentence.substring(0, 47) + '...';
2784
+ }
2785
+
2786
+ return firstSentence || 'Process message';
2787
+ }
2788
+
2789
+ /**
2790
+ * Truncate content to specified length
2791
+ * @param {string} content - Content to truncate
2792
+ * @param {number} maxLength - Maximum length
2793
+ * @returns {string} Truncated content
2794
+ * @private
2795
+ */
2796
+ truncateContent(content, maxLength) {
2797
+ if (content.length <= maxLength) return content;
2798
+ return content.substring(0, maxLength - 3) + '...';
2799
+ }
2800
+
2801
+ /**
2802
+ * Calculate content similarity (simple implementation)
2803
+ * @param {string} content1 - First content
2804
+ * @param {string} content2 - Second content
2805
+ * @returns {number} Similarity score (0-1)
2806
+ * @private
2807
+ */
2808
+ calculateContentSimilarity(content1, content2) {
2809
+ // Simple word-based similarity
2810
+ const words1 = content1.toLowerCase().split(/\s+/);
2811
+ const words2 = content2.toLowerCase().split(/\s+/);
2812
+
2813
+ const commonWords = words1.filter(word => words2.includes(word));
2814
+ const totalWords = new Set([...words1, ...words2]).size;
2815
+
2816
+ return commonWords.length / totalWords;
2817
+ }
2818
+
2819
+ /**
2820
+ * Auto-mark highest priority pending task as in-progress (Phase 2)
2821
+ * @param {string} agentId - Agent ID
2822
+ * @private
2823
+ */
2824
+ async autoProgressHighestPriorityTask(agentId) {
2825
+ try {
2826
+ const agent = await this.agentPool.getAgent(agentId);
2827
+ if (!agent || !agent.taskList || !agent.taskList.tasks) {
2828
+ return;
2829
+ }
2830
+
2831
+ // Find highest priority pending task that can be started (respecting dependencies)
2832
+ let pendingTasks = agent.taskList.tasks.filter(task => task.status === TASK_STATUS.PENDING);
2833
+
2834
+ if (pendingTasks.length === 0) {
2835
+ return; // No pending tasks
2836
+ }
2837
+
2838
+ // Phase 3: Filter out blocked tasks (dependencies not met)
2839
+ pendingTasks = this.filterAvailableTasks(agent.taskList.tasks, pendingTasks);
2840
+
2841
+ if (pendingTasks.length === 0) {
2842
+ this.logger?.info(`All pending tasks are blocked by dependencies for agent ${agentId}`);
2843
+ return; // All tasks are blocked
2844
+ }
2845
+
2846
+ // Sort by intelligent priority score, fallback to priority level, then creation date
2847
+ pendingTasks.sort((a, b) => {
2848
+ // Use priority score if available (higher score = higher priority)
2849
+ if (a.priorityScore !== undefined && b.priorityScore !== undefined) {
2850
+ const scoreDiff = b.priorityScore - a.priorityScore;
2851
+ if (Math.abs(scoreDiff) > 0.1) return scoreDiff; // Use score if significantly different
2852
+ }
2853
+
2854
+ // Fallback to traditional priority ordering using constants
2855
+ const priorityA = TASK_PRIORITY_ORDER[a.priority] ?? TASK_PRIORITY_ORDER.medium;
2856
+ const priorityB = TASK_PRIORITY_ORDER[b.priority] ?? TASK_PRIORITY_ORDER.medium;
2857
+ const priorityDiff = priorityA - priorityB;
2858
+ if (priorityDiff !== 0) return priorityDiff;
2859
+
2860
+ // Finally sort by creation date (older first)
2861
+ return new Date(a.createdAt) - new Date(b.createdAt);
2862
+ });
2863
+
2864
+ const taskToProgress = pendingTasks[0];
2865
+
2866
+ // Check if we already have a task in progress
2867
+ const inProgressTasks = agent.taskList.tasks.filter(task => task.status === TASK_STATUS.IN_PROGRESS);
2868
+
2869
+ if (inProgressTasks.length === 0) {
2870
+ // Mark highest priority task as in-progress
2871
+ taskToProgress.status = TASK_STATUS.IN_PROGRESS;
2872
+ taskToProgress.updatedAt = new Date().toISOString();
2873
+ taskToProgress.startedAt = new Date().toISOString();
2874
+
2875
+ agent.taskList.lastUpdated = new Date().toISOString();
2876
+ await this.agentPool.persistAgentState(agentId);
2877
+
2878
+ this.logger?.info(`Auto-progressed task to in-progress`, {
2879
+ agentId,
2880
+ taskId: taskToProgress.id,
2881
+ title: taskToProgress.title,
2882
+ priority: taskToProgress.priority
2883
+ });
2884
+ }
2885
+
2886
+ } catch (error) {
2887
+ this.logger?.error(`Failed to auto-progress task for agent ${agentId}`, {
2888
+ error: error.message
2889
+ });
2890
+ }
2891
+ }
2892
+
2893
+ /**
2894
+ * Filter tasks to only include those that can be started (Phase 3)
2895
+ * @param {Array} allTasks - All tasks for the agent
2896
+ * @param {Array} pendingTasks - Tasks with pending status
2897
+ * @returns {Array} Tasks that can be started (no blocking dependencies)
2898
+ * @private
2899
+ */
2900
+ filterAvailableTasks(allTasks, pendingTasks) {
2901
+ return pendingTasks.filter(task => {
2902
+ // If task has no dependencies, it's available
2903
+ if (!task.dependencies || task.dependencies.length === 0) {
2904
+ return true;
2905
+ }
2906
+
2907
+ // Check all blocking dependencies
2908
+ const blockingDeps = task.dependencies.filter(dep => dep.type === 'blocks');
2909
+
2910
+ for (const dep of blockingDeps) {
2911
+ const depTask = allTasks.find(t => t.id === dep.taskId);
2912
+
2913
+ // If dependency task doesn't exist or isn't completed, task is blocked
2914
+ if (!depTask || depTask.status !== 'completed') {
2915
+ return false;
2916
+ }
2917
+ }
2918
+
2919
+ return true; // All blocking dependencies are satisfied
2920
+ });
2921
+ }
2922
+
2923
+ /**
2924
+ * Update task statuses based on dependency completion (Phase 3)
2925
+ * @param {Object} agent - Agent object
2926
+ * @param {string} completedTaskId - ID of the task that was just completed
2927
+ * @private
2928
+ */
2929
+ async updateDependentTasks(agent, completedTaskId) {
2930
+ try {
2931
+ if (!agent.taskList || !agent.taskList.tasks) {
2932
+ return;
2933
+ }
2934
+
2935
+ let updated = false;
2936
+
2937
+ // Find tasks that were blocked by the completed task
2938
+ for (const task of agent.taskList.tasks) {
2939
+ if (task.status === 'blocked' && task.dependencies) {
2940
+ const blockingDep = task.dependencies.find(
2941
+ dep => dep.type === 'blocks' && dep.taskId === completedTaskId
2942
+ );
2943
+
2944
+ if (blockingDep) {
2945
+ // Check if all other blocking dependencies are also completed
2946
+ const stillBlocked = task.dependencies.some(dep => {
2947
+ if (dep.type !== 'blocks') return false;
2948
+ const depTask = agent.taskList.tasks.find(t => t.id === dep.taskId);
2949
+ return depTask && depTask.status !== 'completed';
2950
+ });
2951
+
2952
+ if (!stillBlocked) {
2953
+ task.status = 'pending';
2954
+ task.updatedAt = new Date().toISOString();
2955
+ updated = true;
2956
+
2957
+ this.logger?.info(`Task unblocked due to dependency completion`, {
2958
+ taskId: task.id,
2959
+ title: task.title,
2960
+ completedDependency: completedTaskId
2961
+ });
2962
+ }
2963
+ }
2964
+ }
2965
+ }
2966
+
2967
+ if (updated) {
2968
+ agent.taskList.lastUpdated = new Date().toISOString();
2969
+ await this.agentPool.persistAgentState(agent.id);
2970
+ }
2971
+
2972
+ } catch (error) {
2973
+ this.logger?.error(`Failed to update dependent tasks for agent ${agent.id}`, {
2974
+ error: error.message,
2975
+ completedTaskId
2976
+ });
2977
+ }
2978
+ }
2979
+
2980
+ /**
2981
+ * Generate a hash representing the agent's most recent output
2982
+ *
2983
+ * IMPORTANT: We only hash AGENT/ASSISTANT responses, not user inputs.
2984
+ * This is because:
2985
+ * - User inputs changing is normal and expected
2986
+ * - Agent producing the SAME OUTPUT repeatedly indicates a loop
2987
+ * - If agent keeps saying "I'll do X" without actually doing it = loop
2988
+ *
2989
+ * @param {Object} agent - Agent object
2990
+ * @returns {string} Hash of the agent's recent output
2991
+ * @private
2992
+ */
2993
+ generateAgentStateHash(agent) {
2994
+ const stateComponents = [];
2995
+
2996
+ // Get the most recent ASSISTANT messages (agent outputs only)
2997
+ const allMessages = agent.conversations?.full?.messages || [];
2998
+ const assistantMessages = allMessages
2999
+ .filter(m => m.role === 'assistant')
3000
+ .slice(-3); // Last 3 assistant responses
3001
+
3002
+ // Hash the agent's actual output content
3003
+ const outputSummary = assistantMessages
3004
+ .map(m => {
3005
+ // Get the meaningful content - strip tool calls for cleaner comparison
3006
+ const content = m.content || '';
3007
+ // Truncate but include enough to detect patterns
3008
+ return content.substring(0, 500);
3009
+ })
3010
+ .join('|');
3011
+
3012
+ stateComponents.push(`output:${outputSummary}`);
3013
+
3014
+ // Also include tool calls from recent assistant messages (agent's actions)
3015
+ // If agent keeps trying to call the same tool = loop
3016
+ const recentToolCalls = assistantMessages
3017
+ .filter(m => m.toolCalls && m.toolCalls.length > 0)
3018
+ .flatMap(m => m.toolCalls)
3019
+ .slice(-5)
3020
+ .map(tc => `${tc.toolId || tc.name}:${JSON.stringify(tc.parameters || tc.params || {}).substring(0, 100)}`)
3021
+ .join(',');
3022
+
3023
+ if (recentToolCalls) {
3024
+ stateComponents.push(`tools:${recentToolCalls}`);
3025
+ }
3026
+
3027
+ // Create hash from agent output only
3028
+ const stateString = stateComponents.join('||');
3029
+
3030
+ // Simple hash function
3031
+ let hash = 0;
3032
+ for (let i = 0; i < stateString.length; i++) {
3033
+ const char = stateString.charCodeAt(i);
3034
+ hash = ((hash << 5) - hash) + char;
3035
+ hash = hash & hash; // Convert to 32bit integer
3036
+ }
3037
+
3038
+ return `${hash}_${stateString.length}`;
3039
+ }
3040
+
3041
+ /**
3042
+ * Detect if agent is in a repetitive loop using sliding window approach
3043
+ *
3044
+ * @param {string} agentId - Agent ID
3045
+ * @param {string} stateHash - Current state hash
3046
+ * @returns {{ isLoop: boolean, isImmediateDuplicate: boolean, occurrences: number }}
3047
+ * @private
3048
+ */
3049
+ detectRepetitiveLoop(agentId, stateHash) {
3050
+ const history = this.stateHashHistory.get(agentId) || [];
3051
+ const windowSize = SCHEDULER_CONFIG.STATE_HASH_WINDOW_SIZE;
3052
+ const threshold = SCHEDULER_CONFIG.REPETITION_THRESHOLD;
3053
+
3054
+ // Get the sliding window (last N entries)
3055
+ const window = history.slice(-windowSize);
3056
+
3057
+ // Check if this is an immediate duplicate (same as last hash)
3058
+ const isImmediateDuplicate = window.length > 0 && window[window.length - 1].hash === stateHash;
3059
+
3060
+ // Count occurrences of this hash in the window
3061
+ const occurrences = window.filter(entry => entry.hash === stateHash).length;
3062
+
3063
+ // It's a loop if the same hash appears threshold times or more
3064
+ const isLoop = occurrences >= threshold;
3065
+
3066
+ return {
3067
+ isLoop,
3068
+ isImmediateDuplicate,
3069
+ occurrences,
3070
+ windowSize: window.length
3071
+ };
3072
+ }
3073
+
3074
+ /**
3075
+ * Record a state hash in the sliding window
3076
+ *
3077
+ * @param {string} agentId - Agent ID
3078
+ * @param {string} stateHash - State hash to record
3079
+ * @private
3080
+ */
3081
+ recordStateHash(agentId, stateHash) {
3082
+ if (!this.stateHashHistory.has(agentId)) {
3083
+ this.stateHashHistory.set(agentId, []);
3084
+ }
3085
+
3086
+ const history = this.stateHashHistory.get(agentId);
3087
+
3088
+ // Add new entry with timestamp
3089
+ history.push({
3090
+ hash: stateHash,
3091
+ timestamp: Date.now()
3092
+ });
3093
+
3094
+ // Trim to keep only the sliding window (no arbitrary limits)
3095
+ // We keep slightly more than window size for context, but not unlimited
3096
+ const maxHistorySize = SCHEDULER_CONFIG.STATE_HASH_WINDOW_SIZE * 2;
3097
+ if (history.length > maxHistorySize) {
3098
+ // Remove oldest entries beyond double the window size
3099
+ history.splice(0, history.length - maxHistorySize);
3100
+ }
3101
+ }
3102
+
3103
+ /**
3104
+ * Handle detected repetitive loop - notify user and stop agent
3105
+ *
3106
+ * @param {string} agentId - Agent ID
3107
+ * @param {Object} loopDetection - Loop detection result
3108
+ * @private
3109
+ */
3110
+ async handleRepetitiveLoop(agentId, loopDetection) {
3111
+ const agent = await this.agentPool.getAgent(agentId);
3112
+ if (!agent) return;
3113
+
3114
+ // Create a user-friendly message
3115
+ const interventionMessage = `I notice I've been producing similar responses repeatedly (${loopDetection.occurrences} times). This usually means I'm stuck or need your guidance. I've switched to chat mode so you can provide direction.
3116
+
3117
+ What would you like me to do next? You can:
3118
+ - Give me new instructions or clarify the task
3119
+ - Switch me back to agent mode if you want me to continue
3120
+ - Ask me to try a different approach`;
3121
+
3122
+ // Add the intervention message as an assistant message so it shows in the chat
3123
+ const messageToAdd = {
3124
+ id: `msg-loop-intervention-${Date.now()}`,
3125
+ role: 'assistant',
3126
+ content: interventionMessage,
3127
+ timestamp: new Date().toISOString(),
3128
+ isSystemMessage: true,
3129
+ loopDetection: {
3130
+ occurrences: loopDetection.occurrences,
3131
+ windowSize: loopDetection.windowSize
3132
+ }
3133
+ };
3134
+
3135
+ // Add to the full conversation history directly
3136
+ if (agent.conversations && agent.conversations.full) {
3137
+ agent.conversations.full.messages.push(messageToAdd);
3138
+ agent.conversations.full.lastUpdated = new Date().toISOString();
3139
+ }
3140
+
3141
+ // Stop the agent execution - switch to chat mode
3142
+ agent.mode = AGENT_MODES.CHAT;
3143
+ await this.agentPool.persistAgentState(agentId);
3144
+
3145
+ // Broadcast the message to UI so it appears in the chat
3146
+ const sessionId = this.getAgentSession(agentId) || agent.sessionId;
3147
+ if (sessionId && this.webSocketManager && this.webSocketManager.broadcastToSession) {
3148
+ // Broadcast the intervention message
3149
+ this.webSocketManager.broadcastToSession(sessionId, {
3150
+ type: 'message_added',
3151
+ data: {
3152
+ agentId,
3153
+ message: messageToAdd,
3154
+ type: 'loop_intervention'
3155
+ }
3156
+ });
3157
+
3158
+ // Also broadcast mode change
3159
+ this.webSocketManager.broadcastToSession(sessionId, {
3160
+ type: 'agent_mode_changed',
3161
+ data: {
3162
+ agentId,
3163
+ mode: AGENT_MODES.CHAT,
3164
+ reason: 'loop_detected',
3165
+ timestamp: new Date().toISOString()
3166
+ }
3167
+ });
3168
+ }
3169
+
3170
+ // Clear the hash history for this agent (fresh start when user responds)
3171
+ this.stateHashHistory.delete(agentId);
3172
+
3173
+ this.logger.warn(`Agent ${agentId} stopped due to repetitive loop - awaiting user intervention`, {
3174
+ occurrences: loopDetection.occurrences,
3175
+ agentName: agent.name
3176
+ });
3177
+ }
3178
+
3179
+ /**
3180
+ * Clear hash history for an agent (e.g., when conversation changes significantly)
3181
+ *
3182
+ * @param {string} agentId - Agent ID
3183
+ */
3184
+ clearHashHistory(agentId) {
3185
+ if (this.stateHashHistory.has(agentId)) {
3186
+ this.stateHashHistory.set(agentId, []);
3187
+ }
3188
+ }
3189
+
3190
+ /**
3191
+ * Get scheduler status
3192
+ * Uses AgentActivityService to get current active agents
3193
+ * @returns {Promise<Object>} Scheduler status
3194
+ */
3195
+ async getStatus() {
3196
+ const allAgents = await this.agentPool.getAllAgents();
3197
+ const activeAgentResults = getActiveAgents(allAgents);
3198
+
3199
+ return {
3200
+ isRunning: this.isRunning,
3201
+ activeAgents: activeAgentResults.map(r => ({
3202
+ agentId: r.agentId,
3203
+ reason: r.reason,
3204
+ sessionId: this.getAgentSession(r.agentId)
3205
+ })),
3206
+ agentCount: activeAgentResults.length,
3207
+ sessionMapSize: this.agentSessionMap.size
3208
+ };
3209
+ }
3210
+ }
3211
+
3212
+ export default AgentScheduler;