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
@@ -10,6 +10,13 @@ import fs from 'node:fs';
10
10
  import path from 'node:path';
11
11
  import { EventEmitter } from 'node:events';
12
12
  import { RelayClient } from './client.js';
13
+ import { parseSummaryWithDetails, parseSessionEndFromOutput, isPlaceholderTarget } from './parser.js';
14
+ import { getProjectPaths } from '../utils/project-namespace.js';
15
+ import { getTrailEnvVars } from '../trajectory/integration.js';
16
+ import { findAgentConfig } from '../utils/agent-config.js';
17
+ import { HookRegistry, createTrajectoryHooks } from '../hooks/index.js';
18
+ import { getContinuityManager, parseContinuityCommand, hasContinuityCommand } from '../continuity/index.js';
19
+ import { INJECTION_CONSTANTS, stripAnsi, sleep, buildInjectionString, injectWithRetry as sharedInjectWithRetry, calculateSuccessRate, createInjectionMetrics, getDefaultRelayPrefix, detectCliType, CLI_QUIRKS, } from './shared.js';
13
20
  /** Maximum lines to keep in output buffer */
14
21
  const MAX_BUFFER_LINES = 10000;
15
22
  export class PtyWrapper extends EventEmitter {
@@ -20,26 +27,93 @@ export class PtyWrapper extends EventEmitter {
20
27
  outputBuffer = [];
21
28
  rawBuffer = '';
22
29
  relayPrefix;
30
+ cliType;
23
31
  sentMessageHashes = new Set();
32
+ receivedMessageIds = new Set(); // Dedup incoming messages
24
33
  processedSpawnCommands = new Set();
25
34
  processedReleaseCommands = new Set();
35
+ pendingFencedSpawn = null;
26
36
  messageQueue = [];
27
37
  isInjecting = false;
28
38
  readyForMessages = false;
39
+ lastOutputTime = 0;
40
+ injectionMetrics = createInjectionMetrics();
29
41
  logFilePath;
30
42
  logStream;
31
- hasAcceptedPrompt = false;
43
+ acceptedPrompts = new Set(); // Track which prompts have been accepted
44
+ hookRegistry;
45
+ sessionStartTime = Date.now();
46
+ continuity;
47
+ agentId;
48
+ processedContinuityCommands = new Set();
49
+ lastSummaryRawContent = ''; // Dedup summary event emissions
50
+ sessionEndProcessed = false; // Track if we've already emitted session-end
51
+ inThinkingBlock = false; // Track if inside <thinking>...</thinking>
52
+ lastSummaryTime = Date.now(); // Track when last summary was output
53
+ outputsSinceSummary = 0; // Count outputs since last summary
54
+ detectedTask; // Auto-detected task from agent config
55
+ sessionEndData; // Store SESSION_END data for handoff
56
+ instructionsInjected = false; // Track if init instructions have been injected
57
+ continuityInjected = false; // Track if continuity context has been injected
58
+ recentLogChunks = new Map(); // Dedup log streaming (hash -> timestamp)
59
+ static LOG_DEDUP_WINDOW_MS = 500; // Window for considering logs as duplicates
60
+ static LOG_DEDUP_MAX_SIZE = 100; // Max entries in dedup map
61
+ lastParsedLength = 0; // Track last parsed position to avoid re-parsing entire buffer
62
+ lastContinuityParsedLength = 0; // Same for continuity commands
32
63
  constructor(config) {
33
64
  super();
34
65
  this.config = config;
35
- this.relayPrefix = config.relayPrefix ?? '->relay:';
66
+ this.relayPrefix = config.relayPrefix ?? getDefaultRelayPrefix();
67
+ // Detect CLI type from command for special handling
68
+ this.cliType = config.cliType ?? detectCliType(config.command);
69
+ // Auto-detect agent role from .claude/agents/ or .openagents/ if task not provided
70
+ let detectedTask = config.task;
71
+ if (!detectedTask) {
72
+ const agentConfig = findAgentConfig(config.name, config.cwd);
73
+ if (agentConfig?.description) {
74
+ detectedTask = agentConfig.description;
75
+ // Use stderr for consistency with TmuxWrapper's logStderr pattern
76
+ process.stderr.write(`[pty:${config.name}] Auto-detected role: ${detectedTask.substring(0, 60)}...\n`);
77
+ }
78
+ }
79
+ // Store detected task for use in hook registry
80
+ this.detectedTask = detectedTask;
36
81
  this.client = new RelayClient({
37
82
  agentName: config.name,
38
83
  socketPath: config.socketPath,
39
- cli: 'spawned',
84
+ cli: this.cliType,
85
+ task: detectedTask,
40
86
  workingDirectory: config.cwd ?? process.cwd(),
41
87
  quiet: true,
42
88
  });
89
+ // Initialize hook registry
90
+ const projectPaths = getProjectPaths();
91
+ this.hookRegistry = new HookRegistry({
92
+ agentName: config.name,
93
+ workingDir: config.cwd ?? process.cwd(),
94
+ projectId: projectPaths.projectId,
95
+ task: this.detectedTask,
96
+ env: config.env,
97
+ inject: (text) => this.write(text + '\r'),
98
+ send: async (to, body) => {
99
+ this.client.sendMessage(to, body, 'message');
100
+ },
101
+ });
102
+ // Register trajectory hooks if enabled (default: true if task provided or auto-detected)
103
+ const enableTrajectory = config.trajectoryTracking ?? !!this.detectedTask;
104
+ if (enableTrajectory) {
105
+ const trajectoryHooks = createTrajectoryHooks({
106
+ projectId: projectPaths.projectId,
107
+ agentName: config.name,
108
+ });
109
+ this.hookRegistry.registerLifecycleHooks(trajectoryHooks);
110
+ }
111
+ // Register custom hooks if provided
112
+ if (config.hooks) {
113
+ this.hookRegistry.registerLifecycleHooks(config.hooks);
114
+ }
115
+ // Initialize continuity manager
116
+ this.continuity = getContinuityManager({ defaultCli: 'spawned' });
43
117
  // Handle incoming messages
44
118
  this.client.onMessage = (from, payload, messageId, meta, originalTo) => {
45
119
  this.handleIncomingMessage(from, payload, messageId, meta, originalTo);
@@ -87,6 +161,9 @@ export class PtyWrapper extends EventEmitter {
87
161
  // Log spawn details for debugging
88
162
  console.log(`[pty:${this.config.name}] Spawning: ${this.config.command} ${args.join(' ')}`);
89
163
  console.log(`[pty:${this.config.name}] CWD: ${cwd}`);
164
+ // Get trail environment variables
165
+ const projectPaths = getProjectPaths();
166
+ const trailEnvVars = getTrailEnvVars(projectPaths.projectId, this.config.name, projectPaths.dataDir);
90
167
  // Spawn the process with error handling
91
168
  try {
92
169
  this.ptyProcess = pty.spawn(this.config.command, args, {
@@ -97,6 +174,7 @@ export class PtyWrapper extends EventEmitter {
97
174
  env: {
98
175
  ...process.env,
99
176
  ...this.config.env,
177
+ ...trailEnvVars,
100
178
  AGENT_RELAY_NAME: this.config.name,
101
179
  TERM: 'xterm-256color',
102
180
  },
@@ -109,6 +187,17 @@ export class PtyWrapper extends EventEmitter {
109
187
  throw spawnError;
110
188
  }
111
189
  this.running = true;
190
+ this.sessionStartTime = Date.now();
191
+ // Dispatch session start hook (handles trajectory initialization)
192
+ this.hookRegistry.dispatchSessionStart().catch(err => {
193
+ console.error(`[pty:${this.config.name}] Session start hook error:`, err);
194
+ });
195
+ // Initialize continuity and get agentId, then inject context
196
+ this.initializeAgentId()
197
+ .then(() => this.injectContinuityContext())
198
+ .catch(err => {
199
+ console.error(`[pty:${this.config.name}] Agent ID/continuity initialization error:`, err);
200
+ });
112
201
  // Capture output
113
202
  this.ptyProcess.onData((data) => {
114
203
  this.handleOutput(data);
@@ -128,10 +217,139 @@ export class PtyWrapper extends EventEmitter {
128
217
  this.processMessageQueue();
129
218
  }, 2000);
130
219
  }
220
+ /**
221
+ * Initialize agent ID for continuity/resume functionality
222
+ */
223
+ async initializeAgentId() {
224
+ if (!this.continuity)
225
+ return;
226
+ try {
227
+ let ledger;
228
+ // If resuming from a previous agent ID, try to find that ledger
229
+ if (this.config.resumeAgentId) {
230
+ ledger = await this.continuity.findLedgerByAgentId(this.config.resumeAgentId);
231
+ if (ledger) {
232
+ console.log(`[pty:${this.config.name}] Resuming agent ID: ${ledger.agentId} (from previous session)`);
233
+ }
234
+ else {
235
+ console.error(`[pty:${this.config.name}] Resume agent ID ${this.config.resumeAgentId} not found, creating new`);
236
+ }
237
+ }
238
+ // If not resuming or resume ID not found, get or create ledger
239
+ if (!ledger) {
240
+ ledger = await this.continuity.getOrCreateLedger(this.config.name, 'spawned');
241
+ console.log(`[pty:${this.config.name}] Agent ID: ${ledger.agentId} (use this to resume if agent dies)`);
242
+ }
243
+ this.agentId = ledger.agentId;
244
+ }
245
+ catch (err) {
246
+ console.error(`[pty:${this.config.name}] Failed to initialize agent ID: ${err.message}`);
247
+ }
248
+ }
249
+ /**
250
+ * Get the current agent ID
251
+ */
252
+ getAgentId() {
253
+ return this.agentId;
254
+ }
255
+ /**
256
+ * Inject continuity context from previous session.
257
+ * Called after agent ID initialization to restore state from ledger.
258
+ */
259
+ async injectContinuityContext() {
260
+ if (!this.continuity || !this.running)
261
+ return;
262
+ // Guard: Only inject once per session
263
+ if (this.continuityInjected) {
264
+ console.log(`[pty:${this.config.name}] Continuity context already injected, skipping`);
265
+ return;
266
+ }
267
+ this.continuityInjected = true;
268
+ try {
269
+ const context = await this.continuity.getStartupContext(this.config.name);
270
+ // Skip if no meaningful context (empty ledger or just boilerplate)
271
+ if (!context?.formatted || context.formatted.length < 50) {
272
+ console.log(`[pty:${this.config.name}] Skipping continuity injection (no meaningful context)`);
273
+ return;
274
+ }
275
+ if (context?.formatted) {
276
+ // Build context notification similar to TmuxWrapper
277
+ const taskInfo = context.ledger?.currentTask
278
+ ? `Task: ${context.ledger.currentTask.slice(0, 50)}`
279
+ : '';
280
+ const handoffInfo = context.handoff
281
+ ? `Last handoff: ${context.handoff.createdAt.toISOString().split('T')[0]}`
282
+ : '';
283
+ const statusParts = [taskInfo, handoffInfo].filter(Boolean).join(' | ');
284
+ const notification = `[Continuity] Previous session context loaded.${statusParts ? ` ${statusParts}` : ''}\n\n${context.formatted}`;
285
+ // Queue continuity context directly to messageQueue with 'system' as sender
286
+ // This avoids creating confusing "Agent -> Agent" self-messages in the dashboard
287
+ // Fix for Lead communication issue: continuity checkpoints were creating self-messages
288
+ this.messageQueue.push({
289
+ from: 'system',
290
+ body: notification,
291
+ messageId: `continuity-startup-${Date.now()}`,
292
+ thread: 'continuity-context',
293
+ });
294
+ this.processMessageQueue();
295
+ const mode = context.handoff ? 'resume' : 'continue';
296
+ console.log(`[pty:${this.config.name}] Continuity context injected (${mode})`);
297
+ }
298
+ }
299
+ catch (err) {
300
+ console.error(`[pty:${this.config.name}] Failed to inject continuity context: ${err.message}`);
301
+ }
302
+ }
303
+ /**
304
+ * Parse ->continuity: commands from output and handle them.
305
+ *
306
+ * Supported commands:
307
+ * ->continuity:save <<<...>>> - Save session state to ledger
308
+ * ->continuity:load - Request context injection
309
+ * ->continuity:search "query" - Search past handoffs
310
+ * ->continuity:uncertain "..." - Mark item as uncertain
311
+ * ->continuity:handoff <<<...>>> - Create explicit handoff
312
+ */
313
+ async parseContinuityCommands(content) {
314
+ if (!this.continuity)
315
+ return;
316
+ if (!hasContinuityCommand(content))
317
+ return;
318
+ const command = parseContinuityCommand(content);
319
+ if (!command)
320
+ return;
321
+ // Deduplication: use type + content hash for all commands
322
+ // Fixed: content-less commands (like load) now use static hash to prevent infinite loops
323
+ const cmdHash = `${command.type}:${command.content || command.query || command.item || 'no-content'}`;
324
+ if (this.processedContinuityCommands.has(cmdHash))
325
+ return;
326
+ this.processedContinuityCommands.add(cmdHash);
327
+ // Limit dedup set size
328
+ if (this.processedContinuityCommands.size > 100) {
329
+ const oldest = this.processedContinuityCommands.values().next().value;
330
+ if (oldest)
331
+ this.processedContinuityCommands.delete(oldest);
332
+ }
333
+ try {
334
+ const response = await this.continuity.handleCommand(this.config.name, command);
335
+ if (response) {
336
+ // Inject response via relay message to self
337
+ this.client.sendMessage(this.config.name, response, 'message', {
338
+ thread: 'continuity-response',
339
+ });
340
+ console.log(`[pty:${this.config.name}] Continuity command handled: ${command.type}`);
341
+ }
342
+ }
343
+ catch (err) {
344
+ console.error(`[pty:${this.config.name}] Continuity command error: ${err.message}`);
345
+ }
346
+ }
131
347
  /**
132
348
  * Handle output from the process
133
349
  */
134
350
  handleOutput(data) {
351
+ // Track output timing for stability checks
352
+ this.lastOutputTime = Date.now();
135
353
  // Append to raw buffer
136
354
  this.rawBuffer += data;
137
355
  // Write to log file if available
@@ -141,8 +359,13 @@ export class PtyWrapper extends EventEmitter {
141
359
  // Emit for external listeners
142
360
  this.emit('output', data);
143
361
  // Stream to daemon for dashboard log viewing (if connected)
362
+ // Filter out Claude's extended thinking blocks before streaming
363
+ // Also deduplicate to prevent terminal redraws from causing duplicate log entries
144
364
  if (this.config.streamLogs !== false && this.client.state === 'READY') {
145
- this.client.sendLog(data);
365
+ const filteredData = this.filterThinkingBlocks(data);
366
+ if (filteredData && !this.isDuplicateLogChunk(filteredData)) {
367
+ this.client.sendLog(filteredData);
368
+ }
146
369
  }
147
370
  // Auto-accept Claude's first-run prompt for --dangerously-skip-permissions
148
371
  // The prompt shows: "2. Yes, I accept" - we send "2" to accept
@@ -162,28 +385,172 @@ export class PtyWrapper extends EventEmitter {
162
385
  }
163
386
  // Parse for relay commands
164
387
  this.parseRelayCommands();
388
+ // Dispatch output hook (handles phase detection, etc.)
389
+ const cleanData = stripAnsi(data);
390
+ this.hookRegistry.dispatchOutput(cleanData, data).catch(err => {
391
+ console.error(`[pty:${this.config.name}] Output hook error:`, err);
392
+ });
393
+ // Check for [[SUMMARY]] and [[SESSION_END]] blocks and emit events
394
+ // This allows cloud services to handle persistence without hardcoding storage
395
+ const cleanContent = stripAnsi(this.rawBuffer);
396
+ this.checkForSummaryAndEmit(cleanContent);
397
+ this.checkForSessionEndAndEmit(cleanContent);
398
+ // Parse for continuity commands (->continuity:save, ->continuity:load, etc.)
399
+ // Use rawBuffer (accumulated content) not immediate chunk, since multi-line
400
+ // fenced commands like ->continuity:save <<<...>>> span multiple output events
401
+ // Optimization: Only parse new content with lookback for incomplete fenced commands
402
+ if (cleanContent.length > this.lastContinuityParsedLength) {
403
+ const lookbackStart = Math.max(0, this.lastContinuityParsedLength - 500);
404
+ const contentToParse = cleanContent.substring(lookbackStart);
405
+ this.parseContinuityCommands(contentToParse).catch(err => {
406
+ console.error(`[pty:${this.config.name}] Continuity command parsing error:`, err);
407
+ });
408
+ this.lastContinuityParsedLength = cleanContent.length;
409
+ }
410
+ // Track outputs and potentially remind about summaries
411
+ this.trackOutputAndRemind(data);
412
+ }
413
+ /**
414
+ * Filter Claude's extended thinking blocks from output.
415
+ * Thinking blocks are wrapped in <thinking>...</thinking> tags and should
416
+ * not be streamed to the dashboard or stored in output buffers.
417
+ *
418
+ * This method tracks state across calls to handle multi-line thinking blocks.
419
+ */
420
+ filterThinkingBlocks(data) {
421
+ const THINKING_START = /<thinking>/;
422
+ const THINKING_END = /<\/thinking>/;
423
+ const lines = data.split('\n');
424
+ const outputLines = [];
425
+ for (const line of lines) {
426
+ // If in thinking block, check for end
427
+ if (this.inThinkingBlock) {
428
+ if (THINKING_END.test(line)) {
429
+ this.inThinkingBlock = false;
430
+ // If there's content after </thinking> on the same line, keep it
431
+ const afterEnd = line.split('</thinking>')[1];
432
+ if (afterEnd && afterEnd.trim()) {
433
+ outputLines.push(afterEnd);
434
+ }
435
+ }
436
+ // Skip this line - inside thinking block
437
+ continue;
438
+ }
439
+ // Check for thinking start
440
+ if (THINKING_START.test(line)) {
441
+ this.inThinkingBlock = true;
442
+ // Check if it ends on the same line
443
+ if (THINKING_END.test(line)) {
444
+ this.inThinkingBlock = false;
445
+ }
446
+ // Keep content before <thinking> if any
447
+ const beforeStart = line.split('<thinking>')[0];
448
+ if (beforeStart && beforeStart.trim()) {
449
+ outputLines.push(beforeStart);
450
+ }
451
+ continue;
452
+ }
453
+ // Normal line - keep it
454
+ outputLines.push(line);
455
+ }
456
+ return outputLines.join('\n');
165
457
  }
166
458
  /**
167
- * Auto-accept Claude's first-run prompts for --dangerously-skip-permissions
168
- * Detects the acceptance prompt and sends "2" to select "Yes, I accept"
459
+ * Check if a log chunk is a duplicate (recently streamed).
460
+ * Prevents terminal redraws from causing duplicate log entries in the dashboard.
461
+ *
462
+ * Uses content normalization and time-based deduplication:
463
+ * - Strips whitespace and normalizes content for comparison
464
+ * - Considers chunks with same normalized content within LOG_DEDUP_WINDOW_MS as duplicates
465
+ * - Cleans up old entries to prevent memory growth
466
+ */
467
+ isDuplicateLogChunk(data) {
468
+ // Normalize: strip excessive whitespace, limit to first 200 chars for hash
469
+ // This helps catch redraws that might have slight formatting differences
470
+ const normalized = stripAnsi(data).replace(/\s+/g, ' ').trim().substring(0, 200);
471
+ // Very short chunks (likely control chars or partial output) - allow through
472
+ if (normalized.length < 10) {
473
+ return false;
474
+ }
475
+ // Simple hash using string as key
476
+ const hash = normalized;
477
+ const now = Date.now();
478
+ // Check if this chunk was recently streamed
479
+ const lastSeen = this.recentLogChunks.get(hash);
480
+ if (lastSeen && (now - lastSeen) < PtyWrapper.LOG_DEDUP_WINDOW_MS) {
481
+ return true; // Duplicate
482
+ }
483
+ // Record this chunk
484
+ this.recentLogChunks.set(hash, now);
485
+ // Cleanup: remove old entries if map is getting large
486
+ if (this.recentLogChunks.size > PtyWrapper.LOG_DEDUP_MAX_SIZE) {
487
+ const cutoff = now - PtyWrapper.LOG_DEDUP_WINDOW_MS * 2;
488
+ for (const [key, timestamp] of this.recentLogChunks) {
489
+ if (timestamp < cutoff) {
490
+ this.recentLogChunks.delete(key);
491
+ }
492
+ }
493
+ }
494
+ return false; // Not a duplicate
495
+ }
496
+ /**
497
+ * Auto-accept Claude's first-run prompts
498
+ * Handles multiple prompts in sequence:
499
+ * 1. --dangerously-skip-permissions acceptance ("Yes, I accept")
500
+ * 2. Trust directory prompt ("Yes, I trust this folder")
501
+ * 3. "Ready to code here?" permission prompt ("Yes, continue")
502
+ *
503
+ * Uses a Set to track which prompts have been accepted, allowing
504
+ * multiple different prompts to be handled in sequence.
169
505
  */
170
506
  handleAutoAcceptPrompts(data) {
171
- if (this.hasAcceptedPrompt)
172
- return;
173
507
  if (!this.ptyProcess || !this.running)
174
508
  return;
175
- // Check for the permission acceptance prompt
509
+ const cleanData = stripAnsi(data);
510
+ // Check for the permission acceptance prompt (--dangerously-skip-permissions)
176
511
  // Pattern: "2. Yes, I accept" in the output
177
- const cleanData = this.stripAnsi(data);
178
- if (cleanData.includes('Yes, I accept') && cleanData.includes('No, exit')) {
512
+ if (!this.acceptedPrompts.has('permission') &&
513
+ cleanData.includes('Yes, I accept') && cleanData.includes('No, exit')) {
179
514
  console.log(`[pty:${this.config.name}] Detected permission prompt, auto-accepting...`);
180
- this.hasAcceptedPrompt = true;
515
+ this.acceptedPrompts.add('permission');
181
516
  // Send "2" to select "Yes, I accept" and Enter to confirm
182
517
  setTimeout(() => {
183
518
  if (this.ptyProcess && this.running) {
184
519
  this.ptyProcess.write('2');
185
520
  }
186
521
  }, 100);
522
+ return;
523
+ }
524
+ // Check for the trust directory prompt
525
+ // Pattern: "1. Yes, I trust this folder" with "No, exit"
526
+ if (!this.acceptedPrompts.has('trust') &&
527
+ (cleanData.includes('trust this folder') || cleanData.includes('safety check'))
528
+ && cleanData.includes('No, exit')) {
529
+ console.log(`[pty:${this.config.name}] Detected trust directory prompt, auto-accepting...`);
530
+ this.acceptedPrompts.add('trust');
531
+ // Send Enter to accept first option (already selected)
532
+ setTimeout(() => {
533
+ if (this.ptyProcess && this.running) {
534
+ this.ptyProcess.write('\r');
535
+ }
536
+ }, 300);
537
+ return;
538
+ }
539
+ // Check for "Ready to code here?" permission prompt
540
+ // Pattern: "Yes, continue" with "No, exit" and "Ready to code here?"
541
+ // This prompt asks for permission to work with files in the workspace
542
+ if (!this.acceptedPrompts.has('ready-to-code') &&
543
+ cleanData.includes('Yes, continue') && cleanData.includes('No, exit')
544
+ && (cleanData.includes('Ready to code here') || cleanData.includes('permission to work with your files'))) {
545
+ console.log(`[pty:${this.config.name}] Detected "Ready to code here?" prompt, auto-accepting...`);
546
+ this.acceptedPrompts.add('ready-to-code');
547
+ // Send Enter to accept first option (already selected with ❯)
548
+ setTimeout(() => {
549
+ if (this.ptyProcess && this.running) {
550
+ this.ptyProcess.write('\r');
551
+ }
552
+ }, 300);
553
+ return;
187
554
  }
188
555
  }
189
556
  /**
@@ -218,15 +585,27 @@ export class PtyWrapper extends EventEmitter {
218
585
  * Parse relay commands from output.
219
586
  * Handles both single-line and multi-line (fenced) formats.
220
587
  * Deduplication via sentMessageHashes.
588
+ *
589
+ * Optimization: Only parses new content since last parse to avoid O(n²) behavior.
590
+ * Uses lookback buffer for incomplete fenced messages that span output chunks.
221
591
  */
222
592
  parseRelayCommands() {
223
- const cleanContent = this.stripAnsi(this.rawBuffer);
593
+ const cleanContent = stripAnsi(this.rawBuffer);
594
+ // Skip if no new content
595
+ if (cleanContent.length <= this.lastParsedLength)
596
+ return;
597
+ // For fenced messages, need some lookback for incomplete fences that span chunks
598
+ // 500 chars is enough to capture most relay message headers
599
+ const lookbackStart = Math.max(0, this.lastParsedLength - 500);
600
+ const contentToParse = cleanContent.substring(lookbackStart);
224
601
  // First, try to find fenced multi-line messages: ->relay:Target <<<\n...\n>>>
225
- this.parseFencedMessages(cleanContent);
602
+ this.parseFencedMessages(contentToParse);
226
603
  // Then parse single-line messages
227
- this.parseSingleLineMessages(cleanContent);
604
+ this.parseSingleLineMessages(contentToParse);
228
605
  // Parse spawn/release commands
229
- this.parseSpawnReleaseCommands(cleanContent);
606
+ this.parseSpawnReleaseCommands(contentToParse);
607
+ // Update parsed position
608
+ this.lastParsedLength = cleanContent.length;
230
609
  }
231
610
  /**
232
611
  * Parse fenced multi-line messages: ->relay:Target [thread:xxx] <<<\n...\n>>>
@@ -242,6 +621,14 @@ export class PtyWrapper extends EventEmitter {
242
621
  const threadProject = match[2]; // Optional: project part of thread
243
622
  const threadId = match[3]; // Thread ID
244
623
  const startIdx = match.index + match[0].length;
624
+ // Skip spawn/release commands - they are handled by parseSpawnReleaseCommands
625
+ if (/^spawn$/i.test(target) || /^release$/i.test(target)) {
626
+ continue;
627
+ }
628
+ // Skip placeholder targets (documentation examples like "AgentName", "Lead", etc.)
629
+ if (isPlaceholderTarget(target)) {
630
+ continue;
631
+ }
245
632
  // Find the closing >>>
246
633
  const endIdx = content.indexOf('>>>', startIdx);
247
634
  if (endIdx === -1)
@@ -258,6 +645,10 @@ export class PtyWrapper extends EventEmitter {
258
645
  project = target.substring(0, colonIdx);
259
646
  to = target.substring(colonIdx + 1);
260
647
  }
648
+ // Skip placeholder targets after parsing cross-project syntax
649
+ if (isPlaceholderTarget(to)) {
650
+ continue;
651
+ }
261
652
  this.sendRelayCommand({
262
653
  to,
263
654
  kind: 'message',
@@ -283,6 +674,11 @@ export class PtyWrapper extends EventEmitter {
283
674
  const prefixIdx = line.indexOf(this.relayPrefix);
284
675
  if (prefixIdx === -1)
285
676
  continue;
677
+ // Skip spawn/release commands - they are handled by parseSpawnReleaseCommands
678
+ const afterPrefixForCheck = line.substring(prefixIdx + this.relayPrefix.length);
679
+ if (/^spawn\s+/i.test(afterPrefixForCheck) || /^release\s+/i.test(afterPrefixForCheck)) {
680
+ continue;
681
+ }
286
682
  // Extract everything after the prefix
287
683
  const afterPrefix = line.substring(prefixIdx + this.relayPrefix.length);
288
684
  // Pattern: Target [thread:project:id] body or Target [thread:id] body or Target body
@@ -296,6 +692,9 @@ export class PtyWrapper extends EventEmitter {
296
692
  const [, target, body] = simpleMatch;
297
693
  if (!body)
298
694
  continue;
695
+ // Skip placeholder targets (documentation examples)
696
+ if (isPlaceholderTarget(target))
697
+ continue;
299
698
  // Parse target for cross-project syntax
300
699
  const colonIdx = target.indexOf(':');
301
700
  let to = target;
@@ -304,6 +703,9 @@ export class PtyWrapper extends EventEmitter {
304
703
  project = target.substring(0, colonIdx);
305
704
  to = target.substring(colonIdx + 1);
306
705
  }
706
+ // Skip placeholder targets after parsing cross-project syntax
707
+ if (isPlaceholderTarget(to))
708
+ continue;
307
709
  this.sendRelayCommand({
308
710
  to,
309
711
  kind: 'message',
@@ -316,6 +718,9 @@ export class PtyWrapper extends EventEmitter {
316
718
  const [, target, threadProject, threadId, body] = targetMatch;
317
719
  if (!body)
318
720
  continue;
721
+ // Skip placeholder targets (documentation examples)
722
+ if (isPlaceholderTarget(target))
723
+ continue;
319
724
  // Parse target for cross-project syntax
320
725
  const colonIdx = target.indexOf(':');
321
726
  let to = target;
@@ -324,6 +729,9 @@ export class PtyWrapper extends EventEmitter {
324
729
  project = target.substring(0, colonIdx);
325
730
  to = target.substring(colonIdx + 1);
326
731
  }
732
+ // Skip placeholder targets after parsing cross-project syntax
733
+ if (isPlaceholderTarget(to))
734
+ continue;
327
735
  this.sendRelayCommand({
328
736
  to,
329
737
  kind: 'message',
@@ -335,77 +743,204 @@ export class PtyWrapper extends EventEmitter {
335
743
  });
336
744
  }
337
745
  }
338
- /**
339
- * Strip ANSI escape codes from string.
340
- * Converts cursor movements to spaces to preserve visual layout.
341
- */
342
- stripAnsi(str) {
343
- // Convert cursor forward movements to spaces (CSI n C)
344
- // \x1B[nC means move cursor right n columns
345
- // eslint-disable-next-line no-control-regex
346
- str = str.replace(/\x1B\[(\d+)C/g, (_m, n) => ' '.repeat(parseInt(n, 10) || 1));
347
- // Convert single cursor right (CSI C) to space
348
- // eslint-disable-next-line no-control-regex
349
- str = str.replace(/\x1B\[C/g, ' ');
350
- // Remove carriage returns (causes text overwriting issues)
351
- str = str.replace(/\r(?!\n)/g, '');
352
- // Strip remaining ANSI escape sequences (with \x1B prefix)
353
- // eslint-disable-next-line no-control-regex
354
- str = str.replace(/\x1B(?:\[[0-9;?]*[A-Za-z]|\].*?(?:\x07|\x1B\\)|[@-Z\\-_])/g, '');
355
- // Strip orphaned CSI sequences that lost their escape byte
356
- // These look like [?25h, [?2026l, [0m, etc. at the start of content
357
- str = str.replace(/^\s*(\[\??\d*[A-Za-z])+\s*/g, '');
358
- return str;
359
- }
360
746
  /**
361
747
  * Send relay command to daemon
362
748
  */
363
749
  sendRelayCommand(cmd) {
364
750
  const msgHash = `${cmd.to}:${cmd.body}`;
365
751
  if (this.sentMessageHashes.has(msgHash)) {
752
+ console.log(`[pty:${this.config.name}] Skipping duplicate message to ${cmd.to}`);
366
753
  return;
367
754
  }
368
755
  if (this.client.state !== 'READY') {
756
+ console.log(`[pty:${this.config.name}] Cannot send to ${cmd.to} - relay not ready (state: ${this.client.state})`);
369
757
  return;
370
758
  }
371
759
  const success = this.client.sendMessage(cmd.to, cmd.body, cmd.kind, cmd.data, cmd.thread);
760
+ console.log(`[pty:${this.config.name}] Sent message to ${cmd.to}: ${success ? 'success' : 'failed'}`);
372
761
  if (success) {
373
762
  this.sentMessageHashes.add(msgHash);
763
+ // Dispatch message sent hook
764
+ this.hookRegistry.dispatchMessageSent(cmd.to, cmd.body, cmd.thread).catch(err => {
765
+ console.error(`[pty:${this.config.name}] Message sent hook error:`, err);
766
+ });
374
767
  }
375
768
  }
769
+ /** Valid CLI types for spawn commands */
770
+ static VALID_CLI_TYPES = new Set([
771
+ 'claude', 'codex', 'gemini', 'droid', 'aider', 'cursor', 'cline', 'opencode',
772
+ ]);
773
+ /** Validate agent name format (PascalCase, alphanumeric, 2-30 chars) */
774
+ isValidAgentName(name) {
775
+ // Must start with uppercase letter, contain only alphanumeric chars
776
+ // Length 2-30 characters
777
+ return /^[A-Z][a-zA-Z0-9]{1,29}$/.test(name);
778
+ }
779
+ /** Validate CLI type */
780
+ isValidCliType(cli) {
781
+ return PtyWrapper.VALID_CLI_TYPES.has(cli.toLowerCase());
782
+ }
376
783
  /**
377
784
  * Parse spawn/release commands from output
378
785
  * Uses string-based parsing for robustness with PTY output.
786
+ * Supports two formats:
787
+ * Single-line: ->relay:spawn WorkerName cli "task description"
788
+ * Multi-line (fenced): ->relay:spawn WorkerName cli <<<
789
+ * task description here
790
+ * can span multiple lines>>>
379
791
  * Delegates to dashboard API if dashboardPort is set (for nested spawns).
792
+ *
793
+ * STRICT VALIDATION:
794
+ * - Command must be at start of line (after whitespace)
795
+ * - Agent name must be PascalCase (e.g., Backend, Frontend, Worker1)
796
+ * - CLI must be a known type (claude, codex, gemini, etc.)
380
797
  */
381
798
  parseSpawnReleaseCommands(content) {
382
799
  // Need either API port or callbacks to handle spawn/release
383
- const canSpawn = this.config.dashboardPort || this.config.onSpawn;
800
+ // Also check allowSpawn config - spawned workers should not spawn other agents
801
+ const spawnAllowed = this.config.allowSpawn !== false;
802
+ const canSpawn = spawnAllowed && (this.config.dashboardPort || this.config.onSpawn);
384
803
  const canRelease = this.config.dashboardPort || this.config.onRelease;
804
+ // Debug: always log spawn detection for debugging
805
+ if (content.includes('->relay:spawn')) {
806
+ console.log(`[pty:${this.config.name}] [SPAWN-DEBUG] Spawn pattern detected in content`);
807
+ console.log(`[pty:${this.config.name}] [SPAWN-DEBUG] canSpawn=${canSpawn} (allowSpawn=${spawnAllowed}, dashboardPort=${this.config.dashboardPort}, hasOnSpawn=${!!this.config.onSpawn})`);
808
+ // Log the actual lines containing spawn
809
+ const spawnLines = content.split('\n').filter(l => l.includes('->relay:spawn'));
810
+ spawnLines.forEach((line, i) => {
811
+ console.log(`[pty:${this.config.name}] [SPAWN-DEBUG] Line ${i}: "${line.substring(0, 100)}"`);
812
+ });
813
+ }
814
+ // Debug: always log release detection for debugging
815
+ if (content.includes('->relay:release')) {
816
+ console.log(`[pty:${this.config.name}] [RELEASE-DEBUG] Release pattern detected in content`);
817
+ console.log(`[pty:${this.config.name}] [RELEASE-DEBUG] canRelease=${canRelease} (dashboardPort=${this.config.dashboardPort}, hasOnRelease=${!!this.config.onRelease})`);
818
+ }
385
819
  if (!canSpawn && !canRelease)
386
820
  return;
387
821
  const lines = content.split('\n');
388
822
  const spawnPrefix = '->relay:spawn';
389
823
  const releasePrefix = '->relay:release';
390
824
  for (const line of lines) {
391
- // Check for spawn command
392
- const spawnIdx = line.indexOf(spawnPrefix);
393
- if (spawnIdx !== -1 && canSpawn) {
394
- const afterSpawn = line.substring(spawnIdx + spawnPrefix.length).trim();
395
- // Parse: WorkerName cli OR WorkerName cli "task" (task is optional)
825
+ let trimmed = line.trim();
826
+ // Strip bullet/prompt prefixes but PRESERVE the ->relay: pattern
827
+ // Look for ->relay: in the line and only strip what comes before it
828
+ const relayIdx = trimmed.indexOf('->relay:');
829
+ if (relayIdx > 0) {
830
+ // There's content before ->relay: - check if it's just prefix chars
831
+ const beforeRelay = trimmed.substring(0, relayIdx);
832
+ // Only strip if the prefix is just bullets/prompts/whitespace
833
+ if (/^[\s●•◦‣⁃⏺◆◇○□■│┃┆┇┊┋╎╏✦→➜›»$%#*]+$/.test(beforeRelay)) {
834
+ const originalTrimmed = trimmed;
835
+ trimmed = trimmed.substring(relayIdx);
836
+ console.log(`[pty:${this.config.name}] [SPAWN-DEBUG] Stripped prefix: "${originalTrimmed.substring(0, 60)}" -> "${trimmed.substring(0, 60)}"`);
837
+ }
838
+ }
839
+ // Skip escaped commands: \->relay:spawn should not trigger
840
+ if (trimmed.includes('\\->relay:')) {
841
+ continue;
842
+ }
843
+ // If we're in fenced spawn mode, accumulate lines until we see >>>
844
+ if (this.pendingFencedSpawn) {
845
+ const closeIdx = trimmed.indexOf('>>>');
846
+ if (closeIdx !== -1) {
847
+ // Add content before >>> to task
848
+ const contentBeforeClose = trimmed.substring(0, closeIdx);
849
+ if (contentBeforeClose) {
850
+ this.pendingFencedSpawn.taskLines.push(contentBeforeClose);
851
+ }
852
+ // Execute the spawn with accumulated task
853
+ const { name, cli, taskLines } = this.pendingFencedSpawn;
854
+ const taskStr = taskLines.join('\n').trim();
855
+ const spawnKey = `${name}:${cli}`;
856
+ if (!this.processedSpawnCommands.has(spawnKey)) {
857
+ this.processedSpawnCommands.add(spawnKey);
858
+ console.log(`[pty:${this.config.name}] Spawn command (fenced): ${name} (${cli}) - "${taskStr.substring(0, 50)}..."`);
859
+ this.executeSpawn(name, cli, taskStr);
860
+ }
861
+ this.pendingFencedSpawn = null;
862
+ }
863
+ else {
864
+ // Accumulate line as part of task
865
+ this.pendingFencedSpawn.taskLines.push(line);
866
+ }
867
+ continue;
868
+ }
869
+ // Check for fenced spawn start: ->relay:spawn Name [cli] <<<
870
+ // STRICT: Must be at start of line (after whitespace)
871
+ if (canSpawn && trimmed.startsWith(spawnPrefix)) {
872
+ const afterSpawn = trimmed.substring(spawnPrefix.length).trim();
873
+ console.log(`[pty:${this.config.name}] [SPAWN-DEBUG] Detected spawn prefix, afterSpawn: "${afterSpawn.substring(0, 60)}"`);
874
+ // Check for fenced format: Name [cli] <<< (CLI optional, defaults to 'claude')
875
+ const fencedMatch = afterSpawn.match(/^(\S+)(?:\s+(\S+))?\s+<<<(.*)$/);
876
+ console.log(`[pty:${this.config.name}] [SPAWN-DEBUG] Fenced match result: ${fencedMatch ? 'MATCHED' : 'NO MATCH'}`);
877
+ if (fencedMatch) {
878
+ const [, name, cliOrUndefined, inlineContent] = fencedMatch;
879
+ let cli = cliOrUndefined || 'claude';
880
+ // STRICT: Validate agent name (PascalCase) and CLI type
881
+ if (!this.isValidAgentName(name)) {
882
+ console.warn(`[pty:${this.config.name}] Invalid agent name format, skipping: name=${name} (must be PascalCase)`);
883
+ continue;
884
+ }
885
+ if (!this.isValidCliType(cli)) {
886
+ console.warn(`[pty:${this.config.name}] Unknown CLI type, using default: cli=${cli}`);
887
+ cli = 'claude';
888
+ }
889
+ // Check if fence closes on same line
890
+ const inlineCloseIdx = inlineContent.indexOf('>>>');
891
+ if (inlineCloseIdx !== -1) {
892
+ // Single line fenced: extract task between <<< and >>>
893
+ const taskStr = inlineContent.substring(0, inlineCloseIdx).trim();
894
+ const spawnKey = `${name}:${cli}`;
895
+ if (!this.processedSpawnCommands.has(spawnKey)) {
896
+ this.processedSpawnCommands.add(spawnKey);
897
+ console.log(`[pty:${this.config.name}] Spawn command (fenced): ${name} (${cli}) - "${taskStr.substring(0, 50)}..."`);
898
+ this.executeSpawn(name, cli, taskStr);
899
+ }
900
+ }
901
+ else {
902
+ // Start multi-line fenced mode - but only if not already processed
903
+ const spawnKey = `${name}:${cli}`;
904
+ if (this.processedSpawnCommands.has(spawnKey)) {
905
+ // Already processed this spawn, skip the fenced capture
906
+ continue;
907
+ }
908
+ this.pendingFencedSpawn = {
909
+ name,
910
+ cli,
911
+ taskLines: inlineContent.trim() ? [inlineContent.trim()] : [],
912
+ };
913
+ console.log(`[pty:${this.config.name}] Starting fenced spawn capture: ${name} (${cli})`);
914
+ }
915
+ continue;
916
+ }
917
+ // Parse single-line format: WorkerName [cli] [task]
918
+ // CLI defaults to 'claude' if not provided
396
919
  const parts = afterSpawn.split(/\s+/);
397
- if (parts.length >= 2) {
920
+ if (parts.length >= 1) {
398
921
  const name = parts[0];
399
- const cli = parts[1];
400
- // Task is everything after cli, potentially in quotes (optional)
922
+ // CLI is optional - defaults to 'claude'
923
+ let cli = parts[1] || 'claude';
924
+ // Task is everything after cli (if cli was provided) or after name (if cli was omitted)
401
925
  let task = '';
402
- if (parts.length >= 3) {
403
- const taskPart = parts.slice(2).join(' ');
926
+ const taskStartIndex = parts[1] ? 2 : 1;
927
+ if (parts.length > taskStartIndex) {
928
+ const taskPart = parts.slice(taskStartIndex).join(' ');
404
929
  // Remove surrounding quotes if present
405
930
  const quoteMatch = taskPart.match(/^["'](.*)["']$/);
406
931
  task = quoteMatch ? quoteMatch[1] : taskPart;
407
932
  }
408
- if (name && cli) {
933
+ if (name) {
934
+ // STRICT: Validate agent name (PascalCase) and CLI type
935
+ if (!this.isValidAgentName(name)) {
936
+ // Don't log warning for documentation text - just silently skip
937
+ continue;
938
+ }
939
+ if (!this.isValidCliType(cli)) {
940
+ // Default CLI 'claude' should always be valid, but validate anyway
941
+ console.warn(`[pty:${this.config.name}] Unknown CLI type, using default: cli=${cli}, defaulting to 'claude'`);
942
+ cli = 'claude';
943
+ }
409
944
  const spawnKey = `${name}:${cli}`;
410
945
  if (!this.processedSpawnCommands.has(spawnKey)) {
411
946
  this.processedSpawnCommands.add(spawnKey);
@@ -416,13 +951,18 @@ export class PtyWrapper extends EventEmitter {
416
951
  continue;
417
952
  }
418
953
  // Check for release command
419
- const releaseIdx = line.indexOf(releasePrefix);
420
- if (releaseIdx !== -1 && canRelease) {
421
- const afterRelease = line.substring(releaseIdx + releasePrefix.length).trim();
422
- const name = afterRelease.split(/\s+/)[0];
423
- if (name && !this.processedReleaseCommands.has(name)) {
424
- this.processedReleaseCommands.add(name);
425
- this.executeRelease(name);
954
+ // STRICT: Must be at start of line (after whitespace)
955
+ if (trimmed.startsWith(releasePrefix)) {
956
+ console.log(`[pty:${this.config.name}] [RELEASE-DEBUG] Release prefix detected, canRelease=${canRelease}`);
957
+ if (canRelease) {
958
+ const afterRelease = trimmed.substring(releasePrefix.length).trim();
959
+ const name = afterRelease.split(/\s+/)[0];
960
+ console.log(`[pty:${this.config.name}] [RELEASE-DEBUG] Parsed name: ${name}, isValidName=${name ? this.isValidAgentName(name) : false}, alreadyProcessed=${this.processedReleaseCommands.has(name)}`);
961
+ // STRICT: Validate agent name format
962
+ if (name && this.isValidAgentName(name) && !this.processedReleaseCommands.has(name)) {
963
+ this.processedReleaseCommands.add(name);
964
+ this.executeRelease(name);
965
+ }
426
966
  }
427
967
  }
428
968
  }
@@ -431,6 +971,8 @@ export class PtyWrapper extends EventEmitter {
431
971
  * Execute spawn via API or callback
432
972
  */
433
973
  async executeSpawn(name, cli, task) {
974
+ console.log(`[pty:${this.config.name}] [SPAWN-DEBUG] executeSpawn called: name=${name}, cli=${cli}, task="${task.substring(0, 50)}..."`);
975
+ console.log(`[pty:${this.config.name}] [SPAWN-DEBUG] dashboardPort=${this.config.dashboardPort}, hasOnSpawn=${!!this.config.onSpawn}`);
434
976
  if (this.config.dashboardPort) {
435
977
  // Use dashboard API for spawning (works from spawned agents)
436
978
  try {
@@ -468,7 +1010,7 @@ export class PtyWrapper extends EventEmitter {
468
1010
  if (this.config.dashboardPort) {
469
1011
  // Use dashboard API for releasing
470
1012
  try {
471
- const response = await fetch(`http://localhost:${this.config.dashboardPort}/api/spawned/${name}`, {
1013
+ const response = await fetch(`http://localhost:${this.config.dashboardPort}/api/spawned/${encodeURIComponent(name)}`, {
472
1014
  method: 'DELETE',
473
1015
  });
474
1016
  const result = await response.json();
@@ -498,11 +1040,67 @@ export class PtyWrapper extends EventEmitter {
498
1040
  * @param originalTo - The original 'to' field from sender. '*' indicates this was a broadcast message.
499
1041
  */
500
1042
  handleIncomingMessage(from, payload, messageId, meta, originalTo) {
1043
+ // Deduplicate: skip if we've already received this message
1044
+ if (this.receivedMessageIds.has(messageId)) {
1045
+ console.log(`[pty:${this.config.name}] Skipping duplicate message: ${messageId.substring(0, 8)}`);
1046
+ return;
1047
+ }
1048
+ this.receivedMessageIds.add(messageId);
1049
+ // Limit dedup set size to prevent memory leak
1050
+ if (this.receivedMessageIds.size > 1000) {
1051
+ const oldest = this.receivedMessageIds.values().next().value;
1052
+ if (oldest)
1053
+ this.receivedMessageIds.delete(oldest);
1054
+ }
501
1055
  this.messageQueue.push({ from, body: payload.body, messageId, thread: payload.thread, importance: meta?.importance, data: payload.data, originalTo });
502
1056
  this.processMessageQueue();
1057
+ // Dispatch message received hook
1058
+ this.hookRegistry.dispatchMessageReceived(from, payload.body, messageId).catch(err => {
1059
+ console.error(`[pty:${this.config.name}] Message received hook error:`, err);
1060
+ });
1061
+ }
1062
+ /**
1063
+ * Wait for output to stabilize before injection.
1064
+ * Returns true if output has been stable for the required duration.
1065
+ */
1066
+ async waitForOutputStable() {
1067
+ const startTime = Date.now();
1068
+ let stablePolls = 0;
1069
+ let lastBufferLength = this.rawBuffer.length;
1070
+ while (Date.now() - startTime < INJECTION_CONSTANTS.STABILITY_TIMEOUT_MS) {
1071
+ await sleep(INJECTION_CONSTANTS.STABILITY_POLL_MS);
1072
+ const timeSinceOutput = Date.now() - this.lastOutputTime;
1073
+ const bufferUnchanged = this.rawBuffer.length === lastBufferLength;
1074
+ // Consider stable if no output for at least one poll interval
1075
+ if (timeSinceOutput >= INJECTION_CONSTANTS.STABILITY_POLL_MS && bufferUnchanged) {
1076
+ stablePolls++;
1077
+ if (stablePolls >= INJECTION_CONSTANTS.REQUIRED_STABLE_POLLS) {
1078
+ return true;
1079
+ }
1080
+ }
1081
+ else {
1082
+ stablePolls = 0;
1083
+ lastBufferLength = this.rawBuffer.length;
1084
+ }
1085
+ }
1086
+ // Timeout - return true anyway to avoid blocking forever
1087
+ console.warn(`[pty:${this.config.name}] Stability timeout, proceeding with injection`);
1088
+ return true;
1089
+ }
1090
+ /**
1091
+ * Check if the agent process is still alive and responsive.
1092
+ */
1093
+ isAgentAlive() {
1094
+ return this.running && this.ptyProcess !== undefined;
503
1095
  }
504
1096
  /**
505
- * Process queued messages
1097
+ * Process queued messages with reliability improvements:
1098
+ * 1. Wait for output stability before injection
1099
+ * 2. Verify injection appeared in output
1100
+ * 3. Retry with backoff on failure
1101
+ * 4. Fall back to logging on complete failure
1102
+ *
1103
+ * Uses shared injection logic with PTY-specific callbacks.
506
1104
  */
507
1105
  async processMessageQueue() {
508
1106
  // Wait until instructions have been injected and agent is ready
@@ -510,8 +1108,11 @@ export class PtyWrapper extends EventEmitter {
510
1108
  return;
511
1109
  if (this.isInjecting || this.messageQueue.length === 0)
512
1110
  return;
513
- if (!this.ptyProcess || !this.running)
1111
+ // Health check: is agent still alive?
1112
+ if (!this.isAgentAlive()) {
1113
+ console.error(`[pty:${this.config.name}] Agent not alive, cannot inject messages`);
514
1114
  return;
1115
+ }
515
1116
  this.isInjecting = true;
516
1117
  const msg = this.messageQueue.shift();
517
1118
  if (!msg) {
@@ -519,31 +1120,68 @@ export class PtyWrapper extends EventEmitter {
519
1120
  return;
520
1121
  }
521
1122
  try {
522
- const shortId = msg.messageId.substring(0, 8);
523
- // Strip ANSI escape sequences and orphaned control sequences from message body
524
- const sanitizedBody = this.stripAnsi(msg.body).replace(/[\r\n]+/g, ' ').trim();
525
- // Thread/importance/channel hints to match tmux-wrapper format
526
- const threadHint = msg.thread ? ` [thread:${msg.thread}]` : '';
527
- const importanceHint = msg.importance !== undefined && msg.importance > 75 ? ' [!!]' :
528
- msg.importance !== undefined && msg.importance > 50 ? ' [!]' : '';
529
- // Channel indicator: [#general] for broadcasts - tells agent to reply to * not sender
530
- const channelHint = msg.originalTo === '*' ? ' [#general]' : '';
531
- // Extract attachment file paths if present
532
- let attachmentHint = '';
533
- if (msg.data?.attachments && Array.isArray(msg.data.attachments)) {
534
- const filePaths = msg.data.attachments
535
- .map((att) => att.filePath)
536
- .filter((p) => typeof p === 'string');
537
- if (filePaths.length > 0) {
538
- attachmentHint = ` [Attachments: ${filePaths.join(', ')}]`;
1123
+ // Wait for output to stabilize before injecting
1124
+ await this.waitForOutputStable();
1125
+ // For Gemini: check if at shell prompt, skip injection to avoid shell execution
1126
+ if (this.cliType === 'gemini') {
1127
+ const recentOutput = this.rawBuffer.slice(-200);
1128
+ const lastLine = recentOutput.split('\n').filter(l => l.trim()).pop() || '';
1129
+ if (CLI_QUIRKS.isShellPrompt(lastLine)) {
1130
+ console.log(`[pty:${this.config.name}] Gemini at shell prompt, re-queuing message`);
1131
+ this.messageQueue.unshift(msg);
1132
+ this.isInjecting = false;
1133
+ setTimeout(() => this.processMessageQueue(), 2000);
1134
+ return;
1135
+ }
1136
+ }
1137
+ // Build injection string using shared utility
1138
+ let injection = buildInjectionString(msg);
1139
+ // Gemini-specific: wrap in backticks to prevent shell keyword interpretation
1140
+ if (this.cliType === 'gemini') {
1141
+ // Extract the message body part and wrap it
1142
+ const colonIdx = injection.indexOf(': ');
1143
+ if (colonIdx > 0) {
1144
+ const prefix = injection.substring(0, colonIdx + 2);
1145
+ const body = injection.substring(colonIdx + 2);
1146
+ injection = prefix + CLI_QUIRKS.wrapForGemini(body);
539
1147
  }
540
1148
  }
541
- const injection = `Relay message from ${msg.from} [${shortId}]${threadHint}${importanceHint}${channelHint}${attachmentHint}: ${sanitizedBody}`;
542
- // Write message to PTY, then send Enter separately after a small delay
543
- // This matches how TmuxWrapper does it for better CLI compatibility
544
- this.ptyProcess.write(injection);
545
- await this.sleep(50);
546
- this.ptyProcess.write('\r');
1149
+ const shortId = msg.messageId.substring(0, 8);
1150
+ // Create callbacks for shared injection logic
1151
+ const callbacks = {
1152
+ getOutput: async () => {
1153
+ // Look at last 2000 chars to avoid scanning entire buffer
1154
+ return this.rawBuffer.slice(-2000);
1155
+ },
1156
+ performInjection: async (inj) => {
1157
+ if (!this.ptyProcess || !this.running) {
1158
+ throw new Error('PTY process not running');
1159
+ }
1160
+ // Write message to PTY, then send Enter separately after a small delay
1161
+ this.ptyProcess.write(inj);
1162
+ await sleep(INJECTION_CONSTANTS.ENTER_DELAY_MS);
1163
+ this.ptyProcess.write('\r');
1164
+ },
1165
+ log: (message) => console.log(`[pty:${this.config.name}] ${message}`),
1166
+ logError: (message) => console.error(`[pty:${this.config.name}] ${message}`),
1167
+ getMetrics: () => this.injectionMetrics,
1168
+ // Skip verification for PTY-based injection - CLIs don't echo input back
1169
+ // so verification will always fail. Trust that pty.write() succeeds.
1170
+ skipVerification: true,
1171
+ };
1172
+ // Inject with retry and verification using shared logic
1173
+ const result = await sharedInjectWithRetry(injection, shortId, msg.from, callbacks);
1174
+ if (!result.success) {
1175
+ // Log the failed message for debugging/recovery
1176
+ console.error(`[pty:${this.config.name}] Message delivery failed after ${result.attempts} attempts: ` +
1177
+ `from=${msg.from} id=${shortId}`);
1178
+ // Emit event for external monitoring (e.g., dashboard)
1179
+ this.emit('injection-failed', {
1180
+ messageId: msg.messageId,
1181
+ from: msg.from,
1182
+ attempts: result.attempts,
1183
+ });
1184
+ }
547
1185
  }
548
1186
  catch (err) {
549
1187
  console.error(`[pty:${this.config.name}] Injection failed: ${err.message}`);
@@ -552,24 +1190,34 @@ export class PtyWrapper extends EventEmitter {
552
1190
  this.isInjecting = false;
553
1191
  // Process next message if any
554
1192
  if (this.messageQueue.length > 0) {
555
- setTimeout(() => this.processMessageQueue(), 500);
1193
+ setTimeout(() => this.processMessageQueue(), INJECTION_CONSTANTS.QUEUE_PROCESS_DELAY_MS);
556
1194
  }
557
1195
  }
558
1196
  }
559
1197
  /**
560
- * Inject usage instructions
1198
+ * Queue minimal agent identity notification as the first message.
1199
+ *
1200
+ * Full protocol instructions are in ~/.claude/CLAUDE.md (set up by entrypoint.sh).
1201
+ * We only inject a brief identity message here to let the agent know its name
1202
+ * and that it's connected to the relay.
561
1203
  */
562
1204
  injectInstructions() {
563
- if (!this.running || !this.ptyProcess)
1205
+ if (!this.running)
1206
+ return;
1207
+ // Guard: Only inject once per session
1208
+ if (this.instructionsInjected) {
1209
+ console.log(`[pty:${this.config.name}] Init instructions already injected, skipping`);
564
1210
  return;
565
- const escapedPrefix = '\\' + this.relayPrefix;
566
- const instructions = `[Agent Relay] You are "${this.config.name}" - connected for real-time messaging. SEND: ${escapedPrefix}AgentName message. PROTOCOL: (1) Wait for task via relay. (2) ACK receipt before starting. (3) Send "DONE: <summary>" when complete, then wait for next task.`;
567
- try {
568
- this.ptyProcess.write(instructions + '\r');
569
- }
570
- catch {
571
- // Silent fail
572
1211
  }
1212
+ this.instructionsInjected = true;
1213
+ // Minimal notification - full protocol is in ~/.claude/CLAUDE.md
1214
+ const notification = `You are agent "${this.config.name}" connected to Agent Relay. See CLAUDE.md for the messaging protocol. ACK messages, do work, send DONE when complete.`;
1215
+ // Queue as first message from "system" - will be injected when CLI is ready
1216
+ this.messageQueue.unshift({
1217
+ from: 'system',
1218
+ body: notification,
1219
+ messageId: `init-${Date.now()}`,
1220
+ });
573
1221
  }
574
1222
  /**
575
1223
  * Write directly to the PTY
@@ -597,38 +1245,73 @@ export class PtyWrapper extends EventEmitter {
597
1245
  /**
598
1246
  * Stop the agent process
599
1247
  */
600
- stop() {
1248
+ async stop() {
601
1249
  if (!this.running)
602
1250
  return;
603
1251
  this.running = false;
1252
+ // Auto-save continuity state before stopping
1253
+ // Pass sessionEndData to populate handoff (fixes empty handoff issue)
1254
+ if (this.continuity) {
1255
+ try {
1256
+ await this.continuity.autoSave(this.config.name, 'session_end', this.sessionEndData);
1257
+ }
1258
+ catch (err) {
1259
+ console.error(`[pty:${this.config.name}] Continuity auto-save failed:`, err);
1260
+ }
1261
+ }
1262
+ // Dispatch session end hook (handles trajectory completion)
1263
+ try {
1264
+ await this.hookRegistry.dispatchSessionEnd(0, true);
1265
+ }
1266
+ catch (err) {
1267
+ console.error(`[pty:${this.config.name}] Session end hook error:`, err);
1268
+ }
604
1269
  if (this.ptyProcess) {
605
1270
  // Try graceful termination first
606
1271
  this.ptyProcess.write('\x03'); // Ctrl+C
607
- setTimeout(() => {
608
- if (this.ptyProcess) {
609
- this.ptyProcess.kill();
610
- }
611
- }, 1000);
1272
+ await sleep(1000);
1273
+ if (this.ptyProcess) {
1274
+ this.ptyProcess.kill();
1275
+ }
612
1276
  }
613
1277
  this.closeLogStream();
614
1278
  this.client.destroy();
1279
+ this.hookRegistry.destroy();
615
1280
  }
616
1281
  /**
617
1282
  * Kill the process immediately
618
1283
  */
619
- kill() {
1284
+ async kill() {
620
1285
  this.running = false;
1286
+ // Auto-save continuity state before killing (with timeout to avoid hanging)
1287
+ // Pass sessionEndData if available (may have been parsed before kill)
1288
+ if (this.continuity) {
1289
+ try {
1290
+ await Promise.race([
1291
+ this.continuity.autoSave(this.config.name, 'crash', this.sessionEndData),
1292
+ sleep(2000), // 2s timeout for crash saves
1293
+ ]);
1294
+ }
1295
+ catch (err) {
1296
+ console.error(`[pty:${this.config.name}] Continuity auto-save failed:`, err);
1297
+ }
1298
+ }
1299
+ // Dispatch session end hook (forced termination, with timeout)
1300
+ try {
1301
+ await Promise.race([
1302
+ this.hookRegistry.dispatchSessionEnd(undefined, false),
1303
+ sleep(1000), // 1s timeout for hooks on kill
1304
+ ]);
1305
+ }
1306
+ catch (err) {
1307
+ console.error(`[pty:${this.config.name}] Session end hook error:`, err);
1308
+ }
621
1309
  if (this.ptyProcess) {
622
1310
  this.ptyProcess.kill();
623
1311
  }
624
1312
  this.closeLogStream();
625
1313
  this.client.destroy();
626
- }
627
- /**
628
- * Sleep helper
629
- */
630
- sleep(ms) {
631
- return new Promise((resolve) => setTimeout(resolve, ms));
1314
+ this.hookRegistry.destroy();
632
1315
  }
633
1316
  /**
634
1317
  * Close the log file stream
@@ -652,5 +1335,159 @@ export class PtyWrapper extends EventEmitter {
652
1335
  get logPath() {
653
1336
  return this.logFilePath;
654
1337
  }
1338
+ /**
1339
+ * Track significant outputs and inject summary reminder if needed.
1340
+ * Works with any CLI (Claude, Gemini, Codex, etc.)
1341
+ */
1342
+ trackOutputAndRemind(data) {
1343
+ // Disabled if config.summaryReminder === false or env RELAY_SUMMARY_REMINDER_ENABLED=false
1344
+ if (this.config.summaryReminder === false)
1345
+ return;
1346
+ if (process.env.RELAY_SUMMARY_REMINDER_ENABLED === 'false')
1347
+ return;
1348
+ const config = this.config.summaryReminder ?? {};
1349
+ // Env vars take precedence over config, config takes precedence over defaults
1350
+ const intervalMinutes = process.env.RELAY_SUMMARY_INTERVAL_MINUTES
1351
+ ? parseInt(process.env.RELAY_SUMMARY_INTERVAL_MINUTES, 10)
1352
+ : (config.intervalMinutes ?? 15);
1353
+ const minOutputs = process.env.RELAY_SUMMARY_MIN_OUTPUTS
1354
+ ? parseInt(process.env.RELAY_SUMMARY_MIN_OUTPUTS, 10)
1355
+ : (config.minOutputs ?? 50);
1356
+ // Only count "significant" outputs (more than just whitespace/control chars)
1357
+ const cleanData = stripAnsi(data).trim();
1358
+ if (cleanData.length > 20) {
1359
+ this.outputsSinceSummary++;
1360
+ }
1361
+ // Check if we should remind
1362
+ const minutesSinceSummary = (Date.now() - this.lastSummaryTime) / (1000 * 60);
1363
+ const shouldRemind = minutesSinceSummary >= intervalMinutes &&
1364
+ this.outputsSinceSummary >= minOutputs;
1365
+ if (shouldRemind && this.running && this.ptyProcess) {
1366
+ // Reset counters before injecting (prevent spam)
1367
+ this.lastSummaryTime = Date.now();
1368
+ this.outputsSinceSummary = 0;
1369
+ // Inject reminder as a relay-style message
1370
+ // IMPORTANT: Must be single-line - embedded newlines cause the message to span
1371
+ // multiple lines in the CLI input buffer, and the final Enter only submits
1372
+ // the last (empty) line. Regular relay messages are also single-line (see buildInjectionString).
1373
+ const reminder = `[Agent Relay] It's been ${Math.round(minutesSinceSummary)} minutes. Please output a [[SUMMARY]] block to checkpoint your progress: [[SUMMARY]]{"currentTask": "...", "completedTasks": [...], "context": "..."}[[/SUMMARY]]`;
1374
+ // Delay slightly to not interrupt current output, then write + Enter
1375
+ setTimeout(async () => {
1376
+ if (this.ptyProcess && this.running) {
1377
+ this.ptyProcess.write(reminder);
1378
+ await sleep(INJECTION_CONSTANTS.ENTER_DELAY_MS);
1379
+ this.ptyProcess.write('\r');
1380
+ }
1381
+ }, 1000);
1382
+ }
1383
+ }
1384
+ /**
1385
+ * Check for [[SUMMARY]] blocks and emit 'summary' event.
1386
+ * Allows cloud services to persist summaries without hardcoding storage.
1387
+ * Also updates the local continuity ledger for session recovery.
1388
+ */
1389
+ checkForSummaryAndEmit(content) {
1390
+ const result = parseSummaryWithDetails(content);
1391
+ // No SUMMARY block found
1392
+ if (!result.found)
1393
+ return;
1394
+ // Dedup based on raw content - prevents repeated event emissions for same summary
1395
+ if (result.rawContent === this.lastSummaryRawContent)
1396
+ return;
1397
+ this.lastSummaryRawContent = result.rawContent || '';
1398
+ // Reset reminder counters on any summary (even invalid JSON)
1399
+ this.lastSummaryTime = Date.now();
1400
+ this.outputsSinceSummary = 0;
1401
+ // Invalid JSON - log warning
1402
+ if (!result.valid) {
1403
+ console.warn(`[pty:${this.config.name}] Invalid JSON in SUMMARY block`);
1404
+ return;
1405
+ }
1406
+ const summary = result.summary;
1407
+ // Save to local continuity ledger for session recovery
1408
+ // This ensures the ledger has actual data instead of placeholders
1409
+ if (this.continuity) {
1410
+ this.saveSummaryToLedger(summary).catch(err => {
1411
+ console.error(`[pty:${this.config.name}] Failed to save summary to ledger:`, err);
1412
+ });
1413
+ }
1414
+ // Emit event for external handlers (cloud services, dashboard, etc.)
1415
+ this.emit('summary', {
1416
+ agentName: this.config.name,
1417
+ summary,
1418
+ });
1419
+ }
1420
+ /**
1421
+ * Save a parsed summary to the continuity ledger.
1422
+ * Maps summary fields to ledger fields for session recovery.
1423
+ */
1424
+ async saveSummaryToLedger(summary) {
1425
+ if (!this.continuity)
1426
+ return;
1427
+ const updates = {};
1428
+ // Map summary fields to ledger fields
1429
+ if (summary.currentTask) {
1430
+ updates.currentTask = summary.currentTask;
1431
+ }
1432
+ if (summary.completedTasks && summary.completedTasks.length > 0) {
1433
+ updates.completed = summary.completedTasks;
1434
+ }
1435
+ if (summary.context) {
1436
+ // Store context in inProgress as "next steps" hint
1437
+ updates.inProgress = [summary.context];
1438
+ }
1439
+ if (summary.files && summary.files.length > 0) {
1440
+ updates.fileContext = summary.files.map((f) => ({ path: f }));
1441
+ }
1442
+ // Only save if we have meaningful updates
1443
+ if (Object.keys(updates).length > 0) {
1444
+ await this.continuity.saveLedger(this.config.name, updates);
1445
+ console.log(`[pty:${this.config.name}] Saved summary to continuity ledger`);
1446
+ }
1447
+ }
1448
+ /**
1449
+ * Check for [[SESSION_END]] blocks and emit 'session-end' event.
1450
+ * Allows cloud services to handle session closure without hardcoding storage.
1451
+ * Also stores the data for use in autoSave to populate handoff.
1452
+ */
1453
+ checkForSessionEndAndEmit(content) {
1454
+ if (this.sessionEndProcessed)
1455
+ return; // Only emit once per session
1456
+ const sessionEnd = parseSessionEndFromOutput(content);
1457
+ if (!sessionEnd)
1458
+ return;
1459
+ this.sessionEndProcessed = true;
1460
+ // Store SESSION_END data for use in autoSave (fixes empty handoff issue)
1461
+ this.sessionEndData = sessionEnd;
1462
+ // Emit event for external handlers
1463
+ this.emit('session-end', {
1464
+ agentName: this.config.name,
1465
+ marker: sessionEnd,
1466
+ });
1467
+ }
1468
+ /**
1469
+ * Reset session-specific state for wrapper reuse.
1470
+ * Call this when starting a new session with the same wrapper instance.
1471
+ */
1472
+ resetSessionState() {
1473
+ this.sessionEndProcessed = false;
1474
+ this.lastSummaryRawContent = '';
1475
+ this.sessionEndData = undefined;
1476
+ }
1477
+ /**
1478
+ * Get injection reliability metrics
1479
+ */
1480
+ getInjectionMetrics() {
1481
+ return {
1482
+ ...this.injectionMetrics,
1483
+ successRate: calculateSuccessRate(this.injectionMetrics),
1484
+ };
1485
+ }
1486
+ /**
1487
+ * Get count of pending messages in queue
1488
+ */
1489
+ get pendingMessageCount() {
1490
+ return this.messageQueue.length;
1491
+ }
655
1492
  }
656
1493
  //# sourceMappingURL=pty-wrapper.js.map