onbuzz 3.4.0 → 3.6.2

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 (562) hide show
  1. package/package.json +1 -1
  2. package/scripts/bump-version.js +116 -0
  3. package/src/__test-utils__/fixtures/malformedJson.js +31 -0
  4. package/src/__test-utils__/globalSetup.js +9 -0
  5. package/src/__test-utils__/globalTeardown.js +12 -0
  6. package/src/__test-utils__/mockFactories.js +101 -0
  7. package/src/analyzers/__tests__/CSSAnalyzer.test.js +41 -0
  8. package/src/analyzers/__tests__/ConfigValidator.test.js +362 -0
  9. package/src/analyzers/__tests__/ESLintAnalyzer.test.js +271 -0
  10. package/src/analyzers/__tests__/JavaScriptAnalyzer.test.js +40 -0
  11. package/src/analyzers/__tests__/PrettierFormatter.test.js +197 -0
  12. package/src/analyzers/__tests__/PythonAnalyzer.test.js +208 -0
  13. package/src/analyzers/__tests__/SecurityAnalyzer.test.js +303 -0
  14. package/src/analyzers/__tests__/SparrowAnalyzer.test.js +270 -0
  15. package/src/analyzers/__tests__/TypeScriptAnalyzer.test.js +187 -0
  16. package/src/core/__tests__/agentPool.test.js +601 -0
  17. package/src/core/__tests__/agentScheduler.test.js +576 -0
  18. package/src/core/__tests__/contextManager.test.js +252 -0
  19. package/src/core/__tests__/flowExecutor.test.js +262 -0
  20. package/src/core/__tests__/messageProcessor.test.js +627 -0
  21. package/src/core/__tests__/orchestrator.test.js +257 -0
  22. package/src/core/__tests__/stateManager.test.js +375 -0
  23. package/src/core/agentPool.js +26 -4
  24. package/src/core/agentScheduler.js +79 -21
  25. package/src/core/messageProcessor.js +110 -2
  26. package/src/index.js +27 -11
  27. package/src/interfaces/__tests__/imageServing.test.js +228 -0
  28. package/src/interfaces/terminal/__tests__/smoke/imports.test.js +3 -5
  29. package/src/interfaces/webServer.js +97 -13
  30. package/src/services/__tests__/agentActivityService.test.js +319 -0
  31. package/src/services/__tests__/apiKeyManager.test.js +206 -0
  32. package/src/services/__tests__/benchmarkService.test.js +184 -0
  33. package/src/services/__tests__/budgetService.test.js +211 -0
  34. package/src/services/__tests__/contextInjectionService.test.js +205 -0
  35. package/src/services/__tests__/conversationCompactionService.test.js +280 -0
  36. package/src/services/__tests__/credentialVault.test.js +469 -0
  37. package/src/services/__tests__/errorHandler.test.js +314 -0
  38. package/src/services/__tests__/fileAttachmentService.test.js +278 -0
  39. package/src/services/__tests__/flowContextService.test.js +199 -0
  40. package/src/services/__tests__/memoryService.test.js +450 -0
  41. package/src/services/__tests__/modelRouterService.test.js +388 -0
  42. package/src/services/__tests__/modelsService.test.js +261 -0
  43. package/src/services/__tests__/portRegistry.test.js +123 -0
  44. package/src/services/__tests__/projectDetector.test.js +34 -0
  45. package/src/services/__tests__/promptService.test.js +242 -0
  46. package/src/services/__tests__/qualityInspector.test.js +97 -0
  47. package/src/services/__tests__/scheduleService.test.js +308 -0
  48. package/src/services/__tests__/serviceRegistry.test.js +74 -0
  49. package/src/services/__tests__/skillsService.test.js +402 -0
  50. package/src/services/__tests__/tokenCountingService.test.js +48 -0
  51. package/src/services/conversationCompactionService.js +2 -2
  52. package/src/services/visualEditorServer.js +26 -7
  53. package/src/tools/__tests__/agentCommunicationTool.test.js +500 -0
  54. package/src/tools/__tests__/agentDelayTool.test.js +342 -0
  55. package/src/tools/__tests__/asyncToolManager.test.js +344 -0
  56. package/src/tools/__tests__/baseTool.test.js +420 -0
  57. package/src/tools/__tests__/codeMapTool.test.js +348 -0
  58. package/src/tools/__tests__/fileContentReplaceTool.test.js +309 -0
  59. package/src/tools/__tests__/fileTreeTool.test.js +274 -0
  60. package/src/tools/__tests__/filesystemTool.test.js +717 -0
  61. package/src/tools/__tests__/helpTool.test.js +204 -0
  62. package/src/tools/__tests__/jobDoneTool.test.js +296 -0
  63. package/src/tools/__tests__/memoryTool.test.js +297 -0
  64. package/src/tools/__tests__/seekTool.test.js +282 -0
  65. package/src/tools/__tests__/skillsTool.test.js +226 -0
  66. package/src/tools/__tests__/staticAnalysisTool.test.js +509 -0
  67. package/src/tools/__tests__/taskManagerTool.test.js +725 -0
  68. package/src/tools/__tests__/terminalTool.test.js +384 -0
  69. package/src/tools/__tests__/userPromptTool.test.js +297 -0
  70. package/src/tools/__tests__/webTool.e2e.test.js +25 -11
  71. package/src/tools/imageTool.js +41 -5
  72. package/src/tools/webTool.js +161 -48
  73. package/src/types/__tests__/agent.test.js +499 -0
  74. package/src/types/__tests__/contextReference.test.js +606 -0
  75. package/src/types/__tests__/conversation.test.js +555 -0
  76. package/src/types/__tests__/toolCommand.test.js +584 -0
  77. package/src/types/contextReference.js +1 -1
  78. package/src/utilities/__tests__/attachmentValidator.test.js +80 -0
  79. package/src/utilities/__tests__/configManager.test.js +397 -0
  80. package/src/utilities/__tests__/constants.test.js +49 -0
  81. package/src/utilities/__tests__/directoryAccessManager.test.js +388 -0
  82. package/src/utilities/__tests__/fileProcessor.test.js +104 -0
  83. package/src/utilities/__tests__/jsonRepair.test.js +104 -0
  84. package/src/utilities/__tests__/logger.test.js +129 -0
  85. package/src/utilities/__tests__/platformUtils.test.js +87 -0
  86. package/src/utilities/__tests__/structuredFileValidator.test.js +263 -0
  87. package/src/utilities/__tests__/tagParser.test.js +887 -0
  88. package/src/utilities/__tests__/toolConstants.test.js +94 -0
  89. package/src/utilities/tagParser.js +2 -2
  90. package/web-ui/build/index.html +2 -2
  91. package/web-ui/build/static/1c-8PZzOTzp.js +1 -0
  92. package/web-ui/build/static/abap-Bcx_Au1F.js +1 -0
  93. package/web-ui/build/static/abnf-BKTLqpWA.js +1 -0
  94. package/web-ui/build/static/abnf-J05BAvJt.js +1 -0
  95. package/web-ui/build/static/accesslog-Cp8_lqVY.js +1 -0
  96. package/web-ui/build/static/actionscript-BK0UaMrm.js +1 -0
  97. package/web-ui/build/static/actionscript-CyqZUddh.js +1 -0
  98. package/web-ui/build/static/ada-BNirS6Nr.js +1 -0
  99. package/web-ui/build/static/ada-BSFWcT1O.js +1 -0
  100. package/web-ui/build/static/agda-D0NJDJg7.js +1 -0
  101. package/web-ui/build/static/al-rWARKtwb.js +1 -0
  102. package/web-ui/build/static/angelscript-fCehtOYk.js +1 -0
  103. package/web-ui/build/static/antlr4-Dn9jrnZN.js +1 -0
  104. package/web-ui/build/static/apache-DaQCsvNW.js +1 -0
  105. package/web-ui/build/static/apacheconf-dY4i0Xvz.js +1 -0
  106. package/web-ui/build/static/apex-vhS4SI46.js +1 -0
  107. package/web-ui/build/static/apl-CKRkxH90.js +1 -0
  108. package/web-ui/build/static/applescript-CWmpQIEB.js +1 -0
  109. package/web-ui/build/static/applescript-DBaX7Uqo.js +1 -0
  110. package/web-ui/build/static/aql-8s41lrIa.js +1 -0
  111. package/web-ui/build/static/arcade-w2_RhAcq.js +1 -0
  112. package/web-ui/build/static/arduino-I7BtZTu6.js +1 -0
  113. package/web-ui/build/static/arduino-h2LZErKQ.js +1 -0
  114. package/web-ui/build/static/arff-C543-5a1.js +1 -0
  115. package/web-ui/build/static/armasm-DyZdFOzz.js +1 -0
  116. package/web-ui/build/static/asciidoc-ZzENlACu.js +1 -0
  117. package/web-ui/build/static/asciidoc-_j9x9bUz.js +1 -0
  118. package/web-ui/build/static/asm6502-CsNsmBfq.js +1 -0
  119. package/web-ui/build/static/asmatmel-CkIVf_tD.js +1 -0
  120. package/web-ui/build/static/aspectj-C6AQLme_.js +1 -0
  121. package/web-ui/build/static/aspnet-5AkdiVyL.js +1 -0
  122. package/web-ui/build/static/autohotkey-BRZVABiS.js +1 -0
  123. package/web-ui/build/static/autohotkey-DVTmfk_f.js +1 -0
  124. package/web-ui/build/static/autoit-3UEcWu5a.js +1 -0
  125. package/web-ui/build/static/autoit-BDByIKSH.js +1 -0
  126. package/web-ui/build/static/avisynth-BHc4uUkP.js +1 -0
  127. package/web-ui/build/static/avrasm-BAPq8_aI.js +1 -0
  128. package/web-ui/build/static/avro-idl-BKEBYUtv.js +1 -0
  129. package/web-ui/build/static/awk-CBCkArRT.js +1 -0
  130. package/web-ui/build/static/axapta-DlOgnXSZ.js +1 -0
  131. package/web-ui/build/static/bash-C6Brp5OE.js +1 -0
  132. package/web-ui/build/static/bash-DkEO7JRq.js +1 -0
  133. package/web-ui/build/static/basic-DG6TYB0R.js +1 -0
  134. package/web-ui/build/static/basic-DRPcNfAn.js +1 -0
  135. package/web-ui/build/static/batch-DdjZ5KC1.js +1 -0
  136. package/web-ui/build/static/bbcode-DCXEEs2w.js +1 -0
  137. package/web-ui/build/static/bicep-CpLhfOwt.js +1 -0
  138. package/web-ui/build/static/birb-DNWkqgQm.js +1 -0
  139. package/web-ui/build/static/bison-DwxbQHJ9.js +1 -0
  140. package/web-ui/build/static/bnf-Cgnt7npj.js +1 -0
  141. package/web-ui/build/static/bnf-DSTq_eu9.js +1 -0
  142. package/web-ui/build/static/brainfuck-Bi8mGutW.js +1 -0
  143. package/web-ui/build/static/brainfuck-DOWfqVtR.js +1 -0
  144. package/web-ui/build/static/brightscript-D95pbP-v.js +1 -0
  145. package/web-ui/build/static/bro-BrDVwXeg.js +1 -0
  146. package/web-ui/build/static/bsl-BMoXI84g.js +1 -0
  147. package/web-ui/build/static/c-CKH4C7-Z.js +1 -0
  148. package/web-ui/build/static/c-Z0txyaeJ.js +1 -0
  149. package/web-ui/build/static/c-like-Dzm9dMmR.js +1 -0
  150. package/web-ui/build/static/cal-DoyAwiUt.js +1 -0
  151. package/web-ui/build/static/capnproto-DeIi9LOH.js +1 -0
  152. package/web-ui/build/static/ceylon-Coim6DIe.js +1 -0
  153. package/web-ui/build/static/cfscript-CwsndC-j.js +1 -0
  154. package/web-ui/build/static/chaiscript-D6Aq-PSv.js +1 -0
  155. package/web-ui/build/static/cil-vi56VRk_.js +1 -0
  156. package/web-ui/build/static/clean-BfpKrTdp.js +1 -0
  157. package/web-ui/build/static/clojure-DUtl6BaB.js +1 -0
  158. package/web-ui/build/static/clojure-DXJHtDlY.js +1 -0
  159. package/web-ui/build/static/clojure-repl-BxwP5C3g.js +1 -0
  160. package/web-ui/build/static/cmake-C9_VZ1vH.js +1 -0
  161. package/web-ui/build/static/cmake-dplO-PGD.js +1 -0
  162. package/web-ui/build/static/cobol-DsZhu02V.js +1 -0
  163. package/web-ui/build/static/coffeescript-Cw9jtGNP.js +1 -0
  164. package/web-ui/build/static/coffeescript-DvDt4T2l.js +1 -0
  165. package/web-ui/build/static/concurnas-Bzc_Dcdd.js +1 -0
  166. package/web-ui/build/static/coq-Cv-5BqGo.js +1 -0
  167. package/web-ui/build/static/coq-DWFe2ssK.js +1 -0
  168. package/web-ui/build/static/cos-D6Lc6Cah.js +1 -0
  169. package/web-ui/build/static/cpp-BFmLjd76.js +1 -0
  170. package/web-ui/build/static/cpp-DVQgbHji.js +1 -0
  171. package/web-ui/build/static/crmsh-Cqveth9p.js +1 -0
  172. package/web-ui/build/static/crystal-0syYaH4Y.js +1 -0
  173. package/web-ui/build/static/crystal-Noptp-kr.js +1 -0
  174. package/web-ui/build/static/csharp-B799cFMH.js +1 -0
  175. package/web-ui/build/static/csharp-_HlvMZzJ.js +1 -0
  176. package/web-ui/build/static/cshtml-CnwOXlhP.js +1 -0
  177. package/web-ui/build/static/csp-1ffxIG_-.js +1 -0
  178. package/web-ui/build/static/csp-Bws60bPu.js +1 -0
  179. package/web-ui/build/static/css-BGdwXzpm.js +1 -0
  180. package/web-ui/build/static/css-extras-DZCECiOa.js +1 -0
  181. package/web-ui/build/static/csv-FMFGT0T4.js +1 -0
  182. package/web-ui/build/static/cypher-DnXoEwRp.js +1 -0
  183. package/web-ui/build/static/d-CBrts1xB.js +1 -0
  184. package/web-ui/build/static/d-qrJLxk2L.js +1 -0
  185. package/web-ui/build/static/dart-3vBSXJVV.js +1 -0
  186. package/web-ui/build/static/dart-Cj5b7BV9.js +1 -0
  187. package/web-ui/build/static/dataweave-BuFf63rk.js +1 -0
  188. package/web-ui/build/static/dax-Cl-se1JI.js +1 -0
  189. package/web-ui/build/static/delphi-CLkRb26y.js +1 -0
  190. package/web-ui/build/static/dhall--TIL2Z--.js +1 -0
  191. package/web-ui/build/static/diff-Bn-XL2om.js +1 -0
  192. package/web-ui/build/static/diff-BsTwly4w.js +1 -0
  193. package/web-ui/build/static/django-BfRtHnTS.js +1 -0
  194. package/web-ui/build/static/django-Dm9O4e3A.js +1 -0
  195. package/web-ui/build/static/dns-BIVEp3uD.js +1 -0
  196. package/web-ui/build/static/dns-zone-file-CO7LnOdh.js +1 -0
  197. package/web-ui/build/static/docker-BhwMip1R.js +1 -0
  198. package/web-ui/build/static/dockerfile-8Tjw9_jF.js +1 -0
  199. package/web-ui/build/static/dos-CRMiAo46.js +1 -0
  200. package/web-ui/build/static/dot-DPpW7LrJ.js +1 -0
  201. package/web-ui/build/static/dsconfig-D0zbYilI.js +1 -0
  202. package/web-ui/build/static/dts-C_-yqWEL.js +1 -0
  203. package/web-ui/build/static/dust-CucJgqnE.js +1 -0
  204. package/web-ui/build/static/ebnf-CCHK0H6j.js +1 -0
  205. package/web-ui/build/static/ebnf-CDdAcveH.js +1 -0
  206. package/web-ui/build/static/editorconfig-f-5b95UM.js +1 -0
  207. package/web-ui/build/static/eiffel-MmghFce7.js +1 -0
  208. package/web-ui/build/static/ejs-CPMN4-jk.js +1 -0
  209. package/web-ui/build/static/elixir-57Ldyw8h.js +1 -0
  210. package/web-ui/build/static/elixir-DTxnmhIt.js +1 -0
  211. package/web-ui/build/static/elm-ClV9zQoT.js +1 -0
  212. package/web-ui/build/static/elm-pX-i6o7U.js +1 -0
  213. package/web-ui/build/static/erb-BPeO9smT.js +1 -0
  214. package/web-ui/build/static/erb-BnZQ4STz.js +1 -0
  215. package/web-ui/build/static/erlang-DPtI7VK_.js +1 -0
  216. package/web-ui/build/static/erlang-KG5mg5wN.js +1 -0
  217. package/web-ui/build/static/erlang-repl-BqPVXZ3Y.js +1 -0
  218. package/web-ui/build/static/etlua-BRc0Qbbs.js +1 -0
  219. package/web-ui/build/static/excel-BAlZ9Hkj.js +1 -0
  220. package/web-ui/build/static/excel-formula-BOW-bnHh.js +1 -0
  221. package/web-ui/build/static/factor-DCCsCpGM.js +1 -0
  222. package/web-ui/build/static/false-CnqnCzBU.js +1 -0
  223. package/web-ui/build/static/firestore-security-rules-DtkQ3uEq.js +1 -0
  224. package/web-ui/build/static/fix-CfPjl4Xr.js +1 -0
  225. package/web-ui/build/static/flix-BCA3BceS.js +1 -0
  226. package/web-ui/build/static/flow-C-5ewqYW.js +1 -0
  227. package/web-ui/build/static/fortran-CfDtl8An.js +1 -0
  228. package/web-ui/build/static/fortran-DQ_knNPt.js +1 -0
  229. package/web-ui/build/static/fsharp-CcVQ3IqX.js +1 -0
  230. package/web-ui/build/static/fsharp-olQ6ojCa.js +1 -0
  231. package/web-ui/build/static/ftl-DIWHDyWt.js +1 -0
  232. package/web-ui/build/static/gams-_NVFTSj5.js +1 -0
  233. package/web-ui/build/static/gap-DIG5TV2b.js +1 -0
  234. package/web-ui/build/static/gauss-C8rjPjTG.js +1 -0
  235. package/web-ui/build/static/gcode-8wJu4gcL.js +1 -0
  236. package/web-ui/build/static/gcode-DjHf417I.js +1 -0
  237. package/web-ui/build/static/gdscript-BZdmRCYu.js +1 -0
  238. package/web-ui/build/static/gedcom-BVFJ8C_Q.js +1 -0
  239. package/web-ui/build/static/gherkin-7NQkoaub.js +1 -0
  240. package/web-ui/build/static/gherkin-bSpNbJ48.js +1 -0
  241. package/web-ui/build/static/git-BRY_UXsc.js +1 -0
  242. package/web-ui/build/static/glsl-Ck6ShGRC.js +1 -0
  243. package/web-ui/build/static/glsl-jwCJ0Pmg.js +1 -0
  244. package/web-ui/build/static/gml-BAjG4Kr2.js +1 -0
  245. package/web-ui/build/static/gml-BnhKb5Da.js +1 -0
  246. package/web-ui/build/static/gn-Dux09iX8.js +1 -0
  247. package/web-ui/build/static/go-BOG-9Cqk.js +1 -0
  248. package/web-ui/build/static/go-BWZB_3b5.js +1 -0
  249. package/web-ui/build/static/go-module-BVLW7KBE.js +1 -0
  250. package/web-ui/build/static/golo-BD_SOfwL.js +1 -0
  251. package/web-ui/build/static/gradle-zSadWOD2.js +1 -0
  252. package/web-ui/build/static/graphql-BQlyj78B.js +1 -0
  253. package/web-ui/build/static/groovy-BurRMqQS.js +1 -0
  254. package/web-ui/build/static/groovy-SP58zwlE.js +1 -0
  255. package/web-ui/build/static/haml-eZ5ah5KY.js +1 -0
  256. package/web-ui/build/static/haml-lFC47IZb.js +1 -0
  257. package/web-ui/build/static/handlebars-B4UXrB-Q.js +1 -0
  258. package/web-ui/build/static/handlebars-CQ-Q5HnC.js +1 -0
  259. package/web-ui/build/static/haskell-AQrRyTSy.js +1 -0
  260. package/web-ui/build/static/haskell-BZTSbaB_.js +1 -0
  261. package/web-ui/build/static/haxe-5l1X6ESp.js +1 -0
  262. package/web-ui/build/static/haxe-DBn90muG.js +1 -0
  263. package/web-ui/build/static/hcl-CnMewPLM.js +1 -0
  264. package/web-ui/build/static/hlsl-RAtuBzr5.js +1 -0
  265. package/web-ui/build/static/hoon-CSqRU_4M.js +1 -0
  266. package/web-ui/build/static/hpkp-_gNbXcHt.js +1 -0
  267. package/web-ui/build/static/hsp-DY1V4au3.js +1 -0
  268. package/web-ui/build/static/hsts-j5z2jJo8.js +1 -0
  269. package/web-ui/build/static/htmlbars-C5EHvatr.js +1 -0
  270. package/web-ui/build/static/http-DgWgQrZh.js +1 -0
  271. package/web-ui/build/static/http-DphJL0q2.js +1 -0
  272. package/web-ui/build/static/hy-DnBqjPsB.js +1 -0
  273. package/web-ui/build/static/ichigojam-DeiCOKyF.js +1 -0
  274. package/web-ui/build/static/icon-CWANFWY5.js +1 -0
  275. package/web-ui/build/static/icu-message-format-C7w3vpgC.js +1 -0
  276. package/web-ui/build/static/idris-BeD8eULz.js +1 -0
  277. package/web-ui/build/static/iecst-BIznHXqY.js +1 -0
  278. package/web-ui/build/static/ignore-BcFgcNaS.js +1 -0
  279. package/web-ui/build/static/index-D8uVofpo.js +13 -0
  280. package/web-ui/build/static/index-DPFadqM6.css +1 -0
  281. package/web-ui/build/static/index-SkOgWEAU.js +1 -0
  282. package/web-ui/build/static/index-Vd3WlhtC.js +747 -0
  283. package/web-ui/build/static/inform7-CkQD_jz-.js +1 -0
  284. package/web-ui/build/static/inform7-phQiuDty.js +1 -0
  285. package/web-ui/build/static/ini-Bw_QAbzV.js +1 -0
  286. package/web-ui/build/static/ini-CB8ZxX7y.js +1 -0
  287. package/web-ui/build/static/io-D6IgpCmL.js +1 -0
  288. package/web-ui/build/static/irpf90-Ctj0koST.js +1 -0
  289. package/web-ui/build/static/isbl-D2mGcH87.js +1 -0
  290. package/web-ui/build/static/j-KvHmDBWH.js +1 -0
  291. package/web-ui/build/static/java-BeBIdo5i.js +1 -0
  292. package/web-ui/build/static/java-llFZkHLe.js +1 -0
  293. package/web-ui/build/static/javadoc-DXvQGu0s.js +1 -0
  294. package/web-ui/build/static/javadoclike-B5qdA9KZ.js +1 -0
  295. package/web-ui/build/static/javascript-De6HzHvc.js +1 -0
  296. package/web-ui/build/static/javastacktrace-C5MolKiP.js +1 -0
  297. package/web-ui/build/static/jboss-cli-2TXd54zo.js +1 -0
  298. package/web-ui/build/static/jexl-W4AVA9fi.js +1 -0
  299. package/web-ui/build/static/jolie-DTJKRMZN.js +1 -0
  300. package/web-ui/build/static/jq-BYg-QQKh.js +1 -0
  301. package/web-ui/build/static/js-extras-BrYWd2VE.js +1 -0
  302. package/web-ui/build/static/js-templates-DorYpbHq.js +1 -0
  303. package/web-ui/build/static/jsdoc-CRF8n9pZ.js +1 -0
  304. package/web-ui/build/static/json-Dlcd7rla.js +1 -0
  305. package/web-ui/build/static/json-rhOJZzzZ.js +1 -0
  306. package/web-ui/build/static/json5-hTq1nNIW.js +1 -0
  307. package/web-ui/build/static/jsonp-CMg9Xvol.js +1 -0
  308. package/web-ui/build/static/jsstacktrace-hEeYxHtB.js +1 -0
  309. package/web-ui/build/static/jsx-B7PtA8WD.js +1 -0
  310. package/web-ui/build/static/julia-CNiEEY-n.js +1 -0
  311. package/web-ui/build/static/julia-eE0_SJlc.js +1 -0
  312. package/web-ui/build/static/julia-repl-CUJTTT4C.js +1 -0
  313. package/web-ui/build/static/keepalived-GUWJBqyD.js +1 -0
  314. package/web-ui/build/static/keyman-Bdf9MJyu.js +1 -0
  315. package/web-ui/build/static/kotlin-Ch6Bej5W.js +1 -0
  316. package/web-ui/build/static/kotlin-DFJ7D7Lw.js +1 -0
  317. package/web-ui/build/static/kumir-DJLIjcCs.js +1 -0
  318. package/web-ui/build/static/kusto-BM0YTwU4.js +1 -0
  319. package/web-ui/build/static/lasso-Z1DVS84K.js +1 -0
  320. package/web-ui/build/static/latex-BWbw71RK.js +1 -0
  321. package/web-ui/build/static/latex-CMzqmbhw.js +1 -0
  322. package/web-ui/build/static/latte-C1g8_grc.js +1 -0
  323. package/web-ui/build/static/ldif-C6QH3OIL.js +1 -0
  324. package/web-ui/build/static/leaf-CbR--ZsH.js +1 -0
  325. package/web-ui/build/static/less-ClVrKh2Z.js +1 -0
  326. package/web-ui/build/static/less-DNSxm8UA.js +1 -0
  327. package/web-ui/build/static/lilypond-lTzf_sWt.js +1 -0
  328. package/web-ui/build/static/liquid-Bn91mVfC.js +1 -0
  329. package/web-ui/build/static/lisp-CU3bHohQ.js +1 -0
  330. package/web-ui/build/static/lisp-DbgzE9W8.js +1 -0
  331. package/web-ui/build/static/livecodeserver-Cse1Uz3H.js +1 -0
  332. package/web-ui/build/static/livescript-BaxbgzWP.js +1 -0
  333. package/web-ui/build/static/livescript-nJt61DBy.js +1 -0
  334. package/web-ui/build/static/llvm-DBboo6UI.js +1 -0
  335. package/web-ui/build/static/llvm-vIy7XYVy.js +1 -0
  336. package/web-ui/build/static/log-CT7nfoDW.js +1 -0
  337. package/web-ui/build/static/lolcode-CUKVytZh.js +1 -0
  338. package/web-ui/build/static/lsl-CsAOlGF2.js +1 -0
  339. package/web-ui/build/static/lua-BuU2FFxP.js +1 -0
  340. package/web-ui/build/static/lua-CiDuKQaa.js +1 -0
  341. package/web-ui/build/static/magma-7vR0zcmS.js +1 -0
  342. package/web-ui/build/static/makefile-Buiz-Msh.js +1 -0
  343. package/web-ui/build/static/makefile-DXW_-6OY.js +1 -0
  344. package/web-ui/build/static/markdown-Bk5DUoGY.js +1 -0
  345. package/web-ui/build/static/markdown-CRS5W_Ai.js +1 -0
  346. package/web-ui/build/static/markup-templating-24odpmF3.js +1 -0
  347. package/web-ui/build/static/mathematica-BxcwhJUp.js +1 -0
  348. package/web-ui/build/static/matlab-3pJYx_Fb.js +1 -0
  349. package/web-ui/build/static/matlab-BqlRrzMf.js +1 -0
  350. package/web-ui/build/static/maxima-DlCfUpcj.js +1 -0
  351. package/web-ui/build/static/maxscript-Cu_gCaFU.js +1 -0
  352. package/web-ui/build/static/mel-D7iQ-5Up.js +1 -0
  353. package/web-ui/build/static/mel-DzBKNpoN.js +1 -0
  354. package/web-ui/build/static/mercury-Dfrb-i8A.js +1 -0
  355. package/web-ui/build/static/mermaid-WN7V2_Eq.js +1 -0
  356. package/web-ui/build/static/mipsasm-CcijzL0q.js +1 -0
  357. package/web-ui/build/static/mizar-Bk68zACP.js +1 -0
  358. package/web-ui/build/static/mizar-Twc2-iZ4.js +1 -0
  359. package/web-ui/build/static/mojolicious-DBbo2S7X.js +1 -0
  360. package/web-ui/build/static/mongodb-2RsFIjgg.js +1 -0
  361. package/web-ui/build/static/monkey-CPXtQ0Bf.js +1 -0
  362. package/web-ui/build/static/monkey-DjV7Wcek.js +1 -0
  363. package/web-ui/build/static/moonscript-B5M5as70.js +1 -0
  364. package/web-ui/build/static/moonscript-D1BHW4Il.js +1 -0
  365. package/web-ui/build/static/n1ql-D0heNDBD.js +1 -0
  366. package/web-ui/build/static/n1ql-DfHqXQD7.js +1 -0
  367. package/web-ui/build/static/n4js-CaPf44Dz.js +1 -0
  368. package/web-ui/build/static/nand2tetris-hdl-D1nf9mn4.js +1 -0
  369. package/web-ui/build/static/naniscript-DnCnu5ZX.js +1 -0
  370. package/web-ui/build/static/nasm-BZrSaMsK.js +1 -0
  371. package/web-ui/build/static/neon-D29Grm2v.js +1 -0
  372. package/web-ui/build/static/nevod-DgSNbQkE.js +1 -0
  373. package/web-ui/build/static/nginx-BAaDGDfT.js +1 -0
  374. package/web-ui/build/static/nginx-BvJ1lrHX.js +1 -0
  375. package/web-ui/build/static/nim--9zzVe5F.js +1 -0
  376. package/web-ui/build/static/nim-Br1relpU.js +1 -0
  377. package/web-ui/build/static/nix--0ftErCy.js +1 -0
  378. package/web-ui/build/static/nix-104ztQqr.js +1 -0
  379. package/web-ui/build/static/node-repl-BUMAf7_p.js +1 -0
  380. package/web-ui/build/static/nsis-BaeKybNA.js +1 -0
  381. package/web-ui/build/static/nsis-CdZEv2iA.js +1 -0
  382. package/web-ui/build/static/objectivec-DBB4ymdg.js +1 -0
  383. package/web-ui/build/static/objectivec-kFYXC6g4.js +1 -0
  384. package/web-ui/build/static/ocaml-D1GXvN7c.js +1 -0
  385. package/web-ui/build/static/ocaml-D80jRMPE.js +1 -0
  386. package/web-ui/build/static/opencl-fb7BfRdO.js +1 -0
  387. package/web-ui/build/static/openqasm-CWUBrR2w.js +1 -0
  388. package/web-ui/build/static/openscad-Dim7ILSL.js +1 -0
  389. package/web-ui/build/static/oxygene-BSwApkwz.js +1 -0
  390. package/web-ui/build/static/oz-CMtRoi5F.js +1 -0
  391. package/web-ui/build/static/parigp-AH8cZ38D.js +1 -0
  392. package/web-ui/build/static/parser-bBNjuhG3.js +1 -0
  393. package/web-ui/build/static/parser3-DUtoWEAd.js +1 -0
  394. package/web-ui/build/static/pascal-Cr3DPIYT.js +1 -0
  395. package/web-ui/build/static/pascaligo-pWW12jfs.js +1 -0
  396. package/web-ui/build/static/pcaxis-DBw9rxmr.js +1 -0
  397. package/web-ui/build/static/peoplecode-aCpMPm_s.js +1 -0
  398. package/web-ui/build/static/perl-BpZ7GmJ3.js +1 -0
  399. package/web-ui/build/static/perl-fnHTrqJL.js +1 -0
  400. package/web-ui/build/static/pf-Dz352ty7.js +1 -0
  401. package/web-ui/build/static/pgsql-CHPUdlI_.js +1 -0
  402. package/web-ui/build/static/php-BRwItjmS.js +1 -0
  403. package/web-ui/build/static/php-CrX_kswO.js +1 -0
  404. package/web-ui/build/static/php-extras-BmeRXDSO.js +1 -0
  405. package/web-ui/build/static/php-template-B0MFJ9RR.js +1 -0
  406. package/web-ui/build/static/phpdoc-wAPkJj9X.js +1 -0
  407. package/web-ui/build/static/plaintext-CmPk1gvP.js +1 -0
  408. package/web-ui/build/static/plsql-pWVw0sCJ.js +1 -0
  409. package/web-ui/build/static/pony-B4SXhyDS.js +1 -0
  410. package/web-ui/build/static/powerquery-ZJ28bdRR.js +1 -0
  411. package/web-ui/build/static/powershell-CWg1ca6z.js +1 -0
  412. package/web-ui/build/static/powershell-Dnl0aBXc.js +1 -0
  413. package/web-ui/build/static/processing-CbYVU7hZ.js +1 -0
  414. package/web-ui/build/static/processing-DnroirEw.js +1 -0
  415. package/web-ui/build/static/profile-DLNc-MTA.js +1 -0
  416. package/web-ui/build/static/prolog-4KlPFQus.js +1 -0
  417. package/web-ui/build/static/prolog-CtUicY87.js +1 -0
  418. package/web-ui/build/static/promql-C_i6OJVg.js +1 -0
  419. package/web-ui/build/static/properties-Cj0lBOSP.js +1 -0
  420. package/web-ui/build/static/properties-vGFibcz9.js +1 -0
  421. package/web-ui/build/static/protobuf-BOIGxbSP.js +1 -0
  422. package/web-ui/build/static/protobuf-CQ3su-J7.js +1 -0
  423. package/web-ui/build/static/psl-DeG5_YUF.js +1 -0
  424. package/web-ui/build/static/pug-BieVVXYz.js +1 -0
  425. package/web-ui/build/static/puppet-Ba40SVKU.js +1 -0
  426. package/web-ui/build/static/puppet-D7BzrcGt.js +1 -0
  427. package/web-ui/build/static/pure-DZnkz1iT.js +1 -0
  428. package/web-ui/build/static/purebasic-CLLZW_6G.js +1 -0
  429. package/web-ui/build/static/purebasic-CYPZo_H6.js +1 -0
  430. package/web-ui/build/static/purescript-Dyjfu5Id.js +1 -0
  431. package/web-ui/build/static/python-BdIWKxdN.js +1 -0
  432. package/web-ui/build/static/python-ofKsqxv7.js +1 -0
  433. package/web-ui/build/static/python-repl-DiTYb1xK.js +1 -0
  434. package/web-ui/build/static/q-B4P0If_I.js +1 -0
  435. package/web-ui/build/static/q-t_17xfY8.js +1 -0
  436. package/web-ui/build/static/qml-B5WhiN48.js +1 -0
  437. package/web-ui/build/static/qml-Dq0cESXJ.js +1 -0
  438. package/web-ui/build/static/qore-DCx30XRf.js +1 -0
  439. package/web-ui/build/static/qsharp-UrBScekp.js +1 -0
  440. package/web-ui/build/static/r-B0Ty1RKQ.js +1 -0
  441. package/web-ui/build/static/r-B0za8QKS.js +1 -0
  442. package/web-ui/build/static/racket-Dj6WEyhS.js +1 -0
  443. package/web-ui/build/static/reason-dj9hJSfr.js +1 -0
  444. package/web-ui/build/static/reasonml-B-q5_wag.js +1 -0
  445. package/web-ui/build/static/regex-4HEc5C1m.js +1 -0
  446. package/web-ui/build/static/rego-BdQe18RK.js +1 -0
  447. package/web-ui/build/static/renpy-CVMA2llL.js +1 -0
  448. package/web-ui/build/static/rest-9B4JWVGr.js +1 -0
  449. package/web-ui/build/static/rib-DR-U8OaT.js +1 -0
  450. package/web-ui/build/static/rip-Bu2t_rFZ.js +1 -0
  451. package/web-ui/build/static/roboconf-CJeXD5-I.js +1 -0
  452. package/web-ui/build/static/roboconf-DzDTVrdM.js +1 -0
  453. package/web-ui/build/static/robotframework-CR7KyPpN.js +1 -0
  454. package/web-ui/build/static/routeros-B2741z2k.js +1 -0
  455. package/web-ui/build/static/rsl-B9F_ZCgv.js +1 -0
  456. package/web-ui/build/static/ruby-I2JTNgyY.js +1 -0
  457. package/web-ui/build/static/ruby-QGDPOmJX.js +1 -0
  458. package/web-ui/build/static/ruleslanguage-CGzXEUCO.js +1 -0
  459. package/web-ui/build/static/rust-BxW5-WOm.js +1 -0
  460. package/web-ui/build/static/rust-CSOA43di.js +1 -0
  461. package/web-ui/build/static/sas-Bclfx4g3.js +1 -0
  462. package/web-ui/build/static/sas-xbQaiYjT.js +1 -0
  463. package/web-ui/build/static/sass-DJPbdNwd.js +1 -0
  464. package/web-ui/build/static/scala-Bo18NtHQ.js +1 -0
  465. package/web-ui/build/static/scala-Cy0JH-SG.js +1 -0
  466. package/web-ui/build/static/scheme-BjcWWjIx.js +1 -0
  467. package/web-ui/build/static/scheme-DQdj8PzN.js +1 -0
  468. package/web-ui/build/static/scilab-Bn1KHdK-.js +1 -0
  469. package/web-ui/build/static/scss-B1twkZBz.js +1 -0
  470. package/web-ui/build/static/scss-DmOuMI4v.js +1 -0
  471. package/web-ui/build/static/shell-BUlkJG0S.js +1 -0
  472. package/web-ui/build/static/shell-session-Bke-svxA.js +1 -0
  473. package/web-ui/build/static/smali-Ch9S16HV.js +1 -0
  474. package/web-ui/build/static/smali-D_yDr_Aj.js +1 -0
  475. package/web-ui/build/static/smalltalk-B9TfQ5Md.js +1 -0
  476. package/web-ui/build/static/smalltalk-EwbZxZsR.js +1 -0
  477. package/web-ui/build/static/smarty-9kDPpeSm.js +1 -0
  478. package/web-ui/build/static/sml-2fEfT7rd.js +1 -0
  479. package/web-ui/build/static/sml-BiwoLNk7.js +1 -0
  480. package/web-ui/build/static/solidity-n_x8Oe0h.js +1 -0
  481. package/web-ui/build/static/solution-file-B2mvjI3e.js +1 -0
  482. package/web-ui/build/static/soy-DPkgKBIS.js +1 -0
  483. package/web-ui/build/static/sparql-Cy95tds0.js +1 -0
  484. package/web-ui/build/static/splunk-spl-Ym3z9ouN.js +1 -0
  485. package/web-ui/build/static/sqf-CXZTG8WE.js +1 -0
  486. package/web-ui/build/static/sqf-Cwi3yg7f.js +1 -0
  487. package/web-ui/build/static/sql-DPxSQY4S.js +1 -0
  488. package/web-ui/build/static/sql-peh7ijGj.js +1 -0
  489. package/web-ui/build/static/sql_more-0YAbAuPw.js +1 -0
  490. package/web-ui/build/static/squirrel-CphzjV0e.js +1 -0
  491. package/web-ui/build/static/stan-0-xZ95-O.js +1 -0
  492. package/web-ui/build/static/stan-CaI4__2g.js +1 -0
  493. package/web-ui/build/static/stata-BrbzrGSs.js +1 -0
  494. package/web-ui/build/static/step21-C_qeyVLw.js +1 -0
  495. package/web-ui/build/static/stylus-Btycb2sZ.js +1 -0
  496. package/web-ui/build/static/stylus-FoBJ7jki.js +1 -0
  497. package/web-ui/build/static/subunit-Dpg-m04-.js +1 -0
  498. package/web-ui/build/static/swift-Cr9uZmgb.js +1 -0
  499. package/web-ui/build/static/swift-hGLFtD7e.js +1 -0
  500. package/web-ui/build/static/systemd-Bls2D9Vj.js +1 -0
  501. package/web-ui/build/static/t4-cs-C4qDO-jJ.js +1 -0
  502. package/web-ui/build/static/t4-templating-BbCFPMPO.js +1 -0
  503. package/web-ui/build/static/t4-vb-D1zoEccT.js +1 -0
  504. package/web-ui/build/static/taggerscript-CWHk9Gih.js +1 -0
  505. package/web-ui/build/static/tap-Bjt0UnzV.js +1 -0
  506. package/web-ui/build/static/tap-BnHKwLQs.js +1 -0
  507. package/web-ui/build/static/tcl-Zo9kx4y-.js +1 -0
  508. package/web-ui/build/static/tcl-fzLmefkt.js +1 -0
  509. package/web-ui/build/static/textile-9lIlUPH5.js +1 -0
  510. package/web-ui/build/static/thrift-M3K6r5Cy.js +1 -0
  511. package/web-ui/build/static/toml-HpaKqckc.js +1 -0
  512. package/web-ui/build/static/tp-DFKuxrKj.js +1 -0
  513. package/web-ui/build/static/tremor-D4_bUtMB.js +1 -0
  514. package/web-ui/build/static/tsx-o1RT-T90.js +1 -0
  515. package/web-ui/build/static/tt2-1xDqcN_2.js +1 -0
  516. package/web-ui/build/static/turtle-Dlt-aGky.js +1 -0
  517. package/web-ui/build/static/twig-CJ_BnGSR.js +1 -0
  518. package/web-ui/build/static/twig-CjsiSQb6.js +1 -0
  519. package/web-ui/build/static/typescript-B8B9zUn-.js +1 -0
  520. package/web-ui/build/static/typescript-D0Jgo8O7.js +1 -0
  521. package/web-ui/build/static/typoscript-C8Qke4ZB.js +1 -0
  522. package/web-ui/build/static/unrealscript-YxJdDNZ3.js +1 -0
  523. package/web-ui/build/static/uorazor-CtEVnqBv.js +1 -0
  524. package/web-ui/build/static/uri-YdaiQl4c.js +1 -0
  525. package/web-ui/build/static/v-CIyttMDD.js +1 -0
  526. package/web-ui/build/static/vala-DGslcym_.js +1 -0
  527. package/web-ui/build/static/vala-GFPx3uEJ.js +1 -0
  528. package/web-ui/build/static/vbnet-B20itab-.js +1 -0
  529. package/web-ui/build/static/vbnet-BdoN6egk.js +1 -0
  530. package/web-ui/build/static/vbscript-PHVh6Fp_.js +1 -0
  531. package/web-ui/build/static/vbscript-html-woH1VZ7U.js +1 -0
  532. package/web-ui/build/static/velocity-DtVfCZeg.js +1 -0
  533. package/web-ui/build/static/verilog-Bt6edXvM.js +1 -0
  534. package/web-ui/build/static/verilog-k_7lr9Zq.js +1 -0
  535. package/web-ui/build/static/vhdl-BMzOgOeK.js +1 -0
  536. package/web-ui/build/static/vhdl-BcAbtPG6.js +1 -0
  537. package/web-ui/build/static/vim-DrinG9a4.js +1 -0
  538. package/web-ui/build/static/vim-WihLATJL.js +1 -0
  539. package/web-ui/build/static/visual-basic-CJnvgPjM.js +1 -0
  540. package/web-ui/build/static/warpscript-zMlbUoZs.js +1 -0
  541. package/web-ui/build/static/wasm-GUnfTBUL.js +1 -0
  542. package/web-ui/build/static/web-idl-CfaLTG_r.js +1 -0
  543. package/web-ui/build/static/wiki-13AlLoOc.js +1 -0
  544. package/web-ui/build/static/wolfram-zHocYNXW.js +1 -0
  545. package/web-ui/build/static/wren-Byq862Iu.js +1 -0
  546. package/web-ui/build/static/x86asm-CLcOnePY.js +1 -0
  547. package/web-ui/build/static/xeora-BVHqWOFS.js +1 -0
  548. package/web-ui/build/static/xl-lXi8OYfr.js +1 -0
  549. package/web-ui/build/static/xml-KZjGBKxi.js +1 -0
  550. package/web-ui/build/static/xml-doc-DrQSDcEW.js +1 -0
  551. package/web-ui/build/static/xojo-DosHeFXU.js +1 -0
  552. package/web-ui/build/static/xquery-BZN1F14Q.js +1 -0
  553. package/web-ui/build/static/xquery-Cnz7ZLFr.js +1 -0
  554. package/web-ui/build/static/yaml-BzXOcy9u.js +1 -0
  555. package/web-ui/build/static/yaml-C207y5bt.js +1 -0
  556. package/web-ui/build/static/yang-ByrBdDIg.js +1 -0
  557. package/web-ui/build/static/zephir-bahTa7of.js +1 -0
  558. package/web-ui/build/static/zig-BlFYhdtC.js +1 -0
  559. package/src/tools/browserTool.js +0 -897
  560. package/src/utilities/platformUtils.test.js +0 -98
  561. package/web-ui/build/static/index-SmQFfvBs.js +0 -746
  562. package/web-ui/build/static/index-V2ySwjHp.css +0 -1
@@ -400,6 +400,10 @@ class AgentPool {
400
400
  newConversation.compactizationStrategy = oldConversation.compactizationStrategy;
401
401
  newConversation.originalTokenCount = oldConversation.originalTokenCount;
402
402
  newConversation.compactedTokenCount = oldConversation.compactedTokenCount;
403
+ // CRITICAL: Copy the sync watermark too — without this, getMessagesForAI
404
+ // cannot sync new messages to compactizedMessages after a model switch,
405
+ // causing the AI to only see the compacted summary and repeat itself endlessly.
406
+ newConversation.originalMessageCountAtCompaction = oldConversation.originalMessageCountAtCompaction;
403
407
  }
404
408
 
405
409
  newConversation.lastUpdated = new Date().toISOString();
@@ -829,7 +833,17 @@ class AgentPool {
829
833
  }
830
834
  } catch (error) {
831
835
  this.logger.warn(`Failed to clean up visual editor for agent: ${error.message}`, { agentId });
832
- // Continue with agent deletion even if visual editor cleanup fails
836
+ }
837
+
838
+ // Kill any running terminal processes for this agent
839
+ try {
840
+ const terminalTool = this.toolsRegistry?.getTool?.('terminal');
841
+ if (terminalTool && typeof terminalTool.cleanupAgent === 'function') {
842
+ await terminalTool.cleanupAgent(agentId);
843
+ this.logger.info(`Terminal processes cleaned up for agent: ${agentId}`);
844
+ }
845
+ } catch (error) {
846
+ this.logger.warn(`Failed to clean up terminal processes for agent: ${error.message}`, { agentId });
833
847
  }
834
848
 
835
849
  // Clean up agent resources
@@ -1554,7 +1568,11 @@ class AgentPool {
1554
1568
  // vs which ones were already included in the compaction (sandwich strategy)
1555
1569
  const compactedLength = conversation.compactizedMessages.length;
1556
1570
  const originalLength = conversation.messages.length;
1557
- const originalCountAtCompaction = conversation.originalMessageCountAtCompaction || originalLength;
1571
+ // SAFETY: If watermark is null/undefined (bug, migration, or cleared state),
1572
+ // fall back to compactedLength — NOT originalLength. Using originalLength silently
1573
+ // drops all unsynced messages because (originalLength > originalLength) is always false.
1574
+ // Using compactedLength ensures any messages beyond what's in the compacted array get synced.
1575
+ const originalCountAtCompaction = conversation.originalMessageCountAtCompaction ?? compactedLength;
1558
1576
 
1559
1577
  // Only sync if NEW messages were added after compaction
1560
1578
  // (i.e., current original length > original length when compaction happened)
@@ -1721,7 +1739,10 @@ class AgentPool {
1721
1739
  if (!conversation || !conversation.compactizedMessages) return { synced: 0 };
1722
1740
 
1723
1741
  const originalLength = conversation.messages.length;
1724
- const originalCountAtCompaction = conversation.originalMessageCountAtCompaction || originalLength;
1742
+ const compactedLength = conversation.compactizedMessages.length;
1743
+ // SAFETY: Use ?? compactedLength instead of || originalLength to prevent silent message loss
1744
+ // when watermark is null (see getMessagesForAI for detailed explanation)
1745
+ const originalCountAtCompaction = conversation.originalMessageCountAtCompaction ?? compactedLength;
1725
1746
 
1726
1747
  if (originalLength > originalCountAtCompaction) {
1727
1748
  const newCount = originalLength - originalCountAtCompaction;
@@ -1734,7 +1755,8 @@ class AgentPool {
1734
1755
  modelId,
1735
1756
  synced: newCount,
1736
1757
  newMessageRoles: newMessages.map(m => m.role),
1737
- compactizedMessagesLength: conversation.compactizedMessages.length
1758
+ compactizedMessagesLength: conversation.compactizedMessages.length,
1759
+ watermarkWasNull: conversation.originalMessageCountAtCompaction === null
1738
1760
  });
1739
1761
 
1740
1762
  return { synced: newCount };
@@ -664,11 +664,11 @@ class AgentScheduler {
664
664
  });
665
665
  }
666
666
 
667
- // Add tool results as context, grouped by the AI turn that triggered them
667
+ // Build tool results as a SEPARATE message (role: 'user' with tool-result type marker)
668
+ // so they are distinguishable from actual user messages in the conversation history.
668
669
  const toolResults = allMessages.filter(m => m.queueType === 'toolResults');
670
+ let toolResultContent = '';
669
671
  if (toolResults.length > 0) {
670
- if (consolidatedContent) consolidatedContent += '\n\n';
671
-
672
672
  // Group results by responseTurnId (the AI message that triggered them)
673
673
  const turnGroups = new Map();
674
674
  for (const msg of toolResults) {
@@ -681,20 +681,20 @@ class AgentScheduler {
681
681
  const turnCount = turnGroups.size;
682
682
  const toolCount = toolResults.length;
683
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`;
684
+ toolResultContent += `[Tool Results — ${toolCount} result${toolCount > 1 ? 's' : ''} from ${turnCount} tool batch${turnCount > 1 ? 'es' : ''}: ${toolIds.join(', ')}]\n`;
685
685
 
686
686
  if (turnCount === 1) {
687
687
  // Single batch — flat list (no sub-headers needed)
688
688
  toolResults.forEach(msg => {
689
- consolidatedContent += `${this.formatToolResult(msg)}\n`;
689
+ toolResultContent += `${this.formatToolResult(msg)}\n`;
690
690
  });
691
691
  } else {
692
692
  // Multiple batches — group with labeled sub-headers
693
693
  let batchIndex = 1;
694
694
  for (const [, group] of turnGroups) {
695
- consolidatedContent += `\n--- Batch ${batchIndex} of ${turnCount} ---\n`;
695
+ toolResultContent += `\n--- Batch ${batchIndex} of ${turnCount} ---\n`;
696
696
  group.forEach(msg => {
697
- consolidatedContent += `${this.formatToolResult(msg)}\n`;
697
+ toolResultContent += `${this.formatToolResult(msg)}\n`;
698
698
  });
699
699
  batchIndex++;
700
700
  }
@@ -709,18 +709,49 @@ class AgentScheduler {
709
709
  // PHASE 2: Auto-create tasks for incoming messages
710
710
  await this.autoCreateTasksForMessages(agentId, userMessages, interAgentMessages);
711
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
- };
712
+ // Decide message strategy based on what's queued:
713
+ // - Tool results only → single message with type: 'tool-result' (most common in agent loops)
714
+ // - User/inter-agent only → single message with type: 'consolidated-input'
715
+ // - Both → must merge into single user message (API requires alternating user/assistant)
716
+ const hasNonToolContent = consolidatedContent.trim().length > 0;
721
717
 
722
- // Add to conversation history (don't broadcast - this is internal)
723
- await this.addMessageToConversation(agentId, consolidatedMessage, false);
718
+ if (toolResultContent && !hasNonToolContent) {
719
+ // TOOL RESULTS ONLY — add as dedicated tool-result message
720
+ const toolMessage = {
721
+ id: `tool-result-${Date.now()}`,
722
+ role: MESSAGE_ROLES.USER,
723
+ content: toolResultContent.trim(),
724
+ timestamp: new Date().toISOString(),
725
+ type: 'tool-result',
726
+ originalMessageCount: toolResults.length
727
+ };
728
+ await this.addMessageToConversation(agentId, toolMessage, false);
729
+ } else if (!toolResultContent && hasNonToolContent) {
730
+ // USER/INTER-AGENT ONLY — add as user message
731
+ const userMessage = {
732
+ id: `consolidated-${Date.now()}`,
733
+ role: MESSAGE_ROLES.USER,
734
+ content: consolidatedContent.trim(),
735
+ timestamp: new Date().toISOString(),
736
+ type: 'consolidated-input',
737
+ originalMessageCount: userMessages.length + interAgentMessages.length
738
+ };
739
+ await this.addMessageToConversation(agentId, userMessage, false);
740
+ } else if (toolResultContent && hasNonToolContent) {
741
+ // BOTH — merge into single message to avoid consecutive user messages
742
+ // (API requires strict user/assistant alternation)
743
+ // Put tool results first, then user content (user content is higher priority context)
744
+ const mergedContent = toolResultContent.trim() + '\n\n' + consolidatedContent.trim();
745
+ const mergedMessage = {
746
+ id: `consolidated-${Date.now()}`,
747
+ role: MESSAGE_ROLES.USER,
748
+ content: mergedContent,
749
+ timestamp: new Date().toISOString(),
750
+ type: 'consolidated-input',
751
+ originalMessageCount: allMessages.length
752
+ };
753
+ await this.addMessageToConversation(agentId, mergedMessage, false);
754
+ }
724
755
 
725
756
  // CRITICAL: Update conversation tracking when inter-agent messages are processed
726
757
  if (agent && interAgentMessages.length > 0) {
@@ -1604,9 +1635,12 @@ class AgentScheduler {
1604
1635
  sessionId: sessionId,
1605
1636
  platformProvided: platformProvided,
1606
1637
  onChunk: (chunk) => {
1638
+ // Normalize chunk to string — some providers (Ollama) send {content, type} objects
1639
+ const chunkText = typeof chunk === 'string' ? chunk : (chunk?.content || chunk?.text || String(chunk));
1640
+
1607
1641
  // Update flow progress if in flow execution
1608
1642
  if (flowProgress) {
1609
- flowProgress.charactersStreamed += chunk.length;
1643
+ flowProgress.charactersStreamed += chunkText.length;
1610
1644
  flowProgress.chunkCount++;
1611
1645
 
1612
1646
  // Broadcast flow progress every 500ms or 50 chunks
@@ -1622,7 +1656,7 @@ class AgentScheduler {
1622
1656
  type: 'stream_chunk',
1623
1657
  agentId,
1624
1658
  messageId: streamMessageId,
1625
- content: chunk,
1659
+ content: chunkText,
1626
1660
  timestamp: new Date().toISOString(),
1627
1661
  // Include flow context if present
1628
1662
  ...(flowContext && {
@@ -2089,7 +2123,10 @@ class AgentScheduler {
2089
2123
  // The scheduler's addMessageToConversation only pushes to conversation.messages.
2090
2124
  if (modelConversation.compactizedMessages) {
2091
2125
  const originalLength = modelConversation.messages.length;
2092
- const originalCount = modelConversation.originalMessageCountAtCompaction || originalLength;
2126
+ const compactedLength = modelConversation.compactizedMessages.length;
2127
+ // SAFETY: Use ?? compactedLength instead of || originalLength to prevent silent message loss
2128
+ // when watermark is null (see agentPool.getMessagesForAI for detailed explanation)
2129
+ const originalCount = modelConversation.originalMessageCountAtCompaction ?? compactedLength;
2093
2130
  if (originalLength > originalCount) {
2094
2131
  const newCount = originalLength - originalCount;
2095
2132
  const newMessages = modelConversation.messages.slice(-newCount);
@@ -2446,6 +2483,27 @@ class AgentScheduler {
2446
2483
  toolCount: toolExecutions.length,
2447
2484
  hasError: !!error
2448
2485
  });
2486
+
2487
+ // If any filesystem tool ran, broadcast updated artifacts to UI
2488
+ const hasFilesystemOps = toolExecutions.some(t => t.toolId === 'filesystem');
2489
+ if (hasFilesystemOps && !error) {
2490
+ try {
2491
+ const agent = this.agentPool.agents?.get(agentId);
2492
+ if (agent?.artifacts && Object.keys(agent.artifacts).length > 0) {
2493
+ this.webSocketManager.broadcastToSession(sessionId, {
2494
+ type: 'artifacts_updated',
2495
+ data: {
2496
+ agentId,
2497
+ artifacts: agent.artifacts,
2498
+ workingDirectory: agent.directoryAccess?.workingDirectory || ''
2499
+ }
2500
+ });
2501
+ console.log('[Artifacts] Broadcast to session:', sessionId, Object.keys(agent.artifacts).length, 'files');
2502
+ }
2503
+ } catch (e) {
2504
+ // Non-fatal — UI can still fetch via API
2505
+ }
2506
+ }
2449
2507
  }
2450
2508
  }
2451
2509
 
@@ -412,12 +412,12 @@ class MessageProcessor {
412
412
  });
413
413
 
414
414
  let result;
415
+ let toolInput = command.parameters; // Hoisted for artifact tracking access
415
416
  if (command.isAsync) {
416
417
  result = await this.executeAsyncTool(command, tool, context);
417
418
  } else {
418
419
  // Synchronous tool execution
419
420
  // If we have parameters object, use it. Otherwise parse the content.
420
- let toolInput = command.parameters;
421
421
 
422
422
  if (!toolInput && command.content) {
423
423
  // Content is a string, need to parse it using tool's parseParameters method
@@ -475,7 +475,16 @@ class MessageProcessor {
475
475
  }
476
476
 
477
477
  results.push(result);
478
-
478
+
479
+ // ── Artifact tracking (fire-and-forget) ─────────────────────
480
+ // After successful filesystem writes, persist artifact metadata
481
+ // on the agent object so the UI can display version history.
482
+ // Non-blocking: uses .catch() to avoid disrupting the tool pipeline.
483
+ if (result.status === 'completed' && command.toolId === TOOL_IDS.FILESYSTEM) {
484
+ this._trackArtifacts({ ...command, parameters: toolInput }, result, context)
485
+ .catch(e => this.logger?.warn?.('[Artifacts] tracking failed:', e.message));
486
+ }
487
+
479
488
  // Store in execution history
480
489
  const historyKey = `${context.agentId}-${Date.now()}`;
481
490
  this.executionHistory.set(historyKey, {
@@ -502,6 +511,105 @@ class MessageProcessor {
502
511
  return results;
503
512
  }
504
513
 
514
+ /**
515
+ * Track filesystem write/append operations as artifacts on the agent.
516
+ * Persists lightweight metadata (path, size, timestamp) and the content
517
+ * so the UI can show version history without re-parsing message content.
518
+ *
519
+ * The agent.artifacts map is persisted via the normal persistAgentState flow
520
+ * that already runs after tool execution in AgentScheduler.
521
+ *
522
+ * @param {Object} command - Tool command with parameters
523
+ * @param {Object} result - Execution result
524
+ * @param {Object} context - Execution context (agentId, projectDir)
525
+ * @private
526
+ */
527
+ async _trackArtifacts(command, result, context) {
528
+ try {
529
+ const agent = await this.agentPool?.getAgent?.(context.agentId);
530
+ if (!agent) {
531
+ console.log('[Artifacts] No agent found for', context.agentId);
532
+ return;
533
+ }
534
+
535
+ // Initialize artifacts map if needed: { [filePath]: { versions: [...] } }
536
+ if (!agent.artifacts) agent.artifacts = {};
537
+
538
+ const toolResult = result.result;
539
+ if (!toolResult?.success) {
540
+ console.log('[Artifacts] Tool result not successful:', { success: toolResult?.success, keys: Object.keys(toolResult || {}) });
541
+ return;
542
+ }
543
+
544
+ // Get the actions from the command parameters
545
+ // The AI may send actions at different levels depending on format:
546
+ // { parameters: { actions: [...] } } — parsed format
547
+ // { actions: [...] } — top-level format (common)
548
+ // { parameters: { type: 'write', ... } } — single action
549
+ const params = command.parameters || {};
550
+ const actions = params.actions || command.actions || (params.type ? [params] : []);
551
+ console.log('[Artifacts] Processing', actions.length, 'actions. Param keys:', Object.keys(params), 'cmd keys:', Object.keys(command));
552
+ const resultActions = toolResult.actions || [toolResult];
553
+
554
+ for (let i = 0; i < actions.length; i++) {
555
+ const action = actions[i];
556
+ const actionResult = resultActions[i] || {};
557
+ const type = action.type || action.action;
558
+
559
+ console.log(`[Artifacts] Action[${i}]:`, { type, filePath: action.filePath || action['file-path'], hasContent: !!action.content, contentLen: action.content?.length, actionKeys: Object.keys(action) });
560
+
561
+ if ((type === 'write' || type === 'append') && actionResult.success !== false) {
562
+ // AI uses various field names: filePath, file-path, outputPath, path
563
+ const rawPath = action.filePath || action['file-path'] || action.outputPath || action.path;
564
+ const content = action.content;
565
+ if (!rawPath || !content) {
566
+ console.log('[Artifacts] Skipped: missing filePath or content', { filePath: !!rawPath, content: !!content, actionKeys: Object.keys(action) });
567
+ continue;
568
+ }
569
+
570
+ // Use the resolved absolute path as key (prevents collisions for same-name files in different dirs)
571
+ // Fall back to raw path if fullPath not available
572
+ const absolutePath = actionResult.fullPath || rawPath;
573
+ const artifactKey = absolutePath.replace(/\\/g, '/');
574
+
575
+ // Relative display path (strip working directory prefix)
576
+ const wd = (context.projectDir || '').replace(/\\/g, '/');
577
+ const displayPath = wd && artifactKey.startsWith(wd + '/')
578
+ ? artifactKey.slice(wd.length + 1)
579
+ : artifactKey;
580
+
581
+ const version = {
582
+ id: `v-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
583
+ content,
584
+ timestamp: result.timestamp || new Date().toISOString(),
585
+ action: type,
586
+ size: Buffer.byteLength(content, 'utf8'),
587
+ fullPath: absolutePath,
588
+ };
589
+
590
+ if (!agent.artifacts[artifactKey]) {
591
+ agent.artifacts[artifactKey] = { displayPath, versions: [] };
592
+ }
593
+
594
+ // Deduplicate: skip if content identical to latest
595
+ const versions = agent.artifacts[artifactKey].versions;
596
+ const latest = versions[versions.length - 1];
597
+ if (latest && latest.content === content) continue;
598
+
599
+ versions.push(version);
600
+ console.log('[Artifacts] Tracked:', displayPath, 'v' + versions.length, '(' + version.size + ' bytes)');
601
+
602
+ // Cap at 50 versions per file to keep state reasonable
603
+ if (versions.length > 50) {
604
+ versions.splice(0, versions.length - 50);
605
+ }
606
+ }
607
+ }
608
+ } catch (e) {
609
+ this.logger?.warn?.('Artifact tracking failed (non-fatal):', e.message);
610
+ }
611
+ }
612
+
505
613
  /**
506
614
  * Execute async tool
507
615
  * @param {Object} command - Tool command
package/src/index.js CHANGED
@@ -32,8 +32,6 @@ import { ToolsRegistry } from './tools/baseTool.js';
32
32
  import AgentDelayTool from './tools/agentDelayTool.js';
33
33
  import TerminalTool from './tools/terminalTool.js';
34
34
  import FileSystemTool from './tools/fileSystemTool.js';
35
- // BrowserTool is DEPRECATED - use WebTool instead
36
- // import BrowserTool from './tools/browserTool.js';
37
35
  import JobDoneTool from './tools/jobDoneTool.js';
38
36
  import AgentCommunicationTool from './tools/agentCommunicationTool.js';
39
37
  import TaskManagerTool from './tools/taskManagerTool.js';
@@ -486,11 +484,6 @@ class LoxiaApplication {
486
484
  this.logger.info('ToolsRegistry set for Help Tool');
487
485
  }
488
486
 
489
- // NOTE: BrowserTool is DEPRECATED as of December 2024
490
- // Use WebTool (toolId: "web") for all browser automation tasks
491
- // The Browser tool registration has been removed - WebTool provides
492
- // equivalent functionality with better architecture (singleton browser instance)
493
-
494
487
  // Set AgentPool dependency for AgentDelayTool
495
488
  const agentDelayTool = this.toolsRegistry.getTool('agentdelay');
496
489
  if (agentDelayTool && typeof agentDelayTool.setAgentPool === 'function') {
@@ -611,8 +604,8 @@ class LoxiaApplication {
611
604
  if (interfaceConfig.web?.enabled !== false) {
612
605
  // Read port from environment variables (set by CLI) or use config defaults
613
606
  const webPort = parseInt(process.env.LOXIA_PORT || process.env.PORT, 10) || 8080;
614
- // Use env var, then config, then 127.0.0.1 (avoid 'localhost' due to IPv6 issues)
615
- const webHost = process.env.LOXIA_HOST || interfaceConfig.web?.host || '127.0.0.1';
607
+ // Use env var, then config, then 0.0.0.0 (accept connections from all interfaces)
608
+ const webHost = process.env.LOXIA_HOST || interfaceConfig.web?.host || '0.0.0.0';
616
609
 
617
610
  const webConfig = {
618
611
  ...interfaceConfig.web,
@@ -848,9 +841,9 @@ class LoxiaApplication {
848
841
  this.logger?.info('Models service retries cancelled');
849
842
  }
850
843
 
851
- // Close Puppeteer browsers (webTool, browserTool) — they hold DevTools ports
844
+ // Close Puppeteer browser (webTool) — it holds DevTools ports
852
845
  if (this.toolsRegistry) {
853
- for (const toolId of ['web', 'browser']) {
846
+ for (const toolId of ['web']) {
854
847
  try {
855
848
  const tool = this.toolsRegistry.getTool(toolId);
856
849
  if (tool?.cleanup) {
@@ -863,6 +856,29 @@ class LoxiaApplication {
863
856
  }
864
857
  }
865
858
 
859
+ // Kill ALL running terminal processes across all agents
860
+ if (this.toolsRegistry) {
861
+ try {
862
+ const terminalTool = this.toolsRegistry.getTool('terminal');
863
+ if (terminalTool?.commandTracker) {
864
+ let killed = 0;
865
+ for (const [cmdId, cmdInfo] of terminalTool.commandTracker) {
866
+ if (cmdInfo.process && cmdInfo.state === 'RUNNING') {
867
+ try {
868
+ cmdInfo.process.kill('SIGTERM');
869
+ killed++;
870
+ } catch { /* already dead */ }
871
+ }
872
+ }
873
+ if (killed > 0) {
874
+ this.logger?.info(`Killed ${killed} running terminal process(es) on shutdown`);
875
+ }
876
+ }
877
+ } catch (error) {
878
+ this.logger?.warn('Failed to cleanup terminal processes', { error: error.message });
879
+ }
880
+ }
881
+
866
882
  // Shutdown interfaces (web server, visual editor, WS connections)
867
883
  for (const [type, interface_] of this.interfaces) {
868
884
  try {
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Unit tests for the /api/images/:sessionId/:filename endpoint.
3
+ *
4
+ * Covers:
5
+ * - Path traversal rejection
6
+ * - Serving images from project dir (with and without active session)
7
+ * - Serving images from OS temp dir
8
+ * - Serving images from agent working directories
9
+ * - 404 when image doesn't exist anywhere
10
+ * - Content-type correctness for different extensions
11
+ */
12
+
13
+ import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
14
+ import express from 'express';
15
+ import { createServer } from 'http';
16
+ import path from 'path';
17
+ import fs from 'fs/promises';
18
+ import os from 'os';
19
+
20
+ // ── Minimal WebServer stand-in ──────────────────────────────────
21
+ // We only mount the single route under test so the full webServer
22
+ // dependency tree isn't required.
23
+
24
+ const HTTP_STATUS = { BAD_REQUEST: 400, NOT_FOUND: 404, INTERNAL_SERVER_ERROR: 500, FORBIDDEN: 403 };
25
+
26
+ function buildApp({ sessions, orchestrator, projectDir, logger }) {
27
+ const app = express();
28
+
29
+ // Replicate the exact route from webServer.js
30
+ app.get('/api/images/:sessionId/:filename', async (req, res) => {
31
+ try {
32
+ const { sessionId, filename } = req.params;
33
+
34
+ const normalizedFilename = path.basename(filename);
35
+ if (normalizedFilename !== filename || filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
36
+ return res.status(HTTP_STATUS.BAD_REQUEST).json({ success: false, error: 'Invalid filename' });
37
+ }
38
+
39
+ const session = sessions.get(sessionId);
40
+ const effectiveProjectDir = session?.projectDir || projectDir || process.cwd();
41
+
42
+ let imagePath = null;
43
+ const searchPaths = [
44
+ path.join(effectiveProjectDir, 'images', normalizedFilename),
45
+ path.join('/tmp/loxia-images', sessionId, normalizedFilename),
46
+ path.join('/tmp/loxia-images', normalizedFilename),
47
+ path.join(process.env.TEMP || process.env.TMP || '/tmp', 'loxia-images', sessionId, normalizedFilename),
48
+ path.join(process.env.TEMP || process.env.TMP || '/tmp', 'loxia-images', normalizedFilename)
49
+ ];
50
+
51
+ if (orchestrator?.agentPool) {
52
+ try {
53
+ const agents = await orchestrator.agentPool.getAllAgents();
54
+ for (const agent of agents) {
55
+ if (agent.directoryAccess?.workingDirectory) {
56
+ searchPaths.push(path.join(agent.directoryAccess.workingDirectory, 'images', normalizedFilename));
57
+ }
58
+ }
59
+ } catch (_) { /* ignore */ }
60
+ }
61
+
62
+ for (const sp of searchPaths) {
63
+ try {
64
+ const stats = await fs.stat(sp);
65
+ if (stats.isFile()) { imagePath = sp; break; }
66
+ } catch (_) { continue; }
67
+ }
68
+
69
+ if (!imagePath) {
70
+ return res.status(HTTP_STATUS.NOT_FOUND).json({ success: false, error: 'Image not found' });
71
+ }
72
+
73
+ res.sendFile(imagePath, (err) => {
74
+ if (err && !res.headersSent) {
75
+ res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ success: false, error: 'Failed to serve image' });
76
+ }
77
+ });
78
+ } catch (error) {
79
+ if (!res.headersSent) {
80
+ res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ success: false, error: error.message });
81
+ }
82
+ }
83
+ });
84
+
85
+ return app;
86
+ }
87
+
88
+ // ── Helpers ─────────────────────────────────────────────────────
89
+
90
+ async function fetch(url) {
91
+ const resp = await globalThis.fetch(url);
92
+ const body = resp.headers.get('content-type')?.includes('json')
93
+ ? await resp.json()
94
+ : await resp.text();
95
+ return { status: resp.status, body, headers: resp.headers };
96
+ }
97
+
98
+ // ── Test suite ──────────────────────────────────────────────────
99
+
100
+ describe('/api/images/:sessionId/:filename', () => {
101
+ let server, baseUrl;
102
+ let tmpProjectDir;
103
+ let tmpAgentDir;
104
+ const sessions = new Map();
105
+ const sessionId = 'test-session-abc123';
106
+
107
+ beforeAll(async () => {
108
+ // Create temp directories that simulate project and agent dirs
109
+ tmpProjectDir = path.join(os.tmpdir(), `loxia-img-test-proj-${Date.now()}`);
110
+ tmpAgentDir = path.join(os.tmpdir(), `loxia-img-test-agent-${Date.now()}`);
111
+ await fs.mkdir(path.join(tmpProjectDir, 'images'), { recursive: true });
112
+ await fs.mkdir(path.join(tmpAgentDir, 'images'), { recursive: true });
113
+
114
+ // Also create the OS temp search path
115
+ const osTempPath = path.join(process.env.TEMP || process.env.TMP || '/tmp', 'loxia-images', sessionId);
116
+ await fs.mkdir(osTempPath, { recursive: true });
117
+
118
+ // Write test images
119
+ const pngHeader = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); // PNG magic bytes
120
+ await fs.writeFile(path.join(tmpProjectDir, 'images', 'project-image.png'), pngHeader);
121
+ await fs.writeFile(path.join(tmpAgentDir, 'images', 'agent-image.png'), pngHeader);
122
+ await fs.writeFile(path.join(osTempPath, 'temp-image.png'), pngHeader);
123
+
124
+ // Register session
125
+ sessions.set(sessionId, { projectDir: tmpProjectDir });
126
+
127
+ const orchestrator = {
128
+ agentPool: {
129
+ getAllAgents: async () => [
130
+ { directoryAccess: { workingDirectory: tmpAgentDir } }
131
+ ]
132
+ }
133
+ };
134
+
135
+ const app = buildApp({
136
+ sessions,
137
+ orchestrator,
138
+ projectDir: tmpProjectDir,
139
+ logger: { info() {}, warn() {}, error() {}, debug() {} }
140
+ });
141
+
142
+ server = createServer(app);
143
+ await new Promise(resolve => server.listen(0, resolve));
144
+ const port = server.address().port;
145
+ baseUrl = `http://localhost:${port}`;
146
+ });
147
+
148
+ afterAll(async () => {
149
+ server?.close();
150
+ await fs.rm(tmpProjectDir, { recursive: true, force: true }).catch(() => {});
151
+ await fs.rm(tmpAgentDir, { recursive: true, force: true }).catch(() => {});
152
+ const osTempPath = path.join(process.env.TEMP || process.env.TMP || '/tmp', 'loxia-images', sessionId);
153
+ await fs.rm(osTempPath, { recursive: true, force: true }).catch(() => {});
154
+ });
155
+
156
+ // ── Security ──────────────────────────────────────────────
157
+
158
+ it('rejects path traversal with ..', async () => {
159
+ const { status, body } = await fetch(`${baseUrl}/api/images/${sessionId}/..%2F..%2Fetc%2Fpasswd`);
160
+ expect(status).toBe(400);
161
+ expect(body.error).toBe('Invalid filename');
162
+ });
163
+
164
+ it('rejects filenames with slashes', async () => {
165
+ const { status, body } = await fetch(`${baseUrl}/api/images/${sessionId}/sub%2Ffile.png`);
166
+ expect(status).toBe(400);
167
+ expect(body.error).toBe('Invalid filename');
168
+ });
169
+
170
+ it('rejects filenames with backslashes', async () => {
171
+ const { status, body } = await fetch(`${baseUrl}/api/images/${sessionId}/sub%5Cfile.png`);
172
+ expect(status).toBe(400);
173
+ expect(body.error).toBe('Invalid filename');
174
+ });
175
+
176
+ // ── Serving from project directory (session active) ───────
177
+
178
+ it('serves image from project/images/ when session is active', async () => {
179
+ const { status, headers } = await fetch(`${baseUrl}/api/images/${sessionId}/project-image.png`);
180
+ expect(status).toBe(200);
181
+ expect(headers.get('content-type')).toMatch(/image|octet/);
182
+ });
183
+
184
+ // ── Serving without active session (session expired) ──────
185
+
186
+ it('still serves image when session has expired (falls back to cwd)', async () => {
187
+ // Use a sessionId that doesn't exist in the map
188
+ // Image is in the OS temp dir under the expired sessionId
189
+ const expiredSessionId = sessionId; // temp image was created under this id
190
+ // Remove session temporarily
191
+ sessions.delete(sessionId);
192
+
193
+ // The image is in OS temp: $TEMP/loxia-images/test-session-abc123/temp-image.png
194
+ const { status } = await fetch(`${baseUrl}/api/images/${expiredSessionId}/temp-image.png`);
195
+ expect(status).toBe(200);
196
+
197
+ // Restore session for other tests
198
+ sessions.set(sessionId, { projectDir: tmpProjectDir });
199
+ });
200
+
201
+ // ── Serving from agent working directory ──────────────────
202
+
203
+ it('finds image in agent working directory', async () => {
204
+ const { status } = await fetch(`${baseUrl}/api/images/${sessionId}/agent-image.png`);
205
+ expect(status).toBe(200);
206
+ });
207
+
208
+ // ── Serving from OS temp directory ────────────────────────
209
+
210
+ it('finds image in OS temp loxia-images directory', async () => {
211
+ const { status } = await fetch(`${baseUrl}/api/images/${sessionId}/temp-image.png`);
212
+ expect(status).toBe(200);
213
+ });
214
+
215
+ // ── 404 for missing images ────────────────────────────────
216
+
217
+ it('returns 404 for non-existent image', async () => {
218
+ const { status, body } = await fetch(`${baseUrl}/api/images/${sessionId}/does-not-exist.png`);
219
+ expect(status).toBe(404);
220
+ expect(body.error).toBe('Image not found');
221
+ });
222
+
223
+ it('returns 404 for unknown session + missing file', async () => {
224
+ const { status, body } = await fetch(`${baseUrl}/api/images/unknown-session-xyz/nope.png`);
225
+ expect(status).toBe(404);
226
+ expect(body.error).toBe('Image not found');
227
+ });
228
+ });