agent-relay 1.0.22 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (613) hide show
  1. package/README.md +1 -1
  2. package/dist/bridge/shadow-cli.d.ts +17 -0
  3. package/dist/bridge/shadow-cli.d.ts.map +1 -0
  4. package/dist/bridge/shadow-cli.js +75 -0
  5. package/dist/bridge/shadow-cli.js.map +1 -0
  6. package/dist/bridge/shadow-config.d.ts +87 -0
  7. package/dist/bridge/shadow-config.d.ts.map +1 -0
  8. package/dist/bridge/shadow-config.js +134 -0
  9. package/dist/bridge/shadow-config.js.map +1 -0
  10. package/dist/bridge/spawner.d.ts +68 -1
  11. package/dist/bridge/spawner.d.ts.map +1 -1
  12. package/dist/bridge/spawner.js +360 -16
  13. package/dist/bridge/spawner.js.map +1 -1
  14. package/dist/bridge/types.d.ts +67 -0
  15. package/dist/bridge/types.d.ts.map +1 -1
  16. package/dist/cli/index.js +1196 -15
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/cloud/api/auth.d.ts +20 -0
  19. package/dist/cloud/api/auth.d.ts.map +1 -0
  20. package/dist/cloud/api/auth.js +128 -0
  21. package/dist/cloud/api/auth.js.map +1 -0
  22. package/dist/cloud/api/billing.d.ts +17 -0
  23. package/dist/cloud/api/billing.d.ts.map +1 -0
  24. package/dist/cloud/api/billing.js +353 -0
  25. package/dist/cloud/api/billing.js.map +1 -0
  26. package/dist/cloud/api/cli-pty-runner.d.ts +54 -0
  27. package/dist/cloud/api/cli-pty-runner.d.ts.map +1 -0
  28. package/dist/cloud/api/cli-pty-runner.js +119 -0
  29. package/dist/cloud/api/cli-pty-runner.js.map +1 -0
  30. package/dist/cloud/api/coordinators.d.ts +8 -0
  31. package/dist/cloud/api/coordinators.d.ts.map +1 -0
  32. package/dist/cloud/api/coordinators.js +347 -0
  33. package/dist/cloud/api/coordinators.js.map +1 -0
  34. package/dist/cloud/api/daemons.d.ts +12 -0
  35. package/dist/cloud/api/daemons.d.ts.map +1 -0
  36. package/dist/cloud/api/daemons.js +320 -0
  37. package/dist/cloud/api/daemons.js.map +1 -0
  38. package/dist/cloud/api/generic-webhooks.d.ts +8 -0
  39. package/dist/cloud/api/generic-webhooks.d.ts.map +1 -0
  40. package/dist/cloud/api/generic-webhooks.js +129 -0
  41. package/dist/cloud/api/generic-webhooks.js.map +1 -0
  42. package/dist/cloud/api/git.d.ts +8 -0
  43. package/dist/cloud/api/git.d.ts.map +1 -0
  44. package/dist/cloud/api/git.js +131 -0
  45. package/dist/cloud/api/git.js.map +1 -0
  46. package/dist/cloud/api/github-app.d.ts +11 -0
  47. package/dist/cloud/api/github-app.d.ts.map +1 -0
  48. package/dist/cloud/api/github-app.js +189 -0
  49. package/dist/cloud/api/github-app.js.map +1 -0
  50. package/dist/cloud/api/middleware/planLimits.d.ts +43 -0
  51. package/dist/cloud/api/middleware/planLimits.d.ts.map +1 -0
  52. package/dist/cloud/api/middleware/planLimits.js +202 -0
  53. package/dist/cloud/api/middleware/planLimits.js.map +1 -0
  54. package/dist/cloud/api/monitoring.d.ts +11 -0
  55. package/dist/cloud/api/monitoring.d.ts.map +1 -0
  56. package/dist/cloud/api/monitoring.js +578 -0
  57. package/dist/cloud/api/monitoring.js.map +1 -0
  58. package/dist/cloud/api/nango-auth.d.ts +9 -0
  59. package/dist/cloud/api/nango-auth.d.ts.map +1 -0
  60. package/dist/cloud/api/nango-auth.js +377 -0
  61. package/dist/cloud/api/nango-auth.js.map +1 -0
  62. package/dist/cloud/api/onboarding.d.ts +15 -0
  63. package/dist/cloud/api/onboarding.d.ts.map +1 -0
  64. package/dist/cloud/api/onboarding.js +588 -0
  65. package/dist/cloud/api/onboarding.js.map +1 -0
  66. package/dist/cloud/api/policy.d.ts +8 -0
  67. package/dist/cloud/api/policy.d.ts.map +1 -0
  68. package/dist/cloud/api/policy.js +229 -0
  69. package/dist/cloud/api/policy.js.map +1 -0
  70. package/dist/cloud/api/providers.d.ts +7 -0
  71. package/dist/cloud/api/providers.d.ts.map +1 -0
  72. package/dist/cloud/api/providers.js +507 -0
  73. package/dist/cloud/api/providers.js.map +1 -0
  74. package/dist/cloud/api/repos.d.ts +7 -0
  75. package/dist/cloud/api/repos.d.ts.map +1 -0
  76. package/dist/cloud/api/repos.js +314 -0
  77. package/dist/cloud/api/repos.js.map +1 -0
  78. package/dist/cloud/api/teams.d.ts +7 -0
  79. package/dist/cloud/api/teams.d.ts.map +1 -0
  80. package/dist/cloud/api/teams.js +279 -0
  81. package/dist/cloud/api/teams.js.map +1 -0
  82. package/dist/cloud/api/test-helpers.d.ts +10 -0
  83. package/dist/cloud/api/test-helpers.d.ts.map +1 -0
  84. package/dist/cloud/api/test-helpers.js +575 -0
  85. package/dist/cloud/api/test-helpers.js.map +1 -0
  86. package/dist/cloud/api/usage.d.ts +7 -0
  87. package/dist/cloud/api/usage.d.ts.map +1 -0
  88. package/dist/cloud/api/usage.js +98 -0
  89. package/dist/cloud/api/usage.js.map +1 -0
  90. package/dist/cloud/api/webhooks.d.ts +7 -0
  91. package/dist/cloud/api/webhooks.d.ts.map +1 -0
  92. package/dist/cloud/api/webhooks.js +496 -0
  93. package/dist/cloud/api/webhooks.js.map +1 -0
  94. package/dist/cloud/api/workspaces.d.ts +7 -0
  95. package/dist/cloud/api/workspaces.d.ts.map +1 -0
  96. package/dist/cloud/api/workspaces.js +727 -0
  97. package/dist/cloud/api/workspaces.js.map +1 -0
  98. package/dist/cloud/billing/index.d.ts +9 -0
  99. package/dist/cloud/billing/index.d.ts.map +1 -0
  100. package/dist/cloud/billing/index.js +9 -0
  101. package/dist/cloud/billing/index.js.map +1 -0
  102. package/dist/cloud/billing/plans.d.ts +39 -0
  103. package/dist/cloud/billing/plans.d.ts.map +1 -0
  104. package/dist/cloud/billing/plans.js +245 -0
  105. package/dist/cloud/billing/plans.js.map +1 -0
  106. package/dist/cloud/billing/service.d.ts +80 -0
  107. package/dist/cloud/billing/service.d.ts.map +1 -0
  108. package/dist/cloud/billing/service.js +388 -0
  109. package/dist/cloud/billing/service.js.map +1 -0
  110. package/dist/cloud/billing/types.d.ts +141 -0
  111. package/dist/cloud/billing/types.d.ts.map +1 -0
  112. package/dist/cloud/billing/types.js +7 -0
  113. package/dist/cloud/billing/types.js.map +1 -0
  114. package/dist/cloud/config.d.ts +66 -0
  115. package/dist/cloud/config.d.ts.map +1 -0
  116. package/dist/cloud/config.js +92 -0
  117. package/dist/cloud/config.js.map +1 -0
  118. package/dist/cloud/db/drizzle.d.ts +215 -0
  119. package/dist/cloud/db/drizzle.d.ts.map +1 -0
  120. package/dist/cloud/db/drizzle.js +1083 -0
  121. package/dist/cloud/db/drizzle.js.map +1 -0
  122. package/dist/cloud/db/index.d.ts +35 -0
  123. package/dist/cloud/db/index.d.ts.map +1 -0
  124. package/dist/cloud/db/index.js +52 -0
  125. package/dist/cloud/db/index.js.map +1 -0
  126. package/dist/cloud/db/schema.d.ts +4519 -0
  127. package/dist/cloud/db/schema.d.ts.map +1 -0
  128. package/dist/cloud/db/schema.js +547 -0
  129. package/dist/cloud/db/schema.js.map +1 -0
  130. package/dist/cloud/index.d.ts +12 -0
  131. package/dist/cloud/index.d.ts.map +1 -0
  132. package/dist/cloud/index.js +39 -0
  133. package/dist/cloud/index.js.map +1 -0
  134. package/dist/cloud/provisioner/index.d.ts +75 -0
  135. package/dist/cloud/provisioner/index.d.ts.map +1 -0
  136. package/dist/cloud/provisioner/index.js +977 -0
  137. package/dist/cloud/provisioner/index.js.map +1 -0
  138. package/dist/cloud/server.d.ts +17 -0
  139. package/dist/cloud/server.d.ts.map +1 -0
  140. package/dist/cloud/server.js +534 -0
  141. package/dist/cloud/server.js.map +1 -0
  142. package/dist/cloud/services/auto-scaler.d.ts +152 -0
  143. package/dist/cloud/services/auto-scaler.d.ts.map +1 -0
  144. package/dist/cloud/services/auto-scaler.js +439 -0
  145. package/dist/cloud/services/auto-scaler.js.map +1 -0
  146. package/dist/cloud/services/capacity-manager.d.ts +148 -0
  147. package/dist/cloud/services/capacity-manager.d.ts.map +1 -0
  148. package/dist/cloud/services/capacity-manager.js +449 -0
  149. package/dist/cloud/services/capacity-manager.js.map +1 -0
  150. package/dist/cloud/services/ci-agent-spawner.d.ts +49 -0
  151. package/dist/cloud/services/ci-agent-spawner.d.ts.map +1 -0
  152. package/dist/cloud/services/ci-agent-spawner.js +373 -0
  153. package/dist/cloud/services/ci-agent-spawner.js.map +1 -0
  154. package/dist/cloud/services/coordinator.d.ts +62 -0
  155. package/dist/cloud/services/coordinator.d.ts.map +1 -0
  156. package/dist/cloud/services/coordinator.js +389 -0
  157. package/dist/cloud/services/coordinator.js.map +1 -0
  158. package/dist/cloud/services/index.d.ts +12 -0
  159. package/dist/cloud/services/index.d.ts.map +1 -0
  160. package/dist/cloud/services/index.js +15 -0
  161. package/dist/cloud/services/index.js.map +1 -0
  162. package/dist/cloud/services/mention-handler.d.ts +65 -0
  163. package/dist/cloud/services/mention-handler.d.ts.map +1 -0
  164. package/dist/cloud/services/mention-handler.js +405 -0
  165. package/dist/cloud/services/mention-handler.js.map +1 -0
  166. package/dist/cloud/services/nango.d.ts +126 -0
  167. package/dist/cloud/services/nango.d.ts.map +1 -0
  168. package/dist/cloud/services/nango.js +191 -0
  169. package/dist/cloud/services/nango.js.map +1 -0
  170. package/dist/cloud/services/persistence.d.ts +131 -0
  171. package/dist/cloud/services/persistence.d.ts.map +1 -0
  172. package/dist/cloud/services/persistence.js +200 -0
  173. package/dist/cloud/services/persistence.js.map +1 -0
  174. package/dist/cloud/services/planLimits.d.ts +125 -0
  175. package/dist/cloud/services/planLimits.d.ts.map +1 -0
  176. package/dist/cloud/services/planLimits.js +282 -0
  177. package/dist/cloud/services/planLimits.js.map +1 -0
  178. package/dist/cloud/services/scaling-orchestrator.d.ts +159 -0
  179. package/dist/cloud/services/scaling-orchestrator.d.ts.map +1 -0
  180. package/dist/cloud/services/scaling-orchestrator.js +502 -0
  181. package/dist/cloud/services/scaling-orchestrator.js.map +1 -0
  182. package/dist/cloud/services/scaling-policy.d.ts +121 -0
  183. package/dist/cloud/services/scaling-policy.d.ts.map +1 -0
  184. package/dist/cloud/services/scaling-policy.js +415 -0
  185. package/dist/cloud/services/scaling-policy.js.map +1 -0
  186. package/dist/cloud/vault/index.d.ts +76 -0
  187. package/dist/cloud/vault/index.d.ts.map +1 -0
  188. package/dist/cloud/vault/index.js +219 -0
  189. package/dist/cloud/vault/index.js.map +1 -0
  190. package/dist/cloud/webhooks/index.d.ts +24 -0
  191. package/dist/cloud/webhooks/index.d.ts.map +1 -0
  192. package/dist/cloud/webhooks/index.js +29 -0
  193. package/dist/cloud/webhooks/index.js.map +1 -0
  194. package/dist/cloud/webhooks/parsers/github.d.ts +8 -0
  195. package/dist/cloud/webhooks/parsers/github.d.ts.map +1 -0
  196. package/dist/cloud/webhooks/parsers/github.js +234 -0
  197. package/dist/cloud/webhooks/parsers/github.js.map +1 -0
  198. package/dist/cloud/webhooks/parsers/index.d.ts +23 -0
  199. package/dist/cloud/webhooks/parsers/index.d.ts.map +1 -0
  200. package/dist/cloud/webhooks/parsers/index.js +30 -0
  201. package/dist/cloud/webhooks/parsers/index.js.map +1 -0
  202. package/dist/cloud/webhooks/parsers/linear.d.ts +9 -0
  203. package/dist/cloud/webhooks/parsers/linear.d.ts.map +1 -0
  204. package/dist/cloud/webhooks/parsers/linear.js +258 -0
  205. package/dist/cloud/webhooks/parsers/linear.js.map +1 -0
  206. package/dist/cloud/webhooks/parsers/slack.d.ts +9 -0
  207. package/dist/cloud/webhooks/parsers/slack.d.ts.map +1 -0
  208. package/dist/cloud/webhooks/parsers/slack.js +214 -0
  209. package/dist/cloud/webhooks/parsers/slack.js.map +1 -0
  210. package/dist/cloud/webhooks/responders/github.d.ts +8 -0
  211. package/dist/cloud/webhooks/responders/github.d.ts.map +1 -0
  212. package/dist/cloud/webhooks/responders/github.js +73 -0
  213. package/dist/cloud/webhooks/responders/github.js.map +1 -0
  214. package/dist/cloud/webhooks/responders/index.d.ts +23 -0
  215. package/dist/cloud/webhooks/responders/index.d.ts.map +1 -0
  216. package/dist/cloud/webhooks/responders/index.js +30 -0
  217. package/dist/cloud/webhooks/responders/index.js.map +1 -0
  218. package/dist/cloud/webhooks/responders/linear.d.ts +9 -0
  219. package/dist/cloud/webhooks/responders/linear.d.ts.map +1 -0
  220. package/dist/cloud/webhooks/responders/linear.js +149 -0
  221. package/dist/cloud/webhooks/responders/linear.js.map +1 -0
  222. package/dist/cloud/webhooks/responders/slack.d.ts +20 -0
  223. package/dist/cloud/webhooks/responders/slack.d.ts.map +1 -0
  224. package/dist/cloud/webhooks/responders/slack.js +178 -0
  225. package/dist/cloud/webhooks/responders/slack.js.map +1 -0
  226. package/dist/cloud/webhooks/router.d.ts +25 -0
  227. package/dist/cloud/webhooks/router.d.ts.map +1 -0
  228. package/dist/cloud/webhooks/router.js +504 -0
  229. package/dist/cloud/webhooks/router.js.map +1 -0
  230. package/dist/cloud/webhooks/rules-engine.d.ts +24 -0
  231. package/dist/cloud/webhooks/rules-engine.d.ts.map +1 -0
  232. package/dist/cloud/webhooks/rules-engine.js +287 -0
  233. package/dist/cloud/webhooks/rules-engine.js.map +1 -0
  234. package/dist/cloud/webhooks/types.d.ts +186 -0
  235. package/dist/cloud/webhooks/types.d.ts.map +1 -0
  236. package/dist/cloud/webhooks/types.js +8 -0
  237. package/dist/cloud/webhooks/types.js.map +1 -0
  238. package/dist/continuity/formatter.d.ts +51 -0
  239. package/dist/continuity/formatter.d.ts.map +1 -0
  240. package/dist/continuity/formatter.js +313 -0
  241. package/dist/continuity/formatter.js.map +1 -0
  242. package/dist/continuity/handoff-store.d.ts +67 -0
  243. package/dist/continuity/handoff-store.d.ts.map +1 -0
  244. package/dist/continuity/handoff-store.js +472 -0
  245. package/dist/continuity/handoff-store.js.map +1 -0
  246. package/dist/continuity/index.d.ts +45 -0
  247. package/dist/continuity/index.d.ts.map +1 -0
  248. package/dist/continuity/index.js +48 -0
  249. package/dist/continuity/index.js.map +1 -0
  250. package/dist/continuity/ledger-store.d.ts +110 -0
  251. package/dist/continuity/ledger-store.d.ts.map +1 -0
  252. package/dist/continuity/ledger-store.js +500 -0
  253. package/dist/continuity/ledger-store.js.map +1 -0
  254. package/dist/continuity/manager.d.ts +178 -0
  255. package/dist/continuity/manager.d.ts.map +1 -0
  256. package/dist/continuity/manager.js +562 -0
  257. package/dist/continuity/manager.js.map +1 -0
  258. package/dist/continuity/parser.d.ts +76 -0
  259. package/dist/continuity/parser.d.ts.map +1 -0
  260. package/dist/continuity/parser.js +579 -0
  261. package/dist/continuity/parser.js.map +1 -0
  262. package/dist/continuity/types.d.ts +180 -0
  263. package/dist/continuity/types.d.ts.map +1 -0
  264. package/dist/continuity/types.js +9 -0
  265. package/dist/continuity/types.js.map +1 -0
  266. package/dist/daemon/agent-manager.d.ts +114 -0
  267. package/dist/daemon/agent-manager.d.ts.map +1 -0
  268. package/dist/daemon/agent-manager.js +513 -0
  269. package/dist/daemon/agent-manager.js.map +1 -0
  270. package/dist/daemon/agent-registry.d.ts +34 -0
  271. package/dist/daemon/agent-registry.d.ts.map +1 -1
  272. package/dist/daemon/agent-registry.js +45 -2
  273. package/dist/daemon/agent-registry.js.map +1 -1
  274. package/dist/daemon/api.d.ts +81 -0
  275. package/dist/daemon/api.d.ts.map +1 -0
  276. package/dist/daemon/api.js +554 -0
  277. package/dist/daemon/api.js.map +1 -0
  278. package/dist/daemon/cli-auth.d.ts +67 -0
  279. package/dist/daemon/cli-auth.d.ts.map +1 -0
  280. package/dist/daemon/cli-auth.js +537 -0
  281. package/dist/daemon/cli-auth.js.map +1 -0
  282. package/dist/daemon/cloud-sync.d.ts +101 -0
  283. package/dist/daemon/cloud-sync.d.ts.map +1 -0
  284. package/dist/daemon/cloud-sync.js +263 -0
  285. package/dist/daemon/cloud-sync.js.map +1 -0
  286. package/dist/daemon/index.d.ts +4 -0
  287. package/dist/daemon/index.d.ts.map +1 -1
  288. package/dist/daemon/index.js +6 -0
  289. package/dist/daemon/index.js.map +1 -1
  290. package/dist/daemon/orchestrator.d.ts +155 -0
  291. package/dist/daemon/orchestrator.d.ts.map +1 -0
  292. package/dist/daemon/orchestrator.js +766 -0
  293. package/dist/daemon/orchestrator.js.map +1 -0
  294. package/dist/daemon/router.d.ts +29 -0
  295. package/dist/daemon/router.d.ts.map +1 -1
  296. package/dist/daemon/router.js +143 -21
  297. package/dist/daemon/router.js.map +1 -1
  298. package/dist/daemon/server.d.ts +42 -0
  299. package/dist/daemon/server.d.ts.map +1 -1
  300. package/dist/daemon/server.js +199 -16
  301. package/dist/daemon/server.js.map +1 -1
  302. package/dist/daemon/services/browser-testing.d.ts +88 -0
  303. package/dist/daemon/services/browser-testing.d.ts.map +1 -0
  304. package/dist/daemon/services/browser-testing.js +244 -0
  305. package/dist/daemon/services/browser-testing.js.map +1 -0
  306. package/dist/daemon/services/container-spawner.d.ts +135 -0
  307. package/dist/daemon/services/container-spawner.d.ts.map +1 -0
  308. package/dist/daemon/services/container-spawner.js +313 -0
  309. package/dist/daemon/services/container-spawner.js.map +1 -0
  310. package/dist/daemon/types.d.ts +131 -0
  311. package/dist/daemon/types.d.ts.map +1 -0
  312. package/dist/daemon/types.js +6 -0
  313. package/dist/daemon/types.js.map +1 -0
  314. package/dist/daemon/workspace-manager.d.ts +75 -0
  315. package/dist/daemon/workspace-manager.d.ts.map +1 -0
  316. package/dist/daemon/workspace-manager.js +289 -0
  317. package/dist/daemon/workspace-manager.js.map +1 -0
  318. package/dist/dashboard/out/404.html +1 -1
  319. package/dist/dashboard/out/_next/static/chunks/116-2502180def231162.js +1 -0
  320. package/dist/dashboard/out/_next/static/chunks/282-980c2eb8fff20123.js +1 -0
  321. package/dist/dashboard/out/_next/static/chunks/480-2d4111711d4e473c.js +1 -0
  322. package/dist/dashboard/out/_next/static/chunks/724-73c1ee5f60abe860.js +9 -0
  323. package/dist/dashboard/out/_next/static/chunks/766-c3a14283c88d815b.js +1 -0
  324. package/dist/dashboard/out/_next/static/chunks/app/app/page-7120be68bea622f3.js +1 -0
  325. package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-dc2e3a1a22478efc.js +1 -0
  326. package/dist/dashboard/out/_next/static/chunks/app/history/page-56a8b4616a90dc43.js +1 -0
  327. package/dist/dashboard/out/_next/static/chunks/app/layout-2433bb48965f4333.js +1 -0
  328. package/dist/dashboard/out/_next/static/chunks/app/login/page-3eac37ea6f5dd153.js +1 -0
  329. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-1081dd190a331a91.js +1 -0
  330. package/dist/dashboard/out/_next/static/chunks/app/page-daf87e86f783f980.js +1 -0
  331. package/dist/dashboard/out/_next/static/chunks/app/pricing/page-4d72d5a5d8a9b618.js +1 -0
  332. package/dist/dashboard/out/_next/static/chunks/app/providers/page-b68a681526eb145e.js +1 -0
  333. package/dist/dashboard/out/_next/static/chunks/app/signup/page-fee4ed1709070bcd.js +1 -0
  334. package/dist/dashboard/out/_next/static/chunks/e868780c-48e5f147c90a3a41.js +18 -0
  335. package/dist/dashboard/out/_next/static/chunks/{main-e0a1f53fe0617a63.js → main-97850e03d723ea8c.js} +1 -1
  336. package/dist/dashboard/out/_next/static/chunks/main-app-5d692157a8eb1fd9.js +1 -0
  337. package/dist/dashboard/out/_next/static/chunks/webpack-1cdd8ed57114d5e1.js +1 -0
  338. package/dist/dashboard/out/_next/static/css/29852f26181969a0.css +1 -0
  339. package/dist/dashboard/out/_next/static/css/411ce23ffeae9f76.css +1 -0
  340. package/dist/dashboard/out/alt-logos/agent-relay-logo-128.png +0 -0
  341. package/dist/dashboard/out/alt-logos/agent-relay-logo-256.png +0 -0
  342. package/dist/dashboard/out/alt-logos/agent-relay-logo-32.png +0 -0
  343. package/dist/dashboard/out/alt-logos/agent-relay-logo-512.png +0 -0
  344. package/dist/dashboard/out/alt-logos/agent-relay-logo-64.png +0 -0
  345. package/dist/dashboard/out/alt-logos/agent-relay-logo.svg +45 -0
  346. package/dist/dashboard/out/alt-logos/logo.svg +38 -0
  347. package/dist/dashboard/out/alt-logos/monogram-logo-128.png +0 -0
  348. package/dist/dashboard/out/alt-logos/monogram-logo-256.png +0 -0
  349. package/dist/dashboard/out/alt-logos/monogram-logo-32.png +0 -0
  350. package/dist/dashboard/out/alt-logos/monogram-logo-512.png +0 -0
  351. package/dist/dashboard/out/alt-logos/monogram-logo-64.png +0 -0
  352. package/dist/dashboard/out/alt-logos/monogram-logo.svg +38 -0
  353. package/dist/dashboard/out/app.html +1 -0
  354. package/dist/dashboard/out/app.txt +7 -0
  355. package/dist/dashboard/out/connect-repos.html +1 -0
  356. package/dist/dashboard/out/connect-repos.txt +7 -0
  357. package/dist/dashboard/out/history.html +1 -0
  358. package/dist/dashboard/out/history.txt +7 -0
  359. package/dist/dashboard/out/index.html +1 -1
  360. package/dist/dashboard/out/index.txt +2 -2
  361. package/dist/dashboard/out/login.html +6 -0
  362. package/dist/dashboard/out/login.txt +7 -0
  363. package/dist/dashboard/out/metrics.html +1 -515
  364. package/dist/dashboard/out/metrics.txt +2 -2
  365. package/dist/dashboard/out/pricing.html +13 -0
  366. package/dist/dashboard/out/pricing.txt +7 -0
  367. package/dist/dashboard/out/providers.html +1 -0
  368. package/dist/dashboard/out/providers.txt +7 -0
  369. package/dist/dashboard/out/signup.html +6 -0
  370. package/dist/dashboard/out/signup.txt +7 -0
  371. package/dist/dashboard-server/metrics.d.ts.map +1 -1
  372. package/dist/dashboard-server/metrics.js +3 -2
  373. package/dist/dashboard-server/metrics.js.map +1 -1
  374. package/dist/dashboard-server/server.d.ts.map +1 -1
  375. package/dist/dashboard-server/server.js +2653 -130
  376. package/dist/dashboard-server/server.js.map +1 -1
  377. package/dist/hooks/emitter.d.ts +40 -0
  378. package/dist/hooks/emitter.d.ts.map +1 -0
  379. package/dist/hooks/emitter.js +63 -0
  380. package/dist/hooks/emitter.js.map +1 -0
  381. package/dist/hooks/index.d.ts +3 -0
  382. package/dist/hooks/index.d.ts.map +1 -1
  383. package/dist/hooks/index.js +3 -0
  384. package/dist/hooks/index.js.map +1 -1
  385. package/dist/hooks/registry.d.ts +173 -0
  386. package/dist/hooks/registry.d.ts.map +1 -0
  387. package/dist/hooks/registry.js +476 -0
  388. package/dist/hooks/registry.js.map +1 -0
  389. package/dist/hooks/trajectory-hooks.d.ts +52 -0
  390. package/dist/hooks/trajectory-hooks.d.ts.map +1 -0
  391. package/dist/hooks/trajectory-hooks.js +183 -0
  392. package/dist/hooks/trajectory-hooks.js.map +1 -0
  393. package/dist/hooks/types.d.ts +141 -0
  394. package/dist/hooks/types.d.ts.map +1 -1
  395. package/dist/index.d.ts +2 -0
  396. package/dist/index.d.ts.map +1 -1
  397. package/dist/index.js +3 -0
  398. package/dist/index.js.map +1 -1
  399. package/dist/memory/adapters/index.d.ts +8 -0
  400. package/dist/memory/adapters/index.d.ts.map +1 -0
  401. package/dist/memory/adapters/index.js +8 -0
  402. package/dist/memory/adapters/index.js.map +1 -0
  403. package/dist/memory/adapters/inmemory.d.ts +59 -0
  404. package/dist/memory/adapters/inmemory.d.ts.map +1 -0
  405. package/dist/memory/adapters/inmemory.js +195 -0
  406. package/dist/memory/adapters/inmemory.js.map +1 -0
  407. package/dist/memory/adapters/supermemory.d.ts +71 -0
  408. package/dist/memory/adapters/supermemory.d.ts.map +1 -0
  409. package/dist/memory/adapters/supermemory.js +338 -0
  410. package/dist/memory/adapters/supermemory.js.map +1 -0
  411. package/dist/memory/factory.d.ts +48 -0
  412. package/dist/memory/factory.d.ts.map +1 -0
  413. package/dist/memory/factory.js +143 -0
  414. package/dist/memory/factory.js.map +1 -0
  415. package/dist/memory/index.d.ts +32 -0
  416. package/dist/memory/index.d.ts.map +1 -0
  417. package/dist/memory/index.js +32 -0
  418. package/dist/memory/index.js.map +1 -0
  419. package/dist/memory/memory-hooks.d.ts +60 -0
  420. package/dist/memory/memory-hooks.d.ts.map +1 -0
  421. package/dist/memory/memory-hooks.js +313 -0
  422. package/dist/memory/memory-hooks.js.map +1 -0
  423. package/dist/memory/service.d.ts +49 -0
  424. package/dist/memory/service.d.ts.map +1 -0
  425. package/dist/memory/service.js +146 -0
  426. package/dist/memory/service.js.map +1 -0
  427. package/dist/memory/types.d.ts +195 -0
  428. package/dist/memory/types.d.ts.map +1 -0
  429. package/dist/memory/types.js +8 -0
  430. package/dist/memory/types.js.map +1 -0
  431. package/dist/policy/agent-policy.d.ts +225 -0
  432. package/dist/policy/agent-policy.d.ts.map +1 -0
  433. package/dist/policy/agent-policy.js +665 -0
  434. package/dist/policy/agent-policy.js.map +1 -0
  435. package/dist/policy/cloud-policy-fetcher.d.ts +12 -0
  436. package/dist/policy/cloud-policy-fetcher.d.ts.map +1 -0
  437. package/dist/policy/cloud-policy-fetcher.js +64 -0
  438. package/dist/policy/cloud-policy-fetcher.js.map +1 -0
  439. package/dist/protocol/types.d.ts +10 -1
  440. package/dist/protocol/types.d.ts.map +1 -1
  441. package/dist/resiliency/context-persistence.d.ts +140 -0
  442. package/dist/resiliency/context-persistence.d.ts.map +1 -0
  443. package/dist/resiliency/context-persistence.js +397 -0
  444. package/dist/resiliency/context-persistence.js.map +1 -0
  445. package/dist/resiliency/crash-insights.d.ts +156 -0
  446. package/dist/resiliency/crash-insights.d.ts.map +1 -0
  447. package/dist/resiliency/crash-insights.js +492 -0
  448. package/dist/resiliency/crash-insights.js.map +1 -0
  449. package/dist/resiliency/gossip-health.d.ts +137 -0
  450. package/dist/resiliency/gossip-health.d.ts.map +1 -0
  451. package/dist/resiliency/gossip-health.js +241 -0
  452. package/dist/resiliency/gossip-health.js.map +1 -0
  453. package/dist/resiliency/health-monitor.d.ts +97 -0
  454. package/dist/resiliency/health-monitor.d.ts.map +1 -0
  455. package/dist/resiliency/health-monitor.js +291 -0
  456. package/dist/resiliency/health-monitor.js.map +1 -0
  457. package/dist/resiliency/index.d.ts +68 -0
  458. package/dist/resiliency/index.d.ts.map +1 -0
  459. package/dist/resiliency/index.js +68 -0
  460. package/dist/resiliency/index.js.map +1 -0
  461. package/dist/resiliency/leader-watchdog.d.ts +109 -0
  462. package/dist/resiliency/leader-watchdog.d.ts.map +1 -0
  463. package/dist/resiliency/leader-watchdog.js +189 -0
  464. package/dist/resiliency/leader-watchdog.js.map +1 -0
  465. package/dist/resiliency/logger.d.ts +114 -0
  466. package/dist/resiliency/logger.d.ts.map +1 -0
  467. package/dist/resiliency/logger.js +250 -0
  468. package/dist/resiliency/logger.js.map +1 -0
  469. package/dist/resiliency/memory-monitor.d.ts +172 -0
  470. package/dist/resiliency/memory-monitor.d.ts.map +1 -0
  471. package/dist/resiliency/memory-monitor.js +593 -0
  472. package/dist/resiliency/memory-monitor.js.map +1 -0
  473. package/dist/resiliency/metrics.d.ts +115 -0
  474. package/dist/resiliency/metrics.d.ts.map +1 -0
  475. package/dist/resiliency/metrics.js +239 -0
  476. package/dist/resiliency/metrics.js.map +1 -0
  477. package/dist/resiliency/provider-context.d.ts +100 -0
  478. package/dist/resiliency/provider-context.d.ts.map +1 -0
  479. package/dist/resiliency/provider-context.js +360 -0
  480. package/dist/resiliency/provider-context.js.map +1 -0
  481. package/dist/resiliency/stateless-lead.d.ts +149 -0
  482. package/dist/resiliency/stateless-lead.d.ts.map +1 -0
  483. package/dist/resiliency/stateless-lead.js +308 -0
  484. package/dist/resiliency/stateless-lead.js.map +1 -0
  485. package/dist/resiliency/supervisor.d.ts +147 -0
  486. package/dist/resiliency/supervisor.d.ts.map +1 -0
  487. package/dist/resiliency/supervisor.js +459 -0
  488. package/dist/resiliency/supervisor.js.map +1 -0
  489. package/dist/shared/cli-auth-config.d.ts +91 -0
  490. package/dist/shared/cli-auth-config.d.ts.map +1 -0
  491. package/dist/shared/cli-auth-config.js +264 -0
  492. package/dist/shared/cli-auth-config.js.map +1 -0
  493. package/dist/storage/adapter.d.ts +3 -1
  494. package/dist/storage/adapter.d.ts.map +1 -1
  495. package/dist/storage/adapter.js +12 -2
  496. package/dist/storage/adapter.js.map +1 -1
  497. package/dist/storage/sqlite-adapter.d.ts.map +1 -1
  498. package/dist/storage/sqlite-adapter.js +18 -14
  499. package/dist/storage/sqlite-adapter.js.map +1 -1
  500. package/dist/trajectory/config.d.ts +84 -0
  501. package/dist/trajectory/config.d.ts.map +1 -0
  502. package/dist/trajectory/config.js +163 -0
  503. package/dist/trajectory/config.js.map +1 -0
  504. package/dist/trajectory/index.d.ts +8 -0
  505. package/dist/trajectory/index.d.ts.map +1 -0
  506. package/dist/trajectory/index.js +8 -0
  507. package/dist/trajectory/index.js.map +1 -0
  508. package/dist/trajectory/integration.d.ts +292 -0
  509. package/dist/trajectory/integration.d.ts.map +1 -0
  510. package/dist/trajectory/integration.js +834 -0
  511. package/dist/trajectory/integration.js.map +1 -0
  512. package/dist/utils/index.d.ts +1 -0
  513. package/dist/utils/index.d.ts.map +1 -1
  514. package/dist/utils/index.js +1 -0
  515. package/dist/utils/index.js.map +1 -1
  516. package/dist/utils/logger.d.ts +40 -0
  517. package/dist/utils/logger.d.ts.map +1 -0
  518. package/dist/utils/logger.js +84 -0
  519. package/dist/utils/logger.js.map +1 -0
  520. package/dist/utils/project-namespace.d.ts +24 -0
  521. package/dist/utils/project-namespace.d.ts.map +1 -1
  522. package/dist/utils/project-namespace.js +84 -0
  523. package/dist/utils/project-namespace.js.map +1 -1
  524. package/dist/wrapper/client.d.ts +16 -1
  525. package/dist/wrapper/client.d.ts.map +1 -1
  526. package/dist/wrapper/client.js +32 -1
  527. package/dist/wrapper/client.js.map +1 -1
  528. package/dist/wrapper/parser.d.ts +13 -0
  529. package/dist/wrapper/parser.d.ts.map +1 -1
  530. package/dist/wrapper/parser.js +217 -47
  531. package/dist/wrapper/parser.js.map +1 -1
  532. package/dist/wrapper/pty-wrapper.d.ts +219 -17
  533. package/dist/wrapper/pty-wrapper.d.ts.map +1 -1
  534. package/dist/wrapper/pty-wrapper.js +1050 -104
  535. package/dist/wrapper/pty-wrapper.js.map +1 -1
  536. package/dist/wrapper/shared.d.ts +165 -0
  537. package/dist/wrapper/shared.d.ts.map +1 -0
  538. package/dist/wrapper/shared.js +270 -0
  539. package/dist/wrapper/shared.js.map +1 -0
  540. package/dist/wrapper/tmux-wrapper.d.ts +78 -11
  541. package/dist/wrapper/tmux-wrapper.d.ts.map +1 -1
  542. package/dist/wrapper/tmux-wrapper.js +567 -106
  543. package/dist/wrapper/tmux-wrapper.js.map +1 -1
  544. package/docs/CLOUD-ARCHITECTURE.md +804 -0
  545. package/docs/CLOUD-ONBOARDING-DESIGN.md +1983 -0
  546. package/docs/HOOKS_API.md +394 -0
  547. package/docs/WRAPPER_EVENTS.md +358 -0
  548. package/docs/agent-policy-snippet.md +40 -0
  549. package/docs/agent-relay-protocol.md +238 -0
  550. package/docs/agent-relay-snippet.md +115 -6
  551. package/docs/archive/EXECUTIVE_SUMMARY.md +358 -0
  552. package/docs/archive/ROADMAP.md +329 -0
  553. package/docs/archive/TESTING_PRESENCE_FEATURES.md +327 -0
  554. package/docs/competitive/GASTOWN.md +451 -0
  555. package/docs/{COMPETITIVE_ANALYSIS.md → competitive/OVERVIEW.md} +1 -0
  556. package/docs/competitive/README.md +34 -0
  557. package/docs/competitive/TMUX_ORCHESTRATOR.md +605 -0
  558. package/docs/dashboard.png +0 -0
  559. package/docs/design/ci-failure-webhooks.md +812 -0
  560. package/docs/design/comprehensive-integrations.md +238 -0
  561. package/docs/design/e2b-sandbox-integration.md +504 -0
  562. package/docs/design/github-app-permissions.md +264 -0
  563. package/docs/guides/CLOUD.md +236 -0
  564. package/docs/guides/LOCAL.md +535 -0
  565. package/docs/guides/SELF-HOSTED.md +494 -0
  566. package/docs/local-testing.md +428 -0
  567. package/docs/proposals/continuous-claude-integration.md +622 -0
  568. package/docs/proposals/custom-commands.md +368 -0
  569. package/docs/proposals/shadow-as-subagent.md +765 -0
  570. package/docs/proposals/slack-bot-integration.md +1457 -0
  571. package/docs/tasks/global-skills-system.tasks.md +230 -0
  572. package/docs/tasks/webhook-integrations.tasks.md +184 -0
  573. package/docs/tasks/workspace-capabilities.tasks.md +121 -0
  574. package/docs/testing/RESILIENCY-TEST-PLAN-2026-01-01.md +366 -0
  575. package/package.json +45 -7
  576. package/scripts/cloud-setup.sh +96 -0
  577. package/scripts/manual-qa.sh +293 -0
  578. package/scripts/postinstall.js +60 -0
  579. package/scripts/run-cloud-qa.sh +220 -0
  580. package/scripts/test-cli-auth/Dockerfile +44 -0
  581. package/scripts/test-cli-auth/Dockerfile.real +79 -0
  582. package/scripts/test-cli-auth/README.md +286 -0
  583. package/scripts/test-cli-auth/ci-test-real-clis.ts +251 -0
  584. package/scripts/test-cli-auth/ci-test-runner.ts +263 -0
  585. package/scripts/test-cli-auth/mock-cli.sh +147 -0
  586. package/scripts/test-cli-auth/package.json +14 -0
  587. package/scripts/test-cli-auth/test-oauth-flow.ts +220 -0
  588. package/scripts/test-pty-input-auto.js +222 -0
  589. package/scripts/test-pty-input.js +150 -0
  590. package/dist/dashboard/out/_next/static/chunks/app/layout-c9d8c5d95e48c6bf.js +0 -1
  591. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-8aa9936bc6c771ab.js +0 -1
  592. package/dist/dashboard/out/_next/static/chunks/app/page-4498be09a5157759.js +0 -1
  593. package/dist/dashboard/out/_next/static/chunks/main-app-bae2e535de00de50.js +0 -1
  594. package/dist/dashboard/out/_next/static/chunks/webpack-c81f7fd28659d64f.js +0 -1
  595. package/dist/dashboard/out/_next/static/css/50ed6996e3df7bdd.css +0 -1
  596. /package/dist/dashboard/out/_next/static/{DXFA-jj8wb3PcY5DX2xcU → H5aWG0udPB4iOUIl_gytz}/_buildManifest.js +0 -0
  597. /package/dist/dashboard/out/_next/static/{DXFA-jj8wb3PcY5DX2xcU → H5aWG0udPB4iOUIl_gytz}/_ssgManifest.js +0 -0
  598. /package/dist/dashboard/out/_next/static/chunks/{117-3bef7b19f3e60751.js → 117-b100311aff8d5c61.js} +0 -0
  599. /package/dist/dashboard/out/_next/static/chunks/{648-6cf686106c891ad3.js → 648-a13d3c2b1be45466.js} +0 -0
  600. /package/dist/dashboard/out/_next/static/chunks/app/_not-found/{page-8ff6572bc7c9bc61.js → page-a4973f3e3c82fb67.js} +0 -0
  601. /package/dist/dashboard/out/_next/static/chunks/{fd9d1056-26bd8d656b496dba.js → fd9d1056-bf46c09eb57e019c.js} +0 -0
  602. /package/docs/{CHANGELOG.md → archive/CHANGELOG.md} +0 -0
  603. /package/docs/{CLI-SIMPLIFICATION-COMPLETE.md → archive/CLI-SIMPLIFICATION-COMPLETE.md} +0 -0
  604. /package/docs/{DESIGN_BRIDGE_STAFFING.md → archive/DESIGN_BRIDGE_STAFFING.md} +0 -0
  605. /package/docs/{DESIGN_V2.md → archive/DESIGN_V2.md} +0 -0
  606. /package/docs/{MONETIZATION.md → archive/MONETIZATION.md} +0 -0
  607. /package/docs/{PROPOSAL-trajectories.md → archive/PROPOSAL-trajectories.md} +0 -0
  608. /package/docs/{SCALING_ANALYSIS.md → archive/SCALING_ANALYSIS.md} +0 -0
  609. /package/docs/{TMUX_IMPLEMENTATION_NOTES.md → archive/TMUX_IMPLEMENTATION_NOTES.md} +0 -0
  610. /package/docs/{TMUX_IMPROVEMENTS.md → archive/TMUX_IMPROVEMENTS.md} +0 -0
  611. /package/docs/{dashboard-v2-plan.md → archive/dashboard-v2-plan.md} +0 -0
  612. /package/docs/{removable-code-analysis.md → archive/removable-code-analysis.md} +0 -0
  613. /package/docs/{competitive-analysis-mcp-agent-mail.md → competitive/MCP_AGENT_MAIL.md} +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,28 +27,96 @@ 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;
38
+ readyForMessages = false;
39
+ lastOutputTime = 0;
40
+ injectionMetrics = createInjectionMetrics();
28
41
  logFilePath;
29
42
  logStream;
30
- 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
31
63
  constructor(config) {
32
64
  super();
33
65
  this.config = config;
34
- 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;
35
81
  this.client = new RelayClient({
36
82
  agentName: config.name,
37
83
  socketPath: config.socketPath,
38
- cli: 'spawned',
84
+ cli: this.cliType,
85
+ task: detectedTask,
39
86
  workingDirectory: config.cwd ?? process.cwd(),
40
87
  quiet: true,
41
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' });
42
117
  // Handle incoming messages
43
- this.client.onMessage = (from, payload, messageId) => {
44
- this.handleIncomingMessage(from, payload, messageId);
118
+ this.client.onMessage = (from, payload, messageId, meta, originalTo) => {
119
+ this.handleIncomingMessage(from, payload, messageId, meta, originalTo);
45
120
  };
46
121
  }
47
122
  /**
@@ -65,6 +140,17 @@ export class PtyWrapper extends EventEmitter {
65
140
  // Connect to relay daemon
66
141
  try {
67
142
  await this.client.connect();
143
+ // If this is a shadow agent, bind to the primary after connecting
144
+ if (this.config.shadowOf) {
145
+ const speakOn = this.config.shadowSpeakOn ?? ['EXPLICIT_ASK'];
146
+ const bound = this.client.bindAsShadow(this.config.shadowOf, { speakOn });
147
+ if (bound) {
148
+ console.log(`[pty:${this.config.name}] Bound as shadow of ${this.config.shadowOf} (speakOn: ${speakOn.join(', ')})`);
149
+ }
150
+ else {
151
+ console.error(`[pty:${this.config.name}] Failed to bind as shadow of ${this.config.shadowOf}`);
152
+ }
153
+ }
68
154
  }
69
155
  catch (err) {
70
156
  console.error(`[pty:${this.config.name}] Relay connect failed: ${err.message}`);
@@ -75,6 +161,9 @@ export class PtyWrapper extends EventEmitter {
75
161
  // Log spawn details for debugging
76
162
  console.log(`[pty:${this.config.name}] Spawning: ${this.config.command} ${args.join(' ')}`);
77
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);
78
167
  // Spawn the process with error handling
79
168
  try {
80
169
  this.ptyProcess = pty.spawn(this.config.command, args, {
@@ -85,6 +174,7 @@ export class PtyWrapper extends EventEmitter {
85
174
  env: {
86
175
  ...process.env,
87
176
  ...this.config.env,
177
+ ...trailEnvVars,
88
178
  AGENT_RELAY_NAME: this.config.name,
89
179
  TERM: 'xterm-256color',
90
180
  },
@@ -97,6 +187,17 @@ export class PtyWrapper extends EventEmitter {
97
187
  throw spawnError;
98
188
  }
99
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
+ });
100
201
  // Capture output
101
202
  this.ptyProcess.onData((data) => {
102
203
  this.handleOutput(data);
@@ -108,13 +209,147 @@ export class PtyWrapper extends EventEmitter {
108
209
  this.config.onExit?.(exitCode);
109
210
  this.client.destroy();
110
211
  });
111
- // Inject initial instructions after a delay
112
- setTimeout(() => this.injectInstructions(), 2000);
212
+ // Inject initial instructions after a delay, then mark ready for messages
213
+ setTimeout(() => {
214
+ this.injectInstructions();
215
+ this.readyForMessages = true;
216
+ // Process any messages that arrived while waiting
217
+ this.processMessageQueue();
218
+ }, 2000);
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
+ }
113
346
  }
114
347
  /**
115
348
  * Handle output from the process
116
349
  */
117
350
  handleOutput(data) {
351
+ // Track output timing for stability checks
352
+ this.lastOutputTime = Date.now();
118
353
  // Append to raw buffer
119
354
  this.rawBuffer += data;
120
355
  // Write to log file if available
@@ -123,9 +358,20 @@ export class PtyWrapper extends EventEmitter {
123
358
  }
124
359
  // Emit for external listeners
125
360
  this.emit('output', data);
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
364
+ if (this.config.streamLogs !== false && this.client.state === 'READY') {
365
+ const filteredData = this.filterThinkingBlocks(data);
366
+ if (filteredData && !this.isDuplicateLogChunk(filteredData)) {
367
+ this.client.sendLog(filteredData);
368
+ }
369
+ }
126
370
  // Auto-accept Claude's first-run prompt for --dangerously-skip-permissions
127
371
  // The prompt shows: "2. Yes, I accept" - we send "2" to accept
128
372
  this.handleAutoAcceptPrompts(data);
373
+ // Handle terminal escape sequences that require responses (e.g., cursor position query)
374
+ this.handleTerminalEscapeSequences(data);
129
375
  // Store in line buffer for logs
130
376
  const lines = data.split('\n');
131
377
  for (const line of lines) {
@@ -139,54 +385,250 @@ export class PtyWrapper extends EventEmitter {
139
385
  }
140
386
  // Parse for relay commands
141
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');
457
+ }
458
+ /**
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
142
495
  }
143
496
  /**
144
- * Auto-accept Claude's first-run prompts for --dangerously-skip-permissions
145
- * Detects the acceptance prompt and sends "2" to select "Yes, I accept"
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.
146
505
  */
147
506
  handleAutoAcceptPrompts(data) {
148
- if (this.hasAcceptedPrompt)
149
- return;
150
507
  if (!this.ptyProcess || !this.running)
151
508
  return;
152
- // Check for the permission acceptance prompt
509
+ const cleanData = stripAnsi(data);
510
+ // Check for the permission acceptance prompt (--dangerously-skip-permissions)
153
511
  // Pattern: "2. Yes, I accept" in the output
154
- const cleanData = this.stripAnsi(data);
155
- 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')) {
156
514
  console.log(`[pty:${this.config.name}] Detected permission prompt, auto-accepting...`);
157
- this.hasAcceptedPrompt = true;
515
+ this.acceptedPrompts.add('permission');
158
516
  // Send "2" to select "Yes, I accept" and Enter to confirm
159
517
  setTimeout(() => {
160
518
  if (this.ptyProcess && this.running) {
161
519
  this.ptyProcess.write('2');
162
520
  }
163
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;
554
+ }
555
+ }
556
+ /**
557
+ * Handle terminal escape sequences that require responses.
558
+ *
559
+ * Some CLI tools (like Codex) query terminal capabilities and expect responses.
560
+ * Without proper responses, they timeout and crash.
561
+ *
562
+ * Supported sequences:
563
+ * - CSI 6 n (DSR - Device Status Report for cursor position)
564
+ * Response: CSI row ; col R (we report position 1;1)
565
+ */
566
+ handleTerminalEscapeSequences(data) {
567
+ if (!this.ptyProcess || !this.running)
568
+ return;
569
+ // Check for cursor position query: ESC [ 6 n
570
+ // This can appear as \x1b[6n or \x1b[?6n
571
+ // eslint-disable-next-line no-control-regex
572
+ if (/\x1b\[\??6n/.test(data)) {
573
+ // Respond with cursor at position (1, 1)
574
+ // Format: ESC [ row ; col R
575
+ const response = '\x1b[1;1R';
576
+ // Small delay to ensure the query has been fully processed
577
+ setTimeout(() => {
578
+ if (this.ptyProcess && this.running) {
579
+ this.ptyProcess.write(response);
580
+ }
581
+ }, 10);
164
582
  }
165
583
  }
166
584
  /**
167
585
  * Parse relay commands from output.
168
586
  * Handles both single-line and multi-line (fenced) formats.
169
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.
170
591
  */
171
592
  parseRelayCommands() {
172
- 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);
173
601
  // First, try to find fenced multi-line messages: ->relay:Target <<<\n...\n>>>
174
- this.parseFencedMessages(cleanContent);
602
+ this.parseFencedMessages(contentToParse);
175
603
  // Then parse single-line messages
176
- this.parseSingleLineMessages(cleanContent);
604
+ this.parseSingleLineMessages(contentToParse);
177
605
  // Parse spawn/release commands
178
- this.parseSpawnReleaseCommands(cleanContent);
606
+ this.parseSpawnReleaseCommands(contentToParse);
607
+ // Update parsed position
608
+ this.lastParsedLength = cleanContent.length;
179
609
  }
180
610
  /**
181
- * Parse fenced multi-line messages: ->relay:Target <<<\n...\n>>>
611
+ * Parse fenced multi-line messages: ->relay:Target [thread:xxx] <<<\n...\n>>>
182
612
  */
183
613
  parseFencedMessages(content) {
184
- // Pattern: ->relay:Target <<< (with content on same or following lines until >>>)
185
- const fenceStartPattern = new RegExp(`${this.relayPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\S+)\\s*<<<`, 'g');
614
+ // Pattern: ->relay:Target [thread:xxx] <<< (with content on same or following lines until >>>)
615
+ // Thread is optional, can be [thread:id] or [thread:project:id] for cross-project
616
+ const escapedPrefix = this.relayPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
617
+ const fenceStartPattern = new RegExp(`${escapedPrefix}(\\S+)(?:\\s+\\[thread:(?:([\\w-]+):)?([\\w-]+)\\])?\\s*<<<`, 'g');
186
618
  let match;
187
619
  while ((match = fenceStartPattern.exec(content)) !== null) {
188
620
  const target = match[1];
621
+ const threadProject = match[2]; // Optional: project part of thread
622
+ const threadId = match[3]; // Thread ID
189
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
+ }
190
632
  // Find the closing >>>
191
633
  const endIdx = content.indexOf('>>>', startIdx);
192
634
  if (endIdx === -1)
@@ -203,17 +645,24 @@ export class PtyWrapper extends EventEmitter {
203
645
  project = target.substring(0, colonIdx);
204
646
  to = target.substring(colonIdx + 1);
205
647
  }
648
+ // Skip placeholder targets after parsing cross-project syntax
649
+ if (isPlaceholderTarget(to)) {
650
+ continue;
651
+ }
206
652
  this.sendRelayCommand({
207
653
  to,
208
654
  kind: 'message',
209
655
  body,
210
656
  project,
657
+ thread: threadId || undefined,
658
+ threadProject: threadProject || undefined,
211
659
  raw: match[0],
212
660
  });
213
661
  }
214
662
  }
215
663
  /**
216
664
  * Parse single-line messages (no fenced format)
665
+ * Format: ->relay:Target [thread:xxx] message body
217
666
  */
218
667
  parseSingleLineMessages(content) {
219
668
  const lines = content.split('\n');
@@ -225,18 +674,53 @@ export class PtyWrapper extends EventEmitter {
225
674
  const prefixIdx = line.indexOf(this.relayPrefix);
226
675
  if (prefixIdx === -1)
227
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
+ }
228
682
  // Extract everything after the prefix
229
683
  const afterPrefix = line.substring(prefixIdx + this.relayPrefix.length);
230
- // Find the target (first non-whitespace segment)
231
- const targetMatch = afterPrefix.match(/^(\S+)/);
232
- if (!targetMatch)
684
+ // Pattern: Target [thread:project:id] body or Target [thread:id] body or Target body
685
+ // Thread is optional, can include project prefix
686
+ const targetMatch = afterPrefix.match(/^(\S+)(?:\s+\[thread:(?:([\w-]+):)?([\w-]+)\])?\s+(.+)$/);
687
+ if (!targetMatch) {
688
+ // Fallback: try simpler pattern without thread
689
+ const simpleMatch = afterPrefix.match(/^(\S+)\s+(.+)$/);
690
+ if (!simpleMatch)
691
+ continue;
692
+ const [, target, body] = simpleMatch;
693
+ if (!body)
694
+ continue;
695
+ // Skip placeholder targets (documentation examples)
696
+ if (isPlaceholderTarget(target))
697
+ continue;
698
+ // Parse target for cross-project syntax
699
+ const colonIdx = target.indexOf(':');
700
+ let to = target;
701
+ let project;
702
+ if (colonIdx > 0 && colonIdx < target.length - 1) {
703
+ project = target.substring(0, colonIdx);
704
+ to = target.substring(colonIdx + 1);
705
+ }
706
+ // Skip placeholder targets after parsing cross-project syntax
707
+ if (isPlaceholderTarget(to))
708
+ continue;
709
+ this.sendRelayCommand({
710
+ to,
711
+ kind: 'message',
712
+ body,
713
+ project,
714
+ raw: line,
715
+ });
233
716
  continue;
234
- const target = targetMatch[1];
235
- const bodyStart = targetMatch[0].length;
236
- const body = afterPrefix.substring(bodyStart).trim();
237
- // Skip if no body
717
+ }
718
+ const [, target, threadProject, threadId, body] = targetMatch;
238
719
  if (!body)
239
720
  continue;
721
+ // Skip placeholder targets (documentation examples)
722
+ if (isPlaceholderTarget(target))
723
+ continue;
240
724
  // Parse target for cross-project syntax
241
725
  const colonIdx = target.indexOf(':');
242
726
  let to = target;
@@ -245,80 +729,219 @@ export class PtyWrapper extends EventEmitter {
245
729
  project = target.substring(0, colonIdx);
246
730
  to = target.substring(colonIdx + 1);
247
731
  }
732
+ // Skip placeholder targets after parsing cross-project syntax
733
+ if (isPlaceholderTarget(to))
734
+ continue;
248
735
  this.sendRelayCommand({
249
736
  to,
250
737
  kind: 'message',
251
738
  body,
252
739
  project,
740
+ thread: threadId || undefined,
741
+ threadProject: threadProject || undefined,
253
742
  raw: line,
254
743
  });
255
744
  }
256
745
  }
257
- /**
258
- * Strip ANSI escape codes from string.
259
- * Converts cursor movements to spaces to preserve visual layout.
260
- */
261
- stripAnsi(str) {
262
- // Convert cursor forward movements to spaces (CSI n C)
263
- // \x1B[nC means move cursor right n columns
264
- // eslint-disable-next-line no-control-regex
265
- str = str.replace(/\x1B\[(\d+)C/g, (_m, n) => ' '.repeat(parseInt(n, 10) || 1));
266
- // Convert single cursor right (CSI C) to space
267
- // eslint-disable-next-line no-control-regex
268
- str = str.replace(/\x1B\[C/g, ' ');
269
- // Remove carriage returns (causes text overwriting issues)
270
- str = str.replace(/\r(?!\n)/g, '');
271
- // Strip remaining ANSI escape sequences
272
- // eslint-disable-next-line no-control-regex
273
- return str.replace(/\x1B(?:\[[0-9;]*[A-Za-z]|\].*?(?:\x07|\x1B\\)|[@-Z\\-_])/g, '');
274
- }
275
746
  /**
276
747
  * Send relay command to daemon
277
748
  */
278
749
  sendRelayCommand(cmd) {
279
750
  const msgHash = `${cmd.to}:${cmd.body}`;
280
751
  if (this.sentMessageHashes.has(msgHash)) {
752
+ console.log(`[pty:${this.config.name}] Skipping duplicate message to ${cmd.to}`);
281
753
  return;
282
754
  }
283
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})`);
284
757
  return;
285
758
  }
286
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'}`);
287
761
  if (success) {
288
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
+ });
289
767
  }
290
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
+ }
291
783
  /**
292
784
  * Parse spawn/release commands from output
293
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>>>
294
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.)
295
797
  */
296
798
  parseSpawnReleaseCommands(content) {
297
799
  // Need either API port or callbacks to handle spawn/release
298
- 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);
299
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
+ }
300
819
  if (!canSpawn && !canRelease)
301
820
  return;
302
821
  const lines = content.split('\n');
303
822
  const spawnPrefix = '->relay:spawn';
304
823
  const releasePrefix = '->relay:release';
305
824
  for (const line of lines) {
306
- // Check for spawn command
307
- const spawnIdx = line.indexOf(spawnPrefix);
308
- if (spawnIdx !== -1 && canSpawn) {
309
- const afterSpawn = line.substring(spawnIdx + spawnPrefix.length).trim();
310
- // Parse: WorkerName cli "task" or WorkerName cli 'task'
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
311
919
  const parts = afterSpawn.split(/\s+/);
312
- if (parts.length >= 3) {
920
+ if (parts.length >= 1) {
313
921
  const name = parts[0];
314
- const cli = parts[1];
315
- // Task is everything after cli, potentially in quotes
316
- const taskPart = parts.slice(2).join(' ');
317
- // Remove surrounding quotes if present
318
- const quoteMatch = taskPart.match(/^["'](.*)["']$/);
319
- const task = quoteMatch ? quoteMatch[1] : taskPart;
320
- if (name && cli && task) {
321
- const spawnKey = `${name}:${cli}:${task}`;
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)
925
+ let task = '';
926
+ const taskStartIndex = parts[1] ? 2 : 1;
927
+ if (parts.length > taskStartIndex) {
928
+ const taskPart = parts.slice(taskStartIndex).join(' ');
929
+ // Remove surrounding quotes if present
930
+ const quoteMatch = taskPart.match(/^["'](.*)["']$/);
931
+ task = quoteMatch ? quoteMatch[1] : taskPart;
932
+ }
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
+ }
944
+ const spawnKey = `${name}:${cli}`;
322
945
  if (!this.processedSpawnCommands.has(spawnKey)) {
323
946
  this.processedSpawnCommands.add(spawnKey);
324
947
  this.executeSpawn(name, cli, task);
@@ -328,13 +951,18 @@ export class PtyWrapper extends EventEmitter {
328
951
  continue;
329
952
  }
330
953
  // Check for release command
331
- const releaseIdx = line.indexOf(releasePrefix);
332
- if (releaseIdx !== -1 && canRelease) {
333
- const afterRelease = line.substring(releaseIdx + releasePrefix.length).trim();
334
- const name = afterRelease.split(/\s+/)[0];
335
- if (name && !this.processedReleaseCommands.has(name)) {
336
- this.processedReleaseCommands.add(name);
337
- 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
+ }
338
966
  }
339
967
  }
340
968
  }
@@ -343,6 +971,8 @@ export class PtyWrapper extends EventEmitter {
343
971
  * Execute spawn via API or callback
344
972
  */
345
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}`);
346
976
  if (this.config.dashboardPort) {
347
977
  // Use dashboard API for spawning (works from spawned agents)
348
978
  try {
@@ -380,7 +1010,7 @@ export class PtyWrapper extends EventEmitter {
380
1010
  if (this.config.dashboardPort) {
381
1011
  // Use dashboard API for releasing
382
1012
  try {
383
- 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)}`, {
384
1014
  method: 'DELETE',
385
1015
  });
386
1016
  const result = await response.json();
@@ -407,19 +1037,82 @@ export class PtyWrapper extends EventEmitter {
407
1037
  }
408
1038
  /**
409
1039
  * Handle incoming message from relay
1040
+ * @param originalTo - The original 'to' field from sender. '*' indicates this was a broadcast message.
410
1041
  */
411
- handleIncomingMessage(from, payload, messageId) {
412
- this.messageQueue.push({ from, body: payload.body, messageId });
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
+ }
1055
+ this.messageQueue.push({ from, body: payload.body, messageId, thread: payload.thread, importance: meta?.importance, data: payload.data, originalTo });
413
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;
414
1095
  }
415
1096
  /**
416
- * 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.
417
1104
  */
418
1105
  async processMessageQueue() {
1106
+ // Wait until instructions have been injected and agent is ready
1107
+ if (!this.readyForMessages)
1108
+ return;
419
1109
  if (this.isInjecting || this.messageQueue.length === 0)
420
1110
  return;
421
- 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`);
422
1114
  return;
1115
+ }
423
1116
  this.isInjecting = true;
424
1117
  const msg = this.messageQueue.shift();
425
1118
  if (!msg) {
@@ -427,14 +1120,68 @@ export class PtyWrapper extends EventEmitter {
427
1120
  return;
428
1121
  }
429
1122
  try {
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);
1147
+ }
1148
+ }
430
1149
  const shortId = msg.messageId.substring(0, 8);
431
- const sanitizedBody = msg.body.replace(/[\r\n]+/g, ' ').trim();
432
- const injection = `Relay message from ${msg.from} [${shortId}]: ${sanitizedBody}`;
433
- // Write message to PTY, then send Enter separately after a small delay
434
- // This matches how TmuxWrapper does it for better CLI compatibility
435
- this.ptyProcess.write(injection);
436
- await this.sleep(50);
437
- this.ptyProcess.write('\r');
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
+ }
438
1185
  }
439
1186
  catch (err) {
440
1187
  console.error(`[pty:${this.config.name}] Injection failed: ${err.message}`);
@@ -443,24 +1190,34 @@ export class PtyWrapper extends EventEmitter {
443
1190
  this.isInjecting = false;
444
1191
  // Process next message if any
445
1192
  if (this.messageQueue.length > 0) {
446
- setTimeout(() => this.processMessageQueue(), 500);
1193
+ setTimeout(() => this.processMessageQueue(), INJECTION_CONSTANTS.QUEUE_PROCESS_DELAY_MS);
447
1194
  }
448
1195
  }
449
1196
  }
450
1197
  /**
451
- * 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.
452
1203
  */
453
1204
  injectInstructions() {
454
- 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`);
455
1210
  return;
456
- const escapedPrefix = '\\' + this.relayPrefix;
457
- const instructions = `[Agent Relay] You are "${this.config.name}" - connected for real-time messaging. SEND: ${escapedPrefix}AgentName message`;
458
- try {
459
- this.ptyProcess.write(instructions + '\r');
460
- }
461
- catch {
462
- // Silent fail
463
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
+ });
464
1221
  }
465
1222
  /**
466
1223
  * Write directly to the PTY
@@ -488,38 +1245,73 @@ export class PtyWrapper extends EventEmitter {
488
1245
  /**
489
1246
  * Stop the agent process
490
1247
  */
491
- stop() {
1248
+ async stop() {
492
1249
  if (!this.running)
493
1250
  return;
494
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
+ }
495
1269
  if (this.ptyProcess) {
496
1270
  // Try graceful termination first
497
1271
  this.ptyProcess.write('\x03'); // Ctrl+C
498
- setTimeout(() => {
499
- if (this.ptyProcess) {
500
- this.ptyProcess.kill();
501
- }
502
- }, 1000);
1272
+ await sleep(1000);
1273
+ if (this.ptyProcess) {
1274
+ this.ptyProcess.kill();
1275
+ }
503
1276
  }
504
1277
  this.closeLogStream();
505
1278
  this.client.destroy();
1279
+ this.hookRegistry.destroy();
506
1280
  }
507
1281
  /**
508
1282
  * Kill the process immediately
509
1283
  */
510
- kill() {
1284
+ async kill() {
511
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
+ }
512
1309
  if (this.ptyProcess) {
513
1310
  this.ptyProcess.kill();
514
1311
  }
515
1312
  this.closeLogStream();
516
1313
  this.client.destroy();
517
- }
518
- /**
519
- * Sleep helper
520
- */
521
- sleep(ms) {
522
- return new Promise((resolve) => setTimeout(resolve, ms));
1314
+ this.hookRegistry.destroy();
523
1315
  }
524
1316
  /**
525
1317
  * Close the log file stream
@@ -543,5 +1335,159 @@ export class PtyWrapper extends EventEmitter {
543
1335
  get logPath() {
544
1336
  return this.logFilePath;
545
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
+ }
546
1492
  }
547
1493
  //# sourceMappingURL=pty-wrapper.js.map