onbuzz 4.9.13 → 4.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (451) hide show
  1. package/node_modules/glob/README.md +31 -5
  2. package/node_modules/glob/dist/commonjs/glob.d.ts +8 -0
  3. package/node_modules/glob/dist/commonjs/glob.d.ts.map +1 -1
  4. package/node_modules/glob/dist/commonjs/glob.js +2 -1
  5. package/node_modules/glob/dist/commonjs/glob.js.map +1 -1
  6. package/node_modules/glob/dist/commonjs/index.min.js +3 -3
  7. package/node_modules/glob/dist/commonjs/index.min.js.map +4 -4
  8. package/node_modules/glob/dist/commonjs/pattern.d.ts +3 -0
  9. package/node_modules/glob/dist/commonjs/pattern.d.ts.map +1 -1
  10. package/node_modules/glob/dist/commonjs/pattern.js +4 -0
  11. package/node_modules/glob/dist/commonjs/pattern.js.map +1 -1
  12. package/node_modules/glob/dist/esm/glob.d.ts +8 -0
  13. package/node_modules/glob/dist/esm/glob.d.ts.map +1 -1
  14. package/node_modules/glob/dist/esm/glob.js +2 -1
  15. package/node_modules/glob/dist/esm/glob.js.map +1 -1
  16. package/node_modules/glob/dist/esm/index.min.js +3 -3
  17. package/node_modules/glob/dist/esm/index.min.js.map +4 -4
  18. package/node_modules/glob/dist/esm/pattern.d.ts +3 -0
  19. package/node_modules/glob/dist/esm/pattern.d.ts.map +1 -1
  20. package/node_modules/glob/dist/esm/pattern.js +4 -0
  21. package/node_modules/glob/dist/esm/pattern.js.map +1 -1
  22. package/node_modules/{@isaacs → glob/node_modules}/balanced-match/README.md +7 -10
  23. package/node_modules/{@isaacs → glob/node_modules}/balanced-match/package.json +7 -18
  24. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/README.md +3 -6
  25. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.js +6 -4
  26. package/node_modules/glob/node_modules/brace-expansion/dist/commonjs/index.js.map +1 -0
  27. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.js +6 -4
  28. package/node_modules/glob/node_modules/brace-expansion/dist/esm/index.js.map +1 -0
  29. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/package.json +11 -7
  30. package/node_modules/glob/node_modules/minimatch/README.md +76 -1
  31. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts +1 -1
  32. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts.map +1 -1
  33. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.js.map +1 -1
  34. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts +4 -2
  35. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts.map +1 -1
  36. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js +309 -55
  37. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js.map +1 -1
  38. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.d.ts.map +1 -1
  39. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js +2 -4
  40. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js.map +1 -1
  41. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts +1 -1
  42. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts.map +1 -1
  43. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js +4 -4
  44. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js.map +1 -1
  45. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts +81 -1
  46. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts.map +1 -1
  47. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js +232 -134
  48. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js.map +1 -1
  49. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts +1 -1
  50. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts.map +1 -1
  51. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js +8 -8
  52. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js.map +1 -1
  53. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts +1 -1
  54. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts.map +1 -1
  55. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.js.map +1 -1
  56. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts +4 -2
  57. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts.map +1 -1
  58. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js +309 -55
  59. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js.map +1 -1
  60. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.d.ts.map +1 -1
  61. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js +2 -4
  62. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js.map +1 -1
  63. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts +1 -1
  64. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts.map +1 -1
  65. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js +4 -4
  66. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js.map +1 -1
  67. package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts +81 -1
  68. package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts.map +1 -1
  69. package/node_modules/glob/node_modules/minimatch/dist/esm/index.js +232 -134
  70. package/node_modules/glob/node_modules/minimatch/dist/esm/index.js.map +1 -1
  71. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts +1 -1
  72. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts.map +1 -1
  73. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js +8 -8
  74. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js.map +1 -1
  75. package/node_modules/glob/node_modules/minimatch/package.json +17 -11
  76. package/node_modules/glob/package.json +10 -13
  77. package/node_modules/minipass/LICENSE.md +55 -0
  78. package/node_modules/minipass/dist/commonjs/index.d.ts +12 -16
  79. package/node_modules/minipass/dist/commonjs/index.d.ts.map +1 -1
  80. package/node_modules/minipass/dist/commonjs/index.js +13 -3
  81. package/node_modules/minipass/dist/commonjs/index.js.map +1 -1
  82. package/node_modules/minipass/dist/esm/index.d.ts +12 -16
  83. package/node_modules/minipass/dist/esm/index.d.ts.map +1 -1
  84. package/node_modules/minipass/dist/esm/index.js +3 -1
  85. package/node_modules/minipass/dist/esm/index.js.map +1 -1
  86. package/node_modules/minipass/package.json +9 -14
  87. package/node_modules/path-scurry/node_modules/lru-cache/README.md +96 -10
  88. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel-browser.d.ts.map +1 -0
  89. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel-browser.js.map +1 -0
  90. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.d.ts +5 -0
  91. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.js +7 -0
  92. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.d.ts +1400 -0
  93. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.d.ts.map +1 -0
  94. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js +1726 -0
  95. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js.map +1 -0
  96. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js +2 -0
  97. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js.map +7 -0
  98. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.d.ts +12 -0
  99. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.d.ts.map +1 -0
  100. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js +10 -0
  101. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js.map +1 -0
  102. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.cjs.map +1 -0
  103. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.d.cts.map +1 -0
  104. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.d.ts +5 -0
  105. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.js +7 -0
  106. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts +109 -32
  107. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts.map +1 -1
  108. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js +334 -197
  109. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js.map +1 -1
  110. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js +1 -1
  111. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js.map +4 -4
  112. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel-node.d.ts.map +1 -0
  113. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel-node.js.map +1 -0
  114. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.d.ts +5 -0
  115. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.js +9 -0
  116. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.d.ts +1400 -0
  117. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.d.ts.map +1 -0
  118. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js +1726 -0
  119. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js.map +1 -0
  120. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js +2 -0
  121. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js.map +7 -0
  122. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.d.ts +12 -0
  123. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.d.ts.map +1 -0
  124. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js +10 -0
  125. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js.map +1 -0
  126. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.d.ts +12 -0
  127. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.d.ts.map +1 -0
  128. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js +10 -0
  129. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js.map +1 -0
  130. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel-browser.d.ts.map +1 -0
  131. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel-browser.js.map +1 -0
  132. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.d.ts +5 -0
  133. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.js +4 -0
  134. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.d.ts +1400 -0
  135. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.d.ts.map +1 -0
  136. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js +1722 -0
  137. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js.map +1 -0
  138. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js +2 -0
  139. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js.map +7 -0
  140. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.d.ts +12 -0
  141. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.d.ts.map +1 -0
  142. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js +7 -0
  143. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js.map +1 -0
  144. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.d.mts.map +1 -0
  145. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.mjs.map +1 -0
  146. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.d.ts +5 -0
  147. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.js +19 -0
  148. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts +109 -32
  149. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts.map +1 -1
  150. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js +333 -196
  151. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js.map +1 -1
  152. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js +1 -1
  153. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js.map +4 -4
  154. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel-node.d.ts.map +1 -0
  155. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel-node.js.map +1 -0
  156. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.d.ts +5 -0
  157. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.js +6 -0
  158. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.d.ts +1400 -0
  159. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.d.ts.map +1 -0
  160. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js +1722 -0
  161. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js.map +1 -0
  162. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js +2 -0
  163. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js.map +7 -0
  164. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.d.ts +12 -0
  165. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.d.ts.map +1 -0
  166. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js +7 -0
  167. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js.map +1 -0
  168. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.d.ts +12 -0
  169. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.d.ts.map +1 -0
  170. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js +7 -0
  171. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js.map +1 -0
  172. package/node_modules/path-scurry/node_modules/lru-cache/package.json +71 -18
  173. package/node_modules/path-scurry/package.json +8 -24
  174. package/package.json +1 -1
  175. package/scripts/debug-balance-probe.mjs +35 -35
  176. package/scripts/push-image.sh +43 -43
  177. package/scripts/setup-acr.sh +65 -65
  178. package/scripts/verify-optional-deps.js +96 -1
  179. package/src/__tests__/composioCliFlags.test.js +239 -239
  180. package/src/analyzers/CSSAnalyzer.js +298 -297
  181. package/src/analyzers/ConfigValidator.js +691 -690
  182. package/src/analyzers/ESLintAnalyzer.js +320 -320
  183. package/src/analyzers/JavaScriptAnalyzer.js +260 -261
  184. package/src/analyzers/PrettierFormatter.js +246 -247
  185. package/src/analyzers/PythonAnalyzer.js +283 -283
  186. package/src/analyzers/SecurityAnalyzer.js +729 -729
  187. package/src/analyzers/SparrowAnalyzer.js +341 -341
  188. package/src/analyzers/TypeScriptAnalyzer.js +247 -247
  189. package/src/analyzers/__tests__/CSSAnalyzer.test.js +41 -41
  190. package/src/analyzers/__tests__/ConfigValidator.test.js +362 -362
  191. package/src/analyzers/__tests__/JavaScriptAnalyzer.test.js +40 -40
  192. package/src/analyzers/__tests__/PythonAnalyzer.test.js +205 -208
  193. package/src/analyzers/__tests__/SecurityAnalyzer.test.js +303 -303
  194. package/src/analyzers/__tests__/TypeScriptAnalyzer.test.js +187 -187
  195. package/src/analyzers/codeCloneDetector/analyzer.js +344 -344
  196. package/src/analyzers/codeCloneDetector/detector.js +250 -250
  197. package/src/analyzers/codeCloneDetector/index.js +194 -192
  198. package/src/analyzers/codeCloneDetector/parser.js +199 -199
  199. package/src/core/__tests__/agentPool.test.js +866 -866
  200. package/src/core/__tests__/agentPoolAutoResume.test.js +209 -209
  201. package/src/core/__tests__/agentPoolWakeOnMessage.test.js +315 -315
  202. package/src/core/__tests__/agentScheduler.emptyResponseChatStall.test.js +213 -213
  203. package/src/core/__tests__/agentScheduler.errorCategorisation.test.js +246 -246
  204. package/src/core/__tests__/agentScheduler.firstChunkTimeout.test.js +138 -138
  205. package/src/core/__tests__/agentScheduler.modeTransitions.test.js +233 -233
  206. package/src/core/__tests__/agentScheduler.nativePromptPick.test.js +319 -319
  207. package/src/core/__tests__/agentScheduler.taskLifecycleInstruction.test.js +78 -78
  208. package/src/core/__tests__/agentScheduler.visualizer.test.js +258 -258
  209. package/src/core/__tests__/flowCheckpointStore.test.js +140 -140
  210. package/src/core/__tests__/flowEndToEnd.test.js +565 -565
  211. package/src/core/__tests__/flowFieldMapping.test.js +188 -189
  212. package/src/core/__tests__/flowLintClientMirror.test.js +96 -98
  213. package/src/core/__tests__/flowSavePayload.test.js +170 -169
  214. package/src/core/__tests__/flowTemplates.test.js +311 -311
  215. package/src/core/__tests__/flowVersionStore.test.js +123 -123
  216. package/src/core/__tests__/messageProcessor.test.js +669 -669
  217. package/src/core/__tests__/stateManager.test.js +0 -1
  218. package/src/core/agentPool.js +2474 -2475
  219. package/src/core/agentScheduler.js +1 -4
  220. package/src/core/contextManager.js +708 -708
  221. package/src/core/flowExecutor.js +1510 -1510
  222. package/src/core/flowFieldMapping.js +136 -138
  223. package/src/core/messageProcessor.js +953 -954
  224. package/src/core/orchestrator.js +593 -595
  225. package/src/core/stateManager.js +1765 -1752
  226. package/src/index.js +1221 -1221
  227. package/src/interfaces/__tests__/archivedAgentDelete.test.js +207 -207
  228. package/src/interfaces/__tests__/bulkAgentRoute.test.js +361 -361
  229. package/src/interfaces/__tests__/imageServing.test.js +228 -228
  230. package/src/interfaces/__tests__/remoteSessionAuth.test.js +308 -308
  231. package/src/interfaces/__tests__/videoJobsRoutes.test.js +178 -179
  232. package/src/interfaces/__tests__/webServer.marketplace.test.js +629 -629
  233. package/src/interfaces/schedulerRoutes.js +50 -50
  234. package/src/interfaces/terminal/__tests__/smoke/connection.test.js +341 -350
  235. package/src/interfaces/terminal/__tests__/smoke/enhancements.test.js +156 -156
  236. package/src/interfaces/terminal/__tests__/smoke/imports.test.js +325 -330
  237. package/src/interfaces/terminal/__tests__/smoke/tools.test.js +385 -388
  238. package/src/interfaces/terminal/api/session.js +265 -266
  239. package/src/interfaces/terminal/api/websocket.js +496 -497
  240. package/src/interfaces/terminal/components/AgentCreator.js +691 -705
  241. package/src/interfaces/terminal/components/AgentEditor.js +676 -678
  242. package/src/interfaces/terminal/components/AgentSwitcher.js +331 -330
  243. package/src/interfaces/terminal/components/ErrorPanel.js +263 -264
  244. package/src/interfaces/terminal/components/Header.js +28 -28
  245. package/src/interfaces/terminal/components/Layout.js +598 -603
  246. package/src/interfaces/terminal/components/MessageList.js +280 -281
  247. package/src/interfaces/terminal/components/SettingsPanel.js +410 -415
  248. package/src/interfaces/terminal/components/StatusBar.js +2 -0
  249. package/src/interfaces/terminal/index.js +168 -168
  250. package/src/interfaces/terminal/state/useAgentControl.js +496 -496
  251. package/src/interfaces/terminal/state/useAgents.js +537 -537
  252. package/src/interfaces/terminal/state/useMessages.js +629 -630
  253. package/src/interfaces/terminal/state/useTools.js +554 -554
  254. package/src/interfaces/terminal/utils/debugLogger.js +44 -44
  255. package/src/interfaces/terminal/utils/settingsStorage.js +232 -232
  256. package/src/interfaces/webServer.js +7578 -7579
  257. package/src/interfaces/webServer.js.bak +7046 -7046
  258. package/src/modules/fileExplorer/__tests__/zipDownload.test.js +237 -237
  259. package/src/modules/fileExplorer/controller.js +470 -469
  260. package/src/modules/fileExplorer/routes.js +285 -286
  261. package/src/modules/widget/__tests__/isDisabled.test.js +41 -41
  262. package/src/modules/widget/__tests__/routes.test.js +677 -678
  263. package/src/modules/widget/__tests__/runtime.test.js +401 -401
  264. package/src/modules/widget/__tests__/versioning.test.js +309 -309
  265. package/src/modules/widget/__tests__/webComponentRuntime.test.js +565 -565
  266. package/src/modules/widget/__tests__/widgetTool.test.js +316 -316
  267. package/src/modules/widget/routes.js +435 -435
  268. package/src/modules/widget/runtime/bundle.js +640 -640
  269. package/src/modules/widget/runtime/webComponentBundle.js +470 -470
  270. package/src/modules/widget/schema.js +182 -181
  271. package/src/modules/widget/widgetTool.js +1389 -1389
  272. package/src/services/__tests__/agentActivityService.test.js +401 -402
  273. package/src/services/__tests__/benchmarkService.test.js +184 -184
  274. package/src/services/__tests__/contextInjectionService.test.js +246 -246
  275. package/src/services/__tests__/conversationQuery.test.js +721 -723
  276. package/src/services/__tests__/credentialVault.test.js +469 -469
  277. package/src/services/__tests__/discordService.integration.test.js +638 -639
  278. package/src/services/__tests__/flowContextService.test.js +590 -590
  279. package/src/services/__tests__/memoryService.test.js +1 -1
  280. package/src/services/__tests__/messageSource.test.js +380 -380
  281. package/src/services/__tests__/modelRouterNaming.test.js +111 -111
  282. package/src/services/__tests__/projectDetector.test.js +34 -34
  283. package/src/services/__tests__/promptService.test.js +242 -242
  284. package/src/services/__tests__/telegramService.test.js +941 -941
  285. package/src/services/__tests__/tokenCountingService.test.js +48 -48
  286. package/src/services/agentActivityService.js +419 -420
  287. package/src/services/aiService.js +2997 -3001
  288. package/src/services/apiKeyManager.js +359 -359
  289. package/src/services/benchmarkService.js +196 -196
  290. package/src/services/codebaseKnowledgeService.js +2 -2
  291. package/src/services/composioService.js +738 -738
  292. package/src/services/conversationCompactionService.js +1258 -1257
  293. package/src/services/credentialVault.js +685 -685
  294. package/src/services/discordService.js +792 -793
  295. package/src/services/embeddings/__tests__/azureCustomProvider.test.js +232 -232
  296. package/src/services/embeddings/__tests__/embeddingService.test.js +417 -417
  297. package/src/services/embeddings/__tests__/localProvider.test.js +263 -263
  298. package/src/services/embeddings/autoRecall.js +218 -219
  299. package/src/services/embeddings/indexers/__tests__/agentIndexer.test.js +232 -232
  300. package/src/services/embeddings/indexers/__tests__/memoryIndexer.test.js +418 -418
  301. package/src/services/embeddings/indexers/__tests__/reminisceIndexer.test.js +356 -357
  302. package/src/services/embeddings/indexers/__tests__/skillsIndexer.test.js +145 -145
  303. package/src/services/embeddings/indexers/__tests__/taskIndexer.test.js +146 -146
  304. package/src/services/embeddings/indexers/composioIndexer.js +279 -279
  305. package/src/services/embeddings/providerInterface.js +206 -206
  306. package/src/services/embeddings/providers/localProvider.js +11 -7
  307. package/src/services/embeddings/providers/openaiProvider.js +101 -101
  308. package/src/services/embeddings/vectorStore/inMemoryJsonStore.js +356 -356
  309. package/src/services/errorHandler.js +809 -809
  310. package/src/services/flowContextService.js +586 -586
  311. package/src/services/grounding/MockAdapter.js +125 -125
  312. package/src/services/modelRouterService.js +26 -31
  313. package/src/services/modelsService.js +322 -322
  314. package/src/services/ollamaService.js +452 -452
  315. package/src/services/projectDetector.js +403 -404
  316. package/src/services/promptService.js +418 -418
  317. package/src/services/qualityInspector.js +795 -795
  318. package/src/services/scheduleService.js +726 -726
  319. package/src/services/serviceRegistry.js +386 -386
  320. package/src/services/telegrafBot.js +174 -174
  321. package/src/services/telegramService.js +1972 -1972
  322. package/src/services/visualEditorBridge.js +1033 -1033
  323. package/src/services/visualEditorServer.js +1769 -1774
  324. package/src/services/whatsappService.js +667 -668
  325. package/src/tools/__tests__/agentCommunicationTool.findAgent.test.js +226 -226
  326. package/src/tools/__tests__/agentCommunicationTool.test.js +3 -3
  327. package/src/tools/__tests__/agentDelayTool.test.js +342 -342
  328. package/src/tools/__tests__/baseTool.test.js +3 -3
  329. package/src/tools/__tests__/codeMapTool.test.js +915 -915
  330. package/src/tools/__tests__/fileContentReplaceTool.test.js +309 -309
  331. package/src/tools/__tests__/fileTreeTool.test.js +274 -274
  332. package/src/tools/__tests__/filesystemTool.test.js +815 -815
  333. package/src/tools/__tests__/foundryWebSearchTool.test.js +252 -252
  334. package/src/tools/__tests__/imageTool.validator.test.js +194 -194
  335. package/src/tools/__tests__/jobDoneTool.test.js +580 -581
  336. package/src/tools/__tests__/memoryTool.forgetStale.test.js +272 -272
  337. package/src/tools/__tests__/memoryTool.reminisce.test.js +2 -2
  338. package/src/tools/__tests__/memoryTool.reminisceSemanticSearch.test.js +301 -301
  339. package/src/tools/__tests__/memoryTool.semanticSearch.test.js +405 -405
  340. package/src/tools/__tests__/memoryTool.teamPool.test.js +293 -293
  341. package/src/tools/__tests__/memoryTool.test.js +1 -1
  342. package/src/tools/__tests__/seekTool.test.js +282 -282
  343. package/src/tools/__tests__/skillsTool.search.test.js +164 -164
  344. package/src/tools/__tests__/skillsTool.test.js +226 -226
  345. package/src/tools/__tests__/staticAnalysisTool.test.js +509 -509
  346. package/src/tools/__tests__/taskManagerTool.discipline.test.js +137 -137
  347. package/src/tools/__tests__/taskManagerTool.search.test.js +143 -143
  348. package/src/tools/__tests__/taskManagerTool.test.js +866 -866
  349. package/src/tools/__tests__/terminalTool.test.js +448 -448
  350. package/src/tools/__tests__/toolShapeForgiveness.test.js +259 -260
  351. package/src/tools/__tests__/userPromptTool.test.js +297 -297
  352. package/src/tools/__tests__/videoTool.jobs.test.js +147 -147
  353. package/src/tools/__tests__/webTool.e2e.test.js +609 -603
  354. package/src/tools/__tests__/webTool.unit.test.js +195 -195
  355. package/src/tools/__tests__/webTool.visionModel.test.js +75 -75
  356. package/src/tools/agentCommunicationTool.js +8 -10
  357. package/src/tools/agentDelayTool.js +496 -497
  358. package/src/tools/asyncToolManager.js +602 -603
  359. package/src/tools/baseTool.js +12 -11
  360. package/src/tools/cloneDetectionTool.js +576 -581
  361. package/src/tools/codeMapTool.js +0 -6
  362. package/src/tools/composioTool.js +617 -617
  363. package/src/tools/dependencyResolverTool.js +1211 -1212
  364. package/src/tools/desktop/DesktopTool.js +629 -638
  365. package/src/tools/desktop/__tests__/DesktopTool.e2e.test.js +306 -306
  366. package/src/tools/desktop/__tests__/DesktopTool.test.js +507 -507
  367. package/src/tools/desktop/__tests__/osController.test.js +364 -364
  368. package/src/tools/desktop/osController.js +491 -491
  369. package/src/tools/docxTool.js +623 -623
  370. package/src/tools/excelTool.js +636 -636
  371. package/src/tools/fileContentReplaceTool.js +5 -7
  372. package/src/tools/fileSystemTool.js +12 -19
  373. package/src/tools/fileTreeTool.js +840 -840
  374. package/src/tools/foundryWebSearchTool.js +273 -273
  375. package/src/tools/helpTool.js +198 -198
  376. package/src/tools/imageTool.js +1397 -1397
  377. package/src/tools/importAnalyzerTool.js +1056 -1056
  378. package/src/tools/jobDoneTool.js +495 -495
  379. package/src/tools/memoryTool.js +1 -1
  380. package/src/tools/office/pres/__tests__/presSystem.test.js +365 -365
  381. package/src/tools/office/pres/archetypes/agenda.js +61 -61
  382. package/src/tools/office/pres/archetypes/bentoGrid.js +218 -219
  383. package/src/tools/office/pres/archetypes/bigStat.js +140 -142
  384. package/src/tools/office/pres/archetypes/closing.js +70 -70
  385. package/src/tools/office/pres/archetypes/hero.js +70 -70
  386. package/src/tools/office/pres/archetypes/productHero.js +93 -94
  387. package/src/tools/office/pres/archetypes/table.js +73 -74
  388. package/src/tools/office/pres/backgrounds/orb.js +66 -66
  389. package/src/tools/office/pres/components.js +422 -423
  390. package/src/tools/officeTool.js +441 -441
  391. package/src/tools/pdfTool.js +625 -627
  392. package/src/tools/platformControlTool.js +1081 -1081
  393. package/src/tools/seekTool.js +917 -918
  394. package/src/tools/skillsTool.js +1 -1
  395. package/src/tools/staticAnalysisTool.js +2143 -2146
  396. package/src/tools/taskManagerTool.js +3324 -3324
  397. package/src/tools/terminalTool.js +2615 -2618
  398. package/src/tools/videoTool.js +1303 -1303
  399. package/src/tools/visionTool.js +508 -508
  400. package/src/tools/visualEditorTool.js +1289 -1290
  401. package/src/tools/webTool.js +3368 -3368
  402. package/src/tools/whatsappTool.js +464 -464
  403. package/src/types/__tests__/agent.test.js +499 -499
  404. package/src/types/__tests__/contextReference.test.js +606 -606
  405. package/src/types/__tests__/conversation.test.js +555 -555
  406. package/src/types/__tests__/toolCommand.test.js +584 -584
  407. package/src/types/contextReference.js +974 -971
  408. package/src/types/conversation.js +729 -729
  409. package/src/types/toolCommand.js +746 -746
  410. package/src/utilities/__tests__/attachmentValidator.test.js +80 -80
  411. package/src/utilities/__tests__/auditReport.test.js +328 -328
  412. package/src/utilities/__tests__/directoryAccessManager.test.js +388 -388
  413. package/src/utilities/__tests__/jsonRepair.test.js +103 -104
  414. package/src/utilities/__tests__/modeTransitionReasons.test.js +105 -105
  415. package/src/utilities/__tests__/platformUtils.test.js +80 -87
  416. package/src/utilities/__tests__/structuredFileValidator.test.js +261 -263
  417. package/src/utilities/__tests__/toolConstants.test.js +92 -94
  418. package/src/utilities/__tests__/useIsTouchDevice.detect.test.js +114 -114
  419. package/src/utilities/__tests__/webUiUtilSync.test.js +117 -117
  420. package/src/utilities/attachmentValidator.js +284 -288
  421. package/src/utilities/authCache.js.backup-1779570472481 +121 -121
  422. package/src/utilities/browserStealth.js +631 -630
  423. package/src/utilities/configManager.js +616 -617
  424. package/src/utilities/directoryAccessManager.js +564 -565
  425. package/src/utilities/fileProcessor.js +308 -307
  426. package/src/utilities/humanBehavior.js +454 -453
  427. package/src/utilities/logger.js +479 -479
  428. package/src/utilities/structuredFileValidator.js +696 -699
  429. package/src/utilities/tagParser.js +5 -10
  430. package/src/utilities/userDataDir.js +308 -308
  431. package/node_modules/@isaacs/brace-expansion/dist/commonjs/index.js.map +0 -1
  432. package/node_modules/@isaacs/brace-expansion/dist/esm/index.js.map +0 -1
  433. package/node_modules/minipass/LICENSE +0 -15
  434. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/LICENSE.md +0 -0
  435. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.d.ts +0 -0
  436. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.d.ts.map +0 -0
  437. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.js +0 -0
  438. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.js.map +0 -0
  439. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/package.json +0 -0
  440. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.d.ts +0 -0
  441. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.d.ts.map +0 -0
  442. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.js +0 -0
  443. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.js.map +0 -0
  444. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/package.json +0 -0
  445. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/LICENSE +0 -0
  446. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.d.ts +0 -0
  447. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.d.ts.map +0 -0
  448. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/package.json +0 -0
  449. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.d.ts +0 -0
  450. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.d.ts.map +0 -0
  451. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/package.json +0 -0
@@ -1,1753 +1,1766 @@
1
- /**
2
- * StateManager - Handles state persistence, recovery, and project state management
3
- *
4
- * Purpose:
5
- * - Project state persistence and recovery
6
- * - Agent state management across sessions
7
- * - Multi-model conversation state handling
8
- * - Context reference state management
9
- * - Session recovery and resume functionality
10
- *
11
- * IMPORTANT: State is now stored in a platform-appropriate user data directory
12
- * that persists across npm package updates. See userDataDir.js for details.
13
- */
14
-
15
- import { promises as fs } from 'fs';
16
- import path from 'path';
17
- import { getUserDataPaths, ensureUserDataDirs } from '../utilities/userDataDir.js';
18
-
19
- class StateManager {
20
- constructor(config, logger) {
21
- this.config = config;
22
- this.logger = logger;
23
-
24
- // UPDATED: Use persistent user data directory instead of relative path
25
- // This ensures data survives npm package updates
26
- const userPaths = getUserDataPaths();
27
- this.stateDirectory = userPaths.state;
28
- this.userDataPaths = userPaths;
29
-
30
- // Legacy: Keep for backwards compatibility detection
31
- this.legacyStateDirectory = config.system?.stateDirectory || '.loxia-state';
32
- this.stateVersion = '1.0.0';
33
-
34
- // State file paths
35
- this.stateFiles = {
36
- projectState: 'project-state.json',
37
- agentIndex: 'agent-index.json',
38
- teamIndex: 'team-index.json',
39
- flowIndex: 'flow-index.json',
40
- flowRunIndex: 'flow-run-index.json',
41
- conversationIndex: 'conversation-index.json',
42
- lastSession: 'last-session.json',
43
- contextReferences: 'context-references.json',
44
- asyncOperations: 'operations/async-operations.json',
45
- pausedAgents: 'operations/paused-agents.json',
46
- toolHistory: 'operations/tool-history.json',
47
- modelRouterCache: 'models/model-router-cache.json',
48
- errorRecoveryLog: 'models/error-recovery-log.json'
49
- };
50
- }
51
-
52
- /**
53
- * Get the state directory path
54
- * UPDATED: Now returns the user data directory (absolute path)
55
- * The projectDir parameter is kept for API compatibility but is ignored
56
- * @param {string} projectDir - Ignored, kept for compatibility
57
- * @returns {string} Absolute path to state directory
58
- */
59
- getStateDir(projectDir) {
60
- // Always use the persistent user data directory
61
- return this.stateDirectory;
62
- }
63
-
64
- /**
65
- * Get the agents subdirectory path
66
- * @returns {string} Absolute path to agents directory
67
- */
68
- getAgentsDir() {
69
- return this.userDataPaths.agents;
70
- }
71
-
72
- /**
73
- * Initialize state directory structure
74
- * @param {string} projectDir - Project directory path (now ignored, uses user data dir)
75
- * @returns {Promise<void>}
76
- */
77
- async initializeStateDirectory(projectDir) {
78
- // UPDATED: Use persistent user data directory instead of project-relative path
79
- // The projectDir parameter is kept for API compatibility but now ignored
80
- try {
81
- // Use the centralized utility to create all necessary directories
82
- const paths = await ensureUserDataDirs();
83
-
84
- this.logger.info(`State directory initialized in user data location`, {
85
- stateDir: paths.state,
86
- platform: process.platform
87
- });
88
-
89
- } catch (error) {
90
- this.logger.error(`Failed to initialize state directory: ${error.message}`);
91
- throw error;
92
- }
93
- }
94
-
95
- /**
96
- * Resume project from saved state
97
- * @param {string} projectDir - Project directory path
98
- * @returns {Promise<Object>} Resumed project state
99
- */
100
- async resumeProject(projectDir) {
101
- try {
102
- await this.initializeStateDirectory(projectDir);
103
-
104
- // Load project state
105
- const projectState = await this.loadProjectState(projectDir);
106
- const agentIndex = await this.loadAgentIndex(projectDir);
107
-
108
- // Restore agents with multi-model conversations
109
- const restoredAgents = [];
110
- for (const [agentId, agentInfo] of Object.entries(agentIndex)) {
111
- try {
112
- const agent = await this.restoreAgent(agentId, agentInfo, projectDir);
113
- restoredAgents.push(agent);
114
- } catch (error) {
115
- this.logger.warn(`Failed to restore agent: ${agentId}`, error.message);
116
- }
117
- }
118
-
119
- // Restore async operations
120
- const asyncOperations = await this.restoreAsyncOperations(projectDir);
121
-
122
- // Restore paused agents
123
- const pausedAgents = await this.restorePausedAgents(projectDir);
124
-
125
- // Restore context references
126
- const contextReferences = await this.restoreContextReferences(projectDir);
127
-
128
- const resumedState = {
129
- projectState,
130
- agents: restoredAgents,
131
- asyncOperations,
132
- pausedAgents,
133
- contextReferences,
134
- resumedSuccessfully: true,
135
- resumedAt: new Date().toISOString()
136
- };
137
-
138
- // Update last session
139
- await this.saveLastSession(projectDir, {
140
- resumedAt: new Date().toISOString(),
141
- agentCount: restoredAgents.length,
142
- operationCount: asyncOperations.length
143
- });
144
-
145
- this.logger.info(`Project resumed successfully`, {
146
- projectDir,
147
- agentCount: restoredAgents.length,
148
- operationCount: asyncOperations.length
149
- });
150
-
151
- return resumedState;
152
-
153
- } catch (error) {
154
- this.logger.error(`Project resume failed: ${error.message}`, {
155
- projectDir,
156
- error: error.stack
157
- });
158
-
159
- return {
160
- projectState: null,
161
- agents: [],
162
- asyncOperations: [],
163
- pausedAgents: [],
164
- contextReferences: [],
165
- resumedSuccessfully: false,
166
- error: error.message
167
- };
168
- }
169
- }
170
-
171
- /**
172
- * Persist agent state to storage
173
- * @param {Object} agent - Agent object to persist
174
- * @param {string} projectDir - Project directory path
175
- * @returns {Promise<void>}
176
- */
177
- async persistAgentState(agent, projectDir = process.cwd()) {
178
- const stateDir = this.getStateDir(projectDir);
179
- const agentStateFile = path.join(stateDir, 'agents', `agent-${agent.id}-state.json`);
180
- const agentConversationsFile = path.join(stateDir, 'agents', `agent-${agent.id}-conversations.json`);
181
-
182
- try {
183
- // Separate conversations from main agent state
184
- const { conversations, ...agentState } = agent;
185
-
186
- // Save agent state
187
- await this.saveJSON(agentStateFile, {
188
- version: this.stateVersion,
189
- agentId: agent.id,
190
- state: agentState,
191
- lastPersisted: new Date().toISOString()
192
- });
193
-
194
- // Save conversations separately
195
- await this.saveJSON(agentConversationsFile, {
196
- version: this.stateVersion,
197
- agentId: agent.id,
198
- conversations,
199
- lastPersisted: new Date().toISOString()
200
- });
201
-
202
- // Update agent index
203
- await this.updateAgentIndex(agent, projectDir);
204
-
205
- this.logger.debug(`Agent state persisted: ${agent.id}`);
206
-
207
- } catch (error) {
208
- this.logger.error(`Failed to persist agent state: ${error.message}`, {
209
- agentId: agent.id,
210
- error: error.stack
211
- });
212
- throw error;
213
- }
214
- }
215
-
216
- /**
217
- * Get project state
218
- * @param {string} projectDir - Project directory path
219
- * @returns {Promise<Object>} Project state object
220
- */
221
- async getProjectState(projectDir) {
222
- return await this.loadProjectState(projectDir);
223
- }
224
-
225
- /**
226
- * Load project state from storage
227
- * @param {string} projectDir - Project directory path
228
- * @returns {Promise<Object>} Project state object
229
- */
230
- async loadProjectState(projectDir) {
231
- const stateFile = path.join(this.stateDirectory, this.stateFiles.projectState);
232
-
233
- try {
234
- const data = await this.loadJSON(stateFile);
235
- return data;
236
- } catch (error) {
237
- // Return default project state if file doesn't exist
238
- const defaultState = {
239
- version: this.stateVersion,
240
- projectDir,
241
- createdAt: new Date().toISOString(),
242
- lastModified: new Date().toISOString(),
243
- activeAgents: [],
244
- lastActiveSession: null,
245
- configuration: {
246
- defaultModel: this.config.system?.defaultModel || 'anthropic-sonnet',
247
- allowedTools: ['terminal', 'filesystem', 'browser'],
248
- budgetLimit: 100.00
249
- }
250
- };
251
-
252
- await this.saveProjectState(projectDir, defaultState);
253
- return defaultState;
254
- }
255
- }
256
-
257
- /**
258
- * Save project state to storage
259
- * @param {string} projectDir - Project directory path
260
- * @param {Object} projectState - Project state object
261
- * @returns {Promise<void>}
262
- */
263
- async saveProjectState(projectDir, projectState) {
264
- const stateFile = path.join(this.stateDirectory, this.stateFiles.projectState);
265
-
266
- const stateData = {
267
- ...projectState,
268
- lastModified: new Date().toISOString()
269
- };
270
-
271
- await this.saveJSON(stateFile, stateData);
272
- }
273
-
274
- /**
275
- * Load agent index
276
- * @param {string} projectDir - Project directory path
277
- * @returns {Promise<Object>} Agent index object
278
- */
279
- async loadAgentIndex(projectDir) {
280
- const indexFile = path.join(this.stateDirectory, this.stateFiles.agentIndex);
281
-
282
- try {
283
- return await this.loadJSON(indexFile);
284
- } catch (error) {
285
- return {}; // Return empty index if file doesn't exist
286
- }
287
- }
288
-
289
- /**
290
- * Update agent index
291
- * @param {Object} agent - Agent object
292
- * @param {string} projectDir - Project directory path
293
- * @returns {Promise<void>}
294
- */
295
- async updateAgentIndex(agent, projectDir) {
296
- const indexFile = path.join(this.stateDirectory, this.stateFiles.agentIndex);
297
-
298
- let agentIndex;
299
- try {
300
- agentIndex = await this.loadJSON(indexFile);
301
- } catch {
302
- agentIndex = {};
303
- }
304
-
305
- agentIndex[agent.id] = {
306
- name: agent.name,
307
- type: agent.type,
308
- stateFile: `agents/agent-${agent.id}-state.json`,
309
- conversationsFile: `agents/agent-${agent.id}-conversations.json`,
310
- lastActivity: agent.lastActivity,
311
- model: agent.currentModel,
312
- status: agent.status,
313
- capabilities: agent.capabilities || []
314
- };
315
-
316
- await this.saveJSON(indexFile, agentIndex);
317
- }
318
-
319
- // ==================== TEAM INDEX METHODS ====================
320
-
321
- /**
322
- * Load team index
323
- * @param {string} projectDir - Project directory path (ignored, uses user data dir)
324
- * @returns {Promise<Object>} Team index object
325
- */
326
- async loadTeamIndex(projectDir) {
327
- const indexFile = path.join(this.stateDirectory, this.stateFiles.teamIndex);
328
-
329
- try {
330
- return await this.loadJSON(indexFile);
331
- } catch (error) {
332
- return {}; // Return empty index if file doesn't exist
333
- }
334
- }
335
-
336
- /**
337
- * Save team index
338
- * @param {Object} teamIndex - Team index object to save
339
- * @returns {Promise<void>}
340
- */
341
- async saveTeamIndex(teamIndex) {
342
- const indexFile = path.join(this.stateDirectory, this.stateFiles.teamIndex);
343
- await this.saveJSON(indexFile, teamIndex);
344
- }
345
-
346
- /**
347
- * Get all teams
348
- * @param {string} projectDir - Project directory path (ignored)
349
- * @returns {Promise<Array>} Array of team objects
350
- */
351
- async getAllTeams(projectDir) {
352
- const teamIndex = await this.loadTeamIndex(projectDir);
353
- return Object.entries(teamIndex).map(([id, team]) => ({
354
- id,
355
- ...team
356
- }));
357
- }
358
-
359
- /**
360
- * Get a single team by ID
361
- * @param {string} teamId - Team identifier
362
- * @param {string} projectDir - Project directory path (ignored)
363
- * @returns {Promise<Object|null>} Team object or null if not found
364
- */
365
- async getTeam(teamId, projectDir) {
366
- const teamIndex = await this.loadTeamIndex(projectDir);
367
- if (teamIndex[teamId]) {
368
- return { id: teamId, ...teamIndex[teamId] };
369
- }
370
- return null;
371
- }
372
-
373
- /**
374
- * Create a new team
375
- * @param {Object} teamData - Team data { name, description, color }
376
- * @param {string} projectDir - Project directory path (ignored)
377
- * @returns {Promise<Object>} Created team object
378
- */
379
- async createTeam(teamData, projectDir) {
380
- const teamIndex = await this.loadTeamIndex(projectDir);
381
-
382
- // Generate team ID
383
- const safeName = (teamData.name || 'team').toLowerCase().replace(/[^a-z0-9]/g, '-').slice(0, 20);
384
- const teamId = `team-${safeName}-${Date.now()}`;
385
-
386
- const team = {
387
- name: teamData.name,
388
- description: teamData.description || '',
389
- memberAgentIds: [],
390
- color: teamData.color || '#3B82F6', // Default blue
391
- // Used by platformControlTool's `ownedByMe` team scope. null when
392
- // the team was created via the UI / by no specific agent. When an
393
- // agent creates a team via the platformcontrol tool, this carries
394
- // the caller's id so ownership scope filtering works.
395
- createdBy: typeof teamData.createdBy === 'string' ? teamData.createdBy : null,
396
- createdAt: new Date().toISOString(),
397
- updatedAt: new Date().toISOString()
398
- };
399
-
400
- teamIndex[teamId] = team;
401
- await this.saveTeamIndex(teamIndex);
402
-
403
- this.logger.info(`Team created: ${teamId}`, { name: team.name });
404
-
405
- return { id: teamId, ...team };
406
- }
407
-
408
- /**
409
- * Update an existing team
410
- * @param {string} teamId - Team identifier
411
- * @param {Object} updates - Fields to update
412
- * @param {string} projectDir - Project directory path (ignored)
413
- * @returns {Promise<Object>} Updated team object
414
- */
415
- async updateTeam(teamId, updates, projectDir) {
416
- const teamIndex = await this.loadTeamIndex(projectDir);
417
-
418
- if (!teamIndex[teamId]) {
419
- throw new Error(`Team ${teamId} not found`);
420
- }
421
-
422
- // Only allow updating specific fields
423
- const allowedFields = ['name', 'description', 'color', 'memberAgentIds'];
424
- for (const field of allowedFields) {
425
- if (updates[field] !== undefined) {
426
- teamIndex[teamId][field] = updates[field];
427
- }
428
- }
429
- teamIndex[teamId].updatedAt = new Date().toISOString();
430
-
431
- await this.saveTeamIndex(teamIndex);
432
-
433
- this.logger.info(`Team updated: ${teamId}`, { updates: Object.keys(updates) });
434
-
435
- return { id: teamId, ...teamIndex[teamId] };
436
- }
437
-
438
- /**
439
- * Delete a team
440
- * @param {string} teamId - Team identifier
441
- * @param {string} projectDir - Project directory path (ignored)
442
- * @returns {Promise<boolean>} True if deleted
443
- */
444
- async deleteTeam(teamId, projectDir) {
445
- const teamIndex = await this.loadTeamIndex(projectDir);
446
-
447
- if (!teamIndex[teamId]) {
448
- throw new Error(`Team ${teamId} not found`);
449
- }
450
-
451
- const teamName = teamIndex[teamId].name;
452
- delete teamIndex[teamId];
453
- await this.saveTeamIndex(teamIndex);
454
-
455
- this.logger.info(`Team deleted: ${teamId}`, { name: teamName });
456
-
457
- return true;
458
- }
459
-
460
- /**
461
- * Add an agent to a team
462
- * @param {string} teamId - Team identifier
463
- * @param {string} agentId - Agent identifier to add
464
- * @param {string} projectDir - Project directory path (ignored)
465
- * @returns {Promise<Object>} Updated team object
466
- */
467
- async addAgentToTeam(teamId, agentId, projectDir) {
468
- const teamIndex = await this.loadTeamIndex(projectDir);
469
-
470
- if (!teamIndex[teamId]) {
471
- throw new Error(`Team ${teamId} not found`);
472
- }
473
-
474
- // Check if agent already in team
475
- if (!teamIndex[teamId].memberAgentIds.includes(agentId)) {
476
- teamIndex[teamId].memberAgentIds.push(agentId);
477
- teamIndex[teamId].updatedAt = new Date().toISOString();
478
- await this.saveTeamIndex(teamIndex);
479
-
480
- this.logger.info(`Agent added to team`, { teamId, agentId });
481
- }
482
-
483
- return { id: teamId, ...teamIndex[teamId] };
484
- }
485
-
486
- /**
487
- * Remove an agent from a team
488
- * @param {string} teamId - Team identifier
489
- * @param {string} agentId - Agent identifier to remove
490
- * @param {string} projectDir - Project directory path (ignored)
491
- * @returns {Promise<Object>} Updated team object
492
- */
493
- async removeAgentFromTeam(teamId, agentId, projectDir) {
494
- const teamIndex = await this.loadTeamIndex(projectDir);
495
-
496
- if (!teamIndex[teamId]) {
497
- throw new Error(`Team ${teamId} not found`);
498
- }
499
-
500
- const index = teamIndex[teamId].memberAgentIds.indexOf(agentId);
501
- if (index > -1) {
502
- teamIndex[teamId].memberAgentIds.splice(index, 1);
503
- teamIndex[teamId].updatedAt = new Date().toISOString();
504
- await this.saveTeamIndex(teamIndex);
505
-
506
- this.logger.info(`Agent removed from team`, { teamId, agentId });
507
- }
508
-
509
- return { id: teamId, ...teamIndex[teamId] };
510
- }
511
-
512
- /**
513
- * Get all teams that contain a specific agent
514
- * @param {string} agentId - Agent identifier
515
- * @param {string} projectDir - Project directory path (ignored)
516
- * @returns {Promise<Array>} Array of team objects containing the agent
517
- */
518
- async getAgentTeams(agentId, projectDir) {
519
- const teams = await this.getAllTeams(projectDir);
520
- return teams.filter(team => team.memberAgentIds.includes(agentId));
521
- }
522
-
523
- // ==================== END TEAM INDEX METHODS ====================
524
-
525
- // ==================== FLOW INDEX METHODS ====================
526
-
527
- /**
528
- * Load flow index
529
- * @param {string} projectDir - Project directory path (ignored, uses user data dir)
530
- * @returns {Promise<Object>} Flow index object
531
- */
532
- async loadFlowIndex(projectDir) {
533
- const indexFile = path.join(this.stateDirectory, this.stateFiles.flowIndex);
534
-
535
- try {
536
- return await this.loadJSON(indexFile);
537
- } catch (error) {
538
- return {}; // Return empty index if file doesn't exist
539
- }
540
- }
541
-
542
- /**
543
- * Save flow index
544
- * @param {Object} flowIndex - Flow index object to save
545
- * @returns {Promise<void>}
546
- */
547
- async saveFlowIndex(flowIndex) {
548
- const indexFile = path.join(this.stateDirectory, this.stateFiles.flowIndex);
549
- await this.saveJSON(indexFile, flowIndex);
550
- }
551
-
552
- /**
553
- * Get all flows
554
- * @param {string} projectDir - Project directory path (ignored)
555
- * @returns {Promise<Array>} Array of flow objects
556
- */
557
- async getAllFlows(projectDir) {
558
- const flowIndex = await this.loadFlowIndex(projectDir);
559
- return Object.entries(flowIndex).map(([id, flow]) => ({
560
- id,
561
- ...flow
562
- }));
563
- }
564
-
565
- /**
566
- * Get a single flow by ID
567
- * @param {string} flowId - Flow identifier
568
- * @param {string} projectDir - Project directory path (ignored)
569
- * @returns {Promise<Object|null>} Flow object or null if not found
570
- */
571
- async getFlow(flowId, projectDir) {
572
- const flowIndex = await this.loadFlowIndex(projectDir);
573
- if (flowIndex[flowId]) {
574
- return { id: flowId, ...flowIndex[flowId] };
575
- }
576
- return null;
577
- }
578
-
579
- /**
580
- * Create a new flow
581
- * @param {Object} flowData - Flow data { name, description, nodes, edges, variables }
582
- * @param {string} projectDir - Project directory path (ignored)
583
- * @returns {Promise<Object>} Created flow object
584
- */
585
- async createFlow(flowData, projectDir) {
586
- const flowIndex = await this.loadFlowIndex(projectDir);
587
-
588
- // Generate flow ID
589
- const safeName = (flowData.name || 'flow').toLowerCase().replace(/[^a-z0-9]/g, '-').slice(0, 20);
590
- const flowId = `flow-${safeName}-${Date.now()}`;
591
-
592
- // Stamp edge ids on save. Templates and marketplace-installed flows
593
- // ship without edge ids; ReactFlow refuses to render edges that
594
- // don't have a unique `id`, so unstamped edges show up as
595
- // disconnected dots on the canvas. Doing this here rather than in
596
- // the editor means the durable shape on disk is always renderable.
597
- // Helper is shared with FlowEditor's defense-in-depth layer so the
598
- // id format stays consistent across save + reload.
599
- const { ensureEdgeIds } = await import('../utilities/flowEdgeIds.js');
600
- const stampedEdges = ensureEdgeIds(flowData.edges);
601
-
602
- const flow = {
603
- name: flowData.name,
604
- description: flowData.description || '',
605
- nodes: flowData.nodes || [],
606
- edges: stampedEdges,
607
- variables: flowData.variables || {},
608
- createdAt: new Date().toISOString(),
609
- updatedAt: new Date().toISOString(),
610
- // createdBy: only present when the caller stamps it (e.g.
611
- // platformControlTool's create-flow path). Not required for the
612
- // happy-path UI flow but durable when set.
613
- ...(flowData.createdBy ? { createdBy: flowData.createdBy } : {})
614
- };
615
-
616
- flowIndex[flowId] = flow;
617
- await this.saveFlowIndex(flowIndex);
618
-
619
- this.logger.info(`Flow created: ${flowId}`, { name: flow.name });
620
-
621
- return { id: flowId, ...flow };
622
- }
623
-
624
- /**
625
- * Update an existing flow
626
- * @param {string} flowId - Flow identifier
627
- * @param {Object} updates - Fields to update
628
- * @param {string} projectDir - Project directory path (ignored)
629
- * @returns {Promise<Object>} Updated flow object
630
- */
631
- async updateFlow(flowId, updates, projectDir) {
632
- const flowIndex = await this.loadFlowIndex(projectDir);
633
-
634
- if (!flowIndex[flowId]) {
635
- throw new Error(`Flow ${flowId} not found`);
636
- }
637
-
638
- // Only allow updating specific fields. `version` and `schemaVersion`
639
- // are added so the Phase 6 version-stamp write-back persists, and
640
- // so v2 flows can be marked when typed I/O is added.
641
- const allowedFields = ['name', 'description', 'nodes', 'edges', 'variables', 'version', 'schemaVersion'];
642
- for (const field of allowedFields) {
643
- if (updates[field] !== undefined) {
644
- flowIndex[flowId][field] = updates[field];
645
- }
646
- }
647
- flowIndex[flowId].updatedAt = new Date().toISOString();
648
-
649
- await this.saveFlowIndex(flowIndex);
650
-
651
- this.logger.info(`Flow updated: ${flowId}`, { updates: Object.keys(updates) });
652
-
653
- return { id: flowId, ...flowIndex[flowId] };
654
- }
655
-
656
- /**
657
- * Delete a flow
658
- * @param {string} flowId - Flow identifier
659
- * @param {string} projectDir - Project directory path (ignored)
660
- * @returns {Promise<boolean>} True if deleted
661
- */
662
- async deleteFlow(flowId, projectDir) {
663
- const flowIndex = await this.loadFlowIndex(projectDir);
664
-
665
- if (!flowIndex[flowId]) {
666
- throw new Error(`Flow ${flowId} not found`);
667
- }
668
-
669
- const flowName = flowIndex[flowId].name;
670
- delete flowIndex[flowId];
671
- await this.saveFlowIndex(flowIndex);
672
-
673
- this.logger.info(`Flow deleted: ${flowId}`, { name: flowName });
674
-
675
- return true;
676
- }
677
-
678
- // ==================== FLOW RUN METHODS ====================
679
-
680
- /**
681
- * Load flow run index
682
- * @param {string} projectDir - Project directory path (ignored)
683
- * @returns {Promise<Object>} Flow run index object
684
- */
685
- async loadFlowRunIndex(projectDir) {
686
- const indexFile = path.join(this.stateDirectory, this.stateFiles.flowRunIndex);
687
-
688
- try {
689
- return await this.loadJSON(indexFile);
690
- } catch (error) {
691
- return {}; // Return empty index if file doesn't exist
692
- }
693
- }
694
-
695
- /**
696
- * Save flow run index
697
- * @param {Object} runIndex - Flow run index object to save
698
- * @returns {Promise<void>}
699
- */
700
- async saveFlowRunIndex(runIndex) {
701
- const indexFile = path.join(this.stateDirectory, this.stateFiles.flowRunIndex);
702
- await this.saveJSON(indexFile, runIndex);
703
- }
704
-
705
- /**
706
- * Create a new flow run
707
- * @param {string} flowId - Flow identifier
708
- * @param {Object} initialInput - Initial input for the flow
709
- * @param {string} projectDir - Project directory path (ignored)
710
- * @returns {Promise<Object>} Created flow run object
711
- */
712
- async createFlowRun(flowId, initialInput, projectDir) {
713
- const runIndex = await this.loadFlowRunIndex(projectDir);
714
-
715
- const runId = `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
716
-
717
- const run = {
718
- flowId,
719
- status: 'pending', // pending, running, completed, failed, stopped
720
- initialInput,
721
- nodeStates: {},
722
- startedAt: new Date().toISOString(),
723
- completedAt: null,
724
- error: null
725
- };
726
-
727
- runIndex[runId] = run;
728
- await this.saveFlowRunIndex(runIndex);
729
-
730
- this.logger.info(`Flow run created: ${runId}`, { flowId });
731
-
732
- return { id: runId, ...run };
733
- }
734
-
735
- /**
736
- * Update a flow run
737
- * @param {string} runId - Run identifier
738
- * @param {Object} updates - Fields to update
739
- * @param {string} projectDir - Project directory path (ignored)
740
- * @returns {Promise<Object>} Updated flow run object
741
- */
742
- async updateFlowRun(runId, updates, projectDir) {
743
- const runIndex = await this.loadFlowRunIndex(projectDir);
744
-
745
- if (!runIndex[runId]) {
746
- throw new Error(`Flow run ${runId} not found`);
747
- }
748
-
749
- // Only allow updating specific fields
750
- const allowedFields = ['status', 'nodeStates', 'completedAt', 'error', 'output'];
751
- for (const field of allowedFields) {
752
- if (updates[field] !== undefined) {
753
- runIndex[runId][field] = updates[field];
754
- }
755
- }
756
-
757
- await this.saveFlowRunIndex(runIndex);
758
-
759
- this.logger.info(`Flow run updated: ${runId}`, { status: updates.status });
760
-
761
- return { id: runId, ...runIndex[runId] };
762
- }
763
-
764
- /**
765
- * Get a flow run by ID
766
- * @param {string} runId - Run identifier
767
- * @param {string} projectDir - Project directory path (ignored)
768
- * @returns {Promise<Object|null>} Flow run object or null if not found
769
- */
770
- async getFlowRun(runId, projectDir) {
771
- const runIndex = await this.loadFlowRunIndex(projectDir);
772
- if (runIndex[runId]) {
773
- return { id: runId, ...runIndex[runId] };
774
- }
775
- return null;
776
- }
777
-
778
- /**
779
- * Get all runs for a specific flow
780
- * @param {string} flowId - Flow identifier
781
- * @param {string} projectDir - Project directory path (ignored)
782
- * @returns {Promise<Array>} Array of flow run objects
783
- */
784
- async getFlowRuns(flowId, projectDir) {
785
- const runIndex = await this.loadFlowRunIndex(projectDir);
786
- return Object.entries(runIndex)
787
- .filter(([, run]) => run.flowId === flowId)
788
- .map(([id, run]) => ({ id, ...run }))
789
- .sort((a, b) => new Date(b.startedAt) - new Date(a.startedAt));
790
- }
791
-
792
- // ==================== END FLOW INDEX METHODS ====================
793
-
794
- /**
795
- * Restore agent from saved state
796
- * @param {string} agentId - Agent identifier
797
- * @param {Object} agentInfo - Agent info from index
798
- * @param {string} projectDir - Project directory path
799
- * @returns {Promise<Object>} Restored agent object
800
- */
801
- async restoreAgent(agentId, agentInfo, projectDir) {
802
- const stateDir = this.getStateDir(projectDir);
803
- const stateFile = path.join(stateDir, agentInfo.stateFile);
804
- const conversationsFile = path.join(stateDir, agentInfo.conversationsFile);
805
-
806
- // Skeleton defaults the resilient loader falls back to when a state
807
- // file is missing, empty, or unrepairable. The state skeleton derives
808
- // the agent name from the index entry so even a total wipe leaves
809
- // the agent identifiable in the UI rather than ending up nameless.
810
- const stateDefault = () => ({
811
- version: 2,
812
- agentId,
813
- state: {
814
- id: agentId,
815
- name: agentInfo.name || agentId,
816
- type: agentInfo.type || 'user-created',
817
- status: 'active',
818
- mode: 'chat',
819
- currentModel: agentInfo.preferredModel || agentInfo.model || null,
820
- preferredModel: agentInfo.preferredModel || agentInfo.model || null,
821
- systemPrompt: '',
822
- capabilities: agentInfo.capabilities || [],
823
- taskList: { tasks: [], lastUpdated: new Date().toISOString() },
824
- messageQueues: { userMessages: [], interAgentMessages: [], toolResults: [] },
825
- interAgentTracking: {},
826
- },
827
- lastPersisted: new Date().toISOString(),
828
- });
829
- const conversationsDefault = () => ({
830
- version: 2,
831
- agentId,
832
- conversations: {
833
- // Empty `full` conversation — required by validateModelConversations
834
- // and the agent loop's history reader. New per-model entries are
835
- // added on demand at first use.
836
- full: { messages: [], lastUpdated: new Date().toISOString() },
837
- },
838
- lastPersisted: new Date().toISOString(),
839
- });
840
-
841
- // Recovery reports for any auto-fixed files. Attached to the
842
- // returned agent so the caller (orchestrator → webServer) can
843
- // broadcast each one as a toast in the UI. Empty array on the
844
- // happy path; harmless presence otherwise.
845
- const recoveries = [];
846
-
847
- try {
848
- const stateRes = await this.loadJSONResilient(stateFile, stateDefault, {
849
- label: `agent state (${agentInfo.name || agentId})`,
850
- });
851
- const conversationsRes = await this.loadJSONResilient(conversationsFile, conversationsDefault, {
852
- label: `agent conversations (${agentInfo.name || agentId})`,
853
- });
854
- const stateData = stateRes.data;
855
- const conversationsData = conversationsRes.data;
856
- if (stateRes.recovery) recoveries.push(stateRes.recovery);
857
- if (conversationsRes.recovery) recoveries.push(conversationsRes.recovery);
858
-
859
- // Validate model conversations integrity
860
- await this.validateModelConversations(conversationsData.conversations);
861
-
862
- // Check if agent is paused
863
- const pauseStatus = await this.checkAgentPauseStatus(agentId, projectDir);
864
-
865
- const restoredAgent = {
866
- ...stateData.state,
867
- conversations: conversationsData.conversations,
868
- isPaused: pauseStatus.isPaused,
869
- pausedUntil: pauseStatus.pausedUntil,
870
- isRestored: true,
871
- restoredAt: new Date().toISOString(),
872
- // Non-enumerable would be cleaner, but agentPool serializes the
873
- // whole object — keeping it as a plain field. The orchestrator
874
- // strips it before persisting (see import handler).
875
- _restoreRecoveries: recoveries,
876
- };
877
-
878
- // CRITICAL: Restore interAgentTracking as a Map (it comes as plain object from JSON)
879
- if (!restoredAgent.interAgentTracking || typeof restoredAgent.interAgentTracking !== 'object') {
880
- restoredAgent.interAgentTracking = new Map();
881
- } else if (!(restoredAgent.interAgentTracking instanceof Map)) {
882
- restoredAgent.interAgentTracking = new Map(Object.entries(restoredAgent.interAgentTracking));
883
- }
884
-
885
- this.logger.info(`Agent restored: ${agentId}`, {
886
- name: restoredAgent.name,
887
- status: restoredAgent.status,
888
- messageCount: restoredAgent.conversations?.full?.messages?.length || 0,
889
- recoveryCount: recoveries.length,
890
- });
891
-
892
- return restoredAgent;
893
-
894
- } catch (error) {
895
- this.logger.error(`Agent restoration failed: ${agentId}`, error.message);
896
- throw error;
897
- }
898
- }
899
-
900
- /**
901
- * Restore async operations
902
- * @param {string} projectDir - Project directory path
903
- * @returns {Promise<Array>} Array of active async operations
904
- */
905
- async restoreAsyncOperations(projectDir) {
906
- const operationsFile = path.join(this.stateDirectory, this.stateFiles.asyncOperations);
907
-
908
- try {
909
- const data = await this.loadJSON(operationsFile);
910
- return data.operations || [];
911
- } catch {
912
- return [];
913
- }
914
- }
915
-
916
- /**
917
- * Restore paused agents
918
- * @param {string} projectDir - Project directory path
919
- * @returns {Promise<Object>} Paused agents data
920
- */
921
- async restorePausedAgents(projectDir) {
922
- const pausedFile = path.join(this.stateDirectory, this.stateFiles.pausedAgents);
923
-
924
- try {
925
- const data = await this.loadJSON(pausedFile);
926
- const now = Date.now();
927
-
928
- // Check which agents should be resumed
929
- const toResume = [];
930
- for (const [agentId, pauseInfo] of Object.entries(data.pausedAgents || {})) {
931
- const pausedUntil = new Date(pauseInfo.pausedUntil).getTime();
932
-
933
- if (now >= pausedUntil) {
934
- toResume.push(agentId);
935
- }
936
- }
937
-
938
- // Move expired pauses to history
939
- for (const agentId of toResume) {
940
- const pauseInfo = data.pausedAgents[agentId];
941
- delete data.pausedAgents[agentId];
942
-
943
- data.pauseHistory = data.pauseHistory || [];
944
- data.pauseHistory.push({
945
- agentId,
946
- pausedAt: pauseInfo.pausedAt,
947
- resumedAt: new Date().toISOString(),
948
- reason: pauseInfo.reason,
949
- actualDuration: Math.round((now - new Date(pauseInfo.pausedAt).getTime()) / 1000)
950
- });
951
- }
952
-
953
- // Save updated data
954
- await this.saveJSON(pausedFile, data);
955
-
956
- return data;
957
-
958
- } catch {
959
- return {
960
- pausedAgents: {},
961
- pauseHistory: []
962
- };
963
- }
964
- }
965
-
966
- /**
967
- * Restore context references
968
- * @param {string} projectDir - Project directory path
969
- * @returns {Promise<Object>} Context references data
970
- */
971
- async restoreContextReferences(projectDir) {
972
- const contextFile = path.join(this.stateDirectory, this.stateFiles.contextReferences);
973
-
974
- try {
975
- const data = await this.loadJSON(contextFile);
976
-
977
- // Validate context references (implementation would validate file existence, etc.)
978
- const validatedReferences = [];
979
- for (const reference of data.references || []) {
980
- // Add validation logic here
981
- reference.isValid = true; // Placeholder
982
- reference.lastValidated = new Date().toISOString();
983
- validatedReferences.push(reference);
984
- }
985
-
986
- data.references = validatedReferences;
987
- await this.saveJSON(contextFile, data);
988
-
989
- return data;
990
-
991
- } catch {
992
- return {
993
- references: [],
994
- lastCleanup: new Date().toISOString()
995
- };
996
- }
997
- }
998
-
999
- /**
1000
- * Save last session data
1001
- * @param {string} projectDir - Project directory path
1002
- * @param {Object} sessionData - Session data to save
1003
- * @returns {Promise<void>}
1004
- */
1005
- async saveLastSession(projectDir, sessionData) {
1006
- const sessionFile = path.join(this.stateDirectory, this.stateFiles.lastSession);
1007
-
1008
- const data = {
1009
- ...sessionData,
1010
- savedAt: new Date().toISOString(),
1011
- projectDir
1012
- };
1013
-
1014
- await this.saveJSON(sessionFile, data);
1015
- }
1016
-
1017
- /**
1018
- * Load last session data
1019
- * @param {string} projectDir - Project directory path
1020
- * @returns {Promise<Object>} Last session data
1021
- */
1022
- async loadLastSession(projectDir) {
1023
- const sessionFile = path.join(this.stateDirectory, this.stateFiles.lastSession);
1024
-
1025
- try {
1026
- return await this.loadJSON(sessionFile);
1027
- } catch {
1028
- return null;
1029
- }
1030
- }
1031
-
1032
- /**
1033
- * Save paused agent data
1034
- * @param {string} projectDir - Project directory path
1035
- * @param {string} agentId - Agent identifier
1036
- * @param {Object} pauseData - Pause information
1037
- * @returns {Promise<void>}
1038
- */
1039
- async savePausedAgent(projectDir, agentId, pauseData) {
1040
- const pausedFile = path.join(this.stateDirectory, this.stateFiles.pausedAgents);
1041
-
1042
- let data;
1043
- try {
1044
- data = await this.loadJSON(pausedFile);
1045
- } catch {
1046
- data = { pausedAgents: {}, pauseHistory: [] };
1047
- }
1048
-
1049
- data.pausedAgents[agentId] = pauseData;
1050
- await this.saveJSON(pausedFile, data);
1051
- }
1052
-
1053
- /**
1054
- * Remove paused agent data
1055
- * @param {string} projectDir - Project directory path
1056
- * @param {string} agentId - Agent identifier
1057
- * @returns {Promise<void>}
1058
- */
1059
- async removePausedAgent(projectDir, agentId) {
1060
- const pausedFile = path.join(this.stateDirectory, this.stateFiles.pausedAgents);
1061
-
1062
- try {
1063
- const data = await this.loadJSON(pausedFile);
1064
- delete data.pausedAgents[agentId];
1065
- await this.saveJSON(pausedFile, data);
1066
- } catch {
1067
- // File doesn't exist, nothing to remove
1068
- }
1069
- }
1070
-
1071
- /**
1072
- * Check agent pause status
1073
- * @param {string} agentId - Agent identifier
1074
- * @param {string} projectDir - Project directory path
1075
- * @returns {Promise<Object>} Pause status
1076
- */
1077
- async checkAgentPauseStatus(agentId, projectDir) {
1078
- const pausedFile = path.join(this.stateDirectory, this.stateFiles.pausedAgents);
1079
-
1080
- try {
1081
- const data = await this.loadJSON(pausedFile);
1082
- const pauseInfo = data.pausedAgents[agentId];
1083
-
1084
- if (!pauseInfo) {
1085
- return { isPaused: false, pausedUntil: null };
1086
- }
1087
-
1088
- const now = Date.now();
1089
- const pausedUntil = new Date(pauseInfo.pausedUntil).getTime();
1090
-
1091
- return {
1092
- isPaused: now < pausedUntil,
1093
- pausedUntil: pauseInfo.pausedUntil,
1094
- reason: pauseInfo.reason
1095
- };
1096
-
1097
- } catch {
1098
- return { isPaused: false, pausedUntil: null };
1099
- }
1100
- }
1101
-
1102
- /**
1103
- * Validate model conversations integrity
1104
- * @param {Object} conversations - Conversations object
1105
- * @returns {Promise<void>}
1106
- */
1107
- async validateModelConversations(conversations) {
1108
- if (!conversations || !conversations.full) {
1109
- throw new Error('Invalid conversations structure - missing full conversation');
1110
- }
1111
-
1112
- const fullMessages = conversations.full.messages || [];
1113
- const fullLastUpdated = new Date(conversations.full.lastUpdated);
1114
-
1115
- // Validate each model conversation against full conversation
1116
- for (const [modelName, modelConv] of Object.entries(conversations)) {
1117
- if (modelName === 'full') continue;
1118
-
1119
- if (!modelConv.messages) {
1120
- this.logger.warn(`Model conversation ${modelName} missing messages array`);
1121
- continue;
1122
- }
1123
-
1124
- const modelLastUpdated = new Date(modelConv.lastUpdated);
1125
-
1126
- if (fullLastUpdated > modelLastUpdated) {
1127
- this.logger.warn(`Model conversation ${modelName} is outdated, will sync on next use`);
1128
- modelConv.needsSync = true;
1129
- }
1130
- }
1131
- }
1132
-
1133
- /**
1134
- * Save JSON data to file
1135
- * @private
1136
- */
1137
- async saveJSON(filePath, data) {
1138
- const dir = path.dirname(filePath);
1139
- await fs.mkdir(dir, { recursive: true });
1140
-
1141
- const jsonData = JSON.stringify(data, null, 2);
1142
- await fs.writeFile(filePath, jsonData, 'utf8');
1143
- }
1144
-
1145
- /**
1146
- * Load JSON data from file (strict — throws on missing or corrupt).
1147
- * @private
1148
- */
1149
- async loadJSON(filePath) {
1150
- const data = await fs.readFile(filePath, 'utf8');
1151
- return JSON.parse(data);
1152
- }
1153
-
1154
- /**
1155
- * Load JSON with best-effort recovery from common breakage modes.
1156
- * Designed so a single bad state file doesn't take down the entire
1157
- * agent-load flow instead the caller gets back a usable object plus
1158
- * a `recovery` report that callers can surface as a toast.
1159
- *
1160
- * Recovery ladder (first match wins):
1161
- * 1. Missing file (ENOENT) → recreate with `defaultValue`,
1162
- * report `kind='not-found'`
1163
- * 2. Empty / whitespace-only file → overwrite with `defaultValue`,
1164
- * report `kind='empty-recreated'`
1165
- * 3. Common syntax issues → repaired in memory and saved
1166
- * (trailing comma, BOM, unclosed back to disk; report
1167
- * braces/brackets, stray garbage `kind='repaired'`
1168
- * after the JSON object)
1169
- * 4. Salvageable first JSON block → use the salvaged object,
1170
- * (anywhere in the buffer) archive the original to
1171
- * `<file>.corrupt-<ts>.json`,
1172
- * report `kind='partial'`
1173
- * 5. Nothing usable → use `defaultValue`, archive
1174
- * original (if any), report
1175
- * `kind='unrepairable'`
1176
- *
1177
- * @param {string} filePath - Absolute path to the JSON file
1178
- * @param {Object|Array|null|Function} defaultValue - Value to use when
1179
- * the file is missing/empty/unrepairable. If a function is provided
1180
- * it's called with the file path and its return value is used.
1181
- * @param {Object} [options]
1182
- * @param {boolean} [options.persistRecreated=true] - Whether to write
1183
- * the default back to disk on missing/empty/unrepairable cases.
1184
- * @param {string} [options.label] - Human-readable label used in toast
1185
- * messages (e.g. 'agent state', 'agent conversations'). Falls back
1186
- * to the file's basename.
1187
- * @returns {Promise<{data: any, recovery: ?Object}>}
1188
- */
1189
- async loadJSONResilient(filePath, defaultValue, options = {}) {
1190
- const { persistRecreated = true } = options;
1191
- const label = options.label || path.basename(filePath);
1192
- const defaultOf = () =>
1193
- typeof defaultValue === 'function' ? defaultValue(filePath) : defaultValue;
1194
-
1195
- // 1. Missing file recreate with default
1196
- let raw;
1197
- try {
1198
- raw = await fs.readFile(filePath, 'utf8');
1199
- } catch (err) {
1200
- if (err.code === 'ENOENT') {
1201
- const data = defaultOf();
1202
- if (persistRecreated && data !== undefined) {
1203
- try { await this.saveJSON(filePath, data); } catch { /* tolerate read-only mounts */ }
1204
- }
1205
- const recovery = {
1206
- kind: 'not-found', filePath, label,
1207
- message: `${label} file was missing — created an empty one.`,
1208
- };
1209
- this.logger?.info(`[stateManager] ${recovery.message}`, { filePath });
1210
- return { data, recovery };
1211
- }
1212
- throw err; // unexpected read error (permissions, etc) — let caller handle
1213
- }
1214
-
1215
- // 2. Empty or whitespace-only → recreate
1216
- if (!raw || raw.trim().length === 0) {
1217
- const data = defaultOf();
1218
- if (persistRecreated && data !== undefined) {
1219
- try { await this.saveJSON(filePath, data); } catch { /* ok */ }
1220
- }
1221
- const recovery = {
1222
- kind: 'empty-recreated', filePath, label,
1223
- message: `${label} file was empty recreated with a default skeleton.`,
1224
- };
1225
- this.logger?.warn(`[stateManager] ${recovery.message}`, { filePath });
1226
- return { data, recovery };
1227
- }
1228
-
1229
- // 3. Strict parse happy path
1230
- try {
1231
- return { data: JSON.parse(raw), recovery: null };
1232
- } catch (firstErr) {
1233
- // 3a. Quick repairs: BOM, trailing comma before close, junk after
1234
- // the closing brace, smart quotes that occasionally creep in via
1235
- // copy-paste. None of these is exotic — they're the top-3 reasons
1236
- // hand-edited or partially-flushed files break.
1237
- let repaired = raw;
1238
- if (repaired.charCodeAt(0) === 0xFEFF) repaired = repaired.slice(1);
1239
- // Strip ASCII control chars except tab/newline/CR
1240
- repaired = repaired.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '');
1241
- // Trim whitespace
1242
- repaired = repaired.trim();
1243
- // Drop trailing commas before a closing } or ]
1244
- repaired = repaired.replace(/,(\s*[}\]])/g, '$1');
1245
- // Cut anything after the last balanced } / ] (junk after JSON).
1246
- const lastClose = Math.max(repaired.lastIndexOf('}'), repaired.lastIndexOf(']'));
1247
- if (lastClose > 0 && lastClose < repaired.length - 1) {
1248
- repaired = repaired.slice(0, lastClose + 1);
1249
- }
1250
- try {
1251
- const data = JSON.parse(repaired);
1252
- if (persistRecreated && repaired !== raw) {
1253
- try { await this.saveJSON(filePath, data); } catch { /* ok */ }
1254
- }
1255
- const recovery = {
1256
- kind: 'repaired', filePath, label,
1257
- message: `${label} file had a minor syntax issue (${firstErr.message}) auto-repaired.`,
1258
- };
1259
- this.logger?.warn(`[stateManager] ${recovery.message}`, { filePath });
1260
- return { data, recovery };
1261
- } catch { /* fall through */ }
1262
-
1263
- // 3b. Salvage: walk the buffer for the first balanced { ... } block
1264
- // and try to parse just that. Catches truncated-mid-file cases.
1265
- const salvaged = this._extractFirstJsonBlock(raw);
1266
- if (salvaged) {
1267
- try {
1268
- const data = JSON.parse(salvaged);
1269
- const archivePath = `${filePath}.corrupt-${Date.now()}.json`;
1270
- try { await fs.writeFile(archivePath, raw, 'utf8'); } catch { /* ok */ }
1271
- if (persistRecreated) {
1272
- try { await this.saveJSON(filePath, data); } catch { /* ok */ }
1273
- }
1274
- const recovery = {
1275
- kind: 'partial', filePath, label,
1276
- message: `${label} file was partially corrupt — recovered the first valid section. Original archived to ${path.basename(archivePath)}.`,
1277
- archivePath,
1278
- };
1279
- this.logger?.warn(`[stateManager] ${recovery.message}`, { filePath, archivePath });
1280
- return { data, recovery };
1281
- } catch { /* fall through */ }
1282
- }
1283
-
1284
- // 4. Nothing parseable archive the corrupt file and use default
1285
- const data = defaultOf();
1286
- const archivePath = `${filePath}.corrupt-${Date.now()}.json`;
1287
- try { await fs.writeFile(archivePath, raw, 'utf8'); } catch { /* ok */ }
1288
- if (persistRecreated && data !== undefined) {
1289
- try { await this.saveJSON(filePath, data); } catch { /* ok */ }
1290
- }
1291
- const recovery = {
1292
- kind: 'unrepairable', filePath, label,
1293
- message: `${label} file is corrupt and could not be repaired — restored a default skeleton. Original archived to ${path.basename(archivePath)}.`,
1294
- archivePath,
1295
- originalError: firstErr.message,
1296
- };
1297
- this.logger?.error(`[stateManager] ${recovery.message}`, { filePath, archivePath, error: firstErr.message });
1298
- return { data, recovery };
1299
- }
1300
- }
1301
-
1302
- /**
1303
- * Walk a string and return the substring of the first balanced
1304
- * `{ ... }` block, respecting string literals so braces inside strings
1305
- * don't confuse the count. Returns null if no complete block exists.
1306
- * @private
1307
- */
1308
- _extractFirstJsonBlock(s) {
1309
- if (typeof s !== 'string') return null;
1310
- const start = s.indexOf('{');
1311
- if (start < 0) return null;
1312
- let depth = 0;
1313
- let inStr = false;
1314
- let escape = false;
1315
- for (let i = start; i < s.length; i++) {
1316
- const ch = s[i];
1317
- if (inStr) {
1318
- if (escape) { escape = false; continue; }
1319
- if (ch === '\\') { escape = true; continue; }
1320
- if (ch === '"') { inStr = false; }
1321
- continue;
1322
- }
1323
- if (ch === '"') { inStr = true; continue; }
1324
- if (ch === '{') depth++;
1325
- else if (ch === '}') {
1326
- depth--;
1327
- if (depth === 0) return s.slice(start, i + 1);
1328
- }
1329
- }
1330
- return null;
1331
- }
1332
-
1333
- /**
1334
- * Delete agent state from storage
1335
- * @param {string} agentId - Agent identifier
1336
- * @param {string} projectDir - Project directory path
1337
- * @returns {Promise<void>}
1338
- */
1339
- async deleteAgentState(agentId, projectDir = process.cwd()) {
1340
- const stateDir = this.getStateDir(projectDir);
1341
- const agentStateFile = path.join(stateDir, 'agents', `agent-${agentId}-state.json`);
1342
- const agentConversationsFile = path.join(stateDir, 'agents', `agent-${agentId}-conversations.json`);
1343
-
1344
- try {
1345
- // Delete agent state file
1346
- try {
1347
- await fs.unlink(agentStateFile);
1348
- this.logger.debug(`Deleted agent state file: ${agentId}`);
1349
- } catch (error) {
1350
- if (error.code !== 'ENOENT') {
1351
- this.logger.warn(`Failed to delete agent state file: ${error.message}`, { agentId });
1352
- }
1353
- }
1354
-
1355
- // Delete agent conversations file
1356
- try {
1357
- await fs.unlink(agentConversationsFile);
1358
- this.logger.debug(`Deleted agent conversations file: ${agentId}`);
1359
- } catch (error) {
1360
- if (error.code !== 'ENOENT') {
1361
- this.logger.warn(`Failed to delete agent conversations file: ${error.message}`, { agentId });
1362
- }
1363
- }
1364
-
1365
- // Remove from agent index
1366
- await this.removeFromAgentIndex(agentId, projectDir);
1367
-
1368
- this.logger.info(`Agent state deleted: ${agentId}`);
1369
-
1370
- } catch (error) {
1371
- this.logger.error(`Failed to delete agent state: ${error.message}`, {
1372
- agentId,
1373
- error: error.stack
1374
- });
1375
- throw error;
1376
- }
1377
- }
1378
-
1379
- /**
1380
- * Remove agent from agent index
1381
- * @param {string} agentId - Agent identifier
1382
- * @param {string} projectDir - Project directory path
1383
- * @returns {Promise<void>}
1384
- */
1385
- async removeFromAgentIndex(agentId, projectDir) {
1386
- const indexFile = path.join(this.stateDirectory, this.stateFiles.agentIndex);
1387
-
1388
- try {
1389
- const agentIndex = await this.loadJSON(indexFile);
1390
- delete agentIndex[agentId];
1391
- await this.saveJSON(indexFile, agentIndex);
1392
- this.logger.debug(`Removed agent from index: ${agentId}`);
1393
- } catch (error) {
1394
- // If index doesn't exist or can't be updated, log but don't throw
1395
- this.logger.warn(`Failed to remove agent from index: ${error.message}`, { agentId });
1396
- }
1397
- }
1398
-
1399
- /**
1400
- * Check if state directory exists
1401
- * @param {string} projectDir - Project directory path
1402
- * @returns {Promise<boolean>} True if state directory exists
1403
- */
1404
- async stateDirectoryExists(projectDir) {
1405
- const stateDir = this.getStateDir(projectDir);
1406
-
1407
- try {
1408
- const stats = await fs.stat(stateDir);
1409
- return stats.isDirectory();
1410
- } catch {
1411
- return false;
1412
- }
1413
- }
1414
-
1415
- /**
1416
- * Clean up old state files
1417
- * @param {string} projectDir - Project directory path
1418
- * @param {number} maxAge - Maximum age in days
1419
- * @returns {Promise<void>}
1420
- */
1421
- async cleanupOldState(projectDir, maxAge = 30) {
1422
- const stateDir = this.getStateDir(projectDir);
1423
- const cutoffDate = Date.now() - (maxAge * 24 * 60 * 60 * 1000);
1424
-
1425
- try {
1426
- const agentsDir = path.join(stateDir, 'agents');
1427
- const files = await fs.readdir(agentsDir);
1428
-
1429
- for (const file of files) {
1430
- const filePath = path.join(agentsDir, file);
1431
- const stats = await fs.stat(filePath);
1432
-
1433
- if (stats.mtime.getTime() < cutoffDate) {
1434
- await fs.unlink(filePath);
1435
- this.logger.info(`Cleaned up old state file: ${file}`);
1436
- }
1437
- }
1438
-
1439
- } catch (error) {
1440
- this.logger.warn(`State cleanup failed: ${error.message}`);
1441
- }
1442
- }
1443
-
1444
- /**
1445
- * Get all available agents (active + archived) from filesystem
1446
- * Scans both the index AND the agents directory to find all agents,
1447
- * including those that may be missing from the index.
1448
- * @param {string} projectDir - Project directory path
1449
- * @param {Object} agentPool - Agent pool instance to check active agents
1450
- * @returns {Promise<Array>} List of all agents with metadata
1451
- */
1452
- async getAllAvailableAgents(projectDir, agentPool) {
1453
- try {
1454
- const agentIndex = await this.loadAgentIndex(projectDir);
1455
- const activeAgentIds = agentPool ? (await agentPool.getAllAgents()).map(a => a.id) : [];
1456
- const agentsDir = this.getAgentsDir();
1457
-
1458
- // Track which agent IDs we've already processed
1459
- const processedAgentIds = new Set();
1460
- const agents = [];
1461
-
1462
- // First, process agents from the index
1463
- for (const [agentId, info] of Object.entries(agentIndex)) {
1464
- // Skip invalid or undefined entries
1465
- if (!info || !info.name) {
1466
- continue;
1467
- }
1468
-
1469
- processedAgentIds.add(agentId);
1470
- agents.push({
1471
- agentId,
1472
- name: info.name,
1473
- type: info.type,
1474
- model: info.model,
1475
- lastActivity: info.lastActivity,
1476
- status: info.status,
1477
- stateFile: info.stateFile,
1478
- conversationsFile: info.conversationsFile,
1479
- capabilities: info.capabilities || [],
1480
- isLoaded: activeAgentIds.includes(agentId),
1481
- canImport: !activeAgentIds.includes(agentId)
1482
- });
1483
- }
1484
-
1485
- // Second, scan the agents directory for any agents not in the index
1486
- try {
1487
- const files = await fs.readdir(agentsDir);
1488
- const stateFiles = files.filter(f => f.endsWith('-state.json'));
1489
-
1490
- let indexUpdated = false;
1491
-
1492
- for (const stateFile of stateFiles) {
1493
- // Extract agent ID from filename: agent-{agentId}-state.json
1494
- const match = stateFile.match(/^agent-(.+)-state\.json$/);
1495
- if (!match) continue;
1496
-
1497
- const agentId = match[1];
1498
- if (processedAgentIds.has(agentId)) continue;
1499
-
1500
- // Found an agent not in the index - load its state
1501
- try {
1502
- const statePath = path.join(agentsDir, stateFile);
1503
- const stateData = await this.loadJSON(statePath);
1504
- const state = stateData.state || stateData;
1505
-
1506
- // Build agent info from state file
1507
- const agentInfo = {
1508
- agentId,
1509
- name: state.name || stateData.name || `Recovered Agent ${agentId.slice(-8)}`,
1510
- type: state.type || 'user-created',
1511
- model: state.currentModel || state.preferredModel || 'unknown',
1512
- lastActivity: state.lastActivity || stateData.timestamp || null,
1513
- status: state.status || 'idle',
1514
- stateFile: `agents/${stateFile}`,
1515
- conversationsFile: `agents/agent-${agentId}-conversations.json`,
1516
- capabilities: state.capabilities || [],
1517
- isLoaded: activeAgentIds.includes(agentId),
1518
- canImport: !activeAgentIds.includes(agentId)
1519
- };
1520
-
1521
- agents.push(agentInfo);
1522
- processedAgentIds.add(agentId);
1523
-
1524
- // Update the index with this recovered agent
1525
- agentIndex[agentId] = {
1526
- name: agentInfo.name,
1527
- type: agentInfo.type,
1528
- stateFile: agentInfo.stateFile,
1529
- conversationsFile: agentInfo.conversationsFile,
1530
- lastActivity: agentInfo.lastActivity,
1531
- model: agentInfo.model,
1532
- status: agentInfo.status,
1533
- capabilities: agentInfo.capabilities
1534
- };
1535
- indexUpdated = true;
1536
-
1537
- this.logger.info(`Recovered agent from disk: ${agentInfo.name} (${agentId})`);
1538
- } catch (err) {
1539
- this.logger.warn(`Failed to recover agent from ${stateFile}: ${err.message}`);
1540
- }
1541
- }
1542
-
1543
- // Save the updated index if we found missing agents
1544
- if (indexUpdated) {
1545
- const indexFile = path.join(this.stateDirectory, this.stateFiles.agentIndex);
1546
- await this.saveJSON(indexFile, agentIndex);
1547
- this.logger.info(`Updated agent index with ${agents.length - Object.keys(agentIndex).length + Object.keys(processedAgentIds).size} recovered agents`);
1548
- }
1549
- } catch (err) {
1550
- this.logger.warn(`Failed to scan agents directory: ${err.message}`);
1551
- }
1552
-
1553
- // Sort by last activity (most recent first)
1554
- agents.sort((a, b) => {
1555
- const dateA = a.lastActivity ? new Date(a.lastActivity) : new Date(0);
1556
- const dateB = b.lastActivity ? new Date(b.lastActivity) : new Date(0);
1557
- return dateB - dateA;
1558
- });
1559
-
1560
- // Enrich agents with firstUserMessage snippet
1561
- await this._enrichAgentsWithSnippets(agents, agentPool);
1562
-
1563
- this.logger.info(`Found ${agents.length} available agents (${agents.filter(a => a.isLoaded).length} active, ${agents.filter(a => !a.isLoaded).length} archived)`);
1564
-
1565
- return agents;
1566
- } catch (error) {
1567
- this.logger.error(`Failed to get available agents: ${error.message}`);
1568
- throw error;
1569
- }
1570
- }
1571
-
1572
- /**
1573
- * Enrich agent list with firstUserMessage snippets.
1574
- * For loaded agents, reads from agentPool. For archived agents, peeks into conversations file.
1575
- */
1576
- async _enrichAgentsWithSnippets(agents, agentPool) {
1577
- const extractSnippet = (messages) => {
1578
- if (!messages || messages.length === 0) return null;
1579
- const firstUser = messages.find(m =>
1580
- m.role === 'user' && m.content && m.type !== 'task-boundary'
1581
- );
1582
- if (!firstUser) return null;
1583
- const text = typeof firstUser.content === 'string'
1584
- ? firstUser.content
1585
- : Array.isArray(firstUser.content)
1586
- ? firstUser.content.filter(b => b.type === 'text').map(b => b.text).join('\n')
1587
- : null;
1588
- if (!text) return null;
1589
- const lines = text.split('\n').filter(l => l.trim());
1590
- const snippet = lines.slice(0, 2).join('\n');
1591
- return snippet.length > 120 ? snippet.slice(0, 117) + '...' : snippet;
1592
- };
1593
-
1594
- for (const agent of agents) {
1595
- try {
1596
- if (agent.isLoaded && agentPool) {
1597
- // For loaded agents, get from the in-memory agent
1598
- const liveAgent = await agentPool.getAgent(agent.agentId);
1599
- if (liveAgent) {
1600
- agent.firstUserMessage = extractSnippet(liveAgent.conversations?.full?.messages);
1601
- continue;
1602
- }
1603
- }
1604
- // For archived agents, peek into the conversations file on disk
1605
- if (agent.conversationsFile) {
1606
- const convPath = path.join(this.stateDirectory, agent.conversationsFile);
1607
- try {
1608
- const convData = await this.loadJSON(convPath);
1609
- const messages = convData?.conversations?.full?.messages || convData?.full?.messages;
1610
- agent.firstUserMessage = extractSnippet(messages);
1611
- } catch {
1612
- // File may not exist — that's fine
1613
- }
1614
- }
1615
- } catch {
1616
- // Non-critical — just skip this agent's snippet
1617
- }
1618
- }
1619
- }
1620
-
1621
- /**
1622
- * Get agent metadata without full restoration (lightweight preview)
1623
- * @param {string} agentId - Agent ID
1624
- * @param {string} projectDir - Project directory path
1625
- * @returns {Promise<Object>} Agent metadata for preview
1626
- */
1627
- async getAgentMetadata(agentId, projectDir) {
1628
- try {
1629
- // Load agent index
1630
- const agentIndex = await this.loadAgentIndex(projectDir);
1631
- const agentInfo = agentIndex[agentId];
1632
-
1633
- if (!agentInfo) {
1634
- throw new Error(`Agent ${agentId} not found in index`);
1635
- }
1636
-
1637
- // Load just the state file (lightweight)
1638
- const stateDir = this.getStateDir(projectDir);
1639
- const stateFile = path.join(stateDir, agentInfo.stateFile);
1640
- const conversationsFile = path.join(stateDir, agentInfo.conversationsFile);
1641
-
1642
- // Check if files exist
1643
- const stateExists = await fs.access(stateFile).then(() => true).catch(() => false);
1644
- const conversationsExist = await fs.access(conversationsFile).then(() => true).catch(() => false);
1645
-
1646
- if (!stateExists) {
1647
- throw new Error(`State file not found for agent ${agentId}`);
1648
- }
1649
-
1650
- // Load state
1651
- const stateData = await this.loadJSON(stateFile);
1652
- const state = stateData.state || {};
1653
-
1654
- // Load conversation count without loading full messages (for performance)
1655
- let messageCount = 0;
1656
- let lastMessage = null;
1657
- if (conversationsExist) {
1658
- try {
1659
- const conversations = await this.loadJSON(conversationsFile);
1660
- messageCount = Array.isArray(conversations) ? conversations.length : 0;
1661
-
1662
- // Get last message for preview
1663
- if (messageCount > 0) {
1664
- const lastMsg = conversations[conversations.length - 1];
1665
- lastMessage = lastMsg?.content?.substring(0, 100) || null;
1666
- }
1667
- } catch (error) {
1668
- this.logger.warn(`Failed to load conversations for ${agentId}: ${error.message}`);
1669
- }
1670
- }
1671
-
1672
- const metadata = {
1673
- agentId,
1674
- name: agentInfo.name || state.name,
1675
- model: agentInfo.model || state.preferredModel || state.currentModel,
1676
- lastActivity: agentInfo.lastActivity,
1677
- status: agentInfo.status,
1678
- capabilities: state.capabilities || [],
1679
- messageCount,
1680
- lastMessage,
1681
- taskCount: state.taskList?.tasks?.length || 0,
1682
- createdAt: state.createdAt,
1683
- workingDirectory: state.directoryAccess?.workingDirectory,
1684
- mode: state.mode,
1685
- systemPrompt: state.originalSystemPrompt
1686
- };
1687
-
1688
- this.logger.info(`Loaded metadata for agent ${agentId}: ${metadata.messageCount} messages, ${metadata.taskCount} tasks`);
1689
-
1690
- return metadata;
1691
- } catch (error) {
1692
- this.logger.error(`Failed to get agent metadata for ${agentId}: ${error.message}`);
1693
- throw error;
1694
- }
1695
- }
1696
-
1697
- /**
1698
- * Import archived agent from filesystem and add to agent pool
1699
- * @param {string} agentId - Agent ID to import
1700
- * @param {string} projectDir - Project directory path
1701
- * @param {Object} agentPool - Agent pool instance
1702
- * @returns {Promise<Object>} Imported agent object
1703
- */
1704
- async importArchivedAgent(agentId, projectDir, agentPool) {
1705
- try {
1706
- // Validate agent ID format for security
1707
- const AGENT_ID_REGEX = /^agent-[a-z0-9-]+-\d+$/;
1708
- if (!AGENT_ID_REGEX.test(agentId)) {
1709
- throw new Error('Invalid agent ID format');
1710
- }
1711
-
1712
- // Check if already loaded in agent pool
1713
- if (agentPool && await agentPool.getAgent(agentId)) {
1714
- throw new Error(`Agent ${agentId} is already loaded. Use switchAgent() instead.`);
1715
- }
1716
-
1717
- // Load from agent index
1718
- const agentIndex = await this.loadAgentIndex(projectDir);
1719
- const agentInfo = agentIndex[agentId];
1720
-
1721
- if (!agentInfo) {
1722
- throw new Error(`Agent ${agentId} not found in index`);
1723
- }
1724
-
1725
- this.logger.info(`Importing archived agent: ${agentId} (${agentInfo.name})`);
1726
-
1727
- // Restore agent using existing restore logic
1728
- const agent = await this.restoreAgent(agentId, agentInfo, projectDir);
1729
-
1730
- // Update agent's last activity
1731
- agent.lastActivity = new Date().toISOString();
1732
-
1733
- // Add to agent pool if provided
1734
- if (agentPool) {
1735
- agentPool.agents.set(agent.id, agent);
1736
- agentPool._updateAgentDirectory(agent);
1737
- this.logger.info(`Agent ${agentId} added to agent pool`);
1738
- }
1739
-
1740
- // Update agent index with new last activity
1741
- await this.updateAgentIndex(agent, projectDir);
1742
-
1743
- this.logger.info(`Successfully imported agent ${agentId}: ${agent.name}`);
1744
-
1745
- return agent;
1746
- } catch (error) {
1747
- this.logger.error(`Failed to import agent ${agentId}: ${error.message}`);
1748
- throw error;
1749
- }
1750
- }
1751
- }
1752
-
1
+ /**
2
+ * StateManager - Handles state persistence, recovery, and project state management
3
+ *
4
+ * Purpose:
5
+ * - Project state persistence and recovery
6
+ * - Agent state management across sessions
7
+ * - Multi-model conversation state handling
8
+ * - Context reference state management
9
+ * - Session recovery and resume functionality
10
+ *
11
+ * IMPORTANT: State is now stored in a platform-appropriate user data directory
12
+ * that persists across npm package updates. See userDataDir.js for details.
13
+ */
14
+
15
+ import { promises as fs } from 'fs';
16
+ import path from 'path';
17
+ import { getUserDataPaths, ensureUserDataDirs } from '../utilities/userDataDir.js';
18
+
19
+ class StateManager {
20
+ constructor(config, logger) {
21
+ this.config = config;
22
+ this.logger = logger;
23
+
24
+ // UPDATED: Use persistent user data directory instead of relative path
25
+ // This ensures data survives npm package updates
26
+ const userPaths = getUserDataPaths();
27
+ this.stateDirectory = userPaths.state;
28
+ this.userDataPaths = userPaths;
29
+
30
+ // Legacy: Keep for backwards compatibility detection
31
+ this.legacyStateDirectory = config.system?.stateDirectory || '.loxia-state';
32
+ this.stateVersion = '1.0.0';
33
+
34
+ // State file paths
35
+ this.stateFiles = {
36
+ projectState: 'project-state.json',
37
+ agentIndex: 'agent-index.json',
38
+ teamIndex: 'team-index.json',
39
+ flowIndex: 'flow-index.json',
40
+ flowRunIndex: 'flow-run-index.json',
41
+ conversationIndex: 'conversation-index.json',
42
+ lastSession: 'last-session.json',
43
+ contextReferences: 'context-references.json',
44
+ asyncOperations: 'operations/async-operations.json',
45
+ pausedAgents: 'operations/paused-agents.json',
46
+ toolHistory: 'operations/tool-history.json',
47
+ modelRouterCache: 'models/model-router-cache.json',
48
+ errorRecoveryLog: 'models/error-recovery-log.json'
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Get the state directory path
54
+ * UPDATED: Now returns the user data directory (absolute path)
55
+ * The projectDir parameter is kept for API compatibility but is ignored
56
+ * @param {string} projectDir - Ignored, kept for compatibility
57
+ * @returns {string} Absolute path to state directory
58
+ */
59
+ // eslint-disable-next-line no-unused-vars
60
+ getStateDir(projectDir) {
61
+ // Always use the persistent user data directory
62
+ return this.stateDirectory;
63
+ }
64
+
65
+ /**
66
+ * Get the agents subdirectory path
67
+ * @returns {string} Absolute path to agents directory
68
+ */
69
+ getAgentsDir() {
70
+ return this.userDataPaths.agents;
71
+ }
72
+
73
+ /**
74
+ * Initialize state directory structure
75
+ * @param {string} projectDir - Project directory path (now ignored, uses user data dir)
76
+ * @returns {Promise<void>}
77
+ */
78
+ // eslint-disable-next-line no-unused-vars
79
+ async initializeStateDirectory(projectDir) {
80
+ // UPDATED: Use persistent user data directory instead of project-relative path
81
+ // The projectDir parameter is kept for API compatibility but now ignored
82
+ try {
83
+ // Use the centralized utility to create all necessary directories
84
+ const paths = await ensureUserDataDirs();
85
+
86
+ this.logger.info(`State directory initialized in user data location`, {
87
+ stateDir: paths.state,
88
+ platform: process.platform
89
+ });
90
+
91
+ } catch (error) {
92
+ this.logger.error(`Failed to initialize state directory: ${error.message}`);
93
+ throw error;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Resume project from saved state
99
+ * @param {string} projectDir - Project directory path
100
+ * @returns {Promise<Object>} Resumed project state
101
+ */
102
+ async resumeProject(projectDir) {
103
+ try {
104
+ await this.initializeStateDirectory(projectDir);
105
+
106
+ // Load project state
107
+ const projectState = await this.loadProjectState(projectDir);
108
+ const agentIndex = await this.loadAgentIndex(projectDir);
109
+
110
+ // Restore agents with multi-model conversations
111
+ const restoredAgents = [];
112
+ for (const [agentId, agentInfo] of Object.entries(agentIndex)) {
113
+ try {
114
+ const agent = await this.restoreAgent(agentId, agentInfo, projectDir);
115
+ restoredAgents.push(agent);
116
+ } catch (error) {
117
+ this.logger.warn(`Failed to restore agent: ${agentId}`, error.message);
118
+ }
119
+ }
120
+
121
+ // Restore async operations
122
+ const asyncOperations = await this.restoreAsyncOperations(projectDir);
123
+
124
+ // Restore paused agents
125
+ const pausedAgents = await this.restorePausedAgents(projectDir);
126
+
127
+ // Restore context references
128
+ const contextReferences = await this.restoreContextReferences(projectDir);
129
+
130
+ const resumedState = {
131
+ projectState,
132
+ agents: restoredAgents,
133
+ asyncOperations,
134
+ pausedAgents,
135
+ contextReferences,
136
+ resumedSuccessfully: true,
137
+ resumedAt: new Date().toISOString()
138
+ };
139
+
140
+ // Update last session
141
+ await this.saveLastSession(projectDir, {
142
+ resumedAt: new Date().toISOString(),
143
+ agentCount: restoredAgents.length,
144
+ operationCount: asyncOperations.length
145
+ });
146
+
147
+ this.logger.info(`Project resumed successfully`, {
148
+ projectDir,
149
+ agentCount: restoredAgents.length,
150
+ operationCount: asyncOperations.length
151
+ });
152
+
153
+ return resumedState;
154
+
155
+ } catch (error) {
156
+ this.logger.error(`Project resume failed: ${error.message}`, {
157
+ projectDir,
158
+ error: error.stack
159
+ });
160
+
161
+ return {
162
+ projectState: null,
163
+ agents: [],
164
+ asyncOperations: [],
165
+ pausedAgents: [],
166
+ contextReferences: [],
167
+ resumedSuccessfully: false,
168
+ error: error.message
169
+ };
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Persist agent state to storage
175
+ * @param {Object} agent - Agent object to persist
176
+ * @param {string} projectDir - Project directory path
177
+ * @returns {Promise<void>}
178
+ */
179
+ async persistAgentState(agent, projectDir = process.cwd()) {
180
+ const stateDir = this.getStateDir(projectDir);
181
+ const agentStateFile = path.join(stateDir, 'agents', `agent-${agent.id}-state.json`);
182
+ const agentConversationsFile = path.join(stateDir, 'agents', `agent-${agent.id}-conversations.json`);
183
+
184
+ try {
185
+ // Separate conversations from main agent state
186
+ const { conversations, ...agentState } = agent;
187
+
188
+ // Save agent state
189
+ await this.saveJSON(agentStateFile, {
190
+ version: this.stateVersion,
191
+ agentId: agent.id,
192
+ state: agentState,
193
+ lastPersisted: new Date().toISOString()
194
+ });
195
+
196
+ // Save conversations separately
197
+ await this.saveJSON(agentConversationsFile, {
198
+ version: this.stateVersion,
199
+ agentId: agent.id,
200
+ conversations,
201
+ lastPersisted: new Date().toISOString()
202
+ });
203
+
204
+ // Update agent index
205
+ await this.updateAgentIndex(agent, projectDir);
206
+
207
+ this.logger.debug(`Agent state persisted: ${agent.id}`);
208
+
209
+ } catch (error) {
210
+ this.logger.error(`Failed to persist agent state: ${error.message}`, {
211
+ agentId: agent.id,
212
+ error: error.stack
213
+ });
214
+ throw error;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Get project state
220
+ * @param {string} projectDir - Project directory path
221
+ * @returns {Promise<Object>} Project state object
222
+ */
223
+ async getProjectState(projectDir) {
224
+ return await this.loadProjectState(projectDir);
225
+ }
226
+
227
+ /**
228
+ * Load project state from storage
229
+ * @param {string} projectDir - Project directory path
230
+ * @returns {Promise<Object>} Project state object
231
+ */
232
+ async loadProjectState(projectDir) {
233
+ const stateFile = path.join(this.stateDirectory, this.stateFiles.projectState);
234
+
235
+ try {
236
+ const data = await this.loadJSON(stateFile);
237
+ return data;
238
+ } catch {
239
+ // Return default project state if file doesn't exist
240
+ const defaultState = {
241
+ version: this.stateVersion,
242
+ projectDir,
243
+ createdAt: new Date().toISOString(),
244
+ lastModified: new Date().toISOString(),
245
+ activeAgents: [],
246
+ lastActiveSession: null,
247
+ configuration: {
248
+ defaultModel: this.config.system?.defaultModel || 'anthropic-sonnet',
249
+ allowedTools: ['terminal', 'filesystem', 'browser'],
250
+ budgetLimit: 100.00
251
+ }
252
+ };
253
+
254
+ await this.saveProjectState(projectDir, defaultState);
255
+ return defaultState;
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Save project state to storage
261
+ * @param {string} projectDir - Project directory path
262
+ * @param {Object} projectState - Project state object
263
+ * @returns {Promise<void>}
264
+ */
265
+ async saveProjectState(projectDir, projectState) {
266
+ const stateFile = path.join(this.stateDirectory, this.stateFiles.projectState);
267
+
268
+ const stateData = {
269
+ ...projectState,
270
+ lastModified: new Date().toISOString()
271
+ };
272
+
273
+ await this.saveJSON(stateFile, stateData);
274
+ }
275
+
276
+ /**
277
+ * Load agent index
278
+ * @param {string} projectDir - Project directory path
279
+ * @returns {Promise<Object>} Agent index object
280
+ */
281
+ // eslint-disable-next-line no-unused-vars
282
+ async loadAgentIndex(projectDir) {
283
+ const indexFile = path.join(this.stateDirectory, this.stateFiles.agentIndex);
284
+
285
+ try {
286
+ return await this.loadJSON(indexFile);
287
+ } catch {
288
+ return {}; // Return empty index if file doesn't exist
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Update agent index
294
+ * @param {Object} agent - Agent object
295
+ * @param {string} projectDir - Project directory path
296
+ * @returns {Promise<void>}
297
+ */
298
+ // eslint-disable-next-line no-unused-vars
299
+ async updateAgentIndex(agent, projectDir) {
300
+ const indexFile = path.join(this.stateDirectory, this.stateFiles.agentIndex);
301
+
302
+ let agentIndex;
303
+ try {
304
+ agentIndex = await this.loadJSON(indexFile);
305
+ } catch {
306
+ agentIndex = {};
307
+ }
308
+
309
+ agentIndex[agent.id] = {
310
+ name: agent.name,
311
+ type: agent.type,
312
+ stateFile: `agents/agent-${agent.id}-state.json`,
313
+ conversationsFile: `agents/agent-${agent.id}-conversations.json`,
314
+ lastActivity: agent.lastActivity,
315
+ model: agent.currentModel,
316
+ status: agent.status,
317
+ capabilities: agent.capabilities || []
318
+ };
319
+
320
+ await this.saveJSON(indexFile, agentIndex);
321
+ }
322
+
323
+ // ==================== TEAM INDEX METHODS ====================
324
+
325
+ /**
326
+ * Load team index
327
+ * @param {string} projectDir - Project directory path (ignored, uses user data dir)
328
+ * @returns {Promise<Object>} Team index object
329
+ */
330
+ // eslint-disable-next-line no-unused-vars
331
+ async loadTeamIndex(projectDir) {
332
+ const indexFile = path.join(this.stateDirectory, this.stateFiles.teamIndex);
333
+
334
+ try {
335
+ return await this.loadJSON(indexFile);
336
+ } catch {
337
+ return {}; // Return empty index if file doesn't exist
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Save team index
343
+ * @param {Object} teamIndex - Team index object to save
344
+ * @returns {Promise<void>}
345
+ */
346
+ async saveTeamIndex(teamIndex) {
347
+ const indexFile = path.join(this.stateDirectory, this.stateFiles.teamIndex);
348
+ await this.saveJSON(indexFile, teamIndex);
349
+ }
350
+
351
+ /**
352
+ * Get all teams
353
+ * @param {string} projectDir - Project directory path (ignored)
354
+ * @returns {Promise<Array>} Array of team objects
355
+ */
356
+ async getAllTeams(projectDir) {
357
+ const teamIndex = await this.loadTeamIndex(projectDir);
358
+ return Object.entries(teamIndex).map(([id, team]) => ({
359
+ id,
360
+ ...team
361
+ }));
362
+ }
363
+
364
+ /**
365
+ * Get a single team by ID
366
+ * @param {string} teamId - Team identifier
367
+ * @param {string} projectDir - Project directory path (ignored)
368
+ * @returns {Promise<Object|null>} Team object or null if not found
369
+ */
370
+ async getTeam(teamId, projectDir) {
371
+ const teamIndex = await this.loadTeamIndex(projectDir);
372
+ if (teamIndex[teamId]) {
373
+ return { id: teamId, ...teamIndex[teamId] };
374
+ }
375
+ return null;
376
+ }
377
+
378
+ /**
379
+ * Create a new team
380
+ * @param {Object} teamData - Team data { name, description, color }
381
+ * @param {string} projectDir - Project directory path (ignored)
382
+ * @returns {Promise<Object>} Created team object
383
+ */
384
+ async createTeam(teamData, projectDir) {
385
+ const teamIndex = await this.loadTeamIndex(projectDir);
386
+
387
+ // Generate team ID
388
+ const safeName = (teamData.name || 'team').toLowerCase().replace(/[^a-z0-9]/g, '-').slice(0, 20);
389
+ const teamId = `team-${safeName}-${Date.now()}`;
390
+
391
+ const team = {
392
+ name: teamData.name,
393
+ description: teamData.description || '',
394
+ memberAgentIds: [],
395
+ color: teamData.color || '#3B82F6', // Default blue
396
+ // Used by platformControlTool's `ownedByMe` team scope. null when
397
+ // the team was created via the UI / by no specific agent. When an
398
+ // agent creates a team via the platformcontrol tool, this carries
399
+ // the caller's id so ownership scope filtering works.
400
+ createdBy: typeof teamData.createdBy === 'string' ? teamData.createdBy : null,
401
+ createdAt: new Date().toISOString(),
402
+ updatedAt: new Date().toISOString()
403
+ };
404
+
405
+ teamIndex[teamId] = team;
406
+ await this.saveTeamIndex(teamIndex);
407
+
408
+ this.logger.info(`Team created: ${teamId}`, { name: team.name });
409
+
410
+ return { id: teamId, ...team };
411
+ }
412
+
413
+ /**
414
+ * Update an existing team
415
+ * @param {string} teamId - Team identifier
416
+ * @param {Object} updates - Fields to update
417
+ * @param {string} projectDir - Project directory path (ignored)
418
+ * @returns {Promise<Object>} Updated team object
419
+ */
420
+ async updateTeam(teamId, updates, projectDir) {
421
+ const teamIndex = await this.loadTeamIndex(projectDir);
422
+
423
+ if (!teamIndex[teamId]) {
424
+ throw new Error(`Team ${teamId} not found`);
425
+ }
426
+
427
+ // Only allow updating specific fields
428
+ const allowedFields = ['name', 'description', 'color', 'memberAgentIds'];
429
+ for (const field of allowedFields) {
430
+ if (updates[field] !== undefined) {
431
+ teamIndex[teamId][field] = updates[field];
432
+ }
433
+ }
434
+ teamIndex[teamId].updatedAt = new Date().toISOString();
435
+
436
+ await this.saveTeamIndex(teamIndex);
437
+
438
+ this.logger.info(`Team updated: ${teamId}`, { updates: Object.keys(updates) });
439
+
440
+ return { id: teamId, ...teamIndex[teamId] };
441
+ }
442
+
443
+ /**
444
+ * Delete a team
445
+ * @param {string} teamId - Team identifier
446
+ * @param {string} projectDir - Project directory path (ignored)
447
+ * @returns {Promise<boolean>} True if deleted
448
+ */
449
+ async deleteTeam(teamId, projectDir) {
450
+ const teamIndex = await this.loadTeamIndex(projectDir);
451
+
452
+ if (!teamIndex[teamId]) {
453
+ throw new Error(`Team ${teamId} not found`);
454
+ }
455
+
456
+ const teamName = teamIndex[teamId].name;
457
+ delete teamIndex[teamId];
458
+ await this.saveTeamIndex(teamIndex);
459
+
460
+ this.logger.info(`Team deleted: ${teamId}`, { name: teamName });
461
+
462
+ return true;
463
+ }
464
+
465
+ /**
466
+ * Add an agent to a team
467
+ * @param {string} teamId - Team identifier
468
+ * @param {string} agentId - Agent identifier to add
469
+ * @param {string} projectDir - Project directory path (ignored)
470
+ * @returns {Promise<Object>} Updated team object
471
+ */
472
+ async addAgentToTeam(teamId, agentId, projectDir) {
473
+ const teamIndex = await this.loadTeamIndex(projectDir);
474
+
475
+ if (!teamIndex[teamId]) {
476
+ throw new Error(`Team ${teamId} not found`);
477
+ }
478
+
479
+ // Check if agent already in team
480
+ if (!teamIndex[teamId].memberAgentIds.includes(agentId)) {
481
+ teamIndex[teamId].memberAgentIds.push(agentId);
482
+ teamIndex[teamId].updatedAt = new Date().toISOString();
483
+ await this.saveTeamIndex(teamIndex);
484
+
485
+ this.logger.info(`Agent added to team`, { teamId, agentId });
486
+ }
487
+
488
+ return { id: teamId, ...teamIndex[teamId] };
489
+ }
490
+
491
+ /**
492
+ * Remove an agent from a team
493
+ * @param {string} teamId - Team identifier
494
+ * @param {string} agentId - Agent identifier to remove
495
+ * @param {string} projectDir - Project directory path (ignored)
496
+ * @returns {Promise<Object>} Updated team object
497
+ */
498
+ async removeAgentFromTeam(teamId, agentId, projectDir) {
499
+ const teamIndex = await this.loadTeamIndex(projectDir);
500
+
501
+ if (!teamIndex[teamId]) {
502
+ throw new Error(`Team ${teamId} not found`);
503
+ }
504
+
505
+ const index = teamIndex[teamId].memberAgentIds.indexOf(agentId);
506
+ if (index > -1) {
507
+ teamIndex[teamId].memberAgentIds.splice(index, 1);
508
+ teamIndex[teamId].updatedAt = new Date().toISOString();
509
+ await this.saveTeamIndex(teamIndex);
510
+
511
+ this.logger.info(`Agent removed from team`, { teamId, agentId });
512
+ }
513
+
514
+ return { id: teamId, ...teamIndex[teamId] };
515
+ }
516
+
517
+ /**
518
+ * Get all teams that contain a specific agent
519
+ * @param {string} agentId - Agent identifier
520
+ * @param {string} projectDir - Project directory path (ignored)
521
+ * @returns {Promise<Array>} Array of team objects containing the agent
522
+ */
523
+ async getAgentTeams(agentId, projectDir) {
524
+ const teams = await this.getAllTeams(projectDir);
525
+ return teams.filter(team => team.memberAgentIds.includes(agentId));
526
+ }
527
+
528
+ // ==================== END TEAM INDEX METHODS ====================
529
+
530
+ // ==================== FLOW INDEX METHODS ====================
531
+
532
+ /**
533
+ * Load flow index
534
+ * @param {string} projectDir - Project directory path (ignored, uses user data dir)
535
+ * @returns {Promise<Object>} Flow index object
536
+ */
537
+ // eslint-disable-next-line no-unused-vars
538
+ async loadFlowIndex(projectDir) {
539
+ const indexFile = path.join(this.stateDirectory, this.stateFiles.flowIndex);
540
+
541
+ try {
542
+ return await this.loadJSON(indexFile);
543
+ } catch {
544
+ return {}; // Return empty index if file doesn't exist
545
+ }
546
+ }
547
+
548
+ /**
549
+ * Save flow index
550
+ * @param {Object} flowIndex - Flow index object to save
551
+ * @returns {Promise<void>}
552
+ */
553
+ async saveFlowIndex(flowIndex) {
554
+ const indexFile = path.join(this.stateDirectory, this.stateFiles.flowIndex);
555
+ await this.saveJSON(indexFile, flowIndex);
556
+ }
557
+
558
+ /**
559
+ * Get all flows
560
+ * @param {string} projectDir - Project directory path (ignored)
561
+ * @returns {Promise<Array>} Array of flow objects
562
+ */
563
+ async getAllFlows(projectDir) {
564
+ const flowIndex = await this.loadFlowIndex(projectDir);
565
+ return Object.entries(flowIndex).map(([id, flow]) => ({
566
+ id,
567
+ ...flow
568
+ }));
569
+ }
570
+
571
+ /**
572
+ * Get a single flow by ID
573
+ * @param {string} flowId - Flow identifier
574
+ * @param {string} projectDir - Project directory path (ignored)
575
+ * @returns {Promise<Object|null>} Flow object or null if not found
576
+ */
577
+ async getFlow(flowId, projectDir) {
578
+ const flowIndex = await this.loadFlowIndex(projectDir);
579
+ if (flowIndex[flowId]) {
580
+ return { id: flowId, ...flowIndex[flowId] };
581
+ }
582
+ return null;
583
+ }
584
+
585
+ /**
586
+ * Create a new flow
587
+ * @param {Object} flowData - Flow data { name, description, nodes, edges, variables }
588
+ * @param {string} projectDir - Project directory path (ignored)
589
+ * @returns {Promise<Object>} Created flow object
590
+ */
591
+ async createFlow(flowData, projectDir) {
592
+ const flowIndex = await this.loadFlowIndex(projectDir);
593
+
594
+ // Generate flow ID
595
+ const safeName = (flowData.name || 'flow').toLowerCase().replace(/[^a-z0-9]/g, '-').slice(0, 20);
596
+ const flowId = `flow-${safeName}-${Date.now()}`;
597
+
598
+ // Stamp edge ids on save. Templates and marketplace-installed flows
599
+ // ship without edge ids; ReactFlow refuses to render edges that
600
+ // don't have a unique `id`, so unstamped edges show up as
601
+ // disconnected dots on the canvas. Doing this here rather than in
602
+ // the editor means the durable shape on disk is always renderable.
603
+ // Helper is shared with FlowEditor's defense-in-depth layer so the
604
+ // id format stays consistent across save + reload.
605
+ const { ensureEdgeIds } = await import('../utilities/flowEdgeIds.js');
606
+ const stampedEdges = ensureEdgeIds(flowData.edges);
607
+
608
+ const flow = {
609
+ name: flowData.name,
610
+ description: flowData.description || '',
611
+ nodes: flowData.nodes || [],
612
+ edges: stampedEdges,
613
+ variables: flowData.variables || {},
614
+ createdAt: new Date().toISOString(),
615
+ updatedAt: new Date().toISOString(),
616
+ // createdBy: only present when the caller stamps it (e.g.
617
+ // platformControlTool's create-flow path). Not required for the
618
+ // happy-path UI flow but durable when set.
619
+ ...(flowData.createdBy ? { createdBy: flowData.createdBy } : {})
620
+ };
621
+
622
+ flowIndex[flowId] = flow;
623
+ await this.saveFlowIndex(flowIndex);
624
+
625
+ this.logger.info(`Flow created: ${flowId}`, { name: flow.name });
626
+
627
+ return { id: flowId, ...flow };
628
+ }
629
+
630
+ /**
631
+ * Update an existing flow
632
+ * @param {string} flowId - Flow identifier
633
+ * @param {Object} updates - Fields to update
634
+ * @param {string} projectDir - Project directory path (ignored)
635
+ * @returns {Promise<Object>} Updated flow object
636
+ */
637
+ async updateFlow(flowId, updates, projectDir) {
638
+ const flowIndex = await this.loadFlowIndex(projectDir);
639
+
640
+ if (!flowIndex[flowId]) {
641
+ throw new Error(`Flow ${flowId} not found`);
642
+ }
643
+
644
+ // Only allow updating specific fields. `version` and `schemaVersion`
645
+ // are added so the Phase 6 version-stamp write-back persists, and
646
+ // so v2 flows can be marked when typed I/O is added.
647
+ const allowedFields = ['name', 'description', 'nodes', 'edges', 'variables', 'version', 'schemaVersion'];
648
+ for (const field of allowedFields) {
649
+ if (updates[field] !== undefined) {
650
+ flowIndex[flowId][field] = updates[field];
651
+ }
652
+ }
653
+ flowIndex[flowId].updatedAt = new Date().toISOString();
654
+
655
+ await this.saveFlowIndex(flowIndex);
656
+
657
+ this.logger.info(`Flow updated: ${flowId}`, { updates: Object.keys(updates) });
658
+
659
+ return { id: flowId, ...flowIndex[flowId] };
660
+ }
661
+
662
+ /**
663
+ * Delete a flow
664
+ * @param {string} flowId - Flow identifier
665
+ * @param {string} projectDir - Project directory path (ignored)
666
+ * @returns {Promise<boolean>} True if deleted
667
+ */
668
+ async deleteFlow(flowId, projectDir) {
669
+ const flowIndex = await this.loadFlowIndex(projectDir);
670
+
671
+ if (!flowIndex[flowId]) {
672
+ throw new Error(`Flow ${flowId} not found`);
673
+ }
674
+
675
+ const flowName = flowIndex[flowId].name;
676
+ delete flowIndex[flowId];
677
+ await this.saveFlowIndex(flowIndex);
678
+
679
+ this.logger.info(`Flow deleted: ${flowId}`, { name: flowName });
680
+
681
+ return true;
682
+ }
683
+
684
+ // ==================== FLOW RUN METHODS ====================
685
+
686
+ /**
687
+ * Load flow run index
688
+ * @param {string} projectDir - Project directory path (ignored)
689
+ * @returns {Promise<Object>} Flow run index object
690
+ */
691
+ // eslint-disable-next-line no-unused-vars
692
+ async loadFlowRunIndex(projectDir) {
693
+ const indexFile = path.join(this.stateDirectory, this.stateFiles.flowRunIndex);
694
+
695
+ try {
696
+ return await this.loadJSON(indexFile);
697
+ } catch {
698
+ return {}; // Return empty index if file doesn't exist
699
+ }
700
+ }
701
+
702
+ /**
703
+ * Save flow run index
704
+ * @param {Object} runIndex - Flow run index object to save
705
+ * @returns {Promise<void>}
706
+ */
707
+ async saveFlowRunIndex(runIndex) {
708
+ const indexFile = path.join(this.stateDirectory, this.stateFiles.flowRunIndex);
709
+ await this.saveJSON(indexFile, runIndex);
710
+ }
711
+
712
+ /**
713
+ * Create a new flow run
714
+ * @param {string} flowId - Flow identifier
715
+ * @param {Object} initialInput - Initial input for the flow
716
+ * @param {string} projectDir - Project directory path (ignored)
717
+ * @returns {Promise<Object>} Created flow run object
718
+ */
719
+ async createFlowRun(flowId, initialInput, projectDir) {
720
+ const runIndex = await this.loadFlowRunIndex(projectDir);
721
+
722
+ const runId = `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
723
+
724
+ const run = {
725
+ flowId,
726
+ status: 'pending', // pending, running, completed, failed, stopped
727
+ initialInput,
728
+ nodeStates: {},
729
+ startedAt: new Date().toISOString(),
730
+ completedAt: null,
731
+ error: null
732
+ };
733
+
734
+ runIndex[runId] = run;
735
+ await this.saveFlowRunIndex(runIndex);
736
+
737
+ this.logger.info(`Flow run created: ${runId}`, { flowId });
738
+
739
+ return { id: runId, ...run };
740
+ }
741
+
742
+ /**
743
+ * Update a flow run
744
+ * @param {string} runId - Run identifier
745
+ * @param {Object} updates - Fields to update
746
+ * @param {string} projectDir - Project directory path (ignored)
747
+ * @returns {Promise<Object>} Updated flow run object
748
+ */
749
+ async updateFlowRun(runId, updates, projectDir) {
750
+ const runIndex = await this.loadFlowRunIndex(projectDir);
751
+
752
+ if (!runIndex[runId]) {
753
+ throw new Error(`Flow run ${runId} not found`);
754
+ }
755
+
756
+ // Only allow updating specific fields
757
+ const allowedFields = ['status', 'nodeStates', 'completedAt', 'error', 'output'];
758
+ for (const field of allowedFields) {
759
+ if (updates[field] !== undefined) {
760
+ runIndex[runId][field] = updates[field];
761
+ }
762
+ }
763
+
764
+ await this.saveFlowRunIndex(runIndex);
765
+
766
+ this.logger.info(`Flow run updated: ${runId}`, { status: updates.status });
767
+
768
+ return { id: runId, ...runIndex[runId] };
769
+ }
770
+
771
+ /**
772
+ * Get a flow run by ID
773
+ * @param {string} runId - Run identifier
774
+ * @param {string} projectDir - Project directory path (ignored)
775
+ * @returns {Promise<Object|null>} Flow run object or null if not found
776
+ */
777
+ async getFlowRun(runId, projectDir) {
778
+ const runIndex = await this.loadFlowRunIndex(projectDir);
779
+ if (runIndex[runId]) {
780
+ return { id: runId, ...runIndex[runId] };
781
+ }
782
+ return null;
783
+ }
784
+
785
+ /**
786
+ * Get all runs for a specific flow
787
+ * @param {string} flowId - Flow identifier
788
+ * @param {string} projectDir - Project directory path (ignored)
789
+ * @returns {Promise<Array>} Array of flow run objects
790
+ */
791
+ async getFlowRuns(flowId, projectDir) {
792
+ const runIndex = await this.loadFlowRunIndex(projectDir);
793
+ return Object.entries(runIndex)
794
+ .filter(([, run]) => run.flowId === flowId)
795
+ .map(([id, run]) => ({ id, ...run }))
796
+ .sort((a, b) => new Date(b.startedAt) - new Date(a.startedAt));
797
+ }
798
+
799
+ // ==================== END FLOW INDEX METHODS ====================
800
+
801
+ /**
802
+ * Restore agent from saved state
803
+ * @param {string} agentId - Agent identifier
804
+ * @param {Object} agentInfo - Agent info from index
805
+ * @param {string} projectDir - Project directory path
806
+ * @returns {Promise<Object>} Restored agent object
807
+ */
808
+ async restoreAgent(agentId, agentInfo, projectDir) {
809
+ const stateDir = this.getStateDir(projectDir);
810
+ const stateFile = path.join(stateDir, agentInfo.stateFile);
811
+ const conversationsFile = path.join(stateDir, agentInfo.conversationsFile);
812
+
813
+ // Skeleton defaults the resilient loader falls back to when a state
814
+ // file is missing, empty, or unrepairable. The state skeleton derives
815
+ // the agent name from the index entry so even a total wipe leaves
816
+ // the agent identifiable in the UI rather than ending up nameless.
817
+ const stateDefault = () => ({
818
+ version: 2,
819
+ agentId,
820
+ state: {
821
+ id: agentId,
822
+ name: agentInfo.name || agentId,
823
+ type: agentInfo.type || 'user-created',
824
+ status: 'active',
825
+ mode: 'chat',
826
+ currentModel: agentInfo.preferredModel || agentInfo.model || null,
827
+ preferredModel: agentInfo.preferredModel || agentInfo.model || null,
828
+ systemPrompt: '',
829
+ capabilities: agentInfo.capabilities || [],
830
+ taskList: { tasks: [], lastUpdated: new Date().toISOString() },
831
+ messageQueues: { userMessages: [], interAgentMessages: [], toolResults: [] },
832
+ interAgentTracking: {},
833
+ },
834
+ lastPersisted: new Date().toISOString(),
835
+ });
836
+ const conversationsDefault = () => ({
837
+ version: 2,
838
+ agentId,
839
+ conversations: {
840
+ // Empty `full` conversation — required by validateModelConversations
841
+ // and the agent loop's history reader. New per-model entries are
842
+ // added on demand at first use.
843
+ full: { messages: [], lastUpdated: new Date().toISOString() },
844
+ },
845
+ lastPersisted: new Date().toISOString(),
846
+ });
847
+
848
+ // Recovery reports for any auto-fixed files. Attached to the
849
+ // returned agent so the caller (orchestrator webServer) can
850
+ // broadcast each one as a toast in the UI. Empty array on the
851
+ // happy path; harmless presence otherwise.
852
+ const recoveries = [];
853
+
854
+ try {
855
+ const stateRes = await this.loadJSONResilient(stateFile, stateDefault, {
856
+ label: `agent state (${agentInfo.name || agentId})`,
857
+ });
858
+ const conversationsRes = await this.loadJSONResilient(conversationsFile, conversationsDefault, {
859
+ label: `agent conversations (${agentInfo.name || agentId})`,
860
+ });
861
+ const stateData = stateRes.data;
862
+ const conversationsData = conversationsRes.data;
863
+ if (stateRes.recovery) recoveries.push(stateRes.recovery);
864
+ if (conversationsRes.recovery) recoveries.push(conversationsRes.recovery);
865
+
866
+ // Validate model conversations integrity
867
+ await this.validateModelConversations(conversationsData.conversations);
868
+
869
+ // Check if agent is paused
870
+ const pauseStatus = await this.checkAgentPauseStatus(agentId, projectDir);
871
+
872
+ const restoredAgent = {
873
+ ...stateData.state,
874
+ conversations: conversationsData.conversations,
875
+ isPaused: pauseStatus.isPaused,
876
+ pausedUntil: pauseStatus.pausedUntil,
877
+ isRestored: true,
878
+ restoredAt: new Date().toISOString(),
879
+ // Non-enumerable would be cleaner, but agentPool serializes the
880
+ // whole object — keeping it as a plain field. The orchestrator
881
+ // strips it before persisting (see import handler).
882
+ _restoreRecoveries: recoveries,
883
+ };
884
+
885
+ // CRITICAL: Restore interAgentTracking as a Map (it comes as plain object from JSON)
886
+ if (!restoredAgent.interAgentTracking || typeof restoredAgent.interAgentTracking !== 'object') {
887
+ restoredAgent.interAgentTracking = new Map();
888
+ } else if (!(restoredAgent.interAgentTracking instanceof Map)) {
889
+ restoredAgent.interAgentTracking = new Map(Object.entries(restoredAgent.interAgentTracking));
890
+ }
891
+
892
+ this.logger.info(`Agent restored: ${agentId}`, {
893
+ name: restoredAgent.name,
894
+ status: restoredAgent.status,
895
+ messageCount: restoredAgent.conversations?.full?.messages?.length || 0,
896
+ recoveryCount: recoveries.length,
897
+ });
898
+
899
+ return restoredAgent;
900
+
901
+ } catch (error) {
902
+ this.logger.error(`Agent restoration failed: ${agentId}`, error.message);
903
+ throw error;
904
+ }
905
+ }
906
+
907
+ /**
908
+ * Restore async operations
909
+ * @param {string} projectDir - Project directory path
910
+ * @returns {Promise<Array>} Array of active async operations
911
+ */
912
+ // eslint-disable-next-line no-unused-vars
913
+ async restoreAsyncOperations(projectDir) {
914
+ const operationsFile = path.join(this.stateDirectory, this.stateFiles.asyncOperations);
915
+
916
+ try {
917
+ const data = await this.loadJSON(operationsFile);
918
+ return data.operations || [];
919
+ } catch {
920
+ return [];
921
+ }
922
+ }
923
+
924
+ /**
925
+ * Restore paused agents
926
+ * @param {string} projectDir - Project directory path
927
+ * @returns {Promise<Object>} Paused agents data
928
+ */
929
+ // eslint-disable-next-line no-unused-vars
930
+ async restorePausedAgents(projectDir) {
931
+ const pausedFile = path.join(this.stateDirectory, this.stateFiles.pausedAgents);
932
+
933
+ try {
934
+ const data = await this.loadJSON(pausedFile);
935
+ const now = Date.now();
936
+
937
+ // Check which agents should be resumed
938
+ const toResume = [];
939
+ for (const [agentId, pauseInfo] of Object.entries(data.pausedAgents || {})) {
940
+ const pausedUntil = new Date(pauseInfo.pausedUntil).getTime();
941
+
942
+ if (now >= pausedUntil) {
943
+ toResume.push(agentId);
944
+ }
945
+ }
946
+
947
+ // Move expired pauses to history
948
+ for (const agentId of toResume) {
949
+ const pauseInfo = data.pausedAgents[agentId];
950
+ delete data.pausedAgents[agentId];
951
+
952
+ data.pauseHistory = data.pauseHistory || [];
953
+ data.pauseHistory.push({
954
+ agentId,
955
+ pausedAt: pauseInfo.pausedAt,
956
+ resumedAt: new Date().toISOString(),
957
+ reason: pauseInfo.reason,
958
+ actualDuration: Math.round((now - new Date(pauseInfo.pausedAt).getTime()) / 1000)
959
+ });
960
+ }
961
+
962
+ // Save updated data
963
+ await this.saveJSON(pausedFile, data);
964
+
965
+ return data;
966
+
967
+ } catch {
968
+ return {
969
+ pausedAgents: {},
970
+ pauseHistory: []
971
+ };
972
+ }
973
+ }
974
+
975
+ /**
976
+ * Restore context references
977
+ * @param {string} projectDir - Project directory path
978
+ * @returns {Promise<Object>} Context references data
979
+ */
980
+ // eslint-disable-next-line no-unused-vars
981
+ async restoreContextReferences(projectDir) {
982
+ const contextFile = path.join(this.stateDirectory, this.stateFiles.contextReferences);
983
+
984
+ try {
985
+ const data = await this.loadJSON(contextFile);
986
+
987
+ // Validate context references (implementation would validate file existence, etc.)
988
+ const validatedReferences = [];
989
+ for (const reference of data.references || []) {
990
+ // Add validation logic here
991
+ reference.isValid = true; // Placeholder
992
+ reference.lastValidated = new Date().toISOString();
993
+ validatedReferences.push(reference);
994
+ }
995
+
996
+ data.references = validatedReferences;
997
+ await this.saveJSON(contextFile, data);
998
+
999
+ return data;
1000
+
1001
+ } catch {
1002
+ return {
1003
+ references: [],
1004
+ lastCleanup: new Date().toISOString()
1005
+ };
1006
+ }
1007
+ }
1008
+
1009
+ /**
1010
+ * Save last session data
1011
+ * @param {string} projectDir - Project directory path
1012
+ * @param {Object} sessionData - Session data to save
1013
+ * @returns {Promise<void>}
1014
+ */
1015
+ async saveLastSession(projectDir, sessionData) {
1016
+ const sessionFile = path.join(this.stateDirectory, this.stateFiles.lastSession);
1017
+
1018
+ const data = {
1019
+ ...sessionData,
1020
+ savedAt: new Date().toISOString(),
1021
+ projectDir
1022
+ };
1023
+
1024
+ await this.saveJSON(sessionFile, data);
1025
+ }
1026
+
1027
+ /**
1028
+ * Load last session data
1029
+ * @param {string} projectDir - Project directory path
1030
+ * @returns {Promise<Object>} Last session data
1031
+ */
1032
+ // eslint-disable-next-line no-unused-vars
1033
+ async loadLastSession(projectDir) {
1034
+ const sessionFile = path.join(this.stateDirectory, this.stateFiles.lastSession);
1035
+
1036
+ try {
1037
+ return await this.loadJSON(sessionFile);
1038
+ } catch {
1039
+ return null;
1040
+ }
1041
+ }
1042
+
1043
+ /**
1044
+ * Save paused agent data
1045
+ * @param {string} projectDir - Project directory path
1046
+ * @param {string} agentId - Agent identifier
1047
+ * @param {Object} pauseData - Pause information
1048
+ * @returns {Promise<void>}
1049
+ */
1050
+ async savePausedAgent(projectDir, agentId, pauseData) {
1051
+ const pausedFile = path.join(this.stateDirectory, this.stateFiles.pausedAgents);
1052
+
1053
+ let data;
1054
+ try {
1055
+ data = await this.loadJSON(pausedFile);
1056
+ } catch {
1057
+ data = { pausedAgents: {}, pauseHistory: [] };
1058
+ }
1059
+
1060
+ data.pausedAgents[agentId] = pauseData;
1061
+ await this.saveJSON(pausedFile, data);
1062
+ }
1063
+
1064
+ /**
1065
+ * Remove paused agent data
1066
+ * @param {string} projectDir - Project directory path
1067
+ * @param {string} agentId - Agent identifier
1068
+ * @returns {Promise<void>}
1069
+ */
1070
+ async removePausedAgent(projectDir, agentId) {
1071
+ const pausedFile = path.join(this.stateDirectory, this.stateFiles.pausedAgents);
1072
+
1073
+ try {
1074
+ const data = await this.loadJSON(pausedFile);
1075
+ delete data.pausedAgents[agentId];
1076
+ await this.saveJSON(pausedFile, data);
1077
+ } catch {
1078
+ // File doesn't exist, nothing to remove
1079
+ }
1080
+ }
1081
+
1082
+ /**
1083
+ * Check agent pause status
1084
+ * @param {string} agentId - Agent identifier
1085
+ * @param {string} projectDir - Project directory path
1086
+ * @returns {Promise<Object>} Pause status
1087
+ */
1088
+ // eslint-disable-next-line no-unused-vars
1089
+ async checkAgentPauseStatus(agentId, projectDir) {
1090
+ const pausedFile = path.join(this.stateDirectory, this.stateFiles.pausedAgents);
1091
+
1092
+ try {
1093
+ const data = await this.loadJSON(pausedFile);
1094
+ const pauseInfo = data.pausedAgents[agentId];
1095
+
1096
+ if (!pauseInfo) {
1097
+ return { isPaused: false, pausedUntil: null };
1098
+ }
1099
+
1100
+ const now = Date.now();
1101
+ const pausedUntil = new Date(pauseInfo.pausedUntil).getTime();
1102
+
1103
+ return {
1104
+ isPaused: now < pausedUntil,
1105
+ pausedUntil: pauseInfo.pausedUntil,
1106
+ reason: pauseInfo.reason
1107
+ };
1108
+
1109
+ } catch {
1110
+ return { isPaused: false, pausedUntil: null };
1111
+ }
1112
+ }
1113
+
1114
+ /**
1115
+ * Validate model conversations integrity
1116
+ * @param {Object} conversations - Conversations object
1117
+ * @returns {Promise<void>}
1118
+ */
1119
+ async validateModelConversations(conversations) {
1120
+ if (!conversations || !conversations.full) {
1121
+ throw new Error('Invalid conversations structure - missing full conversation');
1122
+ }
1123
+
1124
+ const fullLastUpdated = new Date(conversations.full.lastUpdated);
1125
+
1126
+ // Validate each model conversation against full conversation
1127
+ for (const [modelName, modelConv] of Object.entries(conversations)) {
1128
+ if (modelName === 'full') continue;
1129
+
1130
+ if (!modelConv.messages) {
1131
+ this.logger.warn(`Model conversation ${modelName} missing messages array`);
1132
+ continue;
1133
+ }
1134
+
1135
+ const modelLastUpdated = new Date(modelConv.lastUpdated);
1136
+
1137
+ if (fullLastUpdated > modelLastUpdated) {
1138
+ this.logger.warn(`Model conversation ${modelName} is outdated, will sync on next use`);
1139
+ modelConv.needsSync = true;
1140
+ }
1141
+ }
1142
+ }
1143
+
1144
+ /**
1145
+ * Save JSON data to file
1146
+ * @private
1147
+ */
1148
+ async saveJSON(filePath, data) {
1149
+ const dir = path.dirname(filePath);
1150
+ await fs.mkdir(dir, { recursive: true });
1151
+
1152
+ const jsonData = JSON.stringify(data, null, 2);
1153
+ await fs.writeFile(filePath, jsonData, 'utf8');
1154
+ }
1155
+
1156
+ /**
1157
+ * Load JSON data from file (strict throws on missing or corrupt).
1158
+ * @private
1159
+ */
1160
+ async loadJSON(filePath) {
1161
+ const data = await fs.readFile(filePath, 'utf8');
1162
+ return JSON.parse(data);
1163
+ }
1164
+
1165
+ /**
1166
+ * Load JSON with best-effort recovery from common breakage modes.
1167
+ * Designed so a single bad state file doesn't take down the entire
1168
+ * agent-load flow — instead the caller gets back a usable object plus
1169
+ * a `recovery` report that callers can surface as a toast.
1170
+ *
1171
+ * Recovery ladder (first match wins):
1172
+ * 1. Missing file (ENOENT) → recreate with `defaultValue`,
1173
+ * report `kind='not-found'`
1174
+ * 2. Empty / whitespace-only file → overwrite with `defaultValue`,
1175
+ * report `kind='empty-recreated'`
1176
+ * 3. Common syntax issues → repaired in memory and saved
1177
+ * (trailing comma, BOM, unclosed back to disk; report
1178
+ * braces/brackets, stray garbage `kind='repaired'`
1179
+ * after the JSON object)
1180
+ * 4. Salvageable first JSON block → use the salvaged object,
1181
+ * (anywhere in the buffer) archive the original to
1182
+ * `<file>.corrupt-<ts>.json`,
1183
+ * report `kind='partial'`
1184
+ * 5. Nothing usable → use `defaultValue`, archive
1185
+ * original (if any), report
1186
+ * `kind='unrepairable'`
1187
+ *
1188
+ * @param {string} filePath - Absolute path to the JSON file
1189
+ * @param {Object|Array|null|Function} defaultValue - Value to use when
1190
+ * the file is missing/empty/unrepairable. If a function is provided
1191
+ * it's called with the file path and its return value is used.
1192
+ * @param {Object} [options]
1193
+ * @param {boolean} [options.persistRecreated=true] - Whether to write
1194
+ * the default back to disk on missing/empty/unrepairable cases.
1195
+ * @param {string} [options.label] - Human-readable label used in toast
1196
+ * messages (e.g. 'agent state', 'agent conversations'). Falls back
1197
+ * to the file's basename.
1198
+ * @returns {Promise<{data: any, recovery: ?Object}>}
1199
+ */
1200
+ async loadJSONResilient(filePath, defaultValue, options = {}) {
1201
+ const { persistRecreated = true } = options;
1202
+ const label = options.label || path.basename(filePath);
1203
+ const defaultOf = () =>
1204
+ typeof defaultValue === 'function' ? defaultValue(filePath) : defaultValue;
1205
+
1206
+ // 1. Missing file → recreate with default
1207
+ let raw;
1208
+ try {
1209
+ raw = await fs.readFile(filePath, 'utf8');
1210
+ } catch (err) {
1211
+ if (err.code === 'ENOENT') {
1212
+ const data = defaultOf();
1213
+ if (persistRecreated && data !== undefined) {
1214
+ try { await this.saveJSON(filePath, data); } catch { /* tolerate read-only mounts */ }
1215
+ }
1216
+ const recovery = {
1217
+ kind: 'not-found', filePath, label,
1218
+ message: `${label} file was missing created an empty one.`,
1219
+ };
1220
+ this.logger?.info(`[stateManager] ${recovery.message}`, { filePath });
1221
+ return { data, recovery };
1222
+ }
1223
+ throw err; // unexpected read error (permissions, etc) let caller handle
1224
+ }
1225
+
1226
+ // 2. Empty or whitespace-only → recreate
1227
+ if (!raw || raw.trim().length === 0) {
1228
+ const data = defaultOf();
1229
+ if (persistRecreated && data !== undefined) {
1230
+ try { await this.saveJSON(filePath, data); } catch { /* ok */ }
1231
+ }
1232
+ const recovery = {
1233
+ kind: 'empty-recreated', filePath, label,
1234
+ message: `${label} file was empty recreated with a default skeleton.`,
1235
+ };
1236
+ this.logger?.warn(`[stateManager] ${recovery.message}`, { filePath });
1237
+ return { data, recovery };
1238
+ }
1239
+
1240
+ // 3. Strict parse — happy path
1241
+ try {
1242
+ return { data: JSON.parse(raw), recovery: null };
1243
+ } catch (firstErr) {
1244
+ // 3a. Quick repairs: BOM, trailing comma before close, junk after
1245
+ // the closing brace, smart quotes that occasionally creep in via
1246
+ // copy-paste. None of these is exotic — they're the top-3 reasons
1247
+ // hand-edited or partially-flushed files break.
1248
+ let repaired = raw;
1249
+ if (repaired.charCodeAt(0) === 0xFEFF) repaired = repaired.slice(1);
1250
+ // Strip ASCII control chars except tab/newline/CR
1251
+ // eslint-disable-next-line no-control-regex
1252
+ repaired = repaired.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '');
1253
+ // Trim whitespace
1254
+ repaired = repaired.trim();
1255
+ // Drop trailing commas before a closing } or ]
1256
+ repaired = repaired.replace(/,(\s*[}\]])/g, '$1');
1257
+ // Cut anything after the last balanced } / ] (junk after JSON).
1258
+ const lastClose = Math.max(repaired.lastIndexOf('}'), repaired.lastIndexOf(']'));
1259
+ if (lastClose > 0 && lastClose < repaired.length - 1) {
1260
+ repaired = repaired.slice(0, lastClose + 1);
1261
+ }
1262
+ try {
1263
+ const data = JSON.parse(repaired);
1264
+ if (persistRecreated && repaired !== raw) {
1265
+ try { await this.saveJSON(filePath, data); } catch { /* ok */ }
1266
+ }
1267
+ const recovery = {
1268
+ kind: 'repaired', filePath, label,
1269
+ message: `${label} file had a minor syntax issue (${firstErr.message}) — auto-repaired.`,
1270
+ };
1271
+ this.logger?.warn(`[stateManager] ${recovery.message}`, { filePath });
1272
+ return { data, recovery };
1273
+ } catch { /* fall through */ }
1274
+
1275
+ // 3b. Salvage: walk the buffer for the first balanced { ... } block
1276
+ // and try to parse just that. Catches truncated-mid-file cases.
1277
+ const salvaged = this._extractFirstJsonBlock(raw);
1278
+ if (salvaged) {
1279
+ try {
1280
+ const data = JSON.parse(salvaged);
1281
+ const archivePath = `${filePath}.corrupt-${Date.now()}.json`;
1282
+ try { await fs.writeFile(archivePath, raw, 'utf8'); } catch { /* ok */ }
1283
+ if (persistRecreated) {
1284
+ try { await this.saveJSON(filePath, data); } catch { /* ok */ }
1285
+ }
1286
+ const recovery = {
1287
+ kind: 'partial', filePath, label,
1288
+ message: `${label} file was partially corrupt — recovered the first valid section. Original archived to ${path.basename(archivePath)}.`,
1289
+ archivePath,
1290
+ };
1291
+ this.logger?.warn(`[stateManager] ${recovery.message}`, { filePath, archivePath });
1292
+ return { data, recovery };
1293
+ } catch { /* fall through */ }
1294
+ }
1295
+
1296
+ // 4. Nothing parseable — archive the corrupt file and use default
1297
+ const data = defaultOf();
1298
+ const archivePath = `${filePath}.corrupt-${Date.now()}.json`;
1299
+ try { await fs.writeFile(archivePath, raw, 'utf8'); } catch { /* ok */ }
1300
+ if (persistRecreated && data !== undefined) {
1301
+ try { await this.saveJSON(filePath, data); } catch { /* ok */ }
1302
+ }
1303
+ const recovery = {
1304
+ kind: 'unrepairable', filePath, label,
1305
+ message: `${label} file is corrupt and could not be repaired restored a default skeleton. Original archived to ${path.basename(archivePath)}.`,
1306
+ archivePath,
1307
+ originalError: firstErr.message,
1308
+ };
1309
+ this.logger?.error(`[stateManager] ${recovery.message}`, { filePath, archivePath, error: firstErr.message });
1310
+ return { data, recovery };
1311
+ }
1312
+ }
1313
+
1314
+ /**
1315
+ * Walk a string and return the substring of the first balanced
1316
+ * `{ ... }` block, respecting string literals so braces inside strings
1317
+ * don't confuse the count. Returns null if no complete block exists.
1318
+ * @private
1319
+ */
1320
+ _extractFirstJsonBlock(s) {
1321
+ if (typeof s !== 'string') return null;
1322
+ const start = s.indexOf('{');
1323
+ if (start < 0) return null;
1324
+ let depth = 0;
1325
+ let inStr = false;
1326
+ let escape = false;
1327
+ for (let i = start; i < s.length; i++) {
1328
+ const ch = s[i];
1329
+ if (inStr) {
1330
+ if (escape) { escape = false; continue; }
1331
+ if (ch === '\\') { escape = true; continue; }
1332
+ if (ch === '"') { inStr = false; }
1333
+ continue;
1334
+ }
1335
+ if (ch === '"') { inStr = true; continue; }
1336
+ if (ch === '{') depth++;
1337
+ else if (ch === '}') {
1338
+ depth--;
1339
+ if (depth === 0) return s.slice(start, i + 1);
1340
+ }
1341
+ }
1342
+ return null;
1343
+ }
1344
+
1345
+ /**
1346
+ * Delete agent state from storage
1347
+ * @param {string} agentId - Agent identifier
1348
+ * @param {string} projectDir - Project directory path
1349
+ * @returns {Promise<void>}
1350
+ */
1351
+ async deleteAgentState(agentId, projectDir = process.cwd()) {
1352
+ const stateDir = this.getStateDir(projectDir);
1353
+ const agentStateFile = path.join(stateDir, 'agents', `agent-${agentId}-state.json`);
1354
+ const agentConversationsFile = path.join(stateDir, 'agents', `agent-${agentId}-conversations.json`);
1355
+
1356
+ try {
1357
+ // Delete agent state file
1358
+ try {
1359
+ await fs.unlink(agentStateFile);
1360
+ this.logger.debug(`Deleted agent state file: ${agentId}`);
1361
+ } catch (error) {
1362
+ if (error.code !== 'ENOENT') {
1363
+ this.logger.warn(`Failed to delete agent state file: ${error.message}`, { agentId });
1364
+ }
1365
+ }
1366
+
1367
+ // Delete agent conversations file
1368
+ try {
1369
+ await fs.unlink(agentConversationsFile);
1370
+ this.logger.debug(`Deleted agent conversations file: ${agentId}`);
1371
+ } catch (error) {
1372
+ if (error.code !== 'ENOENT') {
1373
+ this.logger.warn(`Failed to delete agent conversations file: ${error.message}`, { agentId });
1374
+ }
1375
+ }
1376
+
1377
+ // Remove from agent index
1378
+ await this.removeFromAgentIndex(agentId, projectDir);
1379
+
1380
+ this.logger.info(`Agent state deleted: ${agentId}`);
1381
+
1382
+ } catch (error) {
1383
+ this.logger.error(`Failed to delete agent state: ${error.message}`, {
1384
+ agentId,
1385
+ error: error.stack
1386
+ });
1387
+ throw error;
1388
+ }
1389
+ }
1390
+
1391
+ /**
1392
+ * Remove agent from agent index
1393
+ * @param {string} agentId - Agent identifier
1394
+ * @param {string} projectDir - Project directory path
1395
+ * @returns {Promise<void>}
1396
+ */
1397
+ // eslint-disable-next-line no-unused-vars
1398
+ async removeFromAgentIndex(agentId, projectDir) {
1399
+ const indexFile = path.join(this.stateDirectory, this.stateFiles.agentIndex);
1400
+
1401
+ try {
1402
+ const agentIndex = await this.loadJSON(indexFile);
1403
+ delete agentIndex[agentId];
1404
+ await this.saveJSON(indexFile, agentIndex);
1405
+ this.logger.debug(`Removed agent from index: ${agentId}`);
1406
+ } catch (error) {
1407
+ // If index doesn't exist or can't be updated, log but don't throw
1408
+ this.logger.warn(`Failed to remove agent from index: ${error.message}`, { agentId });
1409
+ }
1410
+ }
1411
+
1412
+ /**
1413
+ * Check if state directory exists
1414
+ * @param {string} projectDir - Project directory path
1415
+ * @returns {Promise<boolean>} True if state directory exists
1416
+ */
1417
+ async stateDirectoryExists(projectDir) {
1418
+ const stateDir = this.getStateDir(projectDir);
1419
+
1420
+ try {
1421
+ const stats = await fs.stat(stateDir);
1422
+ return stats.isDirectory();
1423
+ } catch {
1424
+ return false;
1425
+ }
1426
+ }
1427
+
1428
+ /**
1429
+ * Clean up old state files
1430
+ * @param {string} projectDir - Project directory path
1431
+ * @param {number} maxAge - Maximum age in days
1432
+ * @returns {Promise<void>}
1433
+ */
1434
+ async cleanupOldState(projectDir, maxAge = 30) {
1435
+ const stateDir = this.getStateDir(projectDir);
1436
+ const cutoffDate = Date.now() - (maxAge * 24 * 60 * 60 * 1000);
1437
+
1438
+ try {
1439
+ const agentsDir = path.join(stateDir, 'agents');
1440
+ const files = await fs.readdir(agentsDir);
1441
+
1442
+ for (const file of files) {
1443
+ const filePath = path.join(agentsDir, file);
1444
+ const stats = await fs.stat(filePath);
1445
+
1446
+ if (stats.mtime.getTime() < cutoffDate) {
1447
+ await fs.unlink(filePath);
1448
+ this.logger.info(`Cleaned up old state file: ${file}`);
1449
+ }
1450
+ }
1451
+
1452
+ } catch (error) {
1453
+ this.logger.warn(`State cleanup failed: ${error.message}`);
1454
+ }
1455
+ }
1456
+
1457
+ /**
1458
+ * Get all available agents (active + archived) from filesystem
1459
+ * Scans both the index AND the agents directory to find all agents,
1460
+ * including those that may be missing from the index.
1461
+ * @param {string} projectDir - Project directory path
1462
+ * @param {Object} agentPool - Agent pool instance to check active agents
1463
+ * @returns {Promise<Array>} List of all agents with metadata
1464
+ */
1465
+ async getAllAvailableAgents(projectDir, agentPool) {
1466
+ try {
1467
+ const agentIndex = await this.loadAgentIndex(projectDir);
1468
+ const activeAgentIds = agentPool ? (await agentPool.getAllAgents()).map(a => a.id) : [];
1469
+ const agentsDir = this.getAgentsDir();
1470
+
1471
+ // Track which agent IDs we've already processed
1472
+ const processedAgentIds = new Set();
1473
+ const agents = [];
1474
+
1475
+ // First, process agents from the index
1476
+ for (const [agentId, info] of Object.entries(agentIndex)) {
1477
+ // Skip invalid or undefined entries
1478
+ if (!info || !info.name) {
1479
+ continue;
1480
+ }
1481
+
1482
+ processedAgentIds.add(agentId);
1483
+ agents.push({
1484
+ agentId,
1485
+ name: info.name,
1486
+ type: info.type,
1487
+ model: info.model,
1488
+ lastActivity: info.lastActivity,
1489
+ status: info.status,
1490
+ stateFile: info.stateFile,
1491
+ conversationsFile: info.conversationsFile,
1492
+ capabilities: info.capabilities || [],
1493
+ isLoaded: activeAgentIds.includes(agentId),
1494
+ canImport: !activeAgentIds.includes(agentId)
1495
+ });
1496
+ }
1497
+
1498
+ // Second, scan the agents directory for any agents not in the index
1499
+ try {
1500
+ const files = await fs.readdir(agentsDir);
1501
+ const stateFiles = files.filter(f => f.endsWith('-state.json'));
1502
+
1503
+ let indexUpdated = false;
1504
+
1505
+ for (const stateFile of stateFiles) {
1506
+ // Extract agent ID from filename: agent-{agentId}-state.json
1507
+ const match = stateFile.match(/^agent-(.+)-state\.json$/);
1508
+ if (!match) continue;
1509
+
1510
+ const agentId = match[1];
1511
+ if (processedAgentIds.has(agentId)) continue;
1512
+
1513
+ // Found an agent not in the index - load its state
1514
+ try {
1515
+ const statePath = path.join(agentsDir, stateFile);
1516
+ const stateData = await this.loadJSON(statePath);
1517
+ const state = stateData.state || stateData;
1518
+
1519
+ // Build agent info from state file
1520
+ const agentInfo = {
1521
+ agentId,
1522
+ name: state.name || stateData.name || `Recovered Agent ${agentId.slice(-8)}`,
1523
+ type: state.type || 'user-created',
1524
+ model: state.currentModel || state.preferredModel || 'unknown',
1525
+ lastActivity: state.lastActivity || stateData.timestamp || null,
1526
+ status: state.status || 'idle',
1527
+ stateFile: `agents/${stateFile}`,
1528
+ conversationsFile: `agents/agent-${agentId}-conversations.json`,
1529
+ capabilities: state.capabilities || [],
1530
+ isLoaded: activeAgentIds.includes(agentId),
1531
+ canImport: !activeAgentIds.includes(agentId)
1532
+ };
1533
+
1534
+ agents.push(agentInfo);
1535
+ processedAgentIds.add(agentId);
1536
+
1537
+ // Update the index with this recovered agent
1538
+ agentIndex[agentId] = {
1539
+ name: agentInfo.name,
1540
+ type: agentInfo.type,
1541
+ stateFile: agentInfo.stateFile,
1542
+ conversationsFile: agentInfo.conversationsFile,
1543
+ lastActivity: agentInfo.lastActivity,
1544
+ model: agentInfo.model,
1545
+ status: agentInfo.status,
1546
+ capabilities: agentInfo.capabilities
1547
+ };
1548
+ indexUpdated = true;
1549
+
1550
+ this.logger.info(`Recovered agent from disk: ${agentInfo.name} (${agentId})`);
1551
+ } catch (err) {
1552
+ this.logger.warn(`Failed to recover agent from ${stateFile}: ${err.message}`);
1553
+ }
1554
+ }
1555
+
1556
+ // Save the updated index if we found missing agents
1557
+ if (indexUpdated) {
1558
+ const indexFile = path.join(this.stateDirectory, this.stateFiles.agentIndex);
1559
+ await this.saveJSON(indexFile, agentIndex);
1560
+ this.logger.info(`Updated agent index with ${agents.length - Object.keys(agentIndex).length + Object.keys(processedAgentIds).size} recovered agents`);
1561
+ }
1562
+ } catch (err) {
1563
+ this.logger.warn(`Failed to scan agents directory: ${err.message}`);
1564
+ }
1565
+
1566
+ // Sort by last activity (most recent first)
1567
+ agents.sort((a, b) => {
1568
+ const dateA = a.lastActivity ? new Date(a.lastActivity) : new Date(0);
1569
+ const dateB = b.lastActivity ? new Date(b.lastActivity) : new Date(0);
1570
+ return dateB - dateA;
1571
+ });
1572
+
1573
+ // Enrich agents with firstUserMessage snippet
1574
+ await this._enrichAgentsWithSnippets(agents, agentPool);
1575
+
1576
+ this.logger.info(`Found ${agents.length} available agents (${agents.filter(a => a.isLoaded).length} active, ${agents.filter(a => !a.isLoaded).length} archived)`);
1577
+
1578
+ return agents;
1579
+ } catch (error) {
1580
+ this.logger.error(`Failed to get available agents: ${error.message}`);
1581
+ throw error;
1582
+ }
1583
+ }
1584
+
1585
+ /**
1586
+ * Enrich agent list with firstUserMessage snippets.
1587
+ * For loaded agents, reads from agentPool. For archived agents, peeks into conversations file.
1588
+ */
1589
+ async _enrichAgentsWithSnippets(agents, agentPool) {
1590
+ const extractSnippet = (messages) => {
1591
+ if (!messages || messages.length === 0) return null;
1592
+ const firstUser = messages.find(m =>
1593
+ m.role === 'user' && m.content && m.type !== 'task-boundary'
1594
+ );
1595
+ if (!firstUser) return null;
1596
+ const text = typeof firstUser.content === 'string'
1597
+ ? firstUser.content
1598
+ : Array.isArray(firstUser.content)
1599
+ ? firstUser.content.filter(b => b.type === 'text').map(b => b.text).join('\n')
1600
+ : null;
1601
+ if (!text) return null;
1602
+ const lines = text.split('\n').filter(l => l.trim());
1603
+ const snippet = lines.slice(0, 2).join('\n');
1604
+ return snippet.length > 120 ? snippet.slice(0, 117) + '...' : snippet;
1605
+ };
1606
+
1607
+ for (const agent of agents) {
1608
+ try {
1609
+ if (agent.isLoaded && agentPool) {
1610
+ // For loaded agents, get from the in-memory agent
1611
+ const liveAgent = await agentPool.getAgent(agent.agentId);
1612
+ if (liveAgent) {
1613
+ agent.firstUserMessage = extractSnippet(liveAgent.conversations?.full?.messages);
1614
+ continue;
1615
+ }
1616
+ }
1617
+ // For archived agents, peek into the conversations file on disk
1618
+ if (agent.conversationsFile) {
1619
+ const convPath = path.join(this.stateDirectory, agent.conversationsFile);
1620
+ try {
1621
+ const convData = await this.loadJSON(convPath);
1622
+ const messages = convData?.conversations?.full?.messages || convData?.full?.messages;
1623
+ agent.firstUserMessage = extractSnippet(messages);
1624
+ } catch {
1625
+ // File may not exist that's fine
1626
+ }
1627
+ }
1628
+ } catch {
1629
+ // Non-critical — just skip this agent's snippet
1630
+ }
1631
+ }
1632
+ }
1633
+
1634
+ /**
1635
+ * Get agent metadata without full restoration (lightweight preview)
1636
+ * @param {string} agentId - Agent ID
1637
+ * @param {string} projectDir - Project directory path
1638
+ * @returns {Promise<Object>} Agent metadata for preview
1639
+ */
1640
+ async getAgentMetadata(agentId, projectDir) {
1641
+ try {
1642
+ // Load agent index
1643
+ const agentIndex = await this.loadAgentIndex(projectDir);
1644
+ const agentInfo = agentIndex[agentId];
1645
+
1646
+ if (!agentInfo) {
1647
+ throw new Error(`Agent ${agentId} not found in index`);
1648
+ }
1649
+
1650
+ // Load just the state file (lightweight)
1651
+ const stateDir = this.getStateDir(projectDir);
1652
+ const stateFile = path.join(stateDir, agentInfo.stateFile);
1653
+ const conversationsFile = path.join(stateDir, agentInfo.conversationsFile);
1654
+
1655
+ // Check if files exist
1656
+ const stateExists = await fs.access(stateFile).then(() => true).catch(() => false);
1657
+ const conversationsExist = await fs.access(conversationsFile).then(() => true).catch(() => false);
1658
+
1659
+ if (!stateExists) {
1660
+ throw new Error(`State file not found for agent ${agentId}`);
1661
+ }
1662
+
1663
+ // Load state
1664
+ const stateData = await this.loadJSON(stateFile);
1665
+ const state = stateData.state || {};
1666
+
1667
+ // Load conversation count without loading full messages (for performance)
1668
+ let messageCount = 0;
1669
+ let lastMessage = null;
1670
+ if (conversationsExist) {
1671
+ try {
1672
+ const conversations = await this.loadJSON(conversationsFile);
1673
+ messageCount = Array.isArray(conversations) ? conversations.length : 0;
1674
+
1675
+ // Get last message for preview
1676
+ if (messageCount > 0) {
1677
+ const lastMsg = conversations[conversations.length - 1];
1678
+ lastMessage = lastMsg?.content?.substring(0, 100) || null;
1679
+ }
1680
+ } catch (error) {
1681
+ this.logger.warn(`Failed to load conversations for ${agentId}: ${error.message}`);
1682
+ }
1683
+ }
1684
+
1685
+ const metadata = {
1686
+ agentId,
1687
+ name: agentInfo.name || state.name,
1688
+ model: agentInfo.model || state.preferredModel || state.currentModel,
1689
+ lastActivity: agentInfo.lastActivity,
1690
+ status: agentInfo.status,
1691
+ capabilities: state.capabilities || [],
1692
+ messageCount,
1693
+ lastMessage,
1694
+ taskCount: state.taskList?.tasks?.length || 0,
1695
+ createdAt: state.createdAt,
1696
+ workingDirectory: state.directoryAccess?.workingDirectory,
1697
+ mode: state.mode,
1698
+ systemPrompt: state.originalSystemPrompt
1699
+ };
1700
+
1701
+ this.logger.info(`Loaded metadata for agent ${agentId}: ${metadata.messageCount} messages, ${metadata.taskCount} tasks`);
1702
+
1703
+ return metadata;
1704
+ } catch (error) {
1705
+ this.logger.error(`Failed to get agent metadata for ${agentId}: ${error.message}`);
1706
+ throw error;
1707
+ }
1708
+ }
1709
+
1710
+ /**
1711
+ * Import archived agent from filesystem and add to agent pool
1712
+ * @param {string} agentId - Agent ID to import
1713
+ * @param {string} projectDir - Project directory path
1714
+ * @param {Object} agentPool - Agent pool instance
1715
+ * @returns {Promise<Object>} Imported agent object
1716
+ */
1717
+ async importArchivedAgent(agentId, projectDir, agentPool) {
1718
+ try {
1719
+ // Validate agent ID format for security
1720
+ const AGENT_ID_REGEX = /^agent-[a-z0-9-]+-\d+$/;
1721
+ if (!AGENT_ID_REGEX.test(agentId)) {
1722
+ throw new Error('Invalid agent ID format');
1723
+ }
1724
+
1725
+ // Check if already loaded in agent pool
1726
+ if (agentPool && await agentPool.getAgent(agentId)) {
1727
+ throw new Error(`Agent ${agentId} is already loaded. Use switchAgent() instead.`);
1728
+ }
1729
+
1730
+ // Load from agent index
1731
+ const agentIndex = await this.loadAgentIndex(projectDir);
1732
+ const agentInfo = agentIndex[agentId];
1733
+
1734
+ if (!agentInfo) {
1735
+ throw new Error(`Agent ${agentId} not found in index`);
1736
+ }
1737
+
1738
+ this.logger.info(`Importing archived agent: ${agentId} (${agentInfo.name})`);
1739
+
1740
+ // Restore agent using existing restore logic
1741
+ const agent = await this.restoreAgent(agentId, agentInfo, projectDir);
1742
+
1743
+ // Update agent's last activity
1744
+ agent.lastActivity = new Date().toISOString();
1745
+
1746
+ // Add to agent pool if provided
1747
+ if (agentPool) {
1748
+ agentPool.agents.set(agent.id, agent);
1749
+ agentPool._updateAgentDirectory(agent);
1750
+ this.logger.info(`Agent ${agentId} added to agent pool`);
1751
+ }
1752
+
1753
+ // Update agent index with new last activity
1754
+ await this.updateAgentIndex(agent, projectDir);
1755
+
1756
+ this.logger.info(`Successfully imported agent ${agentId}: ${agent.name}`);
1757
+
1758
+ return agent;
1759
+ } catch (error) {
1760
+ this.logger.error(`Failed to import agent ${agentId}: ${error.message}`);
1761
+ throw error;
1762
+ }
1763
+ }
1764
+ }
1765
+
1753
1766
  export default StateManager;