agent-relay 1.1.0 → 1.2.3

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 (567) hide show
  1. package/.gitattributes +3 -0
  2. package/.nvmrc +1 -0
  3. package/.trajectories/completed/2026-01/traj_1dviorhnkcb5.json +65 -0
  4. package/.trajectories/completed/2026-01/traj_1dviorhnkcb5.md +37 -0
  5. package/.trajectories/completed/2026-01/traj_1k5if5snst2e.json +65 -0
  6. package/.trajectories/completed/2026-01/traj_1k5if5snst2e.md +37 -0
  7. package/.trajectories/completed/2026-01/traj_1rp3rges5811.json +49 -0
  8. package/.trajectories/completed/2026-01/traj_1rp3rges5811.md +31 -0
  9. package/.trajectories/completed/2026-01/traj_22bhyulruouw.json +113 -0
  10. package/.trajectories/completed/2026-01/traj_22bhyulruouw.md +57 -0
  11. package/.trajectories/completed/2026-01/traj_2dao7ddgnta0.json +53 -0
  12. package/.trajectories/completed/2026-01/traj_2dao7ddgnta0.md +32 -0
  13. package/.trajectories/completed/2026-01/traj_3t0440mjeunc.json +26 -0
  14. package/.trajectories/completed/2026-01/traj_3t0440mjeunc.md +6 -0
  15. package/.trajectories/completed/2026-01/traj_45x9494d9xnr.json +47 -0
  16. package/.trajectories/completed/2026-01/traj_45x9494d9xnr.md +32 -0
  17. package/.trajectories/completed/2026-01/traj_4aa0bb77s4nh.json +53 -0
  18. package/.trajectories/completed/2026-01/traj_4aa0bb77s4nh.md +32 -0
  19. package/.trajectories/completed/2026-01/traj_5lhmzq8rxpqv.json +59 -0
  20. package/.trajectories/completed/2026-01/traj_5lhmzq8rxpqv.md +33 -0
  21. package/.trajectories/completed/2026-01/traj_5vr4e9erb1fs.json +53 -0
  22. package/.trajectories/completed/2026-01/traj_5vr4e9erb1fs.md +32 -0
  23. package/.trajectories/completed/2026-01/traj_6fgiwdoklvym.json +48 -0
  24. package/.trajectories/completed/2026-01/traj_6fgiwdoklvym.md +24 -0
  25. package/.trajectories/completed/2026-01/traj_7ludwvz45veh.json +209 -0
  26. package/.trajectories/completed/2026-01/traj_7ludwvz45veh.md +97 -0
  27. package/.trajectories/completed/2026-01/traj_9921cuhel0pj.json +48 -0
  28. package/.trajectories/completed/2026-01/traj_9921cuhel0pj.md +24 -0
  29. package/.trajectories/completed/2026-01/traj_ajs7zqfux4wc.json +49 -0
  30. package/.trajectories/completed/2026-01/traj_ajs7zqfux4wc.md +23 -0
  31. package/.trajectories/completed/2026-01/traj_cvtqhlwcq9s0.json +53 -0
  32. package/.trajectories/completed/2026-01/traj_cvtqhlwcq9s0.md +32 -0
  33. package/.trajectories/completed/2026-01/traj_cxofprm2m2en.json +49 -0
  34. package/.trajectories/completed/2026-01/traj_cxofprm2m2en.md +31 -0
  35. package/.trajectories/completed/2026-01/traj_d2hhz3k0vrhn.json +26 -0
  36. package/.trajectories/completed/2026-01/traj_d2hhz3k0vrhn.md +6 -0
  37. package/.trajectories/completed/2026-01/traj_dfuvww9pege5.json +59 -0
  38. package/.trajectories/completed/2026-01/traj_dfuvww9pege5.md +37 -0
  39. package/.trajectories/completed/2026-01/traj_g0fisy9h51mf.json +77 -0
  40. package/.trajectories/completed/2026-01/traj_g0fisy9h51mf.md +42 -0
  41. package/.trajectories/completed/2026-01/traj_gjdre5voouod.json +53 -0
  42. package/.trajectories/completed/2026-01/traj_gjdre5voouod.md +32 -0
  43. package/.trajectories/completed/2026-01/traj_gtlyqtta3x8l.json +25 -0
  44. package/.trajectories/completed/2026-01/traj_gtlyqtta3x8l.md +15 -0
  45. package/.trajectories/completed/2026-01/traj_h4xijiuip3w4.json +101 -0
  46. package/.trajectories/completed/2026-01/traj_h4xijiuip3w4.md +44 -0
  47. package/.trajectories/completed/2026-01/traj_hhxte7w4gjjx.json +22 -0
  48. package/.trajectories/completed/2026-01/traj_hhxte7w4gjjx.md +5 -0
  49. package/.trajectories/completed/2026-01/traj_hpungyhoj6v5.json +53 -0
  50. package/.trajectories/completed/2026-01/traj_hpungyhoj6v5.md +32 -0
  51. package/.trajectories/completed/2026-01/traj_m2xkjv0w2sq7.json +25 -0
  52. package/.trajectories/completed/2026-01/traj_m2xkjv0w2sq7.md +15 -0
  53. package/.trajectories/completed/2026-01/traj_noq5zbvnrdvz.json +53 -0
  54. package/.trajectories/completed/2026-01/traj_noq5zbvnrdvz.md +32 -0
  55. package/.trajectories/completed/2026-01/traj_ntbs6ppopf46.json +53 -0
  56. package/.trajectories/completed/2026-01/traj_ntbs6ppopf46.md +32 -0
  57. package/.trajectories/completed/2026-01/traj_ozd98si6a7ns.json +48 -0
  58. package/.trajectories/completed/2026-01/traj_ozd98si6a7ns.md +24 -0
  59. package/.trajectories/completed/2026-01/traj_prdza7a5cxp5.json +53 -0
  60. package/.trajectories/completed/2026-01/traj_prdza7a5cxp5.md +32 -0
  61. package/.trajectories/completed/2026-01/traj_qb3twvvywfwi.json +77 -0
  62. package/.trajectories/completed/2026-01/traj_qb3twvvywfwi.md +42 -0
  63. package/.trajectories/completed/2026-01/traj_qft54mi7nfor.json +53 -0
  64. package/.trajectories/completed/2026-01/traj_qft54mi7nfor.md +32 -0
  65. package/.trajectories/completed/2026-01/traj_qx9uhf8whhxo.json +83 -0
  66. package/.trajectories/completed/2026-01/traj_qx9uhf8whhxo.md +47 -0
  67. package/.trajectories/completed/2026-01/traj_rd9toccj18a0.json +59 -0
  68. package/.trajectories/completed/2026-01/traj_rd9toccj18a0.md +37 -0
  69. package/.trajectories/completed/2026-01/traj_rt4fiw3ecp50.json +48 -0
  70. package/.trajectories/completed/2026-01/traj_rt4fiw3ecp50.md +16 -0
  71. package/.trajectories/completed/2026-01/traj_st8j35b0hrlc.json +59 -0
  72. package/.trajectories/completed/2026-01/traj_st8j35b0hrlc.md +37 -0
  73. package/.trajectories/completed/2026-01/traj_t1yy8m7hbuxp.json +53 -0
  74. package/.trajectories/completed/2026-01/traj_t1yy8m7hbuxp.md +32 -0
  75. package/.trajectories/completed/2026-01/traj_tmux_orchestrator_analysis.json +84 -0
  76. package/.trajectories/completed/2026-01/traj_tmux_orchestrator_analysis.md +109 -0
  77. package/.trajectories/completed/2026-01/traj_u9n9eqasw16k.json +53 -0
  78. package/.trajectories/completed/2026-01/traj_u9n9eqasw16k.md +32 -0
  79. package/.trajectories/completed/2026-01/traj_v87hypnongqx.json +71 -0
  80. package/.trajectories/completed/2026-01/traj_v87hypnongqx.md +42 -0
  81. package/.trajectories/completed/2026-01/traj_wkp2fgzdyinb.json +53 -0
  82. package/.trajectories/completed/2026-01/traj_wkp2fgzdyinb.md +32 -0
  83. package/.trajectories/completed/2026-01/traj_x14t8w8rn7xg.json +20 -0
  84. package/.trajectories/completed/2026-01/traj_x14t8w8rn7xg.md +6 -0
  85. package/.trajectories/completed/2026-01/traj_xnwbznkvv8ua.json +175 -0
  86. package/.trajectories/completed/2026-01/traj_xnwbznkvv8ua.md +82 -0
  87. package/.trajectories/completed/2026-01/traj_ysjc8zaeqtd3.json +47 -0
  88. package/.trajectories/completed/2026-01/traj_ysjc8zaeqtd3.md +32 -0
  89. package/.trajectories/completed/2026-01/traj_yvdadtvdgnz3.json +59 -0
  90. package/.trajectories/completed/2026-01/traj_yvdadtvdgnz3.md +37 -0
  91. package/.trajectories/completed/2026-01/traj_z0vcw1wrzide.json +53 -0
  92. package/.trajectories/completed/2026-01/traj_z0vcw1wrzide.md +32 -0
  93. package/.trajectories/index.json +314 -0
  94. package/ARCHITECTURE.md +1245 -0
  95. package/README.md +1 -1
  96. package/TESTING.md +278 -0
  97. package/deploy/init-db.sql +5 -0
  98. package/deploy/scripts/setup-fly-workspaces.sh +69 -0
  99. package/deploy/scripts/setup-railway.sh +75 -0
  100. package/deploy/workspace/entrypoint-browser.sh +118 -0
  101. package/deploy/workspace/entrypoint.sh +348 -0
  102. package/deploy/workspace/git-credential-relay +111 -0
  103. package/dist/bridge/spawner.d.ts +53 -0
  104. package/dist/bridge/spawner.js +203 -19
  105. package/dist/bridge/types.d.ts +12 -0
  106. package/dist/cli/index.js +618 -5
  107. package/dist/cloud/api/auth.d.ts +3 -2
  108. package/dist/cloud/api/auth.js +10 -98
  109. package/dist/cloud/api/billing.js +30 -9
  110. package/dist/cloud/api/cli-pty-runner.d.ts +54 -0
  111. package/dist/cloud/api/cli-pty-runner.js +119 -0
  112. package/dist/cloud/api/codex-auth-helper.d.ts +15 -0
  113. package/dist/cloud/api/codex-auth-helper.js +100 -0
  114. package/dist/cloud/api/generic-webhooks.d.ts +8 -0
  115. package/dist/cloud/api/generic-webhooks.js +129 -0
  116. package/dist/cloud/api/git.d.ts +8 -0
  117. package/dist/cloud/api/git.js +152 -0
  118. package/dist/cloud/api/github-app.d.ts +11 -0
  119. package/dist/cloud/api/github-app.js +189 -0
  120. package/dist/cloud/api/middleware/planLimits.d.ts +7 -0
  121. package/dist/cloud/api/middleware/planLimits.js +39 -1
  122. package/dist/cloud/api/monitoring.d.ts +11 -0
  123. package/dist/cloud/api/monitoring.js +578 -0
  124. package/dist/cloud/api/nango-auth.d.ts +9 -0
  125. package/dist/cloud/api/nango-auth.js +377 -0
  126. package/dist/cloud/api/onboarding.d.ts +8 -1
  127. package/dist/cloud/api/onboarding.js +313 -119
  128. package/dist/cloud/api/policy.d.ts +8 -0
  129. package/dist/cloud/api/policy.js +229 -0
  130. package/dist/cloud/api/providers.js +114 -42
  131. package/dist/cloud/api/repos.d.ts +1 -0
  132. package/dist/cloud/api/repos.js +186 -0
  133. package/dist/cloud/api/test-helpers.d.ts +10 -0
  134. package/dist/cloud/api/test-helpers.js +575 -0
  135. package/dist/cloud/api/webhooks.d.ts +8 -0
  136. package/dist/cloud/api/webhooks.js +645 -0
  137. package/dist/cloud/api/workspaces.js +320 -12
  138. package/dist/cloud/billing/plans.js +32 -19
  139. package/dist/cloud/billing/types.d.ts +9 -3
  140. package/dist/cloud/config.d.ts +9 -2
  141. package/dist/cloud/config.js +13 -4
  142. package/dist/cloud/db/drizzle.d.ts +84 -1
  143. package/dist/cloud/db/drizzle.js +470 -0
  144. package/dist/cloud/db/index.d.ts +9 -4
  145. package/dist/cloud/db/index.js +11 -3
  146. package/dist/cloud/db/schema.d.ts +3283 -556
  147. package/dist/cloud/db/schema.js +314 -1
  148. package/dist/cloud/index.d.ts +1 -0
  149. package/dist/cloud/index.js +2 -0
  150. package/dist/cloud/provisioner/index.d.ts +56 -0
  151. package/dist/cloud/provisioner/index.js +676 -34
  152. package/dist/cloud/server.d.ts +1 -0
  153. package/dist/cloud/server.js +362 -13
  154. package/dist/cloud/services/auto-scaler.d.ts +152 -0
  155. package/dist/cloud/services/auto-scaler.js +439 -0
  156. package/dist/cloud/services/capacity-manager.d.ts +148 -0
  157. package/dist/cloud/services/capacity-manager.js +449 -0
  158. package/dist/cloud/services/ci-agent-spawner.d.ts +49 -0
  159. package/dist/cloud/services/ci-agent-spawner.js +373 -0
  160. package/dist/cloud/services/index.d.ts +12 -0
  161. package/dist/cloud/services/index.js +15 -0
  162. package/dist/cloud/services/mention-handler.d.ts +65 -0
  163. package/dist/cloud/services/mention-handler.js +405 -0
  164. package/dist/cloud/services/nango.d.ts +186 -0
  165. package/dist/cloud/services/nango.js +344 -0
  166. package/dist/cloud/services/persistence.d.ts +131 -0
  167. package/dist/cloud/services/persistence.js +200 -0
  168. package/dist/cloud/services/planLimits.d.ts +37 -0
  169. package/dist/cloud/services/planLimits.js +86 -5
  170. package/dist/cloud/services/scaling-orchestrator.d.ts +159 -0
  171. package/dist/cloud/services/scaling-orchestrator.js +502 -0
  172. package/dist/cloud/services/scaling-policy.d.ts +121 -0
  173. package/dist/cloud/services/scaling-policy.js +415 -0
  174. package/dist/cloud/vault/index.js +1 -1
  175. package/dist/cloud/webhooks/index.d.ts +24 -0
  176. package/dist/cloud/webhooks/index.js +29 -0
  177. package/dist/cloud/webhooks/parsers/github.d.ts +8 -0
  178. package/dist/cloud/webhooks/parsers/github.js +234 -0
  179. package/dist/cloud/webhooks/parsers/index.d.ts +23 -0
  180. package/dist/cloud/webhooks/parsers/index.js +30 -0
  181. package/dist/cloud/webhooks/parsers/linear.d.ts +9 -0
  182. package/dist/cloud/webhooks/parsers/linear.js +258 -0
  183. package/dist/cloud/webhooks/parsers/slack.d.ts +9 -0
  184. package/dist/cloud/webhooks/parsers/slack.js +214 -0
  185. package/dist/cloud/webhooks/responders/github.d.ts +8 -0
  186. package/dist/cloud/webhooks/responders/github.js +73 -0
  187. package/dist/cloud/webhooks/responders/index.d.ts +23 -0
  188. package/dist/cloud/webhooks/responders/index.js +30 -0
  189. package/dist/cloud/webhooks/responders/linear.d.ts +9 -0
  190. package/dist/cloud/webhooks/responders/linear.js +149 -0
  191. package/dist/cloud/webhooks/responders/slack.d.ts +20 -0
  192. package/dist/cloud/webhooks/responders/slack.js +178 -0
  193. package/dist/cloud/webhooks/router.d.ts +25 -0
  194. package/dist/cloud/webhooks/router.js +504 -0
  195. package/dist/cloud/webhooks/rules-engine.d.ts +24 -0
  196. package/dist/cloud/webhooks/rules-engine.js +287 -0
  197. package/dist/cloud/webhooks/types.d.ts +186 -0
  198. package/dist/cloud/webhooks/types.js +8 -0
  199. package/dist/continuity/formatter.d.ts +51 -0
  200. package/dist/continuity/formatter.js +313 -0
  201. package/dist/continuity/handoff-store.d.ts +67 -0
  202. package/dist/continuity/handoff-store.js +472 -0
  203. package/dist/continuity/index.d.ts +45 -0
  204. package/dist/continuity/index.js +48 -0
  205. package/dist/continuity/ledger-store.d.ts +110 -0
  206. package/dist/continuity/ledger-store.js +500 -0
  207. package/dist/continuity/manager.d.ts +178 -0
  208. package/dist/continuity/manager.js +562 -0
  209. package/dist/continuity/parser.d.ts +76 -0
  210. package/dist/continuity/parser.js +579 -0
  211. package/dist/continuity/types.d.ts +180 -0
  212. package/dist/continuity/types.js +9 -0
  213. package/dist/daemon/agent-manager.d.ts +27 -0
  214. package/dist/daemon/agent-manager.js +107 -6
  215. package/dist/daemon/agent-registry.d.ts +32 -0
  216. package/dist/daemon/agent-registry.js +42 -2
  217. package/dist/daemon/api.d.ts +12 -0
  218. package/dist/daemon/api.js +131 -2
  219. package/dist/daemon/cli-auth.d.ts +67 -0
  220. package/dist/daemon/cli-auth.js +537 -0
  221. package/dist/daemon/cloud-sync.js +9 -7
  222. package/dist/daemon/orchestrator.js +30 -0
  223. package/dist/daemon/router.d.ts +5 -0
  224. package/dist/daemon/router.js +78 -26
  225. package/dist/daemon/server.d.ts +5 -0
  226. package/dist/daemon/server.js +9 -1
  227. package/dist/daemon/services/browser-testing.d.ts +88 -0
  228. package/dist/daemon/services/browser-testing.js +244 -0
  229. package/dist/daemon/services/container-spawner.d.ts +135 -0
  230. package/dist/daemon/services/container-spawner.js +313 -0
  231. package/dist/daemon/types.d.ts +5 -1
  232. package/dist/dashboard/out/404.html +1 -1
  233. package/dist/dashboard/out/_next/static/chunks/116-2502180def231162.js +1 -0
  234. package/dist/dashboard/out/_next/static/chunks/282-980c2eb8fff20123.js +1 -0
  235. package/dist/dashboard/out/_next/static/chunks/699-3b1cd6618a45d259.js +1 -0
  236. package/dist/dashboard/out/_next/static/chunks/724-2dae7627550ab88f.js +9 -0
  237. package/dist/dashboard/out/_next/static/chunks/766-1f2dd8cb7f766b0b.js +1 -0
  238. package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-3fdfa60e53f2810d.js +1 -0
  239. package/dist/dashboard/out/_next/static/chunks/app/app/page-e6381e5a6e1fbcfd.js +1 -0
  240. package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-3538dfe0ffe984b8.js +1 -0
  241. package/dist/dashboard/out/_next/static/chunks/app/history/{page-b6edd4dde8d08194.js → page-abb9ab2d329f56e9.js} +1 -1
  242. package/dist/dashboard/out/_next/static/chunks/app/layout-c0d118c0f92d969c.js +1 -0
  243. package/dist/dashboard/out/_next/static/chunks/app/login/page-c22d080201cbd9fb.js +1 -0
  244. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-67a3e98d9a43a6ed.js +1 -0
  245. package/dist/dashboard/out/_next/static/chunks/app/page-77e9c65420a06cfb.js +1 -0
  246. package/dist/dashboard/out/_next/static/chunks/app/pricing/page-b08ed1c34d14434a.js +1 -0
  247. package/dist/dashboard/out/_next/static/chunks/app/providers/page-e88bc117ef7671c3.js +1 -0
  248. package/dist/dashboard/out/_next/static/chunks/app/signup/page-68d34f50baa8ab6b.js +1 -0
  249. package/dist/dashboard/out/_next/static/chunks/e868780c-48e5f147c90a3a41.js +18 -0
  250. package/dist/dashboard/out/_next/static/chunks/{main-app-5d692157a8eb1fd9.js → main-app-6e8e8d3ef4e0192a.js} +1 -1
  251. package/dist/dashboard/out/_next/static/chunks/{main-c2f423b9c9f4591b.js → main-ed4e1fb6f29c34cf.js} +1 -1
  252. package/dist/dashboard/out/_next/static/chunks/webpack-1cdd8ed57114d5e1.js +1 -0
  253. package/dist/dashboard/out/_next/static/css/29852f26181969a0.css +1 -0
  254. package/dist/dashboard/out/_next/static/css/7c3ae9e8617d42a5.css +1 -0
  255. package/dist/dashboard/out/app/onboarding.html +1 -0
  256. package/dist/dashboard/out/app/onboarding.txt +7 -0
  257. package/dist/dashboard/out/app.html +1 -14
  258. package/dist/dashboard/out/app.txt +2 -2
  259. package/dist/dashboard/out/connect-repos.html +1 -0
  260. package/dist/dashboard/out/connect-repos.txt +7 -0
  261. package/dist/dashboard/out/history.html +1 -1
  262. package/dist/dashboard/out/history.txt +2 -2
  263. package/dist/dashboard/out/index.html +1 -1
  264. package/dist/dashboard/out/index.txt +2 -2
  265. package/dist/dashboard/out/login.html +6 -0
  266. package/dist/dashboard/out/login.txt +7 -0
  267. package/dist/dashboard/out/metrics.html +1 -1
  268. package/dist/dashboard/out/metrics.txt +2 -2
  269. package/dist/dashboard/out/pricing.html +3 -3
  270. package/dist/dashboard/out/pricing.txt +2 -2
  271. package/dist/dashboard/out/providers.html +1 -0
  272. package/dist/dashboard/out/providers.txt +7 -0
  273. package/dist/dashboard/out/signup.html +6 -0
  274. package/dist/dashboard/out/signup.txt +7 -0
  275. package/dist/dashboard-server/server.js +1308 -8
  276. package/dist/hooks/emitter.d.ts +40 -0
  277. package/dist/hooks/emitter.js +63 -0
  278. package/dist/hooks/index.d.ts +3 -0
  279. package/dist/hooks/index.js +3 -0
  280. package/dist/hooks/registry.d.ts +173 -0
  281. package/dist/hooks/registry.js +476 -0
  282. package/dist/hooks/trajectory-hooks.d.ts +52 -0
  283. package/dist/hooks/trajectory-hooks.js +183 -0
  284. package/dist/hooks/types.d.ts +141 -0
  285. package/dist/index.d.ts +2 -0
  286. package/dist/index.js +3 -0
  287. package/dist/memory/adapters/index.d.ts +8 -0
  288. package/dist/memory/adapters/index.js +8 -0
  289. package/dist/memory/adapters/inmemory.d.ts +59 -0
  290. package/dist/memory/adapters/inmemory.js +195 -0
  291. package/dist/memory/adapters/supermemory.d.ts +71 -0
  292. package/dist/memory/adapters/supermemory.js +338 -0
  293. package/dist/memory/factory.d.ts +48 -0
  294. package/dist/memory/factory.js +143 -0
  295. package/dist/memory/index.d.ts +32 -0
  296. package/dist/memory/index.js +32 -0
  297. package/dist/memory/memory-hooks.d.ts +60 -0
  298. package/dist/memory/memory-hooks.js +313 -0
  299. package/dist/memory/service.d.ts +49 -0
  300. package/dist/memory/service.js +146 -0
  301. package/dist/memory/types.d.ts +195 -0
  302. package/dist/memory/types.js +8 -0
  303. package/dist/policy/agent-policy.d.ts +225 -0
  304. package/dist/policy/agent-policy.js +665 -0
  305. package/dist/policy/cloud-policy-fetcher.d.ts +12 -0
  306. package/dist/policy/cloud-policy-fetcher.js +64 -0
  307. package/dist/resiliency/crash-insights.d.ts +156 -0
  308. package/dist/resiliency/crash-insights.js +492 -0
  309. package/dist/resiliency/gossip-health.d.ts +137 -0
  310. package/dist/resiliency/gossip-health.js +241 -0
  311. package/dist/resiliency/index.d.ts +5 -0
  312. package/dist/resiliency/index.js +5 -0
  313. package/dist/resiliency/leader-watchdog.d.ts +109 -0
  314. package/dist/resiliency/leader-watchdog.js +189 -0
  315. package/dist/resiliency/memory-monitor.d.ts +172 -0
  316. package/dist/resiliency/memory-monitor.js +593 -0
  317. package/dist/resiliency/stateless-lead.d.ts +149 -0
  318. package/dist/resiliency/stateless-lead.js +308 -0
  319. package/dist/resiliency/supervisor.d.ts +38 -0
  320. package/dist/resiliency/supervisor.js +122 -0
  321. package/dist/shared/cli-auth-config.d.ts +91 -0
  322. package/dist/shared/cli-auth-config.js +264 -0
  323. package/dist/storage/adapter.d.ts +1 -1
  324. package/dist/trajectory/config.d.ts +84 -0
  325. package/dist/trajectory/config.js +163 -0
  326. package/dist/trajectory/index.d.ts +8 -0
  327. package/dist/trajectory/index.js +8 -0
  328. package/dist/trajectory/integration.d.ts +292 -0
  329. package/dist/trajectory/integration.js +834 -0
  330. package/dist/utils/logger.js +1 -1
  331. package/dist/utils/project-namespace.d.ts +24 -0
  332. package/dist/utils/project-namespace.js +84 -0
  333. package/dist/wrapper/parser.d.ts +10 -0
  334. package/dist/wrapper/parser.js +100 -33
  335. package/dist/wrapper/pty-wrapper.d.ts +197 -16
  336. package/dist/wrapper/pty-wrapper.js +943 -106
  337. package/dist/wrapper/shared.d.ts +165 -0
  338. package/dist/wrapper/shared.js +270 -0
  339. package/dist/wrapper/tmux-wrapper.d.ts +73 -11
  340. package/dist/wrapper/tmux-wrapper.js +541 -120
  341. package/package.json +16 -16
  342. package/scripts/postinstall.js +60 -0
  343. package/test-push.txt +1 -0
  344. package/bin/tmux +0 -0
  345. package/dist/bridge/config.d.ts.map +0 -1
  346. package/dist/bridge/config.js.map +0 -1
  347. package/dist/bridge/index.d.ts.map +0 -1
  348. package/dist/bridge/index.js.map +0 -1
  349. package/dist/bridge/multi-project-client.d.ts.map +0 -1
  350. package/dist/bridge/multi-project-client.js.map +0 -1
  351. package/dist/bridge/shadow-cli.d.ts.map +0 -1
  352. package/dist/bridge/shadow-cli.js.map +0 -1
  353. package/dist/bridge/shadow-config.d.ts.map +0 -1
  354. package/dist/bridge/shadow-config.js.map +0 -1
  355. package/dist/bridge/spawner.d.ts.map +0 -1
  356. package/dist/bridge/spawner.js.map +0 -1
  357. package/dist/bridge/teams-config.d.ts.map +0 -1
  358. package/dist/bridge/teams-config.js.map +0 -1
  359. package/dist/bridge/types.d.ts.map +0 -1
  360. package/dist/bridge/types.js.map +0 -1
  361. package/dist/bridge/utils.d.ts.map +0 -1
  362. package/dist/bridge/utils.js.map +0 -1
  363. package/dist/cli/index.d.ts.map +0 -1
  364. package/dist/cli/index.js.map +0 -1
  365. package/dist/cloud/api/auth.d.ts.map +0 -1
  366. package/dist/cloud/api/auth.js.map +0 -1
  367. package/dist/cloud/api/billing.d.ts.map +0 -1
  368. package/dist/cloud/api/billing.js.map +0 -1
  369. package/dist/cloud/api/coordinators.d.ts.map +0 -1
  370. package/dist/cloud/api/coordinators.js.map +0 -1
  371. package/dist/cloud/api/daemons.d.ts.map +0 -1
  372. package/dist/cloud/api/daemons.js.map +0 -1
  373. package/dist/cloud/api/middleware/planLimits.d.ts.map +0 -1
  374. package/dist/cloud/api/middleware/planLimits.js.map +0 -1
  375. package/dist/cloud/api/onboarding.d.ts.map +0 -1
  376. package/dist/cloud/api/onboarding.js.map +0 -1
  377. package/dist/cloud/api/providers.d.ts.map +0 -1
  378. package/dist/cloud/api/providers.js.map +0 -1
  379. package/dist/cloud/api/repos.d.ts.map +0 -1
  380. package/dist/cloud/api/repos.js.map +0 -1
  381. package/dist/cloud/api/teams.d.ts.map +0 -1
  382. package/dist/cloud/api/teams.js.map +0 -1
  383. package/dist/cloud/api/usage.d.ts.map +0 -1
  384. package/dist/cloud/api/usage.js.map +0 -1
  385. package/dist/cloud/api/workspaces.d.ts.map +0 -1
  386. package/dist/cloud/api/workspaces.js.map +0 -1
  387. package/dist/cloud/billing/index.d.ts.map +0 -1
  388. package/dist/cloud/billing/index.js.map +0 -1
  389. package/dist/cloud/billing/plans.d.ts.map +0 -1
  390. package/dist/cloud/billing/plans.js.map +0 -1
  391. package/dist/cloud/billing/service.d.ts.map +0 -1
  392. package/dist/cloud/billing/service.js.map +0 -1
  393. package/dist/cloud/billing/types.d.ts.map +0 -1
  394. package/dist/cloud/billing/types.js.map +0 -1
  395. package/dist/cloud/config.d.ts.map +0 -1
  396. package/dist/cloud/config.js.map +0 -1
  397. package/dist/cloud/db/drizzle.d.ts.map +0 -1
  398. package/dist/cloud/db/drizzle.js.map +0 -1
  399. package/dist/cloud/db/index.d.ts.map +0 -1
  400. package/dist/cloud/db/index.js.map +0 -1
  401. package/dist/cloud/db/schema.d.ts.map +0 -1
  402. package/dist/cloud/db/schema.js.map +0 -1
  403. package/dist/cloud/index.d.ts.map +0 -1
  404. package/dist/cloud/index.js.map +0 -1
  405. package/dist/cloud/provisioner/index.d.ts.map +0 -1
  406. package/dist/cloud/provisioner/index.js.map +0 -1
  407. package/dist/cloud/server.d.ts.map +0 -1
  408. package/dist/cloud/server.js.map +0 -1
  409. package/dist/cloud/services/coordinator.d.ts.map +0 -1
  410. package/dist/cloud/services/coordinator.js.map +0 -1
  411. package/dist/cloud/services/planLimits.d.ts.map +0 -1
  412. package/dist/cloud/services/planLimits.js.map +0 -1
  413. package/dist/cloud/vault/index.d.ts.map +0 -1
  414. package/dist/cloud/vault/index.js.map +0 -1
  415. package/dist/daemon/agent-manager.d.ts.map +0 -1
  416. package/dist/daemon/agent-manager.js.map +0 -1
  417. package/dist/daemon/agent-registry.d.ts.map +0 -1
  418. package/dist/daemon/agent-registry.js.map +0 -1
  419. package/dist/daemon/api.d.ts.map +0 -1
  420. package/dist/daemon/api.js.map +0 -1
  421. package/dist/daemon/auth.d.ts.map +0 -1
  422. package/dist/daemon/auth.js.map +0 -1
  423. package/dist/daemon/cloud-sync.d.ts.map +0 -1
  424. package/dist/daemon/cloud-sync.js.map +0 -1
  425. package/dist/daemon/connection.d.ts.map +0 -1
  426. package/dist/daemon/connection.js.map +0 -1
  427. package/dist/daemon/index.d.ts.map +0 -1
  428. package/dist/daemon/index.js.map +0 -1
  429. package/dist/daemon/orchestrator.d.ts.map +0 -1
  430. package/dist/daemon/orchestrator.js.map +0 -1
  431. package/dist/daemon/registry.d.ts.map +0 -1
  432. package/dist/daemon/registry.js.map +0 -1
  433. package/dist/daemon/router.d.ts.map +0 -1
  434. package/dist/daemon/router.js.map +0 -1
  435. package/dist/daemon/server.d.ts.map +0 -1
  436. package/dist/daemon/server.js.map +0 -1
  437. package/dist/daemon/types.d.ts.map +0 -1
  438. package/dist/daemon/types.js.map +0 -1
  439. package/dist/daemon/workspace-manager.d.ts.map +0 -1
  440. package/dist/daemon/workspace-manager.js.map +0 -1
  441. package/dist/dashboard/out/_next/static/chunks/693-7b3301d8f6bc5014.js +0 -1
  442. package/dist/dashboard/out/_next/static/chunks/713-f78477eb185f1f4d.js +0 -1
  443. package/dist/dashboard/out/_next/static/chunks/766-e53e1cfe39b0b5b5.js +0 -1
  444. package/dist/dashboard/out/_next/static/chunks/900-037c64bfd797fb2a.js +0 -1
  445. package/dist/dashboard/out/_next/static/chunks/app/app/page-e3d9e1f4466b9bae.js +0 -1
  446. package/dist/dashboard/out/_next/static/chunks/app/layout-2433bb48965f4333.js +0 -1
  447. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-e68825a81db67ba1.js +0 -1
  448. package/dist/dashboard/out/_next/static/chunks/app/page-cc108bf68c8a657f.js +0 -1
  449. package/dist/dashboard/out/_next/static/chunks/app/pricing/page-d80e03a5297f95b6.js +0 -1
  450. package/dist/dashboard/out/_next/static/chunks/webpack-a5acc2831d094776.js +0 -1
  451. package/dist/dashboard/out/_next/static/css/79b80143647a07d7.css +0 -1
  452. package/dist/dashboard/out/_next/static/css/8cf277370ad48cfe.css +0 -1
  453. package/dist/dashboard-server/metrics.d.ts.map +0 -1
  454. package/dist/dashboard-server/metrics.js.map +0 -1
  455. package/dist/dashboard-server/needs-attention.d.ts.map +0 -1
  456. package/dist/dashboard-server/needs-attention.js.map +0 -1
  457. package/dist/dashboard-server/server.d.ts.map +0 -1
  458. package/dist/dashboard-server/server.js.map +0 -1
  459. package/dist/dashboard-server/start.d.ts.map +0 -1
  460. package/dist/dashboard-server/start.js.map +0 -1
  461. package/dist/hooks/inbox-check/hook.d.ts.map +0 -1
  462. package/dist/hooks/inbox-check/hook.js.map +0 -1
  463. package/dist/hooks/inbox-check/index.d.ts.map +0 -1
  464. package/dist/hooks/inbox-check/index.js.map +0 -1
  465. package/dist/hooks/inbox-check/types.d.ts.map +0 -1
  466. package/dist/hooks/inbox-check/types.js.map +0 -1
  467. package/dist/hooks/inbox-check/utils.d.ts.map +0 -1
  468. package/dist/hooks/inbox-check/utils.js.map +0 -1
  469. package/dist/hooks/index.d.ts.map +0 -1
  470. package/dist/hooks/index.js.map +0 -1
  471. package/dist/hooks/types.d.ts.map +0 -1
  472. package/dist/hooks/types.js.map +0 -1
  473. package/dist/index.d.ts.map +0 -1
  474. package/dist/index.js.map +0 -1
  475. package/dist/protocol/framing.d.ts.map +0 -1
  476. package/dist/protocol/framing.js.map +0 -1
  477. package/dist/protocol/index.d.ts.map +0 -1
  478. package/dist/protocol/index.js.map +0 -1
  479. package/dist/protocol/types.d.ts.map +0 -1
  480. package/dist/protocol/types.js.map +0 -1
  481. package/dist/resiliency/context-persistence.d.ts.map +0 -1
  482. package/dist/resiliency/context-persistence.js.map +0 -1
  483. package/dist/resiliency/health-monitor.d.ts.map +0 -1
  484. package/dist/resiliency/health-monitor.js.map +0 -1
  485. package/dist/resiliency/index.d.ts.map +0 -1
  486. package/dist/resiliency/index.js.map +0 -1
  487. package/dist/resiliency/logger.d.ts.map +0 -1
  488. package/dist/resiliency/logger.js.map +0 -1
  489. package/dist/resiliency/metrics.d.ts.map +0 -1
  490. package/dist/resiliency/metrics.js.map +0 -1
  491. package/dist/resiliency/provider-context.d.ts.map +0 -1
  492. package/dist/resiliency/provider-context.js.map +0 -1
  493. package/dist/resiliency/supervisor.d.ts.map +0 -1
  494. package/dist/resiliency/supervisor.js.map +0 -1
  495. package/dist/state/agent-state.d.ts.map +0 -1
  496. package/dist/state/agent-state.js.map +0 -1
  497. package/dist/storage/adapter.d.ts.map +0 -1
  498. package/dist/storage/adapter.js.map +0 -1
  499. package/dist/storage/sqlite-adapter.d.ts.map +0 -1
  500. package/dist/storage/sqlite-adapter.js.map +0 -1
  501. package/dist/utils/agent-config.d.ts.map +0 -1
  502. package/dist/utils/agent-config.js.map +0 -1
  503. package/dist/utils/command-resolver.d.ts.map +0 -1
  504. package/dist/utils/command-resolver.js.map +0 -1
  505. package/dist/utils/index.d.ts.map +0 -1
  506. package/dist/utils/index.js.map +0 -1
  507. package/dist/utils/logger.d.ts.map +0 -1
  508. package/dist/utils/logger.js.map +0 -1
  509. package/dist/utils/name-generator.d.ts.map +0 -1
  510. package/dist/utils/name-generator.js.map +0 -1
  511. package/dist/utils/project-namespace.d.ts.map +0 -1
  512. package/dist/utils/project-namespace.js.map +0 -1
  513. package/dist/utils/tmux-resolver.d.ts.map +0 -1
  514. package/dist/utils/tmux-resolver.js.map +0 -1
  515. package/dist/utils/update-checker.d.ts.map +0 -1
  516. package/dist/utils/update-checker.js.map +0 -1
  517. package/dist/wrapper/client.d.ts.map +0 -1
  518. package/dist/wrapper/client.js.map +0 -1
  519. package/dist/wrapper/inbox.d.ts.map +0 -1
  520. package/dist/wrapper/inbox.js.map +0 -1
  521. package/dist/wrapper/index.d.ts.map +0 -1
  522. package/dist/wrapper/index.js.map +0 -1
  523. package/dist/wrapper/parser.d.ts.map +0 -1
  524. package/dist/wrapper/parser.js.map +0 -1
  525. package/dist/wrapper/pty-wrapper.d.ts.map +0 -1
  526. package/dist/wrapper/pty-wrapper.js.map +0 -1
  527. package/dist/wrapper/tmux-wrapper.d.ts.map +0 -1
  528. package/dist/wrapper/tmux-wrapper.js.map +0 -1
  529. package/docs/AGENTS.md +0 -513
  530. package/docs/ARCHITECTURE_DECISIONS.md +0 -175
  531. package/docs/CHANGELOG.md +0 -11
  532. package/docs/CLI-SIMPLIFICATION-COMPLETE.md +0 -48
  533. package/docs/CLOUD-ARCHITECTURE.md +0 -652
  534. package/docs/CLOUD-ONBOARDING-DESIGN.md +0 -1983
  535. package/docs/COMPETITIVE_ANALYSIS.md +0 -897
  536. package/docs/CONTRIBUTING.md +0 -151
  537. package/docs/DESIGN_BRIDGE_STAFFING.md +0 -878
  538. package/docs/DESIGN_V2.md +0 -1079
  539. package/docs/INTEGRATION-GUIDE.md +0 -926
  540. package/docs/MONETIZATION.md +0 -1679
  541. package/docs/PROPOSAL-trajectories.md +0 -1582
  542. package/docs/PROTOCOL.md +0 -325
  543. package/docs/SCALING_ANALYSIS.md +0 -280
  544. package/docs/TESTING_PRESENCE_FEATURES.md +0 -327
  545. package/docs/TMUX_IMPLEMENTATION_NOTES.md +0 -364
  546. package/docs/TMUX_IMPROVEMENTS.md +0 -968
  547. package/docs/agent-relay-snippet.md +0 -168
  548. package/docs/competitive-analysis-mcp-agent-mail.md +0 -389
  549. package/docs/dashboard-v2-plan.md +0 -179
  550. package/docs/guides/CLOUD.md +0 -236
  551. package/docs/guides/LOCAL.md +0 -535
  552. package/docs/guides/SELF-HOSTED.md +0 -494
  553. package/docs/proposals/shadow-as-subagent.md +0 -765
  554. package/docs/proposals/slack-bot-integration.md +0 -1457
  555. package/docs/removable-code-analysis.md +0 -24
  556. package/scripts/dev/PUBLIC_RELEASE_PLAN.md +0 -88
  557. package/scripts/dev/dev-team-setup.sh +0 -431
  558. package/scripts/e2e-test.sh +0 -119
  559. package/scripts/games/game-protocol.md +0 -79
  560. package/scripts/games/hearts-setup.sh +0 -264
  561. package/scripts/tictactoe-setup.sh +0 -181
  562. /package/dist/dashboard/out/_next/static/chunks/{117-b2cd8d6485aacf2b.js → 117-f7b8ab0809342e77.js} +0 -0
  563. /package/dist/dashboard/out/_next/static/chunks/{648-8f3f26864ce515e5.js → 648-5cc6e1921389a58a.js} +0 -0
  564. /package/dist/dashboard/out/_next/static/chunks/app/_not-found/{page-0b990dbb71d72a98.js → page-53b8a69f76db17d0.js} +0 -0
  565. /package/dist/dashboard/out/_next/static/chunks/{fd9d1056-bf46c09eb57e019c.js → fd9d1056-609918ca7b6280bb.js} +0 -0
  566. /package/dist/dashboard/out/_next/static/{6HHWb2ZmnJ4OSm0zUP7h4 → wPgKJtcOmTFLpUncDg16A}/_buildManifest.js +0 -0
  567. /package/dist/dashboard/out/_next/static/{6HHWb2ZmnJ4OSm0zUP7h4 → wPgKJtcOmTFLpUncDg16A}/_ssgManifest.js +0 -0
@@ -5,6 +5,7 @@ import path from 'path';
5
5
  import fs from 'fs';
6
6
  import os from 'os';
7
7
  import crypto from 'crypto';
8
+ import { exec } from 'child_process';
8
9
  import { fileURLToPath } from 'url';
9
10
  import { SqliteStorageAdapter } from '../storage/sqlite-adapter.js';
10
11
  import { RelayClient } from '../wrapper/client.js';
@@ -12,7 +13,169 @@ import { computeNeedsAttention } from './needs-attention.js';
12
13
  import { computeSystemMetrics, formatPrometheusMetrics } from './metrics.js';
13
14
  import { MultiProjectClient } from '../bridge/multi-project-client.js';
14
15
  import { AgentSpawner } from '../bridge/spawner.js';
16
+ import { listTrajectorySteps, getTrajectoryStatus, getTrajectoryHistory } from '../trajectory/integration.js';
15
17
  import { loadTeamsConfig } from '../bridge/teams-config.js';
18
+ import { getMemoryMonitor } from '../resiliency/memory-monitor.js';
19
+ import { detectWorkspacePath } from '../utils/project-namespace.js';
20
+ import { startCLIAuth, getAuthSession, cancelAuthSession, submitAuthCode, completeAuthSession, getSupportedProviders, } from '../daemon/cli-auth.js';
21
+ /**
22
+ * Initialize cloud persistence for session tracking.
23
+ *
24
+ * Activation modes:
25
+ * 1. Local dev: Set RELAY_CLOUD_ENABLED=true and DATABASE_URL
26
+ * 2. Cloud deployment: Plan-based - user must have Pro+ subscription
27
+ * (enforced at cloud API level when linking daemon or enabling workspace)
28
+ *
29
+ * Session persistence (Pro+ feature) enables:
30
+ * - [[SUMMARY]] blocks saved to PostgreSQL
31
+ * - [[SESSION_END]] markers for session tracking
32
+ * - Session recovery and agent handoff
33
+ *
34
+ * @see canUseSessionPersistence in services/planLimits.ts
35
+ */
36
+ async function initCloudPersistence(workspaceId) {
37
+ // Local dev mode: simple env var check
38
+ // Cloud mode: plan check happens at API level (daemon linking, workspace config)
39
+ if (process.env.RELAY_CLOUD_ENABLED !== 'true') {
40
+ return null;
41
+ }
42
+ try {
43
+ // Dynamic import to avoid loading cloud dependencies unless enabled
44
+ const { getDb } = await import('../cloud/db/drizzle.js');
45
+ const { agentSessions, agentSummaries } = await import('../cloud/db/schema.js');
46
+ const { eq } = await import('drizzle-orm');
47
+ const db = getDb();
48
+ console.log('[dashboard] Cloud persistence enabled');
49
+ // Track active sessions per agent with timestamps for TTL cleanup
50
+ const SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
51
+ const MAX_SESSIONS = 10000;
52
+ const agentSessionIds = new Map();
53
+ // Track pending session creation to prevent race conditions
54
+ const pendingSessionCreation = new Map();
55
+ // Periodic cleanup of stale sessions (every 5 minutes)
56
+ const cleanupInterval = setInterval(() => {
57
+ const now = Date.now();
58
+ let evicted = 0;
59
+ for (const [name, { lastActivity }] of agentSessionIds.entries()) {
60
+ if (now - lastActivity > SESSION_TTL_MS) {
61
+ agentSessionIds.delete(name);
62
+ evicted++;
63
+ }
64
+ }
65
+ if (evicted > 0) {
66
+ console.log(`[cloud] Evicted ${evicted} stale session entries`);
67
+ }
68
+ }, 5 * 60 * 1000);
69
+ // Don't keep process alive just for cleanup
70
+ cleanupInterval.unref();
71
+ // Helper to get or create session with race protection
72
+ const getOrCreateSession = async (agentName) => {
73
+ // Check cache first
74
+ const cached = agentSessionIds.get(agentName);
75
+ if (cached) {
76
+ return cached.id;
77
+ }
78
+ // Check if creation is already in progress
79
+ const pending = pendingSessionCreation.get(agentName);
80
+ if (pending) {
81
+ return pending;
82
+ }
83
+ // Create session with mutex
84
+ const creationPromise = (async () => {
85
+ try {
86
+ // Double-check cache after acquiring "lock"
87
+ const rechecked = agentSessionIds.get(agentName);
88
+ if (rechecked) {
89
+ return rechecked.id;
90
+ }
91
+ // Enforce max size - evict oldest if needed
92
+ if (agentSessionIds.size >= MAX_SESSIONS) {
93
+ let oldest = null;
94
+ for (const [name, { lastActivity }] of agentSessionIds.entries()) {
95
+ if (!oldest || lastActivity < oldest.time) {
96
+ oldest = { name, time: lastActivity };
97
+ }
98
+ }
99
+ if (oldest) {
100
+ agentSessionIds.delete(oldest.name);
101
+ console.log(`[cloud] Evicted oldest session for ${oldest.name} (max sessions reached)`);
102
+ }
103
+ }
104
+ // Create a new session with null safety
105
+ const result = await db.insert(agentSessions).values({
106
+ workspaceId,
107
+ agentName,
108
+ status: 'active',
109
+ startedAt: new Date(),
110
+ }).returning();
111
+ const session = result[0];
112
+ if (!session) {
113
+ throw new Error(`Failed to create session for agent ${agentName}`);
114
+ }
115
+ // Update cache
116
+ agentSessionIds.set(agentName, { id: session.id, lastActivity: Date.now() });
117
+ return session.id;
118
+ }
119
+ finally {
120
+ pendingSessionCreation.delete(agentName);
121
+ }
122
+ })();
123
+ pendingSessionCreation.set(agentName, creationPromise);
124
+ return creationPromise;
125
+ };
126
+ return {
127
+ onSummary: async (agentName, event) => {
128
+ try {
129
+ // Get or create session with race protection
130
+ const sessionId = await getOrCreateSession(agentName);
131
+ // Update activity timestamp
132
+ agentSessionIds.set(agentName, { id: sessionId, lastActivity: Date.now() });
133
+ // Insert summary
134
+ await db.insert(agentSummaries).values({
135
+ sessionId,
136
+ agentName,
137
+ summary: event.summary,
138
+ createdAt: new Date(),
139
+ });
140
+ console.log(`[cloud] Saved summary for ${agentName}: ${event.summary.currentTask || 'no task'}`);
141
+ }
142
+ catch (err) {
143
+ console.error(`[cloud] Failed to save summary for ${agentName}:`, err);
144
+ }
145
+ },
146
+ onSessionEnd: async (agentName, event) => {
147
+ try {
148
+ const cached = agentSessionIds.get(agentName);
149
+ if (cached) {
150
+ // Update session as ended
151
+ await db.update(agentSessions)
152
+ .set({
153
+ status: 'ended',
154
+ endedAt: new Date(),
155
+ endMarker: event.marker,
156
+ })
157
+ .where(eq(agentSessions.id, cached.id));
158
+ agentSessionIds.delete(agentName);
159
+ console.log(`[cloud] Session ended for ${agentName}: ${event.marker.summary || 'no summary'}`);
160
+ }
161
+ }
162
+ catch (err) {
163
+ console.error(`[cloud] Failed to end session for ${agentName}:`, err);
164
+ }
165
+ },
166
+ destroy: () => {
167
+ clearInterval(cleanupInterval);
168
+ agentSessionIds.clear();
169
+ pendingSessionCreation.clear();
170
+ console.log('[cloud] Cloud persistence handler destroyed');
171
+ },
172
+ };
173
+ }
174
+ catch (err) {
175
+ console.warn('[dashboard] Cloud persistence not available:', err);
176
+ return null;
177
+ }
178
+ }
16
179
  const __filename = fileURLToPath(import.meta.url);
17
180
  const __dirname = path.dirname(__filename);
18
181
  /**
@@ -105,9 +268,49 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
105
268
  ? new SqliteStorageAdapter({ dbPath })
106
269
  : undefined;
107
270
  // Initialize spawner if enabled
271
+ // Use detectWorkspacePath to find the actual repo directory in cloud workspaces
272
+ const workspacePath = detectWorkspacePath(projectRoot || dataDir);
273
+ console.log(`[dashboard] Workspace path: ${workspacePath}`);
274
+ // Pass dashboard port to spawner so spawned agents can call spawn/release APIs for nested spawning
108
275
  const spawner = enableSpawner
109
- ? new AgentSpawner(projectRoot || dataDir, tmuxSession)
276
+ ? new AgentSpawner(workspacePath, tmuxSession, port)
110
277
  : undefined;
278
+ // Initialize cloud persistence and memory monitoring if enabled (RELAY_CLOUD_ENABLED=true)
279
+ if (spawner) {
280
+ // Use workspace ID from env or generate from project root
281
+ const workspaceId = process.env.RELAY_WORKSPACE_ID ||
282
+ crypto.createHash('sha256').update(projectRoot || dataDir).digest('hex').slice(0, 36);
283
+ initCloudPersistence(workspaceId).then((cloudHandler) => {
284
+ if (cloudHandler) {
285
+ spawner.setCloudPersistence(cloudHandler);
286
+ }
287
+ }).catch((err) => {
288
+ console.warn('[dashboard] Failed to initialize cloud persistence:', err);
289
+ });
290
+ // Initialize memory monitoring for cloud deployments
291
+ // Memory monitoring is enabled by default when cloud is enabled
292
+ if (process.env.RELAY_CLOUD_ENABLED === 'true' || process.env.RELAY_MEMORY_MONITORING === 'true') {
293
+ try {
294
+ const memoryMonitor = getMemoryMonitor({
295
+ checkIntervalMs: 10000, // Check every 10 seconds
296
+ enableTrendAnalysis: true,
297
+ enableProactiveAlerts: true,
298
+ });
299
+ memoryMonitor.start();
300
+ console.log('[dashboard] Memory monitoring enabled');
301
+ // Register existing workers with memory monitor
302
+ const workers = spawner.getActiveWorkers();
303
+ for (const worker of workers) {
304
+ if (worker.pid) {
305
+ memoryMonitor.register(worker.name, worker.pid);
306
+ }
307
+ }
308
+ }
309
+ catch (err) {
310
+ console.warn('[dashboard] Failed to initialize memory monitoring:', err);
311
+ }
312
+ }
313
+ }
111
314
  process.on('uncaughtException', (err) => {
112
315
  console.error('Uncaught Exception:', err);
113
316
  });
@@ -244,7 +447,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
244
447
  evictedCount++;
245
448
  }
246
449
  }
247
- catch (err) {
450
+ catch (_err) {
248
451
  // Ignore errors for individual files (may have been deleted)
249
452
  }
250
453
  }
@@ -928,20 +1131,27 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
928
1131
  getRecentSessions(),
929
1132
  getAgentSummaries(),
930
1133
  ]);
931
- // Filter agents:
1134
+ // Filter and separate agents from human users:
932
1135
  // 1. Exclude "Dashboard" (internal agent, not a real team member)
933
1136
  // 2. Exclude offline agents (no lastSeen or lastSeen > threshold)
1137
+ // 3. Exclude agents without a known CLI (these are improperly registered or stale)
1138
+ // 4. Separate human users (cli === 'dashboard') from AI agents
934
1139
  const now = Date.now();
935
1140
  // 30 seconds - aligns with heartbeat timeout (5s heartbeat * 6 multiplier = 30s)
936
1141
  // This ensures agents disappear quickly after they stop responding to heartbeats
937
1142
  const OFFLINE_THRESHOLD_MS = 30 * 1000;
938
- const filteredAgents = Array.from(agentsMap.values()).filter(agent => {
1143
+ // First pass: filter out invalid/offline entries
1144
+ const validEntries = Array.from(agentsMap.values())
1145
+ .filter(agent => {
939
1146
  // Exclude Dashboard
940
1147
  if (agent.name === 'Dashboard')
941
1148
  return false;
942
1149
  // Exclude agents starting with __ (internal/system agents)
943
1150
  if (agent.name.startsWith('__'))
944
1151
  return false;
1152
+ // Exclude agents without a proper CLI (improperly registered or stale)
1153
+ if (!agent.cli || agent.cli === 'Unknown')
1154
+ return false;
945
1155
  // Exclude offline agents (no lastSeen or too old)
946
1156
  if (!agent.lastSeen)
947
1157
  return false;
@@ -950,8 +1160,22 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
950
1160
  return false;
951
1161
  return true;
952
1162
  });
1163
+ // Separate AI agents from human users
1164
+ const filteredAgents = validEntries
1165
+ .filter(agent => agent.cli !== 'dashboard')
1166
+ .map(agent => ({
1167
+ ...agent,
1168
+ isHuman: false,
1169
+ }));
1170
+ const humanUsers = validEntries
1171
+ .filter(agent => agent.cli === 'dashboard')
1172
+ .map(agent => ({
1173
+ ...agent,
1174
+ isHuman: true,
1175
+ }));
953
1176
  return {
954
1177
  agents: filteredAgents,
1178
+ users: humanUsers,
955
1179
  messages: allMessages,
956
1180
  activity: allMessages, // For now, activity log is just the message log
957
1181
  sessions,
@@ -1194,8 +1418,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1194
1418
  logSubscriptions.get(agentName).add(ws);
1195
1419
  console.log(`[dashboard] Client subscribed to logs for: ${agentName} (spawned: ${isSpawned}, daemon: ${isDaemon})`);
1196
1420
  if (isSpawned && spawner) {
1197
- // Send initial log history for spawned agents
1198
- const lines = spawner.getWorkerOutput(agentName, 200);
1421
+ // Send initial log history for spawned agents (5000 lines to match xterm scrollback capacity)
1422
+ const lines = spawner.getWorkerOutput(agentName, 5000);
1199
1423
  ws.send(JSON.stringify({
1200
1424
  type: 'history',
1201
1425
  agent: agentName,
@@ -1258,11 +1482,51 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1258
1482
  console.log(`[dashboard] Logs WebSocket client disconnected (code: ${code}, reason: ${reasonStr})`);
1259
1483
  });
1260
1484
  });
1485
+ // Deduplication for log output - prevent same content from being broadcast multiple times
1486
+ // Key: agentName -> Set of recent content hashes (rolling window)
1487
+ const recentLogHashes = new Map();
1488
+ const MAX_LOG_HASH_WINDOW = 50; // Keep last 50 hashes per agent
1489
+ // Simple hash function for log dedup
1490
+ const hashLogContent = (content) => {
1491
+ // Normalize whitespace and create a simple hash
1492
+ const normalized = content.replace(/\s+/g, ' ').trim().slice(0, 200);
1493
+ let hash = 0;
1494
+ for (let i = 0; i < normalized.length; i++) {
1495
+ const char = normalized.charCodeAt(i);
1496
+ hash = ((hash << 5) - hash) + char;
1497
+ hash = hash & hash;
1498
+ }
1499
+ return hash.toString(36);
1500
+ };
1261
1501
  // Function to broadcast log output to subscribed clients
1262
1502
  const broadcastLogOutput = (agentName, output) => {
1263
1503
  const clients = logSubscriptions.get(agentName);
1264
1504
  if (!clients || clients.size === 0)
1265
1505
  return;
1506
+ // Skip empty or whitespace-only output
1507
+ const trimmed = output.trim();
1508
+ if (!trimmed)
1509
+ return;
1510
+ // Dedup: Check if we've recently broadcast this content
1511
+ const hash = hashLogContent(output);
1512
+ let agentHashes = recentLogHashes.get(agentName);
1513
+ if (!agentHashes) {
1514
+ agentHashes = new Set();
1515
+ recentLogHashes.set(agentName, agentHashes);
1516
+ }
1517
+ if (agentHashes.has(hash)) {
1518
+ // Already broadcast this content recently, skip
1519
+ return;
1520
+ }
1521
+ // Add to rolling window
1522
+ agentHashes.add(hash);
1523
+ if (agentHashes.size > MAX_LOG_HASH_WINDOW) {
1524
+ // Remove oldest entry (first in Set iteration order)
1525
+ const oldest = agentHashes.values().next().value;
1526
+ if (oldest !== undefined) {
1527
+ agentHashes.delete(oldest);
1528
+ }
1529
+ }
1266
1530
  const payload = JSON.stringify({
1267
1531
  type: 'output',
1268
1532
  agent: agentName,
@@ -1291,8 +1555,35 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1291
1555
  const getOnlineUsersList = () => {
1292
1556
  return Array.from(onlineUsers.values()).map((state) => state.info);
1293
1557
  };
1558
+ // Heartbeat to detect dead connections (30 seconds)
1559
+ const PRESENCE_HEARTBEAT_INTERVAL = 30000;
1560
+ const presenceHealth = new WeakMap();
1561
+ const presenceHeartbeat = setInterval(() => {
1562
+ wssPresence.clients.forEach((ws) => {
1563
+ const health = presenceHealth.get(ws);
1564
+ if (!health) {
1565
+ presenceHealth.set(ws, { isAlive: true });
1566
+ return;
1567
+ }
1568
+ if (!health.isAlive) {
1569
+ ws.terminate();
1570
+ return;
1571
+ }
1572
+ health.isAlive = false;
1573
+ ws.ping();
1574
+ });
1575
+ }, PRESENCE_HEARTBEAT_INTERVAL);
1576
+ wssPresence.on('close', () => {
1577
+ clearInterval(presenceHeartbeat);
1578
+ });
1294
1579
  wssPresence.on('connection', (ws) => {
1295
- console.log('[dashboard] Presence WebSocket client connected');
1580
+ // Initialize health tracking (no log - too noisy)
1581
+ presenceHealth.set(ws, { isAlive: true });
1582
+ ws.on('pong', () => {
1583
+ const health = presenceHealth.get(ws);
1584
+ if (health)
1585
+ health.isAlive = true;
1586
+ });
1296
1587
  let clientUsername;
1297
1588
  ws.on('message', (data) => {
1298
1589
  try {
@@ -1318,7 +1609,11 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1318
1609
  // Add this connection to existing user
1319
1610
  existing.connections.add(ws);
1320
1611
  existing.info.lastSeen = now;
1321
- console.log(`[dashboard] User ${username} opened new tab (${existing.connections.size} connections)`);
1612
+ // Only log at milestones to reduce noise
1613
+ const count = existing.connections.size;
1614
+ if (count === 2 || count === 5 || count === 10 || count % 50 === 0) {
1615
+ console.log(`[dashboard] User ${username} has ${count} connections`);
1616
+ }
1322
1617
  }
1323
1618
  else {
1324
1619
  // New user - create presence state
@@ -1495,6 +1790,172 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1495
1790
  websocketClients: wss.clients.size,
1496
1791
  });
1497
1792
  });
1793
+ // ===== CLI Auth API (for workspace-based provider authentication) =====
1794
+ /**
1795
+ * POST /auth/cli/:provider/start - Start CLI auth flow
1796
+ * Body: { useDeviceFlow?: boolean }
1797
+ */
1798
+ app.post('/auth/cli/:provider/start', async (req, res) => {
1799
+ const { provider } = req.params;
1800
+ const { useDeviceFlow } = req.body || {};
1801
+ try {
1802
+ const session = await startCLIAuth(provider, { useDeviceFlow });
1803
+ res.json({
1804
+ sessionId: session.id,
1805
+ status: session.status,
1806
+ authUrl: session.authUrl,
1807
+ });
1808
+ }
1809
+ catch (err) {
1810
+ res.status(400).json({
1811
+ error: err instanceof Error ? err.message : 'Failed to start CLI auth',
1812
+ });
1813
+ }
1814
+ });
1815
+ /**
1816
+ * GET /auth/cli/:provider/status/:sessionId - Get auth session status
1817
+ */
1818
+ app.get('/auth/cli/:provider/status/:sessionId', (req, res) => {
1819
+ const { sessionId } = req.params;
1820
+ const session = getAuthSession(sessionId);
1821
+ if (!session) {
1822
+ return res.status(404).json({ error: 'Session not found' });
1823
+ }
1824
+ res.json({
1825
+ status: session.status,
1826
+ authUrl: session.authUrl,
1827
+ error: session.error,
1828
+ });
1829
+ });
1830
+ /**
1831
+ * GET /auth/cli/:provider/creds/:sessionId - Get credentials from completed auth
1832
+ */
1833
+ app.get('/auth/cli/:provider/creds/:sessionId', (req, res) => {
1834
+ const { sessionId } = req.params;
1835
+ const session = getAuthSession(sessionId);
1836
+ if (!session) {
1837
+ return res.status(404).json({ error: 'Session not found' });
1838
+ }
1839
+ if (session.status !== 'success') {
1840
+ return res.status(400).json({ error: 'Auth not complete', status: session.status });
1841
+ }
1842
+ res.json({
1843
+ token: session.token,
1844
+ refreshToken: session.refreshToken,
1845
+ expiresAt: session.tokenExpiresAt?.toISOString(),
1846
+ });
1847
+ });
1848
+ /**
1849
+ * POST /auth/cli/:provider/cancel/:sessionId - Cancel auth session
1850
+ */
1851
+ app.post('/auth/cli/:provider/cancel/:sessionId', (req, res) => {
1852
+ const { sessionId } = req.params;
1853
+ const cancelled = cancelAuthSession(sessionId);
1854
+ if (!cancelled) {
1855
+ return res.status(404).json({ error: 'Session not found' });
1856
+ }
1857
+ res.json({ success: true });
1858
+ });
1859
+ /**
1860
+ * POST /auth/cli/:provider/code/:sessionId - Submit auth code to PTY
1861
+ * Used when OAuth returns a code that must be pasted into the CLI
1862
+ */
1863
+ app.post('/auth/cli/:provider/code/:sessionId', async (req, res) => {
1864
+ const { provider, sessionId } = req.params;
1865
+ const { code } = req.body;
1866
+ console.log('[cli-auth] Auth code submission received', { provider, sessionId, codeLength: code?.length });
1867
+ if (!code || typeof code !== 'string') {
1868
+ return res.status(400).json({ error: 'Auth code is required' });
1869
+ }
1870
+ try {
1871
+ const result = await submitAuthCode(sessionId, code);
1872
+ console.log('[cli-auth] Auth code submission result', { provider, sessionId, result });
1873
+ if (!result.success) {
1874
+ // Use 400 for all errors since they can be retried
1875
+ return res.status(400).json({
1876
+ error: result.error || 'Session not found or process not running',
1877
+ needsRestart: result.needsRestart ?? true,
1878
+ });
1879
+ }
1880
+ // Wait a few seconds for CLI to process and write credentials
1881
+ // The 1s delay in submitAuthCode + CLI processing time means credentials
1882
+ // should be available within 3-5 seconds
1883
+ let sessionStatus = 'waiting_auth';
1884
+ for (let i = 0; i < 10; i++) {
1885
+ await new Promise(resolve => setTimeout(resolve, 500));
1886
+ const session = getAuthSession(sessionId);
1887
+ if (session?.status === 'success') {
1888
+ sessionStatus = 'success';
1889
+ console.log('[cli-auth] Credentials found after code submission', { provider, sessionId, attempt: i + 1 });
1890
+ break;
1891
+ }
1892
+ if (session?.status === 'error') {
1893
+ sessionStatus = 'error';
1894
+ break;
1895
+ }
1896
+ }
1897
+ res.json({
1898
+ success: true,
1899
+ message: 'Auth code submitted',
1900
+ status: sessionStatus,
1901
+ });
1902
+ }
1903
+ catch (err) {
1904
+ console.error('[cli-auth] Auth code submission error', { provider, sessionId, error: String(err) });
1905
+ return res.status(500).json({
1906
+ error: 'Internal error submitting auth code. Please try again.',
1907
+ needsRestart: true,
1908
+ });
1909
+ }
1910
+ });
1911
+ /**
1912
+ * POST /auth/cli/:provider/complete/:sessionId - Complete auth
1913
+ * For providers like Claude: just polls for credentials
1914
+ * For providers like Codex: accepts authCode (redirect URL) and extracts the code
1915
+ */
1916
+ app.post('/auth/cli/:provider/complete/:sessionId', async (req, res) => {
1917
+ const { sessionId } = req.params;
1918
+ const { authCode } = req.body || {};
1919
+ // If authCode provided, try to extract code and submit it
1920
+ if (authCode && typeof authCode === 'string') {
1921
+ let code = authCode;
1922
+ // If it's a URL, extract the code parameter
1923
+ if (authCode.startsWith('http')) {
1924
+ try {
1925
+ const url = new URL(authCode);
1926
+ const codeParam = url.searchParams.get('code');
1927
+ if (codeParam) {
1928
+ code = codeParam;
1929
+ }
1930
+ }
1931
+ catch {
1932
+ // Not a valid URL, use as-is
1933
+ }
1934
+ }
1935
+ // Submit the code to the CLI process
1936
+ const submitResult = await submitAuthCode(sessionId, code);
1937
+ if (!submitResult.success) {
1938
+ return res.status(400).json({
1939
+ error: submitResult.error,
1940
+ needsRestart: submitResult.needsRestart,
1941
+ });
1942
+ }
1943
+ // Wait a moment for credentials to be written
1944
+ await new Promise(resolve => setTimeout(resolve, 2000));
1945
+ }
1946
+ // Poll for credentials
1947
+ const result = await completeAuthSession(sessionId);
1948
+ if (!result.success) {
1949
+ return res.status(400).json({ error: result.error });
1950
+ }
1951
+ res.json({ success: true, message: 'Authentication complete' });
1952
+ });
1953
+ /**
1954
+ * GET /auth/cli/providers - List supported providers
1955
+ */
1956
+ app.get('/auth/cli/providers', (req, res) => {
1957
+ res.json({ providers: getSupportedProviders() });
1958
+ });
1498
1959
  // ===== Metrics API =====
1499
1960
  /**
1500
1961
  * GET /api/metrics - JSON format metrics for dashboard
@@ -1564,6 +2025,197 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1564
2025
  res.status(500).send('# Error computing metrics\n');
1565
2026
  }
1566
2027
  });
2028
+ // ===== Agent Memory Metrics API =====
2029
+ /**
2030
+ * GET /api/metrics/agents - Detailed agent memory and resource metrics
2031
+ */
2032
+ app.get('/api/metrics/agents', async (req, res) => {
2033
+ try {
2034
+ const agents = [];
2035
+ // Get metrics from spawner's active workers
2036
+ if (spawner) {
2037
+ const activeWorkers = spawner.getActiveWorkers();
2038
+ for (const worker of activeWorkers) {
2039
+ // Get memory usage via ps command
2040
+ let rssBytes = 0;
2041
+ let cpuPercent = 0;
2042
+ if (worker.pid) {
2043
+ try {
2044
+ const { execSync } = await import('child_process');
2045
+ const output = execSync(`ps -o rss=,pcpu= -p ${worker.pid}`, {
2046
+ encoding: 'utf8',
2047
+ timeout: 3000,
2048
+ }).trim();
2049
+ const parts = output.split(/\s+/);
2050
+ rssBytes = parseInt(parts[0] || '0', 10) * 1024;
2051
+ cpuPercent = parseFloat(parts[1] || '0');
2052
+ }
2053
+ catch {
2054
+ // Process may have exited
2055
+ }
2056
+ }
2057
+ agents.push({
2058
+ name: worker.name,
2059
+ pid: worker.pid,
2060
+ status: worker.pid ? 'running' : 'unknown',
2061
+ rssBytes,
2062
+ cpuPercent,
2063
+ trend: 'unknown',
2064
+ alertLevel: rssBytes > 1024 * 1024 * 1024 ? 'critical' :
2065
+ rssBytes > 512 * 1024 * 1024 ? 'warning' : 'normal',
2066
+ highWatermark: rssBytes,
2067
+ uptimeMs: worker.spawnedAt ? Date.now() - worker.spawnedAt : 0,
2068
+ startedAt: worker.spawnedAt ? new Date(worker.spawnedAt).toISOString() : undefined,
2069
+ });
2070
+ }
2071
+ }
2072
+ // Also check agents.json for registered agents that may not be spawned
2073
+ const agentsPath = path.join(teamDir, 'agents.json');
2074
+ if (fs.existsSync(agentsPath)) {
2075
+ const data = JSON.parse(fs.readFileSync(agentsPath, 'utf-8'));
2076
+ const registeredAgents = data.agents || [];
2077
+ for (const agent of registeredAgents) {
2078
+ if (!agents.find(a => a.name === agent.name)) {
2079
+ // Check if recently active (within 30 seconds)
2080
+ const lastSeen = agent.lastSeen ? new Date(agent.lastSeen).getTime() : 0;
2081
+ const isActive = Date.now() - lastSeen < 30000;
2082
+ if (isActive) {
2083
+ agents.push({
2084
+ name: agent.name,
2085
+ status: 'active',
2086
+ alertLevel: 'normal',
2087
+ });
2088
+ }
2089
+ }
2090
+ }
2091
+ }
2092
+ res.json({
2093
+ agents,
2094
+ system: {
2095
+ totalMemory: os.totalmem(),
2096
+ freeMemory: os.freemem(),
2097
+ heapUsed: process.memoryUsage().heapUsed,
2098
+ },
2099
+ });
2100
+ }
2101
+ catch (err) {
2102
+ console.error('Failed to get agent metrics', err);
2103
+ res.status(500).json({ error: 'Failed to get agent metrics' });
2104
+ }
2105
+ });
2106
+ /**
2107
+ * GET /api/metrics/health - System health and crash insights
2108
+ */
2109
+ app.get('/api/metrics/health', async (req, res) => {
2110
+ try {
2111
+ // Calculate health score based on available data
2112
+ let healthScore = 100;
2113
+ const issues = [];
2114
+ const recommendations = [];
2115
+ const crashes = [];
2116
+ const alerts = [];
2117
+ let agentCount = 0;
2118
+ const totalCrashes24h = 0;
2119
+ let totalAlerts24h = 0;
2120
+ // Get spawned agent count
2121
+ if (spawner) {
2122
+ const workers = spawner.getActiveWorkers();
2123
+ agentCount = workers.length;
2124
+ // Check for high memory usage
2125
+ for (const worker of workers) {
2126
+ if (worker.pid) {
2127
+ try {
2128
+ const { execSync } = await import('child_process');
2129
+ const output = execSync(`ps -o rss= -p ${worker.pid}`, {
2130
+ encoding: 'utf8',
2131
+ timeout: 3000,
2132
+ }).trim();
2133
+ const rssBytes = parseInt(output, 10) * 1024;
2134
+ if (rssBytes > 1.5 * 1024 * 1024 * 1024) {
2135
+ // > 1.5GB
2136
+ healthScore -= 20;
2137
+ issues.push({
2138
+ severity: 'critical',
2139
+ message: `Agent "${worker.name}" is using ${Math.round(rssBytes / 1024 / 1024)}MB of memory`,
2140
+ });
2141
+ totalAlerts24h++;
2142
+ alerts.push({
2143
+ id: `alert-${Date.now()}-${worker.name}`,
2144
+ agentName: worker.name,
2145
+ alertType: 'oom_imminent',
2146
+ message: `Memory usage critical: ${Math.round(rssBytes / 1024 / 1024)}MB`,
2147
+ createdAt: new Date().toISOString(),
2148
+ });
2149
+ }
2150
+ else if (rssBytes > 1024 * 1024 * 1024) {
2151
+ // > 1GB
2152
+ healthScore -= 10;
2153
+ issues.push({
2154
+ severity: 'high',
2155
+ message: `Agent "${worker.name}" memory usage is elevated (${Math.round(rssBytes / 1024 / 1024)}MB)`,
2156
+ });
2157
+ }
2158
+ }
2159
+ catch {
2160
+ // Process may have exited
2161
+ }
2162
+ }
2163
+ }
2164
+ }
2165
+ // Check registered agents
2166
+ const agentsPath = path.join(teamDir, 'agents.json');
2167
+ if (fs.existsSync(agentsPath)) {
2168
+ const data = JSON.parse(fs.readFileSync(agentsPath, 'utf-8'));
2169
+ const registeredAgents = data.agents || [];
2170
+ const activeAgents = registeredAgents.filter((a) => {
2171
+ const lastSeen = a.lastSeen ? new Date(a.lastSeen).getTime() : 0;
2172
+ return Date.now() - lastSeen < 30000;
2173
+ });
2174
+ agentCount = Math.max(agentCount, activeAgents.length);
2175
+ }
2176
+ // Generate recommendations based on issues
2177
+ if (issues.some(i => i.severity === 'critical')) {
2178
+ recommendations.push('Consider restarting agents with high memory usage');
2179
+ recommendations.push('Monitor system resources closely');
2180
+ }
2181
+ if (agentCount === 0) {
2182
+ recommendations.push('No active agents detected - start agents to begin monitoring');
2183
+ }
2184
+ // Clamp health score
2185
+ healthScore = Math.max(0, Math.min(100, healthScore));
2186
+ // Generate summary
2187
+ let summary;
2188
+ if (healthScore >= 90) {
2189
+ summary = 'System is healthy. All agents operating normally.';
2190
+ }
2191
+ else if (healthScore >= 70) {
2192
+ summary = 'Some issues detected. Review warnings and recommendations.';
2193
+ }
2194
+ else if (healthScore >= 50) {
2195
+ summary = 'Multiple issues detected. Action recommended.';
2196
+ }
2197
+ else {
2198
+ summary = 'Critical issues detected. Immediate action required.';
2199
+ }
2200
+ res.json({
2201
+ healthScore,
2202
+ summary,
2203
+ issues,
2204
+ recommendations,
2205
+ crashes,
2206
+ alerts,
2207
+ stats: {
2208
+ totalCrashes24h,
2209
+ totalAlerts24h,
2210
+ agentCount,
2211
+ },
2212
+ });
2213
+ }
2214
+ catch (err) {
2215
+ console.error('Failed to compute health metrics', err);
2216
+ res.status(500).json({ error: 'Failed to compute health metrics' });
2217
+ }
2218
+ });
1567
2219
  // ===== File Search API =====
1568
2220
  /**
1569
2221
  * GET /api/files - Search for files in the repository
@@ -2087,6 +2739,654 @@ Start by greeting the project leads and asking for status updates.`;
2087
2739
  });
2088
2740
  }
2089
2741
  });
2742
+ /**
2743
+ * GET /api/trajectory - Get current trajectory status
2744
+ */
2745
+ app.get('/api/trajectory', async (_req, res) => {
2746
+ try {
2747
+ const status = await getTrajectoryStatus();
2748
+ res.json({
2749
+ success: true,
2750
+ ...status,
2751
+ });
2752
+ }
2753
+ catch (err) {
2754
+ console.error('[api] Trajectory status error:', err);
2755
+ res.status(500).json({
2756
+ success: false,
2757
+ error: err.message,
2758
+ });
2759
+ }
2760
+ });
2761
+ /**
2762
+ * GET /api/trajectory/steps - List trajectory steps
2763
+ */
2764
+ app.get('/api/trajectory/steps', async (req, res) => {
2765
+ try {
2766
+ const trajectoryId = req.query.trajectoryId;
2767
+ const result = await listTrajectorySteps(trajectoryId);
2768
+ if (result.success) {
2769
+ res.json({
2770
+ success: true,
2771
+ steps: result.steps,
2772
+ });
2773
+ }
2774
+ else {
2775
+ res.status(500).json({
2776
+ success: false,
2777
+ steps: [],
2778
+ error: result.error,
2779
+ });
2780
+ }
2781
+ }
2782
+ catch (err) {
2783
+ console.error('[api] Trajectory steps error:', err);
2784
+ res.status(500).json({
2785
+ success: false,
2786
+ steps: [],
2787
+ error: err.message,
2788
+ });
2789
+ }
2790
+ });
2791
+ /**
2792
+ * GET /api/trajectory/history - List all trajectories (completed and active)
2793
+ */
2794
+ app.get('/api/trajectory/history', async (_req, res) => {
2795
+ try {
2796
+ const result = await getTrajectoryHistory();
2797
+ if (result.success) {
2798
+ res.json({
2799
+ success: true,
2800
+ trajectories: result.trajectories,
2801
+ });
2802
+ }
2803
+ else {
2804
+ res.status(500).json({
2805
+ success: false,
2806
+ trajectories: [],
2807
+ error: result.error,
2808
+ });
2809
+ }
2810
+ }
2811
+ catch (err) {
2812
+ console.error('[api] Trajectory history error:', err);
2813
+ res.status(500).json({
2814
+ success: false,
2815
+ trajectories: [],
2816
+ error: err.message,
2817
+ });
2818
+ }
2819
+ });
2820
+ // ===== Settings API =====
2821
+ /**
2822
+ * GET /api/settings - Get all workspace settings with documentation
2823
+ */
2824
+ app.get('/api/settings', async (_req, res) => {
2825
+ try {
2826
+ const { readRelayConfig, shouldStoreInRepo, getTrajectoriesStorageDescription } = await import('../trajectory/config.js');
2827
+ const config = readRelayConfig();
2828
+ res.json({
2829
+ success: true,
2830
+ settings: {
2831
+ trajectories: {
2832
+ storeInRepo: shouldStoreInRepo(),
2833
+ storageLocation: getTrajectoriesStorageDescription(),
2834
+ description: 'Trajectories record the journey of agent work using the PDERO paradigm (Plan, Design, Execute, Review, Observe). They capture decisions, phase transitions, and retrospectives.',
2835
+ benefits: [
2836
+ 'Track why decisions were made, not just what was built',
2837
+ 'Enable session recovery when agents crash or context is lost',
2838
+ 'Provide learning data for future agents working on similar tasks',
2839
+ 'Create an audit trail of agent work for review',
2840
+ ],
2841
+ learnMore: 'https://pdero.com',
2842
+ optInReason: 'Enable "Store in repo" to version-control your trajectories alongside your code. This is useful for teams who want to review agent decision-making processes.',
2843
+ },
2844
+ },
2845
+ config,
2846
+ });
2847
+ }
2848
+ catch (err) {
2849
+ console.error('[api] Settings error:', err);
2850
+ res.status(500).json({
2851
+ success: false,
2852
+ error: err.message,
2853
+ });
2854
+ }
2855
+ });
2856
+ /**
2857
+ * GET /api/settings/trajectory - Get trajectory storage settings
2858
+ */
2859
+ app.get('/api/settings/trajectory', async (_req, res) => {
2860
+ try {
2861
+ const { readRelayConfig, shouldStoreInRepo, getTrajectoriesStorageDescription } = await import('../trajectory/config.js');
2862
+ const config = readRelayConfig();
2863
+ res.json({
2864
+ success: true,
2865
+ settings: {
2866
+ storeInRepo: shouldStoreInRepo(),
2867
+ storageLocation: getTrajectoriesStorageDescription(),
2868
+ },
2869
+ config: config.trajectories || {},
2870
+ // Documentation for the UI
2871
+ documentation: {
2872
+ title: 'Trajectory Storage',
2873
+ description: 'Trajectories record the journey of agent work using the PDERO paradigm (Plan, Design, Execute, Review, Observe).',
2874
+ whatIsIt: 'A trajectory captures not just what an agent built, but WHY it made specific decisions. This includes phase transitions, key decisions with reasoning, and retrospective summaries.',
2875
+ benefits: [
2876
+ 'Understand agent decision-making for code review',
2877
+ 'Enable session recovery if agents crash',
2878
+ 'Train future agents on your codebase patterns',
2879
+ 'Create audit trails of AI work',
2880
+ ],
2881
+ storeInRepoExplanation: 'When enabled, trajectories are stored in .trajectories/ in your repo and can be committed to source control. When disabled (default), they are stored in your user directory (~/.config/agent-relay/trajectories/).',
2882
+ learnMore: 'https://pdero.com',
2883
+ },
2884
+ });
2885
+ }
2886
+ catch (err) {
2887
+ console.error('[api] Settings trajectory error:', err);
2888
+ res.status(500).json({
2889
+ success: false,
2890
+ error: err.message,
2891
+ });
2892
+ }
2893
+ });
2894
+ /**
2895
+ * PUT /api/settings/trajectory - Update trajectory storage settings
2896
+ *
2897
+ * Body: { storeInRepo: boolean }
2898
+ *
2899
+ * This writes to .relay/config.json in the project root
2900
+ */
2901
+ app.put('/api/settings/trajectory', async (req, res) => {
2902
+ try {
2903
+ const { storeInRepo } = req.body;
2904
+ if (typeof storeInRepo !== 'boolean') {
2905
+ return res.status(400).json({
2906
+ success: false,
2907
+ error: 'storeInRepo must be a boolean',
2908
+ });
2909
+ }
2910
+ const { getRelayConfigPath, readRelayConfig } = await import('../trajectory/config.js');
2911
+ const { getProjectPaths } = await import('../utils/project-namespace.js');
2912
+ const { projectRoot: _projectRoot } = getProjectPaths();
2913
+ // Read existing config
2914
+ const config = readRelayConfig();
2915
+ // Update trajectory settings
2916
+ config.trajectories = {
2917
+ ...config.trajectories,
2918
+ storeInRepo,
2919
+ };
2920
+ // Ensure .relay directory exists
2921
+ const configPath = getRelayConfigPath();
2922
+ const configDir = path.dirname(configPath);
2923
+ if (!fs.existsSync(configDir)) {
2924
+ fs.mkdirSync(configDir, { recursive: true });
2925
+ }
2926
+ // Write updated config
2927
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
2928
+ res.json({
2929
+ success: true,
2930
+ settings: {
2931
+ storeInRepo,
2932
+ storageLocation: storeInRepo ? 'repo (.trajectories/)' : 'user (~/.config/agent-relay/trajectories/)',
2933
+ },
2934
+ });
2935
+ }
2936
+ catch (err) {
2937
+ console.error('[api] Settings trajectory update error:', err);
2938
+ res.status(500).json({
2939
+ success: false,
2940
+ error: err.message,
2941
+ });
2942
+ }
2943
+ });
2944
+ const decisions = new Map();
2945
+ /**
2946
+ * GET /api/decisions - List all pending decisions
2947
+ */
2948
+ app.get('/api/decisions', (_req, res) => {
2949
+ const allDecisions = Array.from(decisions.values())
2950
+ .sort((a, b) => {
2951
+ const urgencyOrder = { critical: 0, high: 1, medium: 2, low: 3 };
2952
+ return urgencyOrder[a.urgency] - urgencyOrder[b.urgency];
2953
+ });
2954
+ res.json({ success: true, decisions: allDecisions });
2955
+ });
2956
+ /**
2957
+ * POST /api/decisions - Create a new decision request
2958
+ * Body: { agentName, title, description, options?, urgency, category, expiresAt?, context? }
2959
+ */
2960
+ app.post('/api/decisions', (req, res) => {
2961
+ const { agentName, title, description, options, urgency, category, expiresAt, context } = req.body;
2962
+ if (!agentName || !title || !urgency || !category) {
2963
+ return res.status(400).json({
2964
+ success: false,
2965
+ error: 'Missing required fields: agentName, title, urgency, category',
2966
+ });
2967
+ }
2968
+ const decision = {
2969
+ id: `decision-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
2970
+ agentName,
2971
+ title,
2972
+ description: description || '',
2973
+ options,
2974
+ urgency,
2975
+ category,
2976
+ createdAt: new Date().toISOString(),
2977
+ expiresAt,
2978
+ context,
2979
+ };
2980
+ decisions.set(decision.id, decision);
2981
+ // Broadcast to WebSocket clients
2982
+ broadcastData().catch(() => { });
2983
+ res.json({ success: true, decision });
2984
+ });
2985
+ /**
2986
+ * POST /api/decisions/:id/approve - Approve/resolve a decision
2987
+ * Body: { optionId?: string, response?: string }
2988
+ */
2989
+ app.post('/api/decisions/:id/approve', async (req, res) => {
2990
+ const { id } = req.params;
2991
+ const { optionId, response } = req.body;
2992
+ const decision = decisions.get(id);
2993
+ if (!decision) {
2994
+ return res.status(404).json({ success: false, error: 'Decision not found' });
2995
+ }
2996
+ // Send response to the agent via relay
2997
+ const agentName = decision.agentName;
2998
+ let responseMessage = `DECISION APPROVED: ${decision.title}`;
2999
+ if (optionId && decision.options) {
3000
+ const option = decision.options.find(o => o.id === optionId);
3001
+ if (option) {
3002
+ responseMessage += `\nSelected: ${option.label}`;
3003
+ }
3004
+ }
3005
+ if (response) {
3006
+ responseMessage += `\nResponse: ${response}`;
3007
+ }
3008
+ // Try to send message to agent
3009
+ try {
3010
+ const client = await getRelayClient('Dashboard');
3011
+ if (client) {
3012
+ await client.sendMessage(agentName, responseMessage, 'message');
3013
+ }
3014
+ }
3015
+ catch (err) {
3016
+ console.warn('[api] Could not send decision response to agent:', err);
3017
+ }
3018
+ decisions.delete(id);
3019
+ broadcastData().catch(() => { });
3020
+ res.json({ success: true, message: 'Decision approved' });
3021
+ });
3022
+ /**
3023
+ * POST /api/decisions/:id/reject - Reject a decision
3024
+ * Body: { reason?: string }
3025
+ */
3026
+ app.post('/api/decisions/:id/reject', async (req, res) => {
3027
+ const { id } = req.params;
3028
+ const { reason } = req.body;
3029
+ const decision = decisions.get(id);
3030
+ if (!decision) {
3031
+ return res.status(404).json({ success: false, error: 'Decision not found' });
3032
+ }
3033
+ // Send rejection to the agent
3034
+ const agentName = decision.agentName;
3035
+ let responseMessage = `DECISION REJECTED: ${decision.title}`;
3036
+ if (reason) {
3037
+ responseMessage += `\nReason: ${reason}`;
3038
+ }
3039
+ try {
3040
+ const client = await getRelayClient('Dashboard');
3041
+ if (client) {
3042
+ await client.sendMessage(agentName, responseMessage, 'message');
3043
+ }
3044
+ }
3045
+ catch (err) {
3046
+ console.warn('[api] Could not send decision rejection to agent:', err);
3047
+ }
3048
+ decisions.delete(id);
3049
+ broadcastData().catch(() => { });
3050
+ res.json({ success: true, message: 'Decision rejected' });
3051
+ });
3052
+ /**
3053
+ * DELETE /api/decisions/:id - Delete/dismiss a decision
3054
+ */
3055
+ app.delete('/api/decisions/:id', (_req, res) => {
3056
+ const { id } = _req.params;
3057
+ if (!decisions.has(id)) {
3058
+ return res.status(404).json({ success: false, error: 'Decision not found' });
3059
+ }
3060
+ decisions.delete(id);
3061
+ broadcastData().catch(() => { });
3062
+ res.json({ success: true, message: 'Decision dismissed' });
3063
+ });
3064
+ /**
3065
+ * GET /api/fleet/servers - Get fleet server overview
3066
+ * Returns local daemon info + any connected bridge servers
3067
+ * Note: When bridge is active, local agents are already included in bridge project agents,
3068
+ * so we don't add a separate "Local Daemon" entry to avoid double-counting.
3069
+ */
3070
+ app.get('/api/fleet/servers', async (_req, res) => {
3071
+ const servers = [];
3072
+ const localAgents = spawner?.getActiveWorkers() || [];
3073
+ const agentStatuses = await loadAgentStatuses();
3074
+ let hasBridgeProjects = false;
3075
+ // Check for bridge connections first
3076
+ const bridgeStatePath = path.join(dataDir, 'bridge-state.json');
3077
+ if (fs.existsSync(bridgeStatePath)) {
3078
+ try {
3079
+ const bridgeState = JSON.parse(fs.readFileSync(bridgeStatePath, 'utf-8'));
3080
+ if (bridgeState.projects && bridgeState.projects.length > 0) {
3081
+ hasBridgeProjects = true;
3082
+ for (const project of bridgeState.projects) {
3083
+ // Enrich with actual online agents from agents.json (same logic as getBridgeData)
3084
+ // This fixes the bug where stale agents were counted
3085
+ let projectAgents = [];
3086
+ if (project.path) {
3087
+ const projectHash = crypto.createHash('sha256').update(project.path).digest('hex').slice(0, 12);
3088
+ const projectDataDir = path.join(path.dirname(dataDir), projectHash);
3089
+ const projectTeamDir = path.join(projectDataDir, 'team');
3090
+ const agentsPath = path.join(projectTeamDir, 'agents.json');
3091
+ if (fs.existsSync(agentsPath)) {
3092
+ try {
3093
+ const agentsData = JSON.parse(fs.readFileSync(agentsPath, 'utf-8'));
3094
+ if (agentsData.agents && Array.isArray(agentsData.agents)) {
3095
+ // Filter to only show online agents (seen within 30 seconds)
3096
+ const thirtySecondsAgo = Date.now() - 30 * 1000;
3097
+ projectAgents = agentsData.agents
3098
+ .filter((a) => {
3099
+ if (!a.lastSeen)
3100
+ return false;
3101
+ return new Date(a.lastSeen).getTime() > thirtySecondsAgo;
3102
+ })
3103
+ .map((a) => ({
3104
+ name: a.name,
3105
+ status: 'online',
3106
+ }));
3107
+ }
3108
+ }
3109
+ catch (e) {
3110
+ console.warn(`[api] Failed to read agents for ${project.path}:`, e);
3111
+ }
3112
+ }
3113
+ }
3114
+ servers.push({
3115
+ id: project.id,
3116
+ name: project.name || project.path.split('/').pop() || project.id,
3117
+ status: project.connected ? 'healthy' : 'offline',
3118
+ agents: projectAgents,
3119
+ cpuUsage: 0,
3120
+ memoryUsage: 0,
3121
+ activeConnections: project.connected ? 1 : 0,
3122
+ uptime: 0,
3123
+ lastHeartbeat: project.lastSeen || new Date().toISOString(),
3124
+ });
3125
+ }
3126
+ }
3127
+ }
3128
+ catch (err) {
3129
+ console.warn('[api] Failed to read bridge state:', err);
3130
+ }
3131
+ }
3132
+ // Only add local daemon entry if we don't have bridge projects
3133
+ // (otherwise local agents are already counted in the bridge project)
3134
+ if (!hasBridgeProjects) {
3135
+ servers.push({
3136
+ id: 'local',
3137
+ name: 'Local Daemon',
3138
+ status: 'healthy',
3139
+ agents: localAgents.map(a => ({
3140
+ name: a.name,
3141
+ status: agentStatuses[a.name]?.status || 'unknown',
3142
+ })),
3143
+ cpuUsage: Math.random() * 30, // Mock - would come from actual metrics
3144
+ memoryUsage: Math.random() * 50,
3145
+ activeConnections: wss.clients.size,
3146
+ uptime: process.uptime(),
3147
+ lastHeartbeat: new Date().toISOString(),
3148
+ });
3149
+ }
3150
+ res.json({ success: true, servers });
3151
+ });
3152
+ /**
3153
+ * GET /api/fleet/stats - Get aggregate fleet statistics
3154
+ */
3155
+ app.get('/api/fleet/stats', async (_req, res) => {
3156
+ const localAgents = spawner?.getActiveWorkers() || [];
3157
+ const agentStatuses = await loadAgentStatuses();
3158
+ const totalAgents = localAgents.length;
3159
+ let onlineAgents = 0;
3160
+ let busyAgents = 0;
3161
+ for (const agent of localAgents) {
3162
+ const status = agentStatuses[agent.name]?.status;
3163
+ if (status === 'online')
3164
+ onlineAgents++;
3165
+ if (status === 'busy')
3166
+ busyAgents++;
3167
+ }
3168
+ res.json({
3169
+ success: true,
3170
+ stats: {
3171
+ totalAgents,
3172
+ onlineAgents,
3173
+ busyAgents,
3174
+ pendingDecisions: decisions.size,
3175
+ activeTasks: Array.from(tasks.values()).filter(t => t.status === 'assigned' || t.status === 'in_progress').length,
3176
+ },
3177
+ });
3178
+ });
3179
+ const tasks = new Map();
3180
+ /**
3181
+ * GET /api/tasks - List all tasks
3182
+ */
3183
+ app.get('/api/tasks', (req, res) => {
3184
+ const status = req.query.status;
3185
+ const agentName = req.query.agent;
3186
+ let allTasks = Array.from(tasks.values());
3187
+ if (status) {
3188
+ allTasks = allTasks.filter(t => t.status === status);
3189
+ }
3190
+ if (agentName) {
3191
+ allTasks = allTasks.filter(t => t.agentName === agentName);
3192
+ }
3193
+ // Sort by priority and creation time
3194
+ allTasks.sort((a, b) => {
3195
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
3196
+ const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
3197
+ if (priorityDiff !== 0)
3198
+ return priorityDiff;
3199
+ return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
3200
+ });
3201
+ res.json({ success: true, tasks: allTasks });
3202
+ });
3203
+ /**
3204
+ * POST /api/tasks - Create and assign a task
3205
+ * Body: { agentName, title, description, priority }
3206
+ */
3207
+ app.post('/api/tasks', async (req, res) => {
3208
+ const { agentName, title, description, priority } = req.body;
3209
+ if (!agentName || !title || !priority) {
3210
+ return res.status(400).json({
3211
+ success: false,
3212
+ error: 'Missing required fields: agentName, title, priority',
3213
+ });
3214
+ }
3215
+ const task = {
3216
+ id: `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
3217
+ agentName,
3218
+ title,
3219
+ description: description || '',
3220
+ priority,
3221
+ status: 'assigned',
3222
+ createdAt: new Date().toISOString(),
3223
+ assignedAt: new Date().toISOString(),
3224
+ };
3225
+ tasks.set(task.id, task);
3226
+ // Send task to agent via relay
3227
+ try {
3228
+ const client = await getRelayClient('Dashboard');
3229
+ if (client) {
3230
+ const taskMessage = `TASK ASSIGNED [${priority.toUpperCase()}]: ${title}\n\n${description || 'No additional details.'}`;
3231
+ await client.sendMessage(agentName, taskMessage, 'message');
3232
+ }
3233
+ }
3234
+ catch (err) {
3235
+ console.warn('[api] Could not send task to agent:', err);
3236
+ }
3237
+ broadcastData().catch(() => { });
3238
+ res.json({ success: true, task });
3239
+ });
3240
+ /**
3241
+ * PATCH /api/tasks/:id - Update task status
3242
+ * Body: { status, result? }
3243
+ */
3244
+ app.patch('/api/tasks/:id', (req, res) => {
3245
+ const { id } = req.params;
3246
+ const { status, result } = req.body;
3247
+ const task = tasks.get(id);
3248
+ if (!task) {
3249
+ return res.status(404).json({ success: false, error: 'Task not found' });
3250
+ }
3251
+ if (status) {
3252
+ task.status = status;
3253
+ if (status === 'completed' || status === 'failed') {
3254
+ task.completedAt = new Date().toISOString();
3255
+ }
3256
+ }
3257
+ if (result !== undefined) {
3258
+ task.result = result;
3259
+ }
3260
+ tasks.set(id, task);
3261
+ broadcastData().catch(() => { });
3262
+ res.json({ success: true, task });
3263
+ });
3264
+ /**
3265
+ * DELETE /api/tasks/:id - Cancel/delete a task
3266
+ */
3267
+ app.delete('/api/tasks/:id', async (req, res) => {
3268
+ const { id } = req.params;
3269
+ const task = tasks.get(id);
3270
+ if (!task) {
3271
+ return res.status(404).json({ success: false, error: 'Task not found' });
3272
+ }
3273
+ // Notify agent of cancellation if task is still active
3274
+ if (task.status === 'pending' || task.status === 'assigned' || task.status === 'in_progress') {
3275
+ try {
3276
+ const client = await getRelayClient('Dashboard');
3277
+ if (client) {
3278
+ await client.sendMessage(task.agentName, `TASK CANCELLED: ${task.title}`, 'message');
3279
+ }
3280
+ }
3281
+ catch (err) {
3282
+ console.warn('[api] Could not send task cancellation to agent:', err);
3283
+ }
3284
+ }
3285
+ tasks.delete(id);
3286
+ broadcastData().catch(() => { });
3287
+ res.json({ success: true, message: 'Task cancelled' });
3288
+ });
3289
+ // ===== Beads Integration API =====
3290
+ /**
3291
+ * POST /api/beads - Create a bead (task/issue) via the bd CLI
3292
+ */
3293
+ app.post('/api/beads', async (req, res) => {
3294
+ const { title, assignee, priority, type, description: _description } = req.body;
3295
+ if (!title || typeof title !== 'string') {
3296
+ return res.status(400).json({ success: false, error: 'Title is required' });
3297
+ }
3298
+ // Build bd create command
3299
+ const args = ['create', `--title="${title.replace(/"/g, '\\"')}"`];
3300
+ if (assignee) {
3301
+ args.push(`--assignee=${assignee}`);
3302
+ }
3303
+ if (priority !== undefined && priority !== null) {
3304
+ args.push(`--priority=${priority}`);
3305
+ }
3306
+ if (type && ['task', 'bug', 'feature'].includes(type)) {
3307
+ args.push(`--type=${type}`);
3308
+ }
3309
+ const cmd = `bd ${args.join(' ')}`;
3310
+ console.log('[api/beads] Creating bead:', cmd);
3311
+ // Execute bd create command
3312
+ exec(cmd, { cwd: dataDir }, (error, stdout, stderr) => {
3313
+ if (error) {
3314
+ console.error('[api/beads] bd create failed:', stderr || error.message);
3315
+ return res.status(500).json({
3316
+ success: false,
3317
+ error: stderr || error.message || 'Failed to create bead',
3318
+ });
3319
+ }
3320
+ // Parse bead ID from output (bd create outputs the ID)
3321
+ const output = stdout.trim();
3322
+ // bd create typically outputs: "Created beads-xxx: title"
3323
+ const idMatch = output.match(/Created\s+(beads-\w+)/i) || output.match(/(beads-\w+)/);
3324
+ const beadId = idMatch ? idMatch[1] : `beads-${Date.now()}`;
3325
+ console.log('[api/beads] Created bead:', beadId);
3326
+ res.json({
3327
+ success: true,
3328
+ bead: {
3329
+ id: beadId,
3330
+ title,
3331
+ assignee,
3332
+ priority,
3333
+ type: type || 'task',
3334
+ },
3335
+ });
3336
+ });
3337
+ });
3338
+ /**
3339
+ * POST /api/relay/send - Send a relay message to an agent
3340
+ */
3341
+ app.post('/api/relay/send', async (req, res) => {
3342
+ const { to, content, thread } = req.body;
3343
+ if (!to || typeof to !== 'string') {
3344
+ return res.status(400).json({ success: false, error: 'Recipient (to) is required' });
3345
+ }
3346
+ if (!content || typeof content !== 'string') {
3347
+ return res.status(400).json({ success: false, error: 'Message content is required' });
3348
+ }
3349
+ try {
3350
+ const client = await getRelayClient('Dashboard');
3351
+ if (!client) {
3352
+ return res.status(503).json({
3353
+ success: false,
3354
+ error: 'Relay client not available',
3355
+ });
3356
+ }
3357
+ const messageId = await client.sendMessage(to, content, thread ? 'message' : 'message');
3358
+ console.log('[api/relay/send] Sent message to', to, ':', messageId);
3359
+ res.json({
3360
+ success: true,
3361
+ messageId: messageId || `msg-${Date.now()}`,
3362
+ });
3363
+ }
3364
+ catch (err) {
3365
+ console.error('[api/relay/send] Failed to send message:', err);
3366
+ res.status(500).json({
3367
+ success: false,
3368
+ error: err instanceof Error ? err.message : 'Failed to send message',
3369
+ });
3370
+ }
3371
+ });
3372
+ // Helper to load agent statuses
3373
+ async function loadAgentStatuses() {
3374
+ const agentsFile = path.join(dataDir, 'agents.json');
3375
+ try {
3376
+ if (fs.existsSync(agentsFile)) {
3377
+ const data = JSON.parse(fs.readFileSync(agentsFile, 'utf-8'));
3378
+ const result = {};
3379
+ for (const agent of data.agents || []) {
3380
+ result[agent.name] = { status: agent.status || 'offline' };
3381
+ }
3382
+ return result;
3383
+ }
3384
+ }
3385
+ catch (err) {
3386
+ console.warn('[api] Failed to load agent statuses:', err);
3387
+ }
3388
+ return {};
3389
+ }
2090
3390
  // Watch for changes
2091
3391
  if (storage) {
2092
3392
  setInterval(() => {