agent-relay 2.0.29 → 2.0.32

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 (894) hide show
  1. package/README.md +19 -0
  2. package/dist/index.cjs +85691 -0
  3. package/dist/src/bridge/index.d.ts.map +1 -0
  4. package/dist/src/bridge/index.js.map +1 -0
  5. package/dist/src/cli/commands/doctor.d.ts +2 -0
  6. package/dist/src/cli/commands/doctor.d.ts.map +1 -0
  7. package/dist/src/cli/commands/doctor.js +451 -0
  8. package/dist/src/cli/commands/doctor.js.map +1 -0
  9. package/dist/src/cli/index.d.ts.map +1 -0
  10. package/dist/src/cli/index.js +29 -1
  11. package/dist/src/cli/index.js.map +1 -0
  12. package/dist/src/config/relay-config.d.ts.map +1 -0
  13. package/dist/src/config/relay-config.js.map +1 -0
  14. package/dist/src/continuity/index.d.ts.map +1 -0
  15. package/dist/src/continuity/index.js.map +1 -0
  16. package/dist/src/daemon/index.d.ts.map +1 -0
  17. package/dist/src/daemon/index.js.map +1 -0
  18. package/dist/src/health-worker-manager.d.ts.map +1 -0
  19. package/dist/src/health-worker-manager.js.map +1 -0
  20. package/dist/src/health-worker.d.ts.map +1 -0
  21. package/dist/src/health-worker.js.map +1 -0
  22. package/dist/src/hooks/index.d.ts.map +1 -0
  23. package/dist/src/hooks/index.js.map +1 -0
  24. package/dist/src/index.d.ts.map +1 -0
  25. package/dist/src/index.js.map +1 -0
  26. package/dist/src/memory/index.d.ts.map +1 -0
  27. package/dist/src/memory/index.js.map +1 -0
  28. package/dist/src/policy/index.d.ts.map +1 -0
  29. package/dist/src/policy/index.js.map +1 -0
  30. package/dist/src/protocol/index.d.ts.map +1 -0
  31. package/dist/src/protocol/index.js.map +1 -0
  32. package/dist/src/resiliency/index.d.ts.map +1 -0
  33. package/dist/src/resiliency/index.js.map +1 -0
  34. package/dist/src/shared/cli-auth-config.d.ts.map +1 -0
  35. package/dist/src/shared/cli-auth-config.js.map +1 -0
  36. package/dist/src/state/index.d.ts.map +1 -0
  37. package/dist/src/state/index.js.map +1 -0
  38. package/dist/src/storage/index.d.ts.map +1 -0
  39. package/dist/src/storage/index.js.map +1 -0
  40. package/dist/src/trajectory/index.d.ts.map +1 -0
  41. package/dist/src/trajectory/index.js.map +1 -0
  42. package/dist/src/utils/index.d.ts.map +1 -0
  43. package/dist/src/utils/index.js.map +1 -0
  44. package/dist/src/wrapper/index.d.ts.map +1 -0
  45. package/dist/src/wrapper/index.js.map +1 -0
  46. package/package.json +83 -20
  47. package/packages/api-types/dist/index.d.ts.map +1 -0
  48. package/packages/api-types/dist/index.js.map +1 -0
  49. package/packages/api-types/dist/schemas/agent.d.ts.map +1 -0
  50. package/packages/api-types/dist/schemas/agent.js.map +1 -0
  51. package/packages/api-types/dist/schemas/api.d.ts.map +1 -0
  52. package/packages/api-types/dist/schemas/api.js.map +1 -0
  53. package/packages/api-types/dist/schemas/decision.d.ts.map +1 -0
  54. package/packages/api-types/dist/schemas/decision.js.map +1 -0
  55. package/packages/api-types/dist/schemas/fleet.d.ts.map +1 -0
  56. package/packages/api-types/dist/schemas/fleet.js.map +1 -0
  57. package/packages/api-types/dist/schemas/history.d.ts.map +1 -0
  58. package/packages/api-types/dist/schemas/history.js.map +1 -0
  59. package/packages/api-types/dist/schemas/index.d.ts.map +1 -0
  60. package/packages/api-types/dist/schemas/index.js.map +1 -0
  61. package/packages/api-types/dist/schemas/message.d.ts.map +1 -0
  62. package/packages/api-types/dist/schemas/message.js.map +1 -0
  63. package/packages/api-types/dist/schemas/session.d.ts.map +1 -0
  64. package/packages/api-types/dist/schemas/session.js.map +1 -0
  65. package/packages/api-types/dist/schemas/task.d.ts.map +1 -0
  66. package/packages/api-types/dist/schemas/task.js.map +1 -0
  67. package/packages/api-types/package.json +1 -1
  68. package/packages/api-types/src/index.ts +22 -0
  69. package/packages/api-types/src/schemas/agent.test.ts +164 -0
  70. package/packages/api-types/src/schemas/agent.ts +110 -0
  71. package/packages/api-types/src/schemas/api.test.ts +372 -0
  72. package/packages/api-types/src/schemas/api.ts +194 -0
  73. package/packages/api-types/src/schemas/decision.test.ts +324 -0
  74. package/packages/api-types/src/schemas/decision.ts +136 -0
  75. package/packages/api-types/src/schemas/fleet.test.ts +212 -0
  76. package/packages/api-types/src/schemas/fleet.ts +83 -0
  77. package/packages/api-types/src/schemas/history.test.ts +242 -0
  78. package/packages/api-types/src/schemas/history.ts +84 -0
  79. package/packages/api-types/src/schemas/index.ts +148 -0
  80. package/packages/api-types/src/schemas/message.test.ts +192 -0
  81. package/packages/api-types/src/schemas/message.ts +98 -0
  82. package/packages/api-types/src/schemas/session.test.ts +104 -0
  83. package/packages/api-types/src/schemas/session.ts +40 -0
  84. package/packages/api-types/src/schemas/task.test.ts +192 -0
  85. package/packages/api-types/src/schemas/task.ts +78 -0
  86. package/packages/api-types/tsconfig.json +19 -0
  87. package/packages/api-types/vitest.config.ts +9 -0
  88. package/packages/benchmark/README.md +200 -0
  89. package/packages/benchmark/datasets/coding-tasks.yaml +127 -0
  90. package/packages/benchmark/datasets/coordination-tasks.yaml +122 -0
  91. package/packages/benchmark/dist/benchmark.d.ts +47 -0
  92. package/packages/benchmark/dist/benchmark.d.ts.map +1 -0
  93. package/packages/benchmark/dist/benchmark.js +224 -0
  94. package/packages/benchmark/dist/benchmark.js.map +1 -0
  95. package/packages/benchmark/dist/cli.d.ts +8 -0
  96. package/packages/benchmark/dist/cli.d.ts.map +1 -0
  97. package/packages/benchmark/dist/cli.js +185 -0
  98. package/packages/benchmark/dist/cli.js.map +1 -0
  99. package/packages/benchmark/dist/harbor.d.ts +53 -0
  100. package/packages/benchmark/dist/harbor.d.ts.map +1 -0
  101. package/packages/benchmark/dist/harbor.js +127 -0
  102. package/packages/benchmark/dist/harbor.js.map +1 -0
  103. package/packages/benchmark/dist/index.d.ts +48 -0
  104. package/packages/benchmark/dist/index.d.ts.map +1 -0
  105. package/packages/benchmark/dist/index.js +50 -0
  106. package/packages/benchmark/dist/index.js.map +1 -0
  107. package/packages/benchmark/dist/runners/base.d.ts +63 -0
  108. package/packages/benchmark/dist/runners/base.d.ts.map +1 -0
  109. package/packages/benchmark/dist/runners/base.js +155 -0
  110. package/packages/benchmark/dist/runners/base.js.map +1 -0
  111. package/packages/benchmark/dist/runners/index.d.ts +10 -0
  112. package/packages/benchmark/dist/runners/index.d.ts.map +1 -0
  113. package/packages/benchmark/dist/runners/index.js +10 -0
  114. package/packages/benchmark/dist/runners/index.js.map +1 -0
  115. package/packages/benchmark/dist/runners/single.d.ts +19 -0
  116. package/packages/benchmark/dist/runners/single.d.ts.map +1 -0
  117. package/packages/benchmark/dist/runners/single.js +111 -0
  118. package/packages/benchmark/dist/runners/single.js.map +1 -0
  119. package/packages/benchmark/dist/runners/subagent.d.ts +32 -0
  120. package/packages/benchmark/dist/runners/subagent.d.ts.map +1 -0
  121. package/packages/benchmark/dist/runners/subagent.js +212 -0
  122. package/packages/benchmark/dist/runners/subagent.js.map +1 -0
  123. package/packages/benchmark/dist/runners/swarm.d.ts +36 -0
  124. package/packages/benchmark/dist/runners/swarm.d.ts.map +1 -0
  125. package/packages/benchmark/dist/runners/swarm.js +273 -0
  126. package/packages/benchmark/dist/runners/swarm.js.map +1 -0
  127. package/packages/benchmark/dist/types.d.ts +178 -0
  128. package/packages/benchmark/dist/types.d.ts.map +1 -0
  129. package/packages/benchmark/dist/types.js +16 -0
  130. package/packages/benchmark/dist/types.js.map +1 -0
  131. package/packages/benchmark/package.json +80 -0
  132. package/packages/benchmark/src/benchmark.ts +298 -0
  133. package/packages/benchmark/src/cli.ts +240 -0
  134. package/packages/benchmark/src/harbor.ts +170 -0
  135. package/packages/benchmark/src/index.ts +73 -0
  136. package/packages/benchmark/src/runners/base.ts +204 -0
  137. package/packages/benchmark/src/runners/index.ts +10 -0
  138. package/packages/benchmark/src/runners/single.ts +121 -0
  139. package/packages/benchmark/src/runners/subagent.ts +240 -0
  140. package/packages/benchmark/src/runners/swarm.ts +326 -0
  141. package/packages/benchmark/src/types.ts +205 -0
  142. package/packages/benchmark/tsconfig.json +20 -0
  143. package/packages/bridge/dist/index.d.ts.map +1 -0
  144. package/packages/bridge/dist/index.js.map +1 -0
  145. package/packages/bridge/dist/multi-project-client.d.ts.map +1 -0
  146. package/packages/bridge/dist/multi-project-client.js.map +1 -0
  147. package/packages/bridge/dist/shadow-cli.d.ts.map +1 -0
  148. package/packages/bridge/dist/shadow-cli.js.map +1 -0
  149. package/packages/bridge/dist/spawner.d.ts.map +1 -0
  150. package/packages/bridge/dist/spawner.js +10 -2
  151. package/packages/bridge/dist/spawner.js.map +1 -0
  152. package/packages/bridge/dist/types.d.ts.map +1 -0
  153. package/packages/bridge/dist/types.js.map +1 -0
  154. package/packages/bridge/dist/utils.d.ts.map +1 -0
  155. package/packages/bridge/dist/utils.js.map +1 -0
  156. package/packages/bridge/package.json +8 -8
  157. package/packages/bridge/src/index.ts +25 -0
  158. package/packages/bridge/src/multi-project-client.test.ts +340 -0
  159. package/packages/bridge/src/multi-project-client.ts +469 -0
  160. package/packages/bridge/src/shadow-cli.ts +95 -0
  161. package/packages/bridge/src/spawner-mcp.test.ts +505 -0
  162. package/packages/bridge/src/spawner.ts +1724 -0
  163. package/packages/bridge/src/types.ts +145 -0
  164. package/packages/bridge/src/utils.test.ts +98 -0
  165. package/packages/bridge/src/utils.ts +67 -0
  166. package/packages/bridge/tsconfig.json +29 -0
  167. package/packages/bridge/vitest.config.ts +9 -0
  168. package/packages/cli-tester/dist/index.d.ts.map +1 -0
  169. package/packages/cli-tester/dist/index.js.map +1 -0
  170. package/packages/cli-tester/dist/utils/credential-check.d.ts.map +1 -0
  171. package/packages/cli-tester/dist/utils/credential-check.js.map +1 -0
  172. package/packages/cli-tester/dist/utils/socket-client.d.ts.map +1 -0
  173. package/packages/cli-tester/dist/utils/socket-client.js.map +1 -0
  174. package/packages/cli-tester/docker/Dockerfile +61 -0
  175. package/packages/cli-tester/docker/docker-compose.yml +71 -0
  176. package/packages/cli-tester/package.json +1 -1
  177. package/packages/cli-tester/src/index.ts +40 -0
  178. package/packages/cli-tester/src/utils/credential-check.ts +284 -0
  179. package/packages/cli-tester/src/utils/socket-client.ts +211 -0
  180. package/packages/cli-tester/tests/credential-check.test.ts +56 -0
  181. package/packages/cli-tester/tsconfig.json +11 -0
  182. package/packages/config/dist/agent-config.d.ts.map +1 -0
  183. package/packages/config/dist/agent-config.js.map +1 -0
  184. package/packages/config/dist/bridge-config.d.ts.map +1 -0
  185. package/packages/config/dist/bridge-config.js.map +1 -0
  186. package/packages/config/dist/bridge-utils.d.ts.map +1 -0
  187. package/packages/config/dist/bridge-utils.js.map +1 -0
  188. package/packages/config/dist/cli-auth-config.d.ts.map +1 -0
  189. package/packages/config/dist/cli-auth-config.js.map +1 -0
  190. package/packages/config/dist/cloud-config.d.ts.map +1 -0
  191. package/packages/config/dist/cloud-config.js.map +1 -0
  192. package/packages/config/dist/index.d.ts.map +1 -0
  193. package/packages/config/dist/index.js.map +1 -0
  194. package/packages/config/dist/project-namespace.d.ts.map +1 -0
  195. package/packages/config/dist/project-namespace.js.map +1 -0
  196. package/packages/config/dist/relay-config.d.ts.map +1 -0
  197. package/packages/config/dist/relay-config.js.map +1 -0
  198. package/packages/config/dist/relay-file-writer.d.ts.map +1 -0
  199. package/packages/config/dist/relay-file-writer.js.map +1 -0
  200. package/packages/config/dist/schemas.d.ts.map +1 -0
  201. package/packages/config/dist/schemas.js.map +1 -0
  202. package/packages/config/dist/shadow-config.d.ts.map +1 -0
  203. package/packages/config/dist/shadow-config.js.map +1 -0
  204. package/packages/config/dist/teams-config.d.ts.map +1 -0
  205. package/packages/config/dist/teams-config.js.map +1 -0
  206. package/packages/config/dist/trajectory-config.d.ts.map +1 -0
  207. package/packages/config/dist/trajectory-config.js.map +1 -0
  208. package/packages/config/package.json +2 -2
  209. package/packages/config/src/agent-config.test.ts +245 -0
  210. package/packages/config/src/agent-config.ts +160 -0
  211. package/packages/config/src/bridge-config.test.ts +132 -0
  212. package/packages/config/src/bridge-config.ts +189 -0
  213. package/packages/config/src/bridge-utils.ts +59 -0
  214. package/packages/config/src/cli-auth-config.ts +548 -0
  215. package/packages/config/src/cloud-config.ts +208 -0
  216. package/packages/config/src/index.ts +12 -0
  217. package/packages/config/src/project-namespace.ts +344 -0
  218. package/packages/config/src/relay-config.test.ts +51 -0
  219. package/packages/config/src/relay-config.ts +36 -0
  220. package/packages/config/src/relay-file-writer.test.ts +351 -0
  221. package/packages/config/src/relay-file-writer.ts +508 -0
  222. package/packages/config/src/schemas.test.ts +59 -0
  223. package/packages/config/src/schemas.ts +201 -0
  224. package/packages/config/src/shadow-config.ts +205 -0
  225. package/packages/config/src/teams-config.ts +135 -0
  226. package/packages/config/src/trajectory-config.ts +222 -0
  227. package/packages/config/tsconfig.json +21 -0
  228. package/packages/config/vitest.config.ts +9 -0
  229. package/packages/continuity/dist/formatter.d.ts.map +1 -0
  230. package/packages/continuity/dist/formatter.js.map +1 -0
  231. package/packages/continuity/dist/handoff-store.d.ts.map +1 -0
  232. package/packages/continuity/dist/handoff-store.js.map +1 -0
  233. package/packages/continuity/dist/index.d.ts.map +1 -0
  234. package/packages/continuity/dist/index.js.map +1 -0
  235. package/packages/continuity/dist/ledger-store.d.ts.map +1 -0
  236. package/packages/continuity/dist/ledger-store.js.map +1 -0
  237. package/packages/continuity/dist/manager.d.ts.map +1 -0
  238. package/packages/continuity/dist/manager.js.map +1 -0
  239. package/packages/continuity/dist/parser.d.ts.map +1 -0
  240. package/packages/continuity/dist/parser.js.map +1 -0
  241. package/packages/continuity/dist/types.d.ts.map +1 -0
  242. package/packages/continuity/dist/types.js.map +1 -0
  243. package/packages/continuity/package.json +1 -1
  244. package/packages/continuity/src/formatter.ts +371 -0
  245. package/packages/continuity/src/handoff-store.ts +523 -0
  246. package/packages/continuity/src/index.ts +9 -0
  247. package/packages/continuity/src/ledger-store.ts +594 -0
  248. package/packages/continuity/src/manager.test.ts +291 -0
  249. package/packages/continuity/src/manager.ts +774 -0
  250. package/packages/continuity/src/parser.test.ts +292 -0
  251. package/packages/continuity/src/parser.ts +680 -0
  252. package/packages/continuity/src/types.ts +211 -0
  253. package/packages/continuity/tsconfig.json +21 -0
  254. package/packages/continuity/vitest.config.ts +9 -0
  255. package/packages/daemon/dist/agent-manager.d.ts.map +1 -0
  256. package/packages/daemon/dist/agent-manager.js.map +1 -0
  257. package/packages/daemon/dist/agent-registry.d.ts.map +1 -0
  258. package/packages/daemon/dist/agent-registry.js.map +1 -0
  259. package/packages/daemon/dist/agent-signing.d.ts.map +1 -0
  260. package/packages/daemon/dist/agent-signing.js.map +1 -0
  261. package/packages/daemon/dist/api.d.ts.map +1 -0
  262. package/packages/daemon/dist/api.js.map +1 -0
  263. package/packages/daemon/dist/auth.d.ts.map +1 -0
  264. package/packages/daemon/dist/auth.js.map +1 -0
  265. package/packages/daemon/dist/channel-membership-store.d.ts.map +1 -0
  266. package/packages/daemon/dist/channel-membership-store.js.map +1 -0
  267. package/packages/daemon/dist/cli-auth.d.ts.map +1 -0
  268. package/packages/daemon/dist/cli-auth.js.map +1 -0
  269. package/packages/daemon/dist/cloud-sync.d.ts.map +1 -0
  270. package/packages/daemon/dist/cloud-sync.js.map +1 -0
  271. package/packages/daemon/dist/connection.d.ts.map +1 -0
  272. package/packages/daemon/dist/connection.js.map +1 -0
  273. package/packages/daemon/dist/consensus-integration.d.ts.map +1 -0
  274. package/packages/daemon/dist/consensus-integration.js.map +1 -0
  275. package/packages/daemon/dist/consensus.d.ts.map +1 -0
  276. package/packages/daemon/dist/consensus.js.map +1 -0
  277. package/packages/daemon/dist/delivery-tracker.d.ts.map +1 -0
  278. package/packages/daemon/dist/delivery-tracker.js.map +1 -0
  279. package/packages/daemon/dist/enhanced-features.d.ts.map +1 -0
  280. package/packages/daemon/dist/enhanced-features.js.map +1 -0
  281. package/packages/daemon/dist/index.d.ts.map +1 -0
  282. package/packages/daemon/dist/index.js.map +1 -0
  283. package/packages/daemon/dist/migrations/index.d.ts.map +1 -0
  284. package/packages/daemon/dist/migrations/index.js.map +1 -0
  285. package/packages/daemon/dist/orchestrator.d.ts.map +1 -0
  286. package/packages/daemon/dist/orchestrator.js.map +1 -0
  287. package/packages/daemon/dist/rate-limiter.d.ts.map +1 -0
  288. package/packages/daemon/dist/rate-limiter.js.map +1 -0
  289. package/packages/daemon/dist/registry.d.ts.map +1 -0
  290. package/packages/daemon/dist/registry.js.map +1 -0
  291. package/packages/daemon/dist/relay-ledger.d.ts.map +1 -0
  292. package/packages/daemon/dist/relay-ledger.js.map +1 -0
  293. package/packages/daemon/dist/relay-watchdog.d.ts.map +1 -0
  294. package/packages/daemon/dist/relay-watchdog.js.map +1 -0
  295. package/packages/daemon/dist/repo-manager.d.ts.map +1 -0
  296. package/packages/daemon/dist/repo-manager.js.map +1 -0
  297. package/packages/daemon/dist/router.d.ts.map +1 -0
  298. package/packages/daemon/dist/router.js.map +1 -0
  299. package/packages/daemon/dist/server.d.ts +1 -0
  300. package/packages/daemon/dist/server.d.ts.map +1 -0
  301. package/packages/daemon/dist/server.js +46 -16
  302. package/packages/daemon/dist/server.js.map +1 -0
  303. package/packages/daemon/dist/spawn-manager.d.ts.map +1 -0
  304. package/packages/daemon/dist/spawn-manager.js.map +1 -0
  305. package/packages/daemon/dist/sync-queue.d.ts.map +1 -0
  306. package/packages/daemon/dist/sync-queue.js.map +1 -0
  307. package/packages/daemon/dist/types.d.ts.map +1 -0
  308. package/packages/daemon/dist/types.js.map +1 -0
  309. package/packages/daemon/dist/workspace-manager.d.ts.map +1 -0
  310. package/packages/daemon/dist/workspace-manager.js.map +1 -0
  311. package/packages/daemon/package.json +12 -12
  312. package/packages/daemon/src/agent-manager.ts +679 -0
  313. package/packages/daemon/src/agent-registry.ts +284 -0
  314. package/packages/daemon/src/agent-signing.ts +707 -0
  315. package/packages/daemon/src/api.ts +1012 -0
  316. package/packages/daemon/src/auth.ts +276 -0
  317. package/packages/daemon/src/channel-membership-store.ts +217 -0
  318. package/packages/daemon/src/cli-auth.ts +906 -0
  319. package/packages/daemon/src/cloud-sync.ts +902 -0
  320. package/packages/daemon/src/connection.ts +534 -0
  321. package/packages/daemon/src/consensus-integration.ts +510 -0
  322. package/packages/daemon/src/consensus.ts +848 -0
  323. package/packages/daemon/src/delivery-tracker.ts +145 -0
  324. package/packages/daemon/src/enhanced-features.ts +390 -0
  325. package/packages/daemon/src/index.ts +52 -0
  326. package/packages/daemon/src/migrations/0001_initial.sql +72 -0
  327. package/packages/daemon/src/migrations/index.test.ts +195 -0
  328. package/packages/daemon/src/migrations/index.ts +286 -0
  329. package/packages/daemon/src/orchestrator.test.ts +231 -0
  330. package/packages/daemon/src/orchestrator.ts +1376 -0
  331. package/packages/daemon/src/rate-limiter.ts +172 -0
  332. package/packages/daemon/src/registry.ts +8 -0
  333. package/packages/daemon/src/relay-ledger.test.ts +358 -0
  334. package/packages/daemon/src/relay-ledger.ts +713 -0
  335. package/packages/daemon/src/relay-watchdog.test.ts +881 -0
  336. package/packages/daemon/src/relay-watchdog.ts +785 -0
  337. package/packages/daemon/src/repo-manager.ts +468 -0
  338. package/packages/daemon/src/router.test.ts +149 -0
  339. package/packages/daemon/src/router.ts +1885 -0
  340. package/packages/daemon/src/server.ts +1871 -0
  341. package/packages/daemon/src/spawn-manager.ts +275 -0
  342. package/packages/daemon/src/sync-queue.ts +477 -0
  343. package/packages/daemon/src/types.ts +158 -0
  344. package/packages/daemon/src/workspace-manager.ts +371 -0
  345. package/packages/daemon/tsconfig.json +21 -0
  346. package/packages/hooks/dist/browser.d.ts.map +1 -0
  347. package/packages/hooks/dist/browser.js.map +1 -0
  348. package/packages/hooks/dist/emitter.d.ts.map +1 -0
  349. package/packages/hooks/dist/emitter.js.map +1 -0
  350. package/packages/hooks/dist/inbox-check/hook.d.ts.map +1 -0
  351. package/packages/hooks/dist/inbox-check/hook.js.map +1 -0
  352. package/packages/hooks/dist/inbox-check/index.d.ts.map +1 -0
  353. package/packages/hooks/dist/inbox-check/index.js.map +1 -0
  354. package/packages/hooks/dist/inbox-check/types.d.ts.map +1 -0
  355. package/packages/hooks/dist/inbox-check/types.js.map +1 -0
  356. package/packages/hooks/dist/inbox-check/utils.d.ts.map +1 -0
  357. package/packages/hooks/dist/inbox-check/utils.js.map +1 -0
  358. package/packages/hooks/dist/index.d.ts.map +1 -0
  359. package/packages/hooks/dist/index.js.map +1 -0
  360. package/packages/hooks/dist/registry.d.ts.map +1 -0
  361. package/packages/hooks/dist/registry.js.map +1 -0
  362. package/packages/hooks/dist/trajectory-hooks.d.ts.map +1 -0
  363. package/packages/hooks/dist/trajectory-hooks.js.map +1 -0
  364. package/packages/hooks/dist/types.d.ts.map +1 -0
  365. package/packages/hooks/dist/types.js.map +1 -0
  366. package/packages/hooks/package.json +4 -4
  367. package/packages/hooks/src/browser.ts +2 -0
  368. package/packages/hooks/src/emitter.ts +84 -0
  369. package/packages/hooks/src/inbox-check/hook.ts +114 -0
  370. package/packages/hooks/src/inbox-check/index.ts +8 -0
  371. package/packages/hooks/src/inbox-check/types.ts +39 -0
  372. package/packages/hooks/src/inbox-check/utils.test.ts +287 -0
  373. package/packages/hooks/src/inbox-check/utils.ts +125 -0
  374. package/packages/hooks/src/index.ts +11 -0
  375. package/packages/hooks/src/registry.ts +614 -0
  376. package/packages/hooks/src/shims.d.ts +3 -0
  377. package/packages/hooks/src/trajectory-hooks.ts +251 -0
  378. package/packages/hooks/src/types.ts +342 -0
  379. package/packages/hooks/tsconfig.json +21 -0
  380. package/packages/hooks/vitest.config.ts +9 -0
  381. package/packages/mcp/dist/bin.d.ts.map +1 -0
  382. package/packages/mcp/dist/bin.js.map +1 -0
  383. package/packages/mcp/dist/client.d.ts +9 -15
  384. package/packages/mcp/dist/client.d.ts.map +1 -0
  385. package/packages/mcp/dist/client.js +42 -74
  386. package/packages/mcp/dist/client.js.map +1 -0
  387. package/packages/mcp/dist/cloud.d.ts.map +1 -0
  388. package/packages/mcp/dist/cloud.js.map +1 -0
  389. package/packages/mcp/dist/errors.d.ts.map +1 -0
  390. package/packages/mcp/dist/errors.js.map +1 -0
  391. package/packages/mcp/dist/file-transport.d.ts.map +1 -0
  392. package/packages/mcp/dist/file-transport.js.map +1 -0
  393. package/packages/mcp/dist/hybrid-client.d.ts.map +1 -0
  394. package/packages/mcp/dist/hybrid-client.js.map +1 -0
  395. package/packages/mcp/dist/index.d.ts.map +1 -0
  396. package/packages/mcp/dist/index.js.map +1 -0
  397. package/packages/mcp/dist/install-cli.d.ts.map +1 -0
  398. package/packages/mcp/dist/install-cli.js.map +1 -0
  399. package/packages/mcp/dist/install.d.ts.map +1 -0
  400. package/packages/mcp/dist/install.js.map +1 -0
  401. package/packages/mcp/dist/prompts/index.d.ts.map +1 -0
  402. package/packages/mcp/dist/prompts/index.js.map +1 -0
  403. package/packages/mcp/dist/prompts/protocol.d.ts.map +1 -0
  404. package/packages/mcp/dist/prompts/protocol.js.map +1 -0
  405. package/packages/mcp/dist/resources/agents.d.ts.map +1 -0
  406. package/packages/mcp/dist/resources/agents.js.map +1 -0
  407. package/packages/mcp/dist/resources/inbox.d.ts.map +1 -0
  408. package/packages/mcp/dist/resources/inbox.js.map +1 -0
  409. package/packages/mcp/dist/resources/index.d.ts.map +1 -0
  410. package/packages/mcp/dist/resources/index.js.map +1 -0
  411. package/packages/mcp/dist/resources/project.d.ts.map +1 -0
  412. package/packages/mcp/dist/resources/project.js.map +1 -0
  413. package/packages/mcp/dist/server.d.ts.map +1 -0
  414. package/packages/mcp/dist/server.js.map +1 -0
  415. package/packages/mcp/dist/simple.d.ts +2 -5
  416. package/packages/mcp/dist/simple.d.ts.map +1 -0
  417. package/packages/mcp/dist/simple.js.map +1 -0
  418. package/packages/mcp/dist/tools/index.d.ts.map +1 -0
  419. package/packages/mcp/dist/tools/index.js.map +1 -0
  420. package/packages/mcp/dist/tools/relay-broadcast.d.ts.map +1 -0
  421. package/packages/mcp/dist/tools/relay-broadcast.js.map +1 -0
  422. package/packages/mcp/dist/tools/relay-channel.d.ts.map +1 -0
  423. package/packages/mcp/dist/tools/relay-channel.js.map +1 -0
  424. package/packages/mcp/dist/tools/relay-connected.d.ts.map +1 -0
  425. package/packages/mcp/dist/tools/relay-connected.js.map +1 -0
  426. package/packages/mcp/dist/tools/relay-consensus.d.ts.map +1 -0
  427. package/packages/mcp/dist/tools/relay-consensus.js.map +1 -0
  428. package/packages/mcp/dist/tools/relay-continuity.d.ts.map +1 -0
  429. package/packages/mcp/dist/tools/relay-continuity.js.map +1 -0
  430. package/packages/mcp/dist/tools/relay-health.d.ts.map +1 -0
  431. package/packages/mcp/dist/tools/relay-health.js.map +1 -0
  432. package/packages/mcp/dist/tools/relay-inbox.d.ts.map +1 -0
  433. package/packages/mcp/dist/tools/relay-inbox.js.map +1 -0
  434. package/packages/mcp/dist/tools/relay-logs.d.ts.map +1 -0
  435. package/packages/mcp/dist/tools/relay-logs.js.map +1 -0
  436. package/packages/mcp/dist/tools/relay-metrics.d.ts.map +1 -0
  437. package/packages/mcp/dist/tools/relay-metrics.js.map +1 -0
  438. package/packages/mcp/dist/tools/relay-release.d.ts.map +1 -0
  439. package/packages/mcp/dist/tools/relay-release.js.map +1 -0
  440. package/packages/mcp/dist/tools/relay-remove-agent.d.ts.map +1 -0
  441. package/packages/mcp/dist/tools/relay-remove-agent.js.map +1 -0
  442. package/packages/mcp/dist/tools/relay-send.d.ts.map +1 -0
  443. package/packages/mcp/dist/tools/relay-send.js +4 -2
  444. package/packages/mcp/dist/tools/relay-send.js.map +1 -0
  445. package/packages/mcp/dist/tools/relay-shadow.d.ts.map +1 -0
  446. package/packages/mcp/dist/tools/relay-shadow.js.map +1 -0
  447. package/packages/mcp/dist/tools/relay-spawn.d.ts.map +1 -0
  448. package/packages/mcp/dist/tools/relay-spawn.js.map +1 -0
  449. package/packages/mcp/dist/tools/relay-status.d.ts.map +1 -0
  450. package/packages/mcp/dist/tools/relay-status.js.map +1 -0
  451. package/packages/mcp/dist/tools/relay-subscribe.d.ts.map +1 -0
  452. package/packages/mcp/dist/tools/relay-subscribe.js.map +1 -0
  453. package/packages/mcp/dist/tools/relay-who.d.ts.map +1 -0
  454. package/packages/mcp/dist/tools/relay-who.js.map +1 -0
  455. package/packages/mcp/package.json +3 -3
  456. package/packages/mcp/src/bin.ts +149 -0
  457. package/packages/mcp/src/client.ts +400 -0
  458. package/packages/mcp/src/cloud.ts +523 -0
  459. package/packages/mcp/src/errors.ts +54 -0
  460. package/packages/mcp/src/file-transport.ts +268 -0
  461. package/packages/mcp/src/hybrid-client.ts +209 -0
  462. package/packages/mcp/src/index.ts +122 -0
  463. package/packages/mcp/src/install-cli.ts +210 -0
  464. package/packages/mcp/src/install.ts +745 -0
  465. package/packages/mcp/src/prompts/index.ts +1 -0
  466. package/packages/mcp/src/prompts/protocol.ts +164 -0
  467. package/packages/mcp/src/resources/agents.ts +21 -0
  468. package/packages/mcp/src/resources/inbox.ts +21 -0
  469. package/packages/mcp/src/resources/index.ts +3 -0
  470. package/packages/mcp/src/resources/project.ts +29 -0
  471. package/packages/mcp/src/server.ts +431 -0
  472. package/packages/mcp/src/simple.ts +214 -0
  473. package/packages/mcp/src/tools/index.ts +133 -0
  474. package/packages/mcp/src/tools/relay-broadcast.ts +32 -0
  475. package/packages/mcp/src/tools/relay-channel.ts +93 -0
  476. package/packages/mcp/src/tools/relay-connected.ts +52 -0
  477. package/packages/mcp/src/tools/relay-consensus.ts +92 -0
  478. package/packages/mcp/src/tools/relay-continuity.ts +127 -0
  479. package/packages/mcp/src/tools/relay-health.ts +148 -0
  480. package/packages/mcp/src/tools/relay-inbox.ts +70 -0
  481. package/packages/mcp/src/tools/relay-logs.ts +106 -0
  482. package/packages/mcp/src/tools/relay-metrics.ts +140 -0
  483. package/packages/mcp/src/tools/relay-release.ts +54 -0
  484. package/packages/mcp/src/tools/relay-remove-agent.ts +58 -0
  485. package/packages/mcp/src/tools/relay-send.ts +84 -0
  486. package/packages/mcp/src/tools/relay-shadow.ts +67 -0
  487. package/packages/mcp/src/tools/relay-spawn.ts +87 -0
  488. package/packages/mcp/src/tools/relay-status.ts +57 -0
  489. package/packages/mcp/src/tools/relay-subscribe.ts +61 -0
  490. package/packages/mcp/src/tools/relay-who.ts +59 -0
  491. package/packages/mcp/tests/client.test.ts +476 -0
  492. package/packages/mcp/tests/discover.test.ts +195 -0
  493. package/packages/mcp/tests/install.test.ts +123 -0
  494. package/packages/mcp/tests/prompts.test.ts +12 -0
  495. package/packages/mcp/tests/resources.test.ts +53 -0
  496. package/packages/mcp/tests/tools.test.ts +1242 -0
  497. package/packages/mcp/tsconfig.json +22 -0
  498. package/packages/mcp/vitest.config.ts +9 -0
  499. package/packages/memory/dist/adapters/index.d.ts.map +1 -0
  500. package/packages/memory/dist/adapters/index.js.map +1 -0
  501. package/packages/memory/dist/adapters/inmemory.d.ts.map +1 -0
  502. package/packages/memory/dist/adapters/inmemory.js.map +1 -0
  503. package/packages/memory/dist/adapters/supermemory.d.ts.map +1 -0
  504. package/packages/memory/dist/adapters/supermemory.js.map +1 -0
  505. package/packages/memory/dist/context-compaction.d.ts.map +1 -0
  506. package/packages/memory/dist/context-compaction.js.map +1 -0
  507. package/packages/memory/dist/factory.d.ts.map +1 -0
  508. package/packages/memory/dist/factory.js.map +1 -0
  509. package/packages/memory/dist/index.d.ts.map +1 -0
  510. package/packages/memory/dist/index.js.map +1 -0
  511. package/packages/memory/dist/memory-hooks.d.ts.map +1 -0
  512. package/packages/memory/dist/memory-hooks.js.map +1 -0
  513. package/packages/memory/dist/service.d.ts.map +1 -0
  514. package/packages/memory/dist/service.js.map +1 -0
  515. package/packages/memory/dist/types.d.ts.map +1 -0
  516. package/packages/memory/dist/types.js.map +1 -0
  517. package/packages/memory/package.json +2 -2
  518. package/packages/memory/src/adapters/index.ts +8 -0
  519. package/packages/memory/src/adapters/inmemory.ts +265 -0
  520. package/packages/memory/src/adapters/supermemory.ts +449 -0
  521. package/packages/memory/src/context-compaction.test.ts +660 -0
  522. package/packages/memory/src/context-compaction.ts +612 -0
  523. package/packages/memory/src/factory.ts +170 -0
  524. package/packages/memory/src/index.ts +33 -0
  525. package/packages/memory/src/memory-hooks.ts +410 -0
  526. package/packages/memory/src/service.ts +194 -0
  527. package/packages/memory/src/types.ts +211 -0
  528. package/packages/memory/tsconfig.json +21 -0
  529. package/packages/memory/vitest.config.ts +9 -0
  530. package/packages/policy/dist/agent-policy.d.ts.map +1 -0
  531. package/packages/policy/dist/agent-policy.js.map +1 -0
  532. package/packages/policy/dist/cloud-policy-fetcher.d.ts.map +1 -0
  533. package/packages/policy/dist/cloud-policy-fetcher.js.map +1 -0
  534. package/packages/policy/dist/index.d.ts.map +1 -0
  535. package/packages/policy/dist/index.js.map +1 -0
  536. package/packages/policy/package.json +2 -2
  537. package/packages/policy/src/agent-policy.ts +866 -0
  538. package/packages/policy/src/cloud-policy-fetcher.ts +78 -0
  539. package/packages/policy/src/index.ts +21 -0
  540. package/packages/policy/tsconfig.json +21 -0
  541. package/packages/policy/vitest.config.ts +9 -0
  542. package/packages/protocol/dist/channels.d.ts.map +1 -0
  543. package/packages/protocol/dist/channels.js.map +1 -0
  544. package/packages/protocol/dist/framing.d.ts.map +1 -0
  545. package/packages/protocol/dist/framing.js.map +1 -0
  546. package/packages/protocol/dist/id-generator.d.ts.map +1 -0
  547. package/packages/protocol/dist/id-generator.js.map +1 -0
  548. package/packages/protocol/dist/index.d.ts.map +1 -0
  549. package/packages/protocol/dist/index.js.map +1 -0
  550. package/packages/protocol/dist/relay-pty-schemas.d.ts +70 -2
  551. package/packages/protocol/dist/relay-pty-schemas.d.ts.map +1 -0
  552. package/packages/protocol/dist/relay-pty-schemas.js.map +1 -0
  553. package/packages/protocol/dist/types.d.ts +8 -0
  554. package/packages/protocol/dist/types.d.ts.map +1 -0
  555. package/packages/protocol/dist/types.js.map +1 -0
  556. package/packages/protocol/package.json +1 -1
  557. package/packages/protocol/src/channels.test.ts +330 -0
  558. package/packages/protocol/src/channels.ts +270 -0
  559. package/packages/protocol/src/framing.test.ts +164 -0
  560. package/packages/protocol/src/framing.ts +242 -0
  561. package/packages/protocol/src/id-generator.ts +69 -0
  562. package/packages/protocol/src/index.ts +4 -0
  563. package/packages/protocol/src/relay-pty-schemas.ts +400 -0
  564. package/packages/protocol/src/types.test.ts +271 -0
  565. package/packages/protocol/src/types.ts +846 -0
  566. package/packages/protocol/tsconfig.json +21 -0
  567. package/packages/protocol/vitest.config.ts +9 -0
  568. package/packages/resiliency/dist/cgroup-manager.d.ts.map +1 -0
  569. package/packages/resiliency/dist/cgroup-manager.js.map +1 -0
  570. package/packages/resiliency/dist/context-persistence.d.ts.map +1 -0
  571. package/packages/resiliency/dist/context-persistence.js.map +1 -0
  572. package/packages/resiliency/dist/crash-insights.d.ts.map +1 -0
  573. package/packages/resiliency/dist/crash-insights.js.map +1 -0
  574. package/packages/resiliency/dist/gossip-health.d.ts.map +1 -0
  575. package/packages/resiliency/dist/gossip-health.js.map +1 -0
  576. package/packages/resiliency/dist/health-monitor.d.ts.map +1 -0
  577. package/packages/resiliency/dist/health-monitor.js.map +1 -0
  578. package/packages/resiliency/dist/index.d.ts.map +1 -0
  579. package/packages/resiliency/dist/index.js.map +1 -0
  580. package/packages/resiliency/dist/leader-watchdog.d.ts.map +1 -0
  581. package/packages/resiliency/dist/leader-watchdog.js.map +1 -0
  582. package/packages/resiliency/dist/logger.d.ts.map +1 -0
  583. package/packages/resiliency/dist/logger.js.map +1 -0
  584. package/packages/resiliency/dist/memory-monitor.d.ts.map +1 -0
  585. package/packages/resiliency/dist/memory-monitor.js.map +1 -0
  586. package/packages/resiliency/dist/metrics.d.ts.map +1 -0
  587. package/packages/resiliency/dist/metrics.js.map +1 -0
  588. package/packages/resiliency/dist/provider-context.d.ts.map +1 -0
  589. package/packages/resiliency/dist/provider-context.js.map +1 -0
  590. package/packages/resiliency/dist/stateless-lead.d.ts.map +1 -0
  591. package/packages/resiliency/dist/stateless-lead.js.map +1 -0
  592. package/packages/resiliency/dist/supervisor.d.ts.map +1 -0
  593. package/packages/resiliency/dist/supervisor.js.map +1 -0
  594. package/packages/resiliency/package.json +1 -1
  595. package/packages/resiliency/src/cgroup-manager.ts +468 -0
  596. package/packages/resiliency/src/context-persistence.ts +538 -0
  597. package/packages/resiliency/src/crash-insights.test.ts +620 -0
  598. package/packages/resiliency/src/crash-insights.ts +660 -0
  599. package/packages/resiliency/src/gossip-health.ts +333 -0
  600. package/packages/resiliency/src/health-monitor.ts +371 -0
  601. package/packages/resiliency/src/index.ts +157 -0
  602. package/packages/resiliency/src/leader-watchdog.ts +260 -0
  603. package/packages/resiliency/src/logger.ts +320 -0
  604. package/packages/resiliency/src/memory-monitor.test.ts +637 -0
  605. package/packages/resiliency/src/memory-monitor.ts +740 -0
  606. package/packages/resiliency/src/metrics.ts +311 -0
  607. package/packages/resiliency/src/provider-context.ts +452 -0
  608. package/packages/resiliency/src/stateless-lead.ts +408 -0
  609. package/packages/resiliency/src/supervisor.ts +578 -0
  610. package/packages/resiliency/tsconfig.json +21 -0
  611. package/packages/resiliency/vitest.config.ts +9 -0
  612. package/packages/sdk/dist/client.d.ts.map +1 -0
  613. package/packages/sdk/dist/client.js.map +1 -0
  614. package/packages/sdk/dist/index.d.ts.map +1 -0
  615. package/packages/sdk/dist/index.js.map +1 -0
  616. package/packages/sdk/dist/logs.d.ts.map +1 -0
  617. package/packages/sdk/dist/logs.js.map +1 -0
  618. package/packages/sdk/dist/protocol/index.d.ts.map +1 -0
  619. package/packages/sdk/dist/protocol/index.js.map +1 -0
  620. package/packages/sdk/dist/standalone.d.ts.map +1 -0
  621. package/packages/sdk/dist/standalone.js.map +1 -0
  622. package/packages/sdk/examples/SWARM_CAPABILITIES.md +498 -0
  623. package/packages/sdk/examples/SWARM_PATTERNS.md +541 -0
  624. package/packages/sdk/package.json +2 -2
  625. package/packages/sdk/src/client.test.ts +568 -0
  626. package/packages/sdk/src/client.ts +1418 -0
  627. package/packages/sdk/src/index.ts +103 -0
  628. package/packages/sdk/src/logs.test.ts +98 -0
  629. package/packages/sdk/src/logs.ts +126 -0
  630. package/packages/sdk/src/protocol/framing.test.ts +164 -0
  631. package/packages/sdk/src/protocol/index.ts +8 -0
  632. package/packages/sdk/src/standalone.ts +176 -0
  633. package/packages/sdk/tsconfig.json +22 -0
  634. package/packages/sdk/vitest.config.ts +9 -0
  635. package/packages/spawner/.trajectories/index.json +5 -0
  636. package/packages/spawner/dist/index.d.ts.map +1 -0
  637. package/packages/spawner/dist/index.js.map +1 -0
  638. package/packages/spawner/dist/types.d.ts.map +1 -0
  639. package/packages/spawner/dist/types.js.map +1 -0
  640. package/packages/spawner/package.json +1 -1
  641. package/packages/spawner/src/index.ts +8 -0
  642. package/packages/spawner/src/types.test.ts +385 -0
  643. package/packages/spawner/src/types.ts +228 -0
  644. package/packages/spawner/tsconfig.json +19 -0
  645. package/packages/spawner/vitest.config.ts +9 -0
  646. package/packages/state/dist/agent-state.d.ts.map +1 -0
  647. package/packages/state/dist/agent-state.js.map +1 -0
  648. package/packages/state/dist/index.d.ts.map +1 -0
  649. package/packages/state/dist/index.js.map +1 -0
  650. package/packages/state/package.json +1 -1
  651. package/packages/state/src/agent-state.test.ts +335 -0
  652. package/packages/state/src/agent-state.ts +153 -0
  653. package/packages/state/src/index.ts +12 -0
  654. package/packages/state/tsconfig.json +21 -0
  655. package/packages/state/vitest.config.ts +9 -0
  656. package/packages/storage/dist/adapter.d.ts +28 -1
  657. package/packages/storage/dist/adapter.d.ts.map +1 -0
  658. package/packages/storage/dist/adapter.js +104 -10
  659. package/packages/storage/dist/adapter.js.map +1 -0
  660. package/packages/storage/dist/batched-sqlite-adapter.d.ts.map +1 -0
  661. package/packages/storage/dist/batched-sqlite-adapter.js.map +1 -0
  662. package/packages/storage/dist/dead-letter-queue.d.ts.map +1 -0
  663. package/packages/storage/dist/dead-letter-queue.js.map +1 -0
  664. package/packages/storage/dist/dlq-adapter.d.ts.map +1 -0
  665. package/packages/storage/dist/dlq-adapter.js.map +1 -0
  666. package/packages/storage/dist/index.d.ts +1 -0
  667. package/packages/storage/dist/index.d.ts.map +1 -0
  668. package/packages/storage/dist/index.js +1 -0
  669. package/packages/storage/dist/index.js.map +1 -0
  670. package/packages/storage/dist/jsonl-adapter.d.ts +77 -0
  671. package/packages/storage/dist/jsonl-adapter.d.ts.map +1 -0
  672. package/packages/storage/dist/jsonl-adapter.js +505 -0
  673. package/packages/storage/dist/jsonl-adapter.js.map +1 -0
  674. package/packages/storage/dist/sqlite-adapter.d.ts +6 -1
  675. package/packages/storage/dist/sqlite-adapter.d.ts.map +1 -0
  676. package/packages/storage/dist/sqlite-adapter.js +47 -0
  677. package/packages/storage/dist/sqlite-adapter.js.map +1 -0
  678. package/packages/storage/package.json +2 -2
  679. package/packages/storage/src/adapter.ts +438 -0
  680. package/packages/storage/src/batched-sqlite-adapter.test.ts +240 -0
  681. package/packages/storage/src/batched-sqlite-adapter.ts +239 -0
  682. package/packages/storage/src/dead-letter-queue.ts +643 -0
  683. package/packages/storage/src/dlq-adapter.test.ts +492 -0
  684. package/packages/storage/src/dlq-adapter.ts +954 -0
  685. package/packages/storage/src/index.ts +6 -0
  686. package/packages/storage/src/jsonl-adapter.test.ts +200 -0
  687. package/packages/storage/src/jsonl-adapter.ts +618 -0
  688. package/packages/storage/src/memory-adapter.test.ts +36 -0
  689. package/packages/storage/src/sqlite-adapter.test.ts +562 -0
  690. package/packages/storage/src/sqlite-adapter.ts +1058 -0
  691. package/packages/storage/tsconfig.json +21 -0
  692. package/packages/storage/vitest.config.ts +9 -0
  693. package/packages/telemetry/dist/client.d.ts.map +1 -0
  694. package/packages/telemetry/dist/client.js.map +1 -0
  695. package/packages/telemetry/dist/config.d.ts.map +1 -0
  696. package/packages/telemetry/dist/config.js.map +1 -0
  697. package/packages/telemetry/dist/events.d.ts.map +1 -0
  698. package/packages/telemetry/dist/events.js.map +1 -0
  699. package/packages/telemetry/dist/index.d.ts.map +1 -0
  700. package/packages/telemetry/dist/index.js.map +1 -0
  701. package/packages/telemetry/dist/machine-id.d.ts.map +1 -0
  702. package/packages/telemetry/dist/machine-id.js.map +1 -0
  703. package/packages/telemetry/dist/posthog-config.d.ts.map +1 -0
  704. package/packages/telemetry/dist/posthog-config.js.map +1 -0
  705. package/packages/telemetry/package.json +1 -1
  706. package/packages/telemetry/src/client.ts +158 -0
  707. package/packages/telemetry/src/config.ts +110 -0
  708. package/packages/telemetry/src/events.ts +137 -0
  709. package/packages/telemetry/src/index.ts +46 -0
  710. package/packages/telemetry/src/machine-id.ts +63 -0
  711. package/packages/telemetry/src/posthog-config.ts +39 -0
  712. package/packages/telemetry/tsconfig.json +21 -0
  713. package/packages/trajectory/dist/index.d.ts.map +1 -0
  714. package/packages/trajectory/dist/index.js.map +1 -0
  715. package/packages/trajectory/dist/integration.d.ts.map +1 -0
  716. package/packages/trajectory/dist/integration.js.map +1 -0
  717. package/packages/trajectory/package.json +2 -2
  718. package/packages/trajectory/src/index.ts +1 -0
  719. package/packages/trajectory/src/integration.ts +1268 -0
  720. package/packages/trajectory/tsconfig.json +21 -0
  721. package/packages/trajectory/vitest.config.ts +9 -0
  722. package/packages/user-directory/dist/index.d.ts.map +1 -0
  723. package/packages/user-directory/dist/index.js.map +1 -0
  724. package/packages/user-directory/dist/user-directory.d.ts.map +1 -0
  725. package/packages/user-directory/dist/user-directory.js.map +1 -0
  726. package/packages/user-directory/package.json +2 -2
  727. package/packages/user-directory/src/index.ts +12 -0
  728. package/packages/user-directory/src/user-directory.ts +393 -0
  729. package/packages/user-directory/tsconfig.json +21 -0
  730. package/packages/user-directory/vitest.config.ts +9 -0
  731. package/packages/utils/dist/cjs/client-helpers.js +127 -0
  732. package/packages/utils/dist/cjs/command-resolver.js +89 -0
  733. package/packages/utils/dist/cjs/error-tracking.js +106 -0
  734. package/packages/utils/dist/cjs/git-remote.js +120 -0
  735. package/packages/utils/dist/cjs/index.js +40 -0
  736. package/packages/utils/dist/cjs/logger.js +105 -0
  737. package/packages/utils/dist/cjs/model-mapping.js +54 -0
  738. package/packages/utils/dist/cjs/name-generator.js +179 -0
  739. package/packages/utils/dist/cjs/package.json +3 -0
  740. package/packages/utils/dist/cjs/precompiled-patterns.js +271 -0
  741. package/packages/utils/dist/cjs/relay-pty-path.js +143 -0
  742. package/packages/utils/dist/cjs/update-checker.js +185 -0
  743. package/packages/utils/dist/client-helpers.d.ts +73 -0
  744. package/packages/utils/dist/client-helpers.d.ts.map +1 -0
  745. package/packages/utils/dist/client-helpers.js +130 -0
  746. package/packages/utils/dist/client-helpers.js.map +1 -0
  747. package/packages/utils/dist/command-resolver.d.ts.map +1 -0
  748. package/packages/utils/dist/command-resolver.js.map +1 -0
  749. package/packages/utils/dist/error-tracking.d.ts.map +1 -0
  750. package/packages/utils/dist/error-tracking.js.map +1 -0
  751. package/packages/utils/dist/git-remote.d.ts.map +1 -0
  752. package/packages/utils/dist/git-remote.js.map +1 -0
  753. package/packages/utils/dist/index.d.ts +1 -0
  754. package/packages/utils/dist/index.d.ts.map +1 -0
  755. package/packages/utils/dist/index.js +1 -0
  756. package/packages/utils/dist/index.js.map +1 -0
  757. package/packages/utils/dist/logger.d.ts.map +1 -0
  758. package/packages/utils/dist/logger.js.map +1 -0
  759. package/packages/utils/dist/model-mapping.d.ts.map +1 -0
  760. package/packages/utils/dist/model-mapping.js.map +1 -0
  761. package/packages/utils/dist/name-generator.d.ts.map +1 -0
  762. package/packages/utils/dist/name-generator.js.map +1 -0
  763. package/packages/utils/dist/precompiled-patterns.d.ts.map +1 -0
  764. package/packages/utils/dist/precompiled-patterns.js.map +1 -0
  765. package/packages/utils/dist/relay-pty-path.d.ts +11 -5
  766. package/packages/utils/dist/relay-pty-path.d.ts.map +1 -0
  767. package/packages/utils/dist/relay-pty-path.js +60 -5
  768. package/packages/utils/dist/relay-pty-path.js.map +1 -0
  769. package/packages/utils/dist/update-checker.d.ts.map +1 -0
  770. package/packages/utils/dist/update-checker.js.map +1 -0
  771. package/packages/utils/package.json +37 -14
  772. package/packages/utils/scripts/build-cjs.mjs +24 -0
  773. package/packages/utils/src/client-helpers.ts +221 -0
  774. package/packages/utils/src/command-resolver.ts +82 -0
  775. package/packages/utils/src/error-tracking.ts +189 -0
  776. package/packages/utils/src/git-remote.ts +143 -0
  777. package/packages/utils/src/index.ts +10 -0
  778. package/packages/utils/src/logger.ts +107 -0
  779. package/packages/utils/src/model-mapping.test.ts +122 -0
  780. package/packages/utils/src/model-mapping.ts +58 -0
  781. package/packages/utils/src/name-generator.test.ts +259 -0
  782. package/packages/utils/src/name-generator.ts +56 -0
  783. package/packages/utils/src/precompiled-patterns.test.ts +452 -0
  784. package/packages/utils/src/precompiled-patterns.ts +395 -0
  785. package/packages/utils/src/relay-pty-path.ts +196 -0
  786. package/packages/utils/src/update-checker.test.ts +260 -0
  787. package/packages/utils/src/update-checker.ts +211 -0
  788. package/packages/utils/tsconfig.json +21 -0
  789. package/packages/utils/vitest.config.ts +9 -0
  790. package/packages/wrapper/dist/__fixtures__/claude-outputs.d.ts.map +1 -0
  791. package/packages/wrapper/dist/__fixtures__/claude-outputs.js.map +1 -0
  792. package/packages/wrapper/dist/__fixtures__/codex-outputs.d.ts.map +1 -0
  793. package/packages/wrapper/dist/__fixtures__/codex-outputs.js.map +1 -0
  794. package/packages/wrapper/dist/__fixtures__/gemini-outputs.d.ts.map +1 -0
  795. package/packages/wrapper/dist/__fixtures__/gemini-outputs.js.map +1 -0
  796. package/packages/wrapper/dist/__fixtures__/index.d.ts.map +1 -0
  797. package/packages/wrapper/dist/__fixtures__/index.js.map +1 -0
  798. package/packages/wrapper/dist/auth-detection.d.ts.map +1 -0
  799. package/packages/wrapper/dist/auth-detection.js.map +1 -0
  800. package/packages/wrapper/dist/base-wrapper.d.ts.map +1 -0
  801. package/packages/wrapper/dist/base-wrapper.js.map +1 -0
  802. package/packages/wrapper/dist/client.d.ts.map +1 -0
  803. package/packages/wrapper/dist/client.js.map +1 -0
  804. package/packages/wrapper/dist/id-generator.d.ts.map +1 -0
  805. package/packages/wrapper/dist/id-generator.js.map +1 -0
  806. package/packages/wrapper/dist/idle-detector.d.ts.map +1 -0
  807. package/packages/wrapper/dist/idle-detector.js.map +1 -0
  808. package/packages/wrapper/dist/inbox.d.ts.map +1 -0
  809. package/packages/wrapper/dist/inbox.js.map +1 -0
  810. package/packages/wrapper/dist/index.d.ts.map +1 -0
  811. package/packages/wrapper/dist/index.js.map +1 -0
  812. package/packages/wrapper/dist/parser.d.ts.map +1 -0
  813. package/packages/wrapper/dist/parser.js.map +1 -0
  814. package/packages/wrapper/dist/prompt-composer.d.ts.map +1 -0
  815. package/packages/wrapper/dist/prompt-composer.js.map +1 -0
  816. package/packages/wrapper/dist/relay-pty-orchestrator.d.ts +10 -0
  817. package/packages/wrapper/dist/relay-pty-orchestrator.d.ts.map +1 -0
  818. package/packages/wrapper/dist/relay-pty-orchestrator.js +69 -0
  819. package/packages/wrapper/dist/relay-pty-orchestrator.js.map +1 -0
  820. package/packages/wrapper/dist/shared.d.ts.map +1 -0
  821. package/packages/wrapper/dist/shared.js.map +1 -0
  822. package/packages/wrapper/dist/stuck-detector.d.ts.map +1 -0
  823. package/packages/wrapper/dist/stuck-detector.js.map +1 -0
  824. package/packages/wrapper/dist/tmux-resolver.d.ts.map +1 -0
  825. package/packages/wrapper/dist/tmux-resolver.js.map +1 -0
  826. package/packages/wrapper/dist/tmux-wrapper.d.ts.map +1 -0
  827. package/packages/wrapper/dist/tmux-wrapper.js.map +1 -0
  828. package/packages/wrapper/dist/trajectory-integration.d.ts.map +1 -0
  829. package/packages/wrapper/dist/trajectory-integration.js.map +1 -0
  830. package/packages/wrapper/dist/wrapper-types.d.ts.map +1 -0
  831. package/packages/wrapper/dist/wrapper-types.js.map +1 -0
  832. package/packages/wrapper/package.json +6 -9
  833. package/packages/wrapper/src/__fixtures__/claude-outputs.ts +471 -0
  834. package/packages/wrapper/src/__fixtures__/codex-outputs.ts +99 -0
  835. package/packages/wrapper/src/__fixtures__/gemini-outputs.ts +151 -0
  836. package/packages/wrapper/src/__fixtures__/index.ts +47 -0
  837. package/packages/wrapper/src/auth-detection.ts +244 -0
  838. package/packages/wrapper/src/base-wrapper.test.ts +589 -0
  839. package/packages/wrapper/src/base-wrapper.ts +810 -0
  840. package/packages/wrapper/src/client.test.ts +262 -0
  841. package/packages/wrapper/src/client.ts +984 -0
  842. package/packages/wrapper/src/id-generator.test.ts +71 -0
  843. package/packages/wrapper/src/id-generator.ts +69 -0
  844. package/packages/wrapper/src/idle-detector.test.ts +418 -0
  845. package/packages/wrapper/src/idle-detector.ts +384 -0
  846. package/packages/wrapper/src/inbox.test.ts +233 -0
  847. package/packages/wrapper/src/inbox.ts +89 -0
  848. package/packages/wrapper/src/index.ts +170 -0
  849. package/packages/wrapper/src/parser.regression.test.ts +251 -0
  850. package/packages/wrapper/src/parser.test.ts +1359 -0
  851. package/packages/wrapper/src/parser.ts +1477 -0
  852. package/packages/wrapper/src/prompt-composer.test.ts +219 -0
  853. package/packages/wrapper/src/prompt-composer.ts +231 -0
  854. package/packages/wrapper/src/relay-pty-orchestrator.test.ts +1204 -0
  855. package/packages/wrapper/src/relay-pty-orchestrator.ts +2626 -0
  856. package/packages/wrapper/src/shared.test.ts +322 -0
  857. package/packages/wrapper/src/shared.ts +495 -0
  858. package/packages/wrapper/src/stuck-detector.test.ts +303 -0
  859. package/packages/wrapper/src/stuck-detector.ts +511 -0
  860. package/packages/wrapper/src/tmux-resolver.test.ts +104 -0
  861. package/packages/wrapper/src/tmux-resolver.ts +207 -0
  862. package/packages/wrapper/src/tmux-wrapper.test.ts +316 -0
  863. package/packages/wrapper/src/tmux-wrapper.ts +2095 -0
  864. package/packages/wrapper/src/trajectory-detection.test.ts +151 -0
  865. package/packages/wrapper/src/trajectory-integration.ts +1261 -0
  866. package/packages/wrapper/src/wrapper-types.ts +45 -0
  867. package/packages/wrapper/tsconfig.json +19 -0
  868. package/packages/wrapper/vitest.config.ts +9 -0
  869. package/scripts/build-cjs.mjs +23 -0
  870. package/scripts/postinstall.js +132 -0
  871. package/.cursor/mcp.json +0 -11
  872. package/.gitattributes +0 -3
  873. package/.gitleaks.toml +0 -26
  874. package/.mcp.json +0 -11
  875. package/.nvmrc +0 -1
  876. package/ARCHITECTURE.md +0 -1245
  877. package/CHANGELOG.md +0 -231
  878. package/TESTING.md +0 -278
  879. package/TRAIL_GIT_AUTH_FIX.md +0 -113
  880. package/scripts/demos/README.md +0 -79
  881. package/scripts/demos/server-capacity.sh +0 -69
  882. package/scripts/demos/sprint-planning.sh +0 -73
  883. package/scripts/hooks/install.sh +0 -16
  884. package/scripts/hooks/pre-commit +0 -60
  885. package/scripts/post-publish-verify/README.md +0 -80
  886. package/scripts/post-publish-verify/run-verify.sh +0 -127
  887. package/scripts/post-publish-verify/verify-install.sh +0 -249
  888. package/scripts/stress-test-orchestrator-integration.mts +0 -1366
  889. package/scripts/stress-test-orchestrator.mjs +0 -584
  890. package/scripts/stress-test-relay-pty.sh +0 -452
  891. package/scripts/test-interactive-terminal.sh +0 -248
  892. package/specs/PRIMITIVES_ROADMAP.md +0 -2154
  893. package/tests/benchmarks/protocol.bench.ts +0 -310
  894. package/turbo.json +0 -37
@@ -0,0 +1,2626 @@
1
+ /**
2
+ * RelayPtyOrchestrator - Orchestrates the relay-pty Rust binary
3
+ *
4
+ * This wrapper spawns the relay-pty binary and communicates via Unix socket.
5
+ * It provides the same interface as PtyWrapper but with improved latency
6
+ * (~550ms vs ~1700ms) by using direct PTY writes instead of tmux send-keys.
7
+ *
8
+ * Architecture:
9
+ * 1. Spawn relay-pty --name {agentName} -- {command} as child process
10
+ * 2. Connect to socket for injection:
11
+ * - With WORKSPACE_ID: /tmp/relay/{workspaceId}/sockets/{agentName}.sock
12
+ * - Without: /tmp/relay-pty-{agentName}.sock (legacy)
13
+ * 3. Parse stdout for relay commands (relay-pty echoes all output)
14
+ * 4. Translate SEND envelopes → inject messages via socket
15
+ *
16
+ * @see docs/RUST_WRAPPER_DESIGN.md for protocol details
17
+ */
18
+
19
+ import { spawn, ChildProcess } from 'node:child_process';
20
+ import { createConnection, Socket } from 'node:net';
21
+ import { createHash } from 'node:crypto';
22
+ import { join, dirname } from 'node:path';
23
+ import { homedir } from 'node:os';
24
+ import { existsSync, unlinkSync, mkdirSync, symlinkSync, lstatSync, rmSync, watch, readdirSync, readlinkSync, writeFileSync, appendFileSync } from 'node:fs';
25
+ import type { FSWatcher } from 'node:fs';
26
+ import { getProjectPaths } from '@agent-relay/config/project-namespace';
27
+ import { getAgentOutboxTemplate } from '@agent-relay/config/relay-file-writer';
28
+ import { fileURLToPath } from 'node:url';
29
+
30
+ // Get the directory where this module is located
31
+ const __filename = fileURLToPath(import.meta.url);
32
+ const __dirname = dirname(__filename);
33
+ import { BaseWrapper, type BaseWrapperConfig } from './base-wrapper.js';
34
+ import { parseSummaryWithDetails, parseSessionEndFromOutput } from './parser.js';
35
+ import type { SendPayload, SendMeta, Envelope } from '@agent-relay/protocol/types';
36
+ import type { ChannelMessagePayload } from '@agent-relay/protocol/channels';
37
+ import { findRelayPtyBinary as findRelayPtyBinaryUtil } from '@agent-relay/utils/relay-pty-path';
38
+ import {
39
+ type QueuedMessage,
40
+ stripAnsi,
41
+ sleep,
42
+ buildInjectionString,
43
+ AdaptiveThrottle,
44
+ } from './shared.js';
45
+ import {
46
+ getMemoryMonitor,
47
+ type AgentMemoryMonitor,
48
+ type MemoryAlert,
49
+ formatBytes,
50
+ getCgroupManager,
51
+ type CgroupManager,
52
+ } from '@agent-relay/resiliency';
53
+
54
+ // ============================================================================
55
+ // Types for relay-pty socket protocol
56
+ // ============================================================================
57
+
58
+ const MAX_SOCKET_PATH_LENGTH = 107;
59
+
60
+ function hashWorkspaceId(workspaceId: string): string {
61
+ return createHash('sha256').update(workspaceId).digest('hex').slice(0, 12);
62
+ }
63
+
64
+ /**
65
+ * Request types sent to relay-pty socket
66
+ */
67
+ interface InjectRequest {
68
+ type: 'inject';
69
+ id: string;
70
+ from: string;
71
+ body: string;
72
+ priority: number;
73
+ }
74
+
75
+ interface StatusRequest {
76
+ type: 'status';
77
+ }
78
+
79
+ interface ShutdownRequest {
80
+ type: 'shutdown';
81
+ }
82
+
83
+ /**
84
+ * Send just Enter key (for stuck input recovery)
85
+ * Used when message was written to PTY but Enter wasn't processed
86
+ */
87
+ interface SendEnterRequest {
88
+ type: 'send_enter';
89
+ /** Message ID this is for (for tracking) */
90
+ id: string;
91
+ }
92
+
93
+ type RelayPtyRequest = InjectRequest | StatusRequest | ShutdownRequest | SendEnterRequest;
94
+
95
+ /**
96
+ * Response types received from relay-pty socket
97
+ */
98
+ interface InjectResultResponse {
99
+ type: 'inject_result';
100
+ id: string;
101
+ status: 'queued' | 'injecting' | 'delivered' | 'failed';
102
+ timestamp: number;
103
+ error?: string;
104
+ }
105
+
106
+ interface StatusResponse {
107
+ type: 'status';
108
+ agent_idle: boolean;
109
+ queue_length: number;
110
+ cursor_position?: [number, number];
111
+ last_output_ms: number;
112
+ }
113
+
114
+ interface BackpressureResponse {
115
+ type: 'backpressure';
116
+ queue_length: number;
117
+ accept: boolean;
118
+ }
119
+
120
+ interface ErrorResponse {
121
+ type: 'error';
122
+ message: string;
123
+ }
124
+
125
+ interface ShutdownAckResponse {
126
+ type: 'shutdown_ack';
127
+ }
128
+
129
+ /**
130
+ * Response for SendEnter request (stuck input recovery)
131
+ */
132
+ interface SendEnterResultResponse {
133
+ type: 'send_enter_result';
134
+ /** Message ID this is for */
135
+ id: string;
136
+ /** Whether Enter was sent successfully */
137
+ success: boolean;
138
+ /** Unix timestamp in milliseconds */
139
+ timestamp: number;
140
+ }
141
+
142
+ type RelayPtyResponse =
143
+ | InjectResultResponse
144
+ | StatusResponse
145
+ | BackpressureResponse
146
+ | ErrorResponse
147
+ | ShutdownAckResponse
148
+ | SendEnterResultResponse;
149
+
150
+ /**
151
+ * Configuration for RelayPtyOrchestrator
152
+ */
153
+ export interface RelayPtyOrchestratorConfig extends BaseWrapperConfig {
154
+ /** Path to relay-pty binary (default: searches PATH and ./relay-pty/target/release) */
155
+ relayPtyPath?: string;
156
+ /** Socket connect timeout in ms (default: 5000) */
157
+ socketConnectTimeoutMs?: number;
158
+ /** Socket reconnect attempts (default: 3) */
159
+ socketReconnectAttempts?: number;
160
+ /** Callback when agent exits */
161
+ onExit?: (code: number) => void;
162
+ /** Callback when injection fails after retries */
163
+ onInjectionFailed?: (messageId: string, error: string) => void;
164
+ /** Enable debug logging (default: false) */
165
+ debug?: boolean;
166
+ /** Force headless mode (use pipes instead of inheriting TTY) */
167
+ headless?: boolean;
168
+ /** CPU limit percentage per agent (1-100 per core, e.g., 50 = 50% of one core). Requires cgroups v2. */
169
+ cpuLimitPercent?: number;
170
+ }
171
+
172
+ /**
173
+ * Events emitted by RelayPtyOrchestrator
174
+ */
175
+ export interface RelayPtyOrchestratorEvents {
176
+ output: (data: string) => void;
177
+ exit: (code: number) => void;
178
+ error: (error: Error) => void;
179
+ 'injection-failed': (event: { messageId: string; from: string; error: string }) => void;
180
+ 'backpressure': (event: { queueLength: number; accept: boolean }) => void;
181
+ 'summary': (event: { agentName: string; summary: unknown }) => void;
182
+ 'session-end': (event: { agentName: string; marker: unknown }) => void;
183
+ }
184
+
185
+ /**
186
+ * Orchestrator for relay-pty Rust binary
187
+ *
188
+ * Extends BaseWrapper to provide the same interface as PtyWrapper
189
+ * but uses the relay-pty binary for improved injection reliability.
190
+ */
191
+ export class RelayPtyOrchestrator extends BaseWrapper {
192
+ protected override config: RelayPtyOrchestratorConfig;
193
+
194
+ // Process management
195
+ private relayPtyProcess?: ChildProcess;
196
+ private socketPath: string;
197
+ private _logPath: string;
198
+ private _outboxPath: string;
199
+ private _legacyOutboxPath: string; // Legacy /tmp/relay-outbox path for backwards compat
200
+ private _canonicalOutboxPath: string; // Canonical ~/.agent-relay/outbox path (agents write here)
201
+ private _workspaceId?: string; // For symlink setup
202
+ private socket?: Socket;
203
+ private socketConnected = false;
204
+
205
+ // Output buffering
206
+ private outputBuffer = '';
207
+ private rawBuffer = '';
208
+ private lastParsedLength = 0;
209
+
210
+ // Interactive mode (show output to terminal)
211
+ private isInteractive = false;
212
+
213
+ // Injection state
214
+ private pendingInjections: Map<string, {
215
+ resolve: (success: boolean) => void;
216
+ reject: (error: Error) => void;
217
+ timeout: NodeJS.Timeout;
218
+ from: string; // For verification pattern matching
219
+ shortId: string; // First 8 chars of messageId for verification
220
+ retryCount: number; // Track retry attempts
221
+ originalBody: string; // Original injection content for retries
222
+ }> = new Map();
223
+ private backpressureActive = false;
224
+ private readyForMessages = false;
225
+
226
+ // Adaptive throttle for message queue - adjusts delay based on success/failure
227
+ private throttle = new AdaptiveThrottle();
228
+
229
+ // Unread message indicator state
230
+ private lastUnreadIndicatorTime = 0;
231
+ private readonly UNREAD_INDICATOR_COOLDOWN_MS = 5000; // Don't spam indicators
232
+
233
+ // Track whether any output has been received from the CLI
234
+ private hasReceivedOutput = false;
235
+
236
+ // Queue monitor for stuck message detection
237
+ private queueMonitorTimer?: NodeJS.Timeout;
238
+ private readonly QUEUE_MONITOR_INTERVAL_MS = 5000; // Check every 5 seconds
239
+ private injectionStartTime = 0; // Track when isInjecting was set to true
240
+ private readonly MAX_INJECTION_STUCK_MS = 60000; // Force reset after 60 seconds
241
+
242
+ // Protocol monitor for detecting agent mistakes (e.g., empty AGENT_RELAY_NAME)
243
+ private protocolWatcher?: FSWatcher;
244
+ private protocolReminderCooldown = 0; // Prevent spam
245
+ private readonly PROTOCOL_REMINDER_COOLDOWN_MS = 30000; // 30 second cooldown between reminders
246
+
247
+ // Periodic protocol reminder for long sessions (agents sometimes forget the protocol)
248
+ private periodicReminderTimer?: NodeJS.Timeout;
249
+ private readonly PERIODIC_REMINDER_INTERVAL_MS = 45 * 60 * 1000; // 45 minutes
250
+ private sessionStartTime = 0;
251
+
252
+ // Track if agent is being gracefully stopped (vs crashed)
253
+ private isGracefulStop = false;
254
+
255
+ // Track early process exit for better error messages
256
+ private earlyExitInfo?: { code: number | null; signal: NodeJS.Signals | null; stderr: string };
257
+
258
+ // Memory/CPU monitoring
259
+ private memoryMonitor: AgentMemoryMonitor;
260
+ private memoryAlertHandler: ((alert: MemoryAlert) => void) | null = null;
261
+
262
+ // CPU limiting via cgroups (optional, Linux only)
263
+ private cgroupManager: CgroupManager;
264
+ private hasCgroupSetup = false;
265
+
266
+ // Note: sessionEndProcessed and lastSummaryRawContent are inherited from BaseWrapper
267
+
268
+ constructor(config: RelayPtyOrchestratorConfig) {
269
+ super(config);
270
+ this.config = config;
271
+
272
+ // Validate agent name to prevent path traversal attacks
273
+ if (config.name.includes('..') || config.name.includes('/') || config.name.includes('\\')) {
274
+ throw new Error(`Invalid agent name: "${config.name}" contains path traversal characters`);
275
+ }
276
+
277
+ // Get project paths (used for logs and local mode)
278
+ const projectPaths = getProjectPaths(config.cwd);
279
+
280
+ // Canonical outbox path - agents ALWAYS write here (transparent symlink in workspace mode)
281
+ // Uses ~/.agent-relay/outbox/{agentName}/ so agents don't need to know about workspace IDs
282
+ this._canonicalOutboxPath = join(projectPaths.dataDir, 'outbox', config.name);
283
+
284
+ // Check for workspace namespacing (for multi-tenant cloud deployment)
285
+ // WORKSPACE_ID can be in process.env or passed via config.env
286
+ const workspaceId = config.env?.WORKSPACE_ID || process.env.WORKSPACE_ID;
287
+ this._workspaceId = workspaceId;
288
+
289
+ if (workspaceId) {
290
+ // Workspace mode: relay-pty watches the actual workspace path
291
+ // Canonical path (~/.agent-relay/outbox/) will be symlinked to workspace path
292
+ const getWorkspacePaths = (id: string) => {
293
+ const workspaceDir = `/tmp/relay/${id}`;
294
+ return {
295
+ workspaceDir,
296
+ socketPath: `${workspaceDir}/sockets/${config.name}.sock`,
297
+ outboxPath: `${workspaceDir}/outbox/${config.name}`,
298
+ };
299
+ };
300
+
301
+ let paths = getWorkspacePaths(workspaceId);
302
+ if (paths.socketPath.length > MAX_SOCKET_PATH_LENGTH) {
303
+ const hashedWorkspaceId = hashWorkspaceId(workspaceId);
304
+ const hashedPaths = getWorkspacePaths(hashedWorkspaceId);
305
+ console.warn(
306
+ `[relay-pty-orchestrator:${config.name}] Socket path too long (${paths.socketPath.length} chars); using hashed workspace id ${hashedWorkspaceId}`
307
+ );
308
+ paths = hashedPaths;
309
+ }
310
+
311
+ if (paths.socketPath.length > MAX_SOCKET_PATH_LENGTH) {
312
+ throw new Error(`Socket path exceeds ${MAX_SOCKET_PATH_LENGTH} chars: ${paths.socketPath.length}`);
313
+ }
314
+
315
+ this.socketPath = paths.socketPath;
316
+ // relay-pty watches the actual workspace path
317
+ this._outboxPath = paths.outboxPath;
318
+ // Legacy path for backwards compat (older agents might still use /tmp/relay-outbox)
319
+ this._legacyOutboxPath = `/tmp/relay-outbox/${config.name}`;
320
+ } else {
321
+ // Local mode: use project paths directly (no symlinks needed)
322
+ this._outboxPath = this._canonicalOutboxPath;
323
+ // Socket path: use ~/.agent-relay/sockets/{projectId}/{agentName}.sock
324
+ // This keeps paths short (uses 12-char hashed projectId) while staying organized
325
+ // Example: /Users/foo/.agent-relay/sockets/abc123def456/MyAgent.sock (~65 chars)
326
+ this.socketPath = join(homedir(), '.agent-relay', 'sockets', projectPaths.projectId, `${config.name}.sock`);
327
+ // Legacy path for backwards compat (older agents might still use /tmp/relay-outbox)
328
+ // Even in local mode, we need this symlink for agents with stale instructions
329
+ this._legacyOutboxPath = `/tmp/relay-outbox/${config.name}`;
330
+ }
331
+ if (this.socketPath.length > MAX_SOCKET_PATH_LENGTH) {
332
+ throw new Error(`Socket path exceeds ${MAX_SOCKET_PATH_LENGTH} chars: ${this.socketPath.length}`);
333
+ }
334
+
335
+ // Generate log path using project paths
336
+ this._logPath = join(projectPaths.teamDir, 'worker-logs', `${config.name}.log`);
337
+
338
+ // Check if we're running interactively (stdin is a TTY)
339
+ // If headless mode is forced via config, always use pipes
340
+ this.isInteractive = config.headless ? false : (process.stdin.isTTY === true);
341
+
342
+ // Initialize memory monitor (shared singleton, 10s polling interval)
343
+ this.memoryMonitor = getMemoryMonitor({ checkIntervalMs: 10_000 });
344
+
345
+ // Initialize cgroup manager for CPU limiting (shared singleton)
346
+ this.cgroupManager = getCgroupManager();
347
+ }
348
+
349
+ /**
350
+ * Debug log - only outputs when debug is enabled
351
+ * Writes to log file to avoid polluting TUI output
352
+ */
353
+ private log(message: string): void {
354
+ if (this.config.debug) {
355
+ const logLine = `${new Date().toISOString()} [relay-pty-orchestrator:${this.config.name}] ${message}\n`;
356
+ try {
357
+ const logDir = dirname(this._logPath);
358
+ if (!existsSync(logDir)) {
359
+ mkdirSync(logDir, { recursive: true });
360
+ }
361
+ appendFileSync(this._logPath, logLine);
362
+ } catch {
363
+ // Fallback to stderr if file write fails (only during init before _logPath is set)
364
+ }
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Error log - always outputs (errors are important)
370
+ * Writes to log file to avoid polluting TUI output
371
+ */
372
+ private logError(message: string): void {
373
+ const logLine = `${new Date().toISOString()} [relay-pty-orchestrator:${this.config.name}] ERROR: ${message}\n`;
374
+ try {
375
+ const logDir = dirname(this._logPath);
376
+ if (!existsSync(logDir)) {
377
+ mkdirSync(logDir, { recursive: true });
378
+ }
379
+ appendFileSync(this._logPath, logLine);
380
+ } catch {
381
+ // Fallback to stderr if file write fails (only during init before _logPath is set)
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Get the outbox path for this agent (for documentation purposes)
387
+ */
388
+ get outboxPath(): string {
389
+ return this._outboxPath;
390
+ }
391
+
392
+ // =========================================================================
393
+ // Abstract method implementations (required by BaseWrapper)
394
+ // =========================================================================
395
+
396
+ /**
397
+ * Start the relay-pty process and connect to socket
398
+ */
399
+ override async start(): Promise<void> {
400
+ if (this.running) return;
401
+
402
+ this.log(` Starting...`);
403
+
404
+ // Ensure socket directory exists (for workspace-namespaced paths)
405
+ const socketDir = dirname(this.socketPath);
406
+ try {
407
+ if (!existsSync(socketDir)) {
408
+ mkdirSync(socketDir, { recursive: true });
409
+ this.log(` Created socket directory: ${socketDir}`);
410
+ }
411
+ } catch (err: any) {
412
+ this.logError(` Failed to create socket directory: ${err.message}`);
413
+ }
414
+
415
+ // Clean up any stale socket from previous crashed process
416
+ try {
417
+ if (existsSync(this.socketPath)) {
418
+ this.log(` Removing stale socket: ${this.socketPath}`);
419
+ unlinkSync(this.socketPath);
420
+ }
421
+ } catch (err: any) {
422
+ this.logError(` Failed to clean up socket: ${err.message}`);
423
+ }
424
+
425
+ // Set up outbox directory structure
426
+ // - Workspace mode:
427
+ // 1. Create actual workspace path /tmp/relay/{workspaceId}/outbox/{name}
428
+ // 2. Symlink canonical ~/.agent-relay/outbox/{name} -> workspace path
429
+ // 3. Optional: symlink /tmp/relay-outbox/{name} -> workspace path (backwards compat)
430
+ // - Local mode: just create ~/.agent-relay/{projectId}/outbox/{name} directly
431
+ try {
432
+ // Ensure the actual outbox directory exists (where relay-pty watches)
433
+ const outboxDir = dirname(this._outboxPath);
434
+ if (!existsSync(outboxDir)) {
435
+ mkdirSync(outboxDir, { recursive: true });
436
+ }
437
+ if (!existsSync(this._outboxPath)) {
438
+ mkdirSync(this._outboxPath, { recursive: true });
439
+ }
440
+ this.log(` Created outbox directory: ${this._outboxPath}`);
441
+
442
+ // Helper to create a symlink, cleaning up existing path first
443
+ const createSymlinkSafe = (linkPath: string, targetPath: string) => {
444
+ const linkParent = dirname(linkPath);
445
+ if (!existsSync(linkParent)) {
446
+ mkdirSync(linkParent, { recursive: true });
447
+ }
448
+
449
+ // Remove existing path if it exists (file, symlink, or directory)
450
+ // Use lstatSync instead of existsSync to detect broken symlinks
451
+ // (existsSync returns false for broken symlinks, but the symlink itself still exists)
452
+ let pathExists = false;
453
+ try {
454
+ lstatSync(linkPath);
455
+ pathExists = true;
456
+ } catch {
457
+ // Path doesn't exist at all - proceed to create symlink
458
+ }
459
+
460
+ if (pathExists) {
461
+ try {
462
+ const stats = lstatSync(linkPath);
463
+ if (stats.isSymbolicLink()) {
464
+ // Handle both valid and broken symlinks
465
+ try {
466
+ const currentTarget = readlinkSync(linkPath);
467
+ if (currentTarget === targetPath) {
468
+ // Symlink already points to correct target, no need to recreate
469
+ this.log(` Symlink already exists and is correct: ${linkPath} -> ${targetPath}`);
470
+ return;
471
+ }
472
+ } catch {
473
+ // Broken symlink (target doesn't exist) - remove it
474
+ this.log(` Removing broken symlink: ${linkPath}`);
475
+ }
476
+ unlinkSync(linkPath);
477
+ } else if (stats.isFile()) {
478
+ unlinkSync(linkPath);
479
+ } else if (stats.isDirectory()) {
480
+ // Force remove directory - this is critical for fixing existing directories
481
+ rmSync(linkPath, { recursive: true, force: true });
482
+ // Verify removal succeeded using lstatSync to catch broken symlinks
483
+ try {
484
+ lstatSync(linkPath);
485
+ throw new Error(`Failed to remove existing directory: ${linkPath}`);
486
+ } catch (err: any) {
487
+ if (err.code !== 'ENOENT') {
488
+ throw err; // Re-throw if it's not a "doesn't exist" error
489
+ }
490
+ // Path successfully removed
491
+ }
492
+ }
493
+ } catch (err: any) {
494
+ // Log cleanup errors instead of silently ignoring them
495
+ this.logError(` Failed to clean up existing path ${linkPath}: ${err.message}`);
496
+ throw err; // Re-throw to prevent symlink creation on failed cleanup
497
+ }
498
+ }
499
+
500
+ // Create the symlink
501
+ try {
502
+ symlinkSync(targetPath, linkPath);
503
+ // Verify symlink was created correctly
504
+ if (!existsSync(linkPath)) {
505
+ throw new Error(`Symlink creation failed: ${linkPath}`);
506
+ }
507
+ const verifyStats = lstatSync(linkPath);
508
+ if (!verifyStats.isSymbolicLink()) {
509
+ throw new Error(`Created path is not a symlink: ${linkPath}`);
510
+ }
511
+ const verifyTarget = readlinkSync(linkPath);
512
+ if (verifyTarget !== targetPath) {
513
+ throw new Error(`Symlink points to wrong target: expected ${targetPath}, got ${verifyTarget}`);
514
+ }
515
+ this.log(` Created symlink: ${linkPath} -> ${targetPath}`);
516
+ } catch (err: any) {
517
+ this.logError(` Failed to create symlink ${linkPath} -> ${targetPath}: ${err.message}`);
518
+ throw err;
519
+ }
520
+ };
521
+
522
+ // In workspace mode, create symlinks so agents can use canonical path
523
+ if (this._workspaceId) {
524
+ // Symlink canonical path (~/.agent-relay/outbox/{name}) -> workspace path
525
+ // This is the PRIMARY symlink - agents write to canonical path, relay-pty watches workspace path
526
+ if (this._canonicalOutboxPath !== this._outboxPath) {
527
+ createSymlinkSafe(this._canonicalOutboxPath, this._outboxPath);
528
+ }
529
+
530
+ // Also create legacy /tmp/relay-outbox symlink for backwards compat with older agents
531
+ if (this._legacyOutboxPath !== this._outboxPath && this._legacyOutboxPath !== this._canonicalOutboxPath) {
532
+ createSymlinkSafe(this._legacyOutboxPath, this._outboxPath);
533
+ }
534
+ }
535
+
536
+ // In local mode, also create legacy symlink for backwards compat with stale instructions
537
+ if (!this._workspaceId && this._legacyOutboxPath !== this._outboxPath) {
538
+ createSymlinkSafe(this._legacyOutboxPath, this._outboxPath);
539
+ }
540
+ } catch (err: any) {
541
+ this.logError(` Failed to set up outbox: ${err.message}`);
542
+ }
543
+
544
+ // Write MCP identity file so MCP servers can discover their agent name
545
+ // This is needed because Claude Code may not pass through env vars to MCP server processes
546
+ try {
547
+ const projectPaths = getProjectPaths(this.config.cwd);
548
+ const identityDir = join(projectPaths.dataDir);
549
+ if (!existsSync(identityDir)) {
550
+ mkdirSync(identityDir, { recursive: true });
551
+ }
552
+ // Write a per-process identity file (using PPID so MCP server finds parent's identity)
553
+ const identityPath = join(identityDir, `mcp-identity-${process.pid}`);
554
+ writeFileSync(identityPath, this.config.name, 'utf-8');
555
+ this.log(` Wrote MCP identity file: ${identityPath}`);
556
+
557
+ // Also write a simple identity file (for single-agent scenarios)
558
+ const simpleIdentityPath = join(identityDir, 'mcp-identity');
559
+ writeFileSync(simpleIdentityPath, this.config.name, 'utf-8');
560
+ } catch (err: any) {
561
+ this.logError(` Failed to write MCP identity file: ${err.message}`);
562
+ }
563
+
564
+ // Find relay-pty binary
565
+ const binaryPath = this.findRelayPtyBinary();
566
+ if (!binaryPath) {
567
+ throw new Error('relay-pty binary not found. Build with: cd relay-pty && cargo build --release');
568
+ }
569
+
570
+ this.log(` Using binary: ${binaryPath}`);
571
+
572
+ // Spawn relay-pty process FIRST (before connecting to daemon)
573
+ // This ensures the CLI is actually running before we register with the daemon
574
+ await this.spawnRelayPty(binaryPath);
575
+
576
+ // Wait for socket to become available and connect
577
+ await this.connectToSocket();
578
+
579
+ // Connect to relay daemon AFTER CLI is spawned
580
+ // This prevents the spawner from seeing us as "registered" before the CLI runs
581
+ try {
582
+ await this.client.connect();
583
+ this.log(` Relay daemon connected`);
584
+ } catch (err: any) {
585
+ this.logError(` Relay connect failed: ${err.message}`);
586
+ }
587
+
588
+ this.running = true;
589
+ // DON'T set readyForMessages yet - wait for CLI to be ready first
590
+ // This prevents messages from being injected during CLI startup
591
+ this.startStuckDetection();
592
+ this.startQueueMonitor();
593
+ this.startProtocolMonitor();
594
+ this.startPeriodicReminder();
595
+
596
+ this.log(` Socket connected: ${this.socketConnected}`);
597
+ this.log(` Relay client state: ${this.client.state}`);
598
+
599
+ // Wait for CLI to be fully ready (output received + idle state)
600
+ // This ensures we don't inject messages while the CLI is still starting up
601
+ // Messages arriving via daemon during this time will be queued but not processed
602
+ this.log(` Waiting for CLI to be ready before accepting messages...`);
603
+ const cliReady = await this.waitUntilCliReady(30000, 100);
604
+ if (cliReady) {
605
+ this.log(` CLI is ready, enabling message processing`);
606
+ } else {
607
+ this.log(` CLI readiness timeout, enabling message processing anyway`);
608
+ }
609
+
610
+ // Now enable message processing
611
+ this.readyForMessages = true;
612
+ this.log(` Ready for messages`);
613
+
614
+ // Process any queued messages that arrived during startup
615
+ this.processMessageQueue();
616
+ }
617
+
618
+ /**
619
+ * Stop the relay-pty process gracefully
620
+ */
621
+ override async stop(): Promise<void> {
622
+ if (!this.running) return;
623
+ this.isGracefulStop = true; // Mark as graceful to prevent crash broadcast
624
+ this.running = false;
625
+ this.stopStuckDetection();
626
+ this.stopQueueMonitor();
627
+ this.stopProtocolMonitor();
628
+ this.stopPeriodicReminder();
629
+
630
+ // Clear socket reconnect timer
631
+ if (this.socketReconnectTimer) {
632
+ clearTimeout(this.socketReconnectTimer);
633
+ this.socketReconnectTimer = undefined;
634
+ }
635
+
636
+ // Unregister from memory monitor
637
+ this.memoryMonitor.unregister(this.config.name);
638
+ if (this.memoryAlertHandler) {
639
+ this.memoryMonitor.off('alert', this.memoryAlertHandler);
640
+ this.memoryAlertHandler = null;
641
+ }
642
+
643
+ // Clean up cgroup if we set one up
644
+ if (this.hasCgroupSetup) {
645
+ await this.cgroupManager.removeAgentCgroup(this.config.name);
646
+ this.hasCgroupSetup = false;
647
+ }
648
+
649
+ this.log(` Stopping...`);
650
+
651
+ // Send shutdown command via socket
652
+ if (this.socket && this.socketConnected) {
653
+ try {
654
+ await this.sendSocketRequest({ type: 'shutdown' });
655
+ } catch {
656
+ // Ignore errors during shutdown
657
+ }
658
+ }
659
+
660
+ // Close socket
661
+ this.disconnectSocket();
662
+
663
+ // Kill process if still running
664
+ if (this.relayPtyProcess && !this.relayPtyProcess.killed) {
665
+ this.relayPtyProcess.kill('SIGTERM');
666
+
667
+ // Force kill after timeout
668
+ await Promise.race([
669
+ new Promise<void>((resolve) => {
670
+ this.relayPtyProcess?.on('exit', () => resolve());
671
+ }),
672
+ sleep(5000).then(() => {
673
+ if (this.relayPtyProcess && !this.relayPtyProcess.killed) {
674
+ this.relayPtyProcess.kill('SIGKILL');
675
+ }
676
+ }),
677
+ ]);
678
+ }
679
+
680
+ // Cleanup relay client
681
+ this.destroyClient();
682
+
683
+ // Clean up socket file
684
+ try {
685
+ if (existsSync(this.socketPath)) {
686
+ unlinkSync(this.socketPath);
687
+ this.log(` Cleaned up socket: ${this.socketPath}`);
688
+ }
689
+ } catch (err: any) {
690
+ this.logError(` Failed to clean up socket: ${err.message}`);
691
+ }
692
+
693
+ this.log(` Stopped`);
694
+ }
695
+
696
+ /**
697
+ * Inject content into the agent via socket
698
+ */
699
+ protected async performInjection(_content: string): Promise<void> {
700
+ // This is called by BaseWrapper but we handle injection differently
701
+ // via the socket protocol in processMessageQueue
702
+ throw new Error('Use injectMessage() instead of performInjection()');
703
+ }
704
+
705
+ /**
706
+ * Get cleaned output for parsing
707
+ */
708
+ protected getCleanOutput(): string {
709
+ return stripAnsi(this.rawBuffer);
710
+ }
711
+
712
+ // =========================================================================
713
+ // Process management
714
+ // =========================================================================
715
+
716
+ /**
717
+ * Find the relay-pty binary
718
+ * Uses shared utility from @agent-relay/utils
719
+ */
720
+ private findRelayPtyBinary(): string | null {
721
+ // Check config path first
722
+ if (this.config.relayPtyPath && existsSync(this.config.relayPtyPath)) {
723
+ return this.config.relayPtyPath;
724
+ }
725
+
726
+ // Use shared utility with current module's __dirname
727
+ return findRelayPtyBinaryUtil(__dirname);
728
+ }
729
+
730
+ /**
731
+ * Spawn the relay-pty process
732
+ */
733
+ private async spawnRelayPty(binaryPath: string): Promise<void> {
734
+ // Get terminal dimensions for proper rendering
735
+ const rows = process.stdout.rows || 24;
736
+ const cols = process.stdout.columns || 80;
737
+
738
+ const args = [
739
+ '--name', this.config.name,
740
+ '--socket', this.socketPath,
741
+ '--idle-timeout', String(this.config.idleBeforeInjectMs ?? 500),
742
+ '--json-output', // Enable Rust parsing output
743
+ '--rows', String(rows),
744
+ '--cols', String(cols),
745
+ '--log-level', 'warn', // Only show warnings and errors
746
+ '--log-file', this._logPath, // Enable output logging
747
+ '--outbox', this._outboxPath, // Enable file-based relay messages
748
+ '--', this.config.command,
749
+ ...(this.config.args ?? []),
750
+ ];
751
+
752
+ this.log(` Spawning: ${binaryPath} ${args.join(' ')}`);
753
+
754
+ // Reset early exit info from any previous spawn attempt
755
+ this.earlyExitInfo = undefined;
756
+
757
+ // For interactive mode, let Rust directly inherit stdin/stdout from the terminal
758
+ // This is more robust than manual forwarding through pipes
759
+ // We still pipe stderr to capture JSON parsed commands
760
+ const stdio: ('inherit' | 'pipe')[] = this.isInteractive
761
+ ? ['inherit', 'inherit', 'pipe'] // Rust handles terminal directly
762
+ : ['pipe', 'pipe', 'pipe']; // Headless mode - we handle I/O
763
+
764
+ const proc = spawn(binaryPath, args, {
765
+ cwd: this.config.cwd ?? process.cwd(),
766
+ env: {
767
+ ...process.env,
768
+ ...this.config.env,
769
+ AGENT_RELAY_NAME: this.config.name,
770
+ RELAY_AGENT_NAME: this.config.name, // MCP server uses this env var
771
+ AGENT_RELAY_OUTBOX: this._canonicalOutboxPath, // Agents use this for outbox path
772
+ TERM: 'xterm-256color',
773
+ },
774
+ stdio,
775
+ });
776
+ this.relayPtyProcess = proc;
777
+
778
+ // Handle stdout (agent output) - only in headless mode
779
+ if (!this.isInteractive && proc.stdout) {
780
+ proc.stdout.on('data', (data: Buffer) => {
781
+ const text = data.toString();
782
+ this.handleOutput(text);
783
+ });
784
+ }
785
+
786
+ // Capture stderr for early exit diagnosis
787
+ let stderrBuffer = '';
788
+
789
+ // Handle stderr (relay-pty logs and JSON output) - always needed
790
+ // Also captures to buffer for error diagnostics if process dies early
791
+ if (proc.stderr) {
792
+ proc.stderr.on('data', (data: Buffer) => {
793
+ const text = data.toString();
794
+ stderrBuffer += text;
795
+ this.handleStderr(text);
796
+ });
797
+ }
798
+
799
+ // Handle exit
800
+ proc.on('exit', (code, signal) => {
801
+ const exitCode = code ?? (signal === 'SIGKILL' ? 137 : 1);
802
+ this.log(` Process exited: code=${exitCode} signal=${signal}`);
803
+
804
+ // Capture early exit info for better error messages if socket not yet connected
805
+ if (!this.socketConnected) {
806
+ this.earlyExitInfo = { code, signal, stderr: stderrBuffer };
807
+ }
808
+
809
+ this.running = false;
810
+
811
+ // Get crash context before unregistering from memory monitor
812
+ const crashContext = this.memoryMonitor.getCrashContext(this.config.name);
813
+
814
+ // Unregister from memory monitor
815
+ this.memoryMonitor.unregister(this.config.name);
816
+ if (this.memoryAlertHandler) {
817
+ this.memoryMonitor.off('alert', this.memoryAlertHandler);
818
+ this.memoryAlertHandler = null;
819
+ }
820
+
821
+ // Clean up cgroup (fire and forget - process already exited)
822
+ if (this.hasCgroupSetup) {
823
+ this.cgroupManager.removeAgentCgroup(this.config.name).catch(() => {});
824
+ this.hasCgroupSetup = false;
825
+ }
826
+
827
+ // Broadcast crash notification if not a graceful stop
828
+ if (!this.isGracefulStop && this.client.state === 'READY') {
829
+ const canBroadcast = typeof (this.client as any).broadcast === 'function';
830
+ const isNormalExit = exitCode === 0;
831
+ const wasKilled = signal === 'SIGKILL' || signal === 'SIGTERM' || exitCode === 137;
832
+
833
+ if (!isNormalExit) {
834
+ const reason = wasKilled
835
+ ? `killed by signal ${signal || 'SIGKILL'}`
836
+ : `exit code ${exitCode}`;
837
+
838
+ // Include crash context analysis if available
839
+ const contextInfo = crashContext.likelyCause !== 'unknown'
840
+ ? ` Likely cause: ${crashContext.likelyCause}. ${crashContext.analysisNotes.slice(0, 2).join('. ')}`
841
+ : '';
842
+
843
+ const message = `AGENT CRASHED: "${this.config.name}" has died unexpectedly (${reason}).${contextInfo}`;
844
+
845
+ this.log(` Broadcasting crash notification: ${message}`);
846
+ if (canBroadcast) {
847
+ this.client.broadcast(message, 'message', {
848
+ isSystemMessage: true,
849
+ agentName: this.config.name,
850
+ exitCode,
851
+ signal: signal || undefined,
852
+ crashType: 'unexpected_exit',
853
+ crashContext: {
854
+ likelyCause: crashContext.likelyCause,
855
+ peakMemory: crashContext.peakMemory,
856
+ averageMemory: crashContext.averageMemory,
857
+ memoryTrend: crashContext.memoryTrend,
858
+ },
859
+ });
860
+ } else {
861
+ this.log(' broadcast skipped: client.broadcast not available');
862
+ }
863
+ }
864
+ }
865
+
866
+ this.emit('exit', exitCode);
867
+ this.config.onExit?.(exitCode);
868
+ });
869
+
870
+ // Handle error
871
+ proc.on('error', (err) => {
872
+ this.logError(` Process error: ${err.message}`);
873
+ this.emit('error', err);
874
+ });
875
+
876
+ // Wait for process to start
877
+ await sleep(500);
878
+
879
+ if (proc.exitCode !== null) {
880
+ // Include any captured stderr in the error for debugging
881
+ const stderrInfo = stderrBuffer ? `\nStderr: ${stderrBuffer.slice(0, 500)}` : '';
882
+ throw new Error(`relay-pty exited immediately with code ${proc.exitCode}${stderrInfo}`);
883
+ }
884
+
885
+ // Register for memory/CPU monitoring
886
+ if (proc.pid) {
887
+ this.memoryMonitor.register(this.config.name, proc.pid);
888
+ this.memoryMonitor.start(); // Idempotent - starts if not already running
889
+
890
+ // Set up CPU limiting via cgroups (if configured and available)
891
+ // This prevents one agent from starving others during npm install/build
892
+ if (this.config.cpuLimitPercent && this.config.cpuLimitPercent > 0) {
893
+ this.setupCgroupLimit(proc.pid, this.config.cpuLimitPercent).catch((err) => {
894
+ this.log(` Failed to set up cgroup CPU limit: ${err.message}`);
895
+ });
896
+ }
897
+
898
+ // Set up alert handler to send resource alerts to dashboard only (not other agents)
899
+ this.memoryAlertHandler = (alert: MemoryAlert) => {
900
+ if (alert.agentName !== this.config.name) return;
901
+ if (this.client.state !== 'READY') return;
902
+
903
+ const message = alert.type === 'recovered'
904
+ ? `AGENT RECOVERED: "${this.config.name}" memory usage returned to normal.`
905
+ : `AGENT RESOURCE ALERT: "${this.config.name}" - ${alert.message} (${formatBytes(alert.currentRss)})`;
906
+
907
+ this.log(` Sending resource alert to users: ${message}`);
908
+ // Send to all human users - agents don't need to know about each other's resource usage
909
+ this.client.sendMessage('@users', message, 'message', {
910
+ isSystemMessage: true,
911
+ agentName: this.config.name,
912
+ alertType: alert.type,
913
+ currentMemory: alert.currentRss,
914
+ threshold: alert.threshold,
915
+ recommendation: alert.recommendation,
916
+ });
917
+ };
918
+ this.memoryMonitor.on('alert', this.memoryAlertHandler);
919
+ }
920
+ }
921
+
922
+ /**
923
+ * Set up cgroup CPU limit for this agent
924
+ */
925
+ private async setupCgroupLimit(pid: number, cpuPercent: number): Promise<void> {
926
+ await this.cgroupManager.initialize();
927
+
928
+ if (!this.cgroupManager.isAvailable()) {
929
+ this.log(` cgroups not available, skipping CPU limit`);
930
+ return;
931
+ }
932
+
933
+ const created = await this.cgroupManager.createAgentCgroup(this.config.name, { cpuPercent });
934
+ if (!created) {
935
+ return;
936
+ }
937
+
938
+ const added = await this.cgroupManager.addProcess(this.config.name, pid);
939
+ if (added) {
940
+ this.hasCgroupSetup = true;
941
+ this.log(` CPU limit set to ${cpuPercent}% for agent ${this.config.name}`);
942
+ }
943
+ }
944
+
945
+ /**
946
+ * Handle output from relay-pty stdout (headless mode only)
947
+ * In interactive mode, stdout goes directly to terminal via inherited stdio
948
+ */
949
+ private handleOutput(data: string): void {
950
+ // Skip processing if agent is no longer running (prevents ghost messages after release)
951
+ if (!this.running) {
952
+ return;
953
+ }
954
+
955
+ this.rawBuffer += data;
956
+ this.outputBuffer += data;
957
+ this.hasReceivedOutput = true;
958
+
959
+ // Feed to idle detector
960
+ this.feedIdleDetectorOutput(data);
961
+
962
+ // Check for unread messages and append indicator if needed
963
+ const indicator = this.formatUnreadIndicator();
964
+ const outputWithIndicator = indicator ? data + indicator : data;
965
+
966
+ // Emit output event (with indicator if present)
967
+ this.emit('output', outputWithIndicator);
968
+
969
+ // Stream to daemon if configured
970
+ if (this.config.streamLogs !== false && this.client.state === 'READY') {
971
+ this.client.sendLog(outputWithIndicator);
972
+ }
973
+
974
+ // Parse for relay commands
975
+ this.parseRelayCommands();
976
+
977
+ // Check for summary and session end
978
+ const cleanContent = stripAnsi(this.rawBuffer);
979
+ this.checkForSummary(cleanContent);
980
+ this.checkForSessionEnd(cleanContent);
981
+ }
982
+
983
+ /**
984
+ * Format an unread message indicator if there are pending messages.
985
+ * Returns empty string if no pending messages or within cooldown period.
986
+ *
987
+ * Example output:
988
+ * ───────────────────────────
989
+ * 📬 2 unread messages (from: Alice, Bob)
990
+ */
991
+ private formatUnreadIndicator(): string {
992
+ const queueLength = this.messageQueue.length;
993
+ if (queueLength === 0) {
994
+ return '';
995
+ }
996
+
997
+ // Check cooldown to avoid spamming
998
+ const now = Date.now();
999
+ if (now - this.lastUnreadIndicatorTime < this.UNREAD_INDICATOR_COOLDOWN_MS) {
1000
+ return '';
1001
+ }
1002
+ this.lastUnreadIndicatorTime = now;
1003
+
1004
+ // Collect unique sender names
1005
+ const senders = [...new Set(this.messageQueue.map(m => m.from))];
1006
+ const senderList = senders.slice(0, 3).join(', ');
1007
+ const moreCount = senders.length > 3 ? ` +${senders.length - 3} more` : '';
1008
+
1009
+ const line = '─'.repeat(27);
1010
+ const messageWord = queueLength === 1 ? 'message' : 'messages';
1011
+
1012
+ return `\n${line}\n📬 ${queueLength} unread ${messageWord} (from: ${senderList}${moreCount})\n`;
1013
+ }
1014
+
1015
+ /**
1016
+ * Handle stderr from relay-pty (logs and JSON parsed commands)
1017
+ */
1018
+ private handleStderr(data: string): void {
1019
+ // Skip processing if agent is no longer running (prevents ghost messages after release)
1020
+ if (!this.running) {
1021
+ return;
1022
+ }
1023
+
1024
+ // relay-pty outputs JSON parsed commands to stderr with --json-output
1025
+ const lines = data.split('\n').filter(l => l.trim());
1026
+ for (const line of lines) {
1027
+ if (line.startsWith('{')) {
1028
+ // JSON output - parsed relay command from Rust
1029
+ try {
1030
+ const parsed = JSON.parse(line);
1031
+ if (parsed.type === 'relay_command' && parsed.kind) {
1032
+ // Log parsed commands (only in debug mode to avoid TUI pollution)
1033
+ if (parsed.kind === 'spawn' || parsed.kind === 'release') {
1034
+ this.log(`Rust parsed [${parsed.kind}]: ${JSON.stringify({
1035
+ spawn_name: parsed.spawn_name,
1036
+ spawn_cli: parsed.spawn_cli,
1037
+ spawn_task: parsed.spawn_task?.substring(0, 50),
1038
+ release_name: parsed.release_name,
1039
+ })}`);
1040
+ } else {
1041
+ this.log(`Rust parsed [${parsed.kind}]: ${parsed.from} -> ${parsed.to}`);
1042
+ }
1043
+ this.handleRustParsedCommand(parsed);
1044
+ } else if (parsed.type === 'continuity') {
1045
+ // Handle continuity commands from relay-pty file-based protocol
1046
+ this.log(`Rust parsed [continuity]: action=${parsed.action}`);
1047
+ this.handleRustContinuityCommand(parsed);
1048
+ }
1049
+ } catch (e) {
1050
+ // Not JSON, just log (only in debug mode)
1051
+ if (this.config.debug) {
1052
+ console.error(`[relay-pty:${this.config.name}] ${line}`);
1053
+ }
1054
+ }
1055
+ } else {
1056
+ // Non-JSON stderr - only show in debug mode (logs, info messages)
1057
+ if (this.config.debug) {
1058
+ console.error(`[relay-pty:${this.config.name}] ${line}`);
1059
+ }
1060
+ }
1061
+ }
1062
+ }
1063
+
1064
+ /**
1065
+ * Handle a parsed command from Rust relay-pty
1066
+ * Rust outputs structured JSON with 'kind' field: "message", "spawn", "release"
1067
+ */
1068
+ private handleRustParsedCommand(parsed: {
1069
+ type: string;
1070
+ kind: string;
1071
+ from: string;
1072
+ to: string;
1073
+ body: string;
1074
+ raw: string;
1075
+ thread?: string;
1076
+ spawn_name?: string;
1077
+ spawn_cli?: string;
1078
+ spawn_task?: string;
1079
+ release_name?: string;
1080
+ }): void {
1081
+ switch (parsed.kind) {
1082
+ case 'spawn':
1083
+ if (parsed.spawn_name && parsed.spawn_cli) {
1084
+ this.log(` Spawn detected: ${parsed.spawn_name} (${parsed.spawn_cli})`);
1085
+ this.handleSpawnCommand(parsed.spawn_name, parsed.spawn_cli, parsed.spawn_task || '');
1086
+ }
1087
+ break;
1088
+
1089
+ case 'release':
1090
+ if (parsed.release_name) {
1091
+ this.log(`Release: ${parsed.release_name}`);
1092
+ this.handleReleaseCommand(parsed.release_name);
1093
+ } else {
1094
+ this.logError(`Missing release_name in parsed command: ${JSON.stringify(parsed)}`);
1095
+ }
1096
+ break;
1097
+
1098
+ case 'message':
1099
+ default:
1100
+ this.sendRelayCommand({
1101
+ to: parsed.to,
1102
+ kind: 'message',
1103
+ body: parsed.body,
1104
+ thread: parsed.thread,
1105
+ raw: parsed.raw,
1106
+ });
1107
+ break;
1108
+ }
1109
+ }
1110
+
1111
+ /**
1112
+ * Handle continuity command from Rust relay-pty
1113
+ *
1114
+ * Maps from Rust ContinuityCommand format to TypeScript ContinuityCommand
1115
+ * and forwards to the ContinuityManager.
1116
+ *
1117
+ * Rust format: { type: "continuity", action: string, content: string }
1118
+ * TypeScript format: { type: 'save' | 'load' | 'uncertain', content?: string, item?: string }
1119
+ */
1120
+ private async handleRustContinuityCommand(parsed: {
1121
+ type: string;
1122
+ action: string;
1123
+ content: string;
1124
+ }): Promise<void> {
1125
+ if (!this.continuity) {
1126
+ this.log('Continuity not initialized, skipping continuity command');
1127
+ return;
1128
+ }
1129
+
1130
+ // Map Rust action to TypeScript ContinuityCommand type
1131
+ const action = parsed.action.toLowerCase();
1132
+ if (!['save', 'load', 'uncertain'].includes(action)) {
1133
+ this.logError(`Unknown continuity action: ${parsed.action}`);
1134
+ return;
1135
+ }
1136
+
1137
+ // Build TypeScript ContinuityCommand
1138
+ const command: { type: 'save' | 'load' | 'uncertain'; content?: string; item?: string } = {
1139
+ type: action as 'save' | 'load' | 'uncertain',
1140
+ };
1141
+
1142
+ if (action === 'save' && parsed.content) {
1143
+ command.content = parsed.content;
1144
+ } else if (action === 'uncertain' && parsed.content) {
1145
+ command.item = parsed.content;
1146
+ }
1147
+
1148
+ // Deduplication (same logic as base-wrapper)
1149
+ const cmdHash = `${command.type}:${command.content || command.item || 'no-content'}`;
1150
+ if (command.content && this.processedContinuityCommands.has(cmdHash)) {
1151
+ this.log(`Continuity command already processed: ${cmdHash}`);
1152
+ return;
1153
+ }
1154
+ this.processedContinuityCommands.add(cmdHash);
1155
+
1156
+ // Limit dedup set size
1157
+ if (this.processedContinuityCommands.size > 100) {
1158
+ const oldest = this.processedContinuityCommands.values().next().value;
1159
+ if (oldest) this.processedContinuityCommands.delete(oldest);
1160
+ }
1161
+
1162
+ try {
1163
+ this.log(`Processing continuity command: ${command.type}`);
1164
+ const response = await this.continuity.handleCommand(this.config.name, command);
1165
+ if (response) {
1166
+ // Queue response for injection (e.g., for 'load' command)
1167
+ this.messageQueue.push({
1168
+ from: 'system',
1169
+ body: response,
1170
+ messageId: `continuity-${Date.now()}`,
1171
+ thread: 'continuity-response',
1172
+ });
1173
+ this.log(`Queued continuity response for injection`);
1174
+ } else {
1175
+ this.log(`Continuity command ${command.type} completed (no response)`);
1176
+ }
1177
+ } catch (err: any) {
1178
+ this.logError(`Continuity command failed: ${err.message}`);
1179
+ }
1180
+ }
1181
+
1182
+ /**
1183
+ * Handle spawn command (from Rust stderr JSON parsing)
1184
+ *
1185
+ * Note: We do NOT send the initial task message here because the spawner
1186
+ * now handles it after waitUntilCliReady(). Sending it here would cause
1187
+ * duplicate task delivery.
1188
+ */
1189
+ private handleSpawnCommand(name: string, cli: string, task: string): void {
1190
+ const key = `spawn:${name}:${cli}`;
1191
+ if (this.processedSpawnCommands.has(key)) {
1192
+ this.log(`Spawn already processed: ${key}`);
1193
+ return;
1194
+ }
1195
+ this.processedSpawnCommands.add(key);
1196
+
1197
+ // Log spawn attempts (only in debug mode to avoid TUI pollution)
1198
+ this.log(`SPAWN REQUEST: ${name} (${cli})`);
1199
+ this.log(` dashboardPort=${this.config.dashboardPort}, onSpawn=${!!this.config.onSpawn}`);
1200
+
1201
+ // Try dashboard API first, fall back to callback
1202
+ // The spawner will send the task after waitUntilCliReady()
1203
+ if (this.config.dashboardPort) {
1204
+ this.log(`Calling dashboard API at port ${this.config.dashboardPort}`);
1205
+ this.spawnViaDashboardApi(name, cli, task)
1206
+ .then(() => {
1207
+ this.log(`SPAWN SUCCESS: ${name} via dashboard API`);
1208
+ })
1209
+ .catch(err => {
1210
+ this.logError(`SPAWN FAILED: ${name} - ${err.message}`);
1211
+ if (this.config.onSpawn) {
1212
+ this.log(`Falling back to onSpawn callback`);
1213
+ Promise.resolve(this.config.onSpawn(name, cli, task))
1214
+ .catch(e => this.logError(`SPAWN CALLBACK FAILED: ${e.message}`));
1215
+ }
1216
+ });
1217
+ } else if (this.config.onSpawn) {
1218
+ this.log(`Using onSpawn callback directly`);
1219
+ Promise.resolve(this.config.onSpawn(name, cli, task))
1220
+ .catch(e => this.logError(`SPAWN CALLBACK FAILED: ${e.message}`));
1221
+ } else {
1222
+ this.logError(`SPAWN FAILED: No spawn mechanism available! (dashboardPort=${this.config.dashboardPort}, onSpawn=${!!this.config.onSpawn})`);
1223
+ }
1224
+ }
1225
+
1226
+ /**
1227
+ * Handle release command
1228
+ */
1229
+ private handleReleaseCommand(name: string): void {
1230
+ const key = `release:${name}`;
1231
+ if (this.processedReleaseCommands.has(key)) {
1232
+ return;
1233
+ }
1234
+ this.processedReleaseCommands.add(key);
1235
+
1236
+ this.log(` Release: ${name}`);
1237
+
1238
+ // Try dashboard API first, fall back to callback
1239
+ if (this.config.dashboardPort) {
1240
+ this.releaseViaDashboardApi(name).catch(err => {
1241
+ this.logError(` Dashboard release failed: ${err.message}`);
1242
+ this.config.onRelease?.(name);
1243
+ });
1244
+ } else if (this.config.onRelease) {
1245
+ this.config.onRelease(name);
1246
+ }
1247
+ }
1248
+
1249
+ /**
1250
+ * Spawn agent via dashboard API
1251
+ */
1252
+ private async spawnViaDashboardApi(name: string, cli: string, task: string): Promise<void> {
1253
+ const url = `http://localhost:${this.config.dashboardPort}/api/spawn`;
1254
+ const body = {
1255
+ name,
1256
+ cli,
1257
+ task,
1258
+ spawnerName: this.config.name, // Include spawner name so task appears from correct agent
1259
+ };
1260
+
1261
+ try {
1262
+ const response = await fetch(url, {
1263
+ method: 'POST',
1264
+ headers: { 'Content-Type': 'application/json' },
1265
+ body: JSON.stringify(body),
1266
+ });
1267
+
1268
+ if (!response.ok) {
1269
+ const errorBody = await response.text().catch(() => 'unknown');
1270
+ throw new Error(`HTTP ${response.status}: ${errorBody}`);
1271
+ }
1272
+
1273
+ const result = await response.json().catch(() => ({})) as { success?: boolean; error?: string };
1274
+ if (result.success === false) {
1275
+ throw new Error(result.error || 'Spawn failed without specific error');
1276
+ }
1277
+ } catch (err: any) {
1278
+ // Enhance error with context
1279
+ if (err.code === 'ECONNREFUSED') {
1280
+ throw new Error(`Dashboard not reachable at ${url} (connection refused)`);
1281
+ }
1282
+ throw err;
1283
+ }
1284
+ }
1285
+
1286
+ /**
1287
+ * Release agent via dashboard API
1288
+ */
1289
+ private async releaseViaDashboardApi(name: string): Promise<void> {
1290
+ const response = await fetch(`http://localhost:${this.config.dashboardPort}/api/spawned/${encodeURIComponent(name)}`, {
1291
+ method: 'DELETE',
1292
+ });
1293
+ if (!response.ok) {
1294
+ const body = await response.json().catch(() => ({ error: 'Unknown' })) as { error?: string };
1295
+ throw new Error(`HTTP ${response.status}: ${body.error || 'Unknown error'}`);
1296
+ }
1297
+ this.log(`Released ${name} via dashboard API`);
1298
+ }
1299
+
1300
+ // =========================================================================
1301
+ // Socket communication
1302
+ // =========================================================================
1303
+
1304
+ /**
1305
+ * Check if the relay-pty process is still alive
1306
+ */
1307
+ private isProcessAlive(): boolean {
1308
+ if (!this.relayPtyProcess || this.relayPtyProcess.exitCode !== null) {
1309
+ return false;
1310
+ }
1311
+ try {
1312
+ // Signal 0 checks if process exists without killing it
1313
+ process.kill(this.relayPtyProcess.pid!, 0);
1314
+ return true;
1315
+ } catch {
1316
+ return false;
1317
+ }
1318
+ }
1319
+
1320
+ /**
1321
+ * Connect to the relay-pty socket
1322
+ */
1323
+ private async connectToSocket(): Promise<void> {
1324
+ const timeout = this.config.socketConnectTimeoutMs ?? 5000;
1325
+ const maxAttempts = this.config.socketReconnectAttempts ?? 3;
1326
+
1327
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1328
+ // Check if relay-pty process died before attempting connection
1329
+ if (!this.isProcessAlive()) {
1330
+ const exitInfo = this.earlyExitInfo;
1331
+ if (exitInfo) {
1332
+ const exitReason = exitInfo.signal
1333
+ ? `signal ${exitInfo.signal}`
1334
+ : `code ${exitInfo.code ?? 'unknown'}`;
1335
+ const stderrHint = exitInfo.stderr
1336
+ ? `\n stderr: ${exitInfo.stderr.trim().slice(0, 500)}`
1337
+ : '';
1338
+ throw new Error(`relay-pty process died early (${exitReason}).${stderrHint}`);
1339
+ }
1340
+ throw new Error('relay-pty process died before socket could be created');
1341
+ }
1342
+
1343
+ try {
1344
+ await this.attemptSocketConnection(timeout);
1345
+ this.log(` Socket connected`);
1346
+ return;
1347
+ } catch (err: any) {
1348
+ this.logError(` Socket connect attempt ${attempt}/${maxAttempts} failed: ${err.message}`);
1349
+ if (attempt < maxAttempts) {
1350
+ await sleep(1000 * attempt); // Exponential backoff
1351
+ }
1352
+ }
1353
+ }
1354
+
1355
+ // Final check for process death after all attempts
1356
+ if (!this.isProcessAlive() && this.earlyExitInfo) {
1357
+ const exitInfo = this.earlyExitInfo;
1358
+ const exitReason = exitInfo.signal
1359
+ ? `signal ${exitInfo.signal}`
1360
+ : `code ${exitInfo.code ?? 'unknown'}`;
1361
+ const stderrHint = exitInfo.stderr
1362
+ ? `\n stderr: ${exitInfo.stderr.trim().slice(0, 500)}`
1363
+ : '';
1364
+ throw new Error(`relay-pty process died during socket connection (${exitReason}).${stderrHint}`);
1365
+ }
1366
+
1367
+ throw new Error(`Failed to connect to socket after ${maxAttempts} attempts`);
1368
+ }
1369
+
1370
+ /**
1371
+ * Attempt a single socket connection
1372
+ */
1373
+ private attemptSocketConnection(timeout: number): Promise<void> {
1374
+ return new Promise((resolve, reject) => {
1375
+ // Clean up any existing socket before creating new one
1376
+ // This prevents orphaned sockets with stale event handlers
1377
+ if (this.socket) {
1378
+ // Remove all listeners to prevent the old socket's 'close' event
1379
+ // from triggering another reconnect cycle
1380
+ this.socket.removeAllListeners();
1381
+ this.socket.destroy();
1382
+ this.socket = undefined;
1383
+ }
1384
+
1385
+ const timer = setTimeout(() => {
1386
+ reject(new Error('Socket connection timeout'));
1387
+ }, timeout);
1388
+
1389
+ this.socket = createConnection(this.socketPath, () => {
1390
+ clearTimeout(timer);
1391
+ this.socketConnected = true;
1392
+ resolve();
1393
+ });
1394
+
1395
+ this.socket.on('error', (err) => {
1396
+ clearTimeout(timer);
1397
+ this.socketConnected = false;
1398
+ reject(err);
1399
+ });
1400
+
1401
+ // Handle 'end' event - server closed its write side (half-close)
1402
+ this.socket.on('end', () => {
1403
+ this.socketConnected = false;
1404
+ this.log(` Socket received end (server closed write side)`);
1405
+ });
1406
+
1407
+ this.socket.on('close', () => {
1408
+ this.socketConnected = false;
1409
+ this.log(` Socket closed`);
1410
+ // Auto-reconnect if not intentionally stopped
1411
+ if (this.running && !this.isGracefulStop) {
1412
+ this.scheduleSocketReconnect();
1413
+ }
1414
+ });
1415
+
1416
+ // Handle incoming data (responses)
1417
+ let buffer = '';
1418
+ this.socket.on('data', (data: Buffer) => {
1419
+ buffer += data.toString();
1420
+
1421
+ // Process complete lines
1422
+ const lines = buffer.split('\n');
1423
+ buffer = lines.pop() ?? ''; // Keep incomplete line in buffer
1424
+
1425
+ for (const line of lines) {
1426
+ if (line.trim()) {
1427
+ this.handleSocketResponse(line);
1428
+ }
1429
+ }
1430
+ });
1431
+ });
1432
+ }
1433
+
1434
+ /**
1435
+ * Disconnect from socket
1436
+ */
1437
+ private disconnectSocket(): void {
1438
+ if (this.socket) {
1439
+ this.socket.destroy();
1440
+ this.socket = undefined;
1441
+ this.socketConnected = false;
1442
+ }
1443
+
1444
+ // Reject all pending injections
1445
+ for (const [_id, pending] of this.pendingInjections) {
1446
+ clearTimeout(pending.timeout);
1447
+ pending.reject(new Error('Socket disconnected'));
1448
+ }
1449
+ this.pendingInjections.clear();
1450
+ }
1451
+
1452
+ /** Timer for socket reconnection */
1453
+ private socketReconnectTimer?: NodeJS.Timeout;
1454
+ /** Current reconnection attempt count */
1455
+ private socketReconnectAttempt = 0;
1456
+
1457
+ /**
1458
+ * Schedule a socket reconnection attempt with exponential backoff
1459
+ */
1460
+ private scheduleSocketReconnect(): void {
1461
+ const maxAttempts = this.config.socketReconnectAttempts ?? 3;
1462
+
1463
+ // Clear any existing timer
1464
+ if (this.socketReconnectTimer) {
1465
+ clearTimeout(this.socketReconnectTimer);
1466
+ this.socketReconnectTimer = undefined;
1467
+ }
1468
+
1469
+ if (this.socketReconnectAttempt >= maxAttempts) {
1470
+ this.logError(` Socket reconnect failed after ${maxAttempts} attempts`);
1471
+ // Reset counter for future reconnects (processMessageQueue can trigger new cycle)
1472
+ this.socketReconnectAttempt = 0;
1473
+ // Note: socketReconnectTimer is already undefined, allowing processMessageQueue
1474
+ // to trigger a new reconnection cycle when new messages arrive
1475
+ return;
1476
+ }
1477
+
1478
+ this.socketReconnectAttempt++;
1479
+ const delay = Math.min(1000 * Math.pow(2, this.socketReconnectAttempt - 1), 10000); // Max 10s
1480
+
1481
+ this.log(` Scheduling socket reconnect in ${delay}ms (attempt ${this.socketReconnectAttempt}/${maxAttempts})`);
1482
+
1483
+ this.socketReconnectTimer = setTimeout(async () => {
1484
+ // Clear timer reference now that callback is executing
1485
+ this.socketReconnectTimer = undefined;
1486
+
1487
+ if (!this.running || this.isGracefulStop) {
1488
+ return;
1489
+ }
1490
+
1491
+ try {
1492
+ const timeout = this.config.socketConnectTimeoutMs ?? 5000;
1493
+ await this.attemptSocketConnection(timeout);
1494
+ this.log(` Socket reconnected successfully`);
1495
+ this.socketReconnectAttempt = 0; // Reset on success
1496
+
1497
+ // Process any queued messages that were waiting
1498
+ if (this.messageQueue.length > 0 && !this.isInjecting) {
1499
+ this.log(` Processing ${this.messageQueue.length} queued messages after reconnect`);
1500
+ this.processMessageQueue();
1501
+ }
1502
+ } catch (err: any) {
1503
+ this.logError(` Socket reconnect attempt ${this.socketReconnectAttempt} failed: ${err.message}`);
1504
+ // Schedule another attempt
1505
+ this.scheduleSocketReconnect();
1506
+ }
1507
+ }, delay);
1508
+ }
1509
+
1510
+ /**
1511
+ * Send a request to the socket and optionally wait for response
1512
+ */
1513
+ private sendSocketRequest(request: RelayPtyRequest): Promise<void> {
1514
+ return new Promise((resolve, reject) => {
1515
+ if (!this.socket || !this.socketConnected) {
1516
+ reject(new Error('Socket not connected'));
1517
+ return;
1518
+ }
1519
+
1520
+ const json = JSON.stringify(request) + '\n';
1521
+ this.socket.write(json, (err) => {
1522
+ if (err) {
1523
+ reject(err);
1524
+ } else {
1525
+ resolve();
1526
+ }
1527
+ });
1528
+ });
1529
+ }
1530
+
1531
+ /**
1532
+ * Handle a response from the socket
1533
+ */
1534
+ private handleSocketResponse(line: string): void {
1535
+ try {
1536
+ const response = JSON.parse(line) as RelayPtyResponse;
1537
+
1538
+ switch (response.type) {
1539
+ case 'inject_result':
1540
+ // handleInjectResult is async (does verification), but we don't await here
1541
+ // Errors are handled internally by the method
1542
+ this.handleInjectResult(response).catch((err: Error) => {
1543
+ this.logError(` Error handling inject result: ${err.message}`);
1544
+ });
1545
+ break;
1546
+
1547
+ case 'status':
1548
+ // Status responses are typically requested explicitly
1549
+ this.log(` Status: idle=${response.agent_idle} queue=${response.queue_length}`);
1550
+ break;
1551
+
1552
+ case 'backpressure':
1553
+ this.handleBackpressure(response);
1554
+ break;
1555
+
1556
+ case 'error':
1557
+ this.logError(` Socket error: ${response.message}`);
1558
+ break;
1559
+
1560
+ case 'shutdown_ack':
1561
+ this.log(` Shutdown acknowledged`);
1562
+ break;
1563
+
1564
+ case 'send_enter_result':
1565
+ // SendEnter is no longer used - trust Rust delivery confirmation
1566
+ this.log(` Received send_enter_result (deprecated)`);
1567
+ break;
1568
+ }
1569
+ } catch (err: any) {
1570
+ this.logError(` Failed to parse socket response: ${err.message}`);
1571
+ }
1572
+ }
1573
+
1574
+ /**
1575
+ * Handle injection result response
1576
+ * After Rust reports 'delivered', verifies the message appeared in output.
1577
+ * If verification fails, retries up to MAX_RETRIES times.
1578
+ */
1579
+ private async handleInjectResult(response: InjectResultResponse): Promise<void> {
1580
+ this.log(` handleInjectResult: id=${response.id.substring(0, 8)} status=${response.status}`);
1581
+
1582
+ const pending = this.pendingInjections.get(response.id);
1583
+ if (!pending) {
1584
+ // Response for unknown message - might be from a previous session
1585
+ this.log(` No pending injection found for ${response.id.substring(0, 8)}`);
1586
+ return;
1587
+ }
1588
+
1589
+ if (response.status === 'delivered') {
1590
+ // Rust says it sent the message + Enter key
1591
+ // Trust Rust's delivery confirmation - relay-pty writes directly to PTY which is very reliable.
1592
+ //
1593
+ // IMPORTANT: We don't verify by looking for the message in output because:
1594
+ // 1. TUI CLIs (Claude, Codex, Gemini) don't echo input like traditional terminals
1595
+ // 2. The injected text appears as INPUT to the PTY, not OUTPUT
1596
+ // 3. Output-based verification always fails for TUIs, causing unnecessary retries
1597
+ //
1598
+ // This is different from tmux-wrapper where we inject via tmux send-keys
1599
+ // and can observe the echoed input in the pane output.
1600
+ this.log(` Message ${pending.shortId} delivered by Rust ✓`);
1601
+
1602
+ clearTimeout(pending.timeout);
1603
+ this.pendingInjections.delete(response.id);
1604
+ if (pending.retryCount === 0) {
1605
+ this.injectionMetrics.successFirstTry++;
1606
+ } else {
1607
+ this.injectionMetrics.successWithRetry++;
1608
+ }
1609
+ this.injectionMetrics.total++;
1610
+ pending.resolve(true);
1611
+ } else if (response.status === 'failed') {
1612
+ clearTimeout(pending.timeout);
1613
+ this.pendingInjections.delete(response.id);
1614
+ this.injectionMetrics.failed++;
1615
+ this.injectionMetrics.total++;
1616
+ pending.resolve(false);
1617
+ this.logError(` Message ${pending.shortId} failed: ${response.error}`);
1618
+ this.emit('injection-failed', {
1619
+ messageId: response.id,
1620
+ from: pending.from,
1621
+ error: response.error ?? 'Unknown error',
1622
+ });
1623
+ }
1624
+ // queued/injecting are intermediate states - wait for final status
1625
+ }
1626
+
1627
+ /**
1628
+ * Handle backpressure notification
1629
+ */
1630
+ private handleBackpressure(response: BackpressureResponse): void {
1631
+ const wasActive = this.backpressureActive;
1632
+ this.backpressureActive = !response.accept;
1633
+
1634
+ if (this.backpressureActive !== wasActive) {
1635
+ this.log(` Backpressure: ${this.backpressureActive ? 'ACTIVE' : 'cleared'} (queue=${response.queue_length})`);
1636
+ this.emit('backpressure', { queueLength: response.queue_length, accept: response.accept });
1637
+
1638
+ // Resume processing if backpressure cleared
1639
+ if (!this.backpressureActive) {
1640
+ this.processMessageQueue();
1641
+ }
1642
+ }
1643
+ }
1644
+
1645
+ // =========================================================================
1646
+ // Message handling
1647
+ // =========================================================================
1648
+
1649
+ /**
1650
+ * Inject a message into the agent via socket
1651
+ */
1652
+ private async injectMessage(msg: QueuedMessage, retryCount = 0): Promise<boolean> {
1653
+ const shortId = msg.messageId.substring(0, 8);
1654
+ this.log(` === INJECT START: ${shortId} from ${msg.from} (attempt ${retryCount + 1}) ===`);
1655
+
1656
+ if (!this.socket || !this.socketConnected) {
1657
+ this.logError(` Cannot inject - socket not connected`);
1658
+ return false;
1659
+ }
1660
+
1661
+ // Build injection content
1662
+ const content = buildInjectionString(msg);
1663
+ this.log(` Injection content (${content.length} bytes): ${content.substring(0, 100)}...`);
1664
+
1665
+ // Create request
1666
+ const request: InjectRequest = {
1667
+ type: 'inject',
1668
+ id: msg.messageId,
1669
+ from: msg.from,
1670
+ body: content,
1671
+ priority: msg.importance ?? 0,
1672
+ };
1673
+
1674
+ this.log(` Sending inject request to socket...`);
1675
+
1676
+ // Create promise for result
1677
+ return new Promise<boolean>((resolve, reject) => {
1678
+ const timeout = setTimeout(() => {
1679
+ this.logError(` Inject timeout for ${shortId} after 30s`);
1680
+ this.pendingInjections.delete(msg.messageId);
1681
+ resolve(false); // Timeout = failure
1682
+ }, 30000); // 30 second timeout for injection
1683
+
1684
+ this.pendingInjections.set(msg.messageId, {
1685
+ resolve,
1686
+ reject,
1687
+ timeout,
1688
+ from: msg.from,
1689
+ shortId,
1690
+ retryCount,
1691
+ originalBody: content,
1692
+ });
1693
+
1694
+ // Send request
1695
+ this.sendSocketRequest(request)
1696
+ .then(() => {
1697
+ this.log(` Socket request sent for ${shortId}`);
1698
+ })
1699
+ .catch((err) => {
1700
+ this.logError(` Socket request failed for ${shortId}: ${err.message}`);
1701
+ clearTimeout(timeout);
1702
+ this.pendingInjections.delete(msg.messageId);
1703
+ resolve(false);
1704
+ });
1705
+ });
1706
+ }
1707
+
1708
+ /**
1709
+ * Process queued messages
1710
+ */
1711
+ private async processMessageQueue(): Promise<void> {
1712
+ // Debug: Log blocking conditions when queue has messages
1713
+ if (this.messageQueue.length > 0) {
1714
+ if (!this.readyForMessages) {
1715
+ this.log(` Queue blocked: readyForMessages=false (queue=${this.messageQueue.length})`);
1716
+ return;
1717
+ }
1718
+ if (this.backpressureActive) {
1719
+ this.log(` Queue blocked: backpressure active (queue=${this.messageQueue.length})`);
1720
+ return;
1721
+ }
1722
+ if (this.isInjecting) {
1723
+ // Already injecting - the finally block will process next message
1724
+ // But add a safety timeout in case injection gets stuck
1725
+ const elapsed = this.injectionStartTime > 0 ? Date.now() - this.injectionStartTime : 0;
1726
+ if (elapsed > 35000) {
1727
+ this.logError(` Injection stuck for ${elapsed}ms, forcing reset`);
1728
+ this.isInjecting = false;
1729
+ this.injectionStartTime = 0;
1730
+ }
1731
+ return;
1732
+ }
1733
+ }
1734
+
1735
+ if (this.messageQueue.length === 0) {
1736
+ return;
1737
+ }
1738
+
1739
+ // Proactively reconnect socket if disconnected and we have messages to send
1740
+ if (!this.socketConnected && !this.socketReconnectTimer) {
1741
+ this.log(` Socket disconnected, triggering reconnect before processing queue`);
1742
+ this.scheduleSocketReconnect();
1743
+ return; // Wait for reconnection to complete
1744
+ }
1745
+
1746
+ if (!this.socketConnected) {
1747
+ // Reconnection in progress, wait for it
1748
+ this.log(` Queue waiting: socket reconnecting (queue=${this.messageQueue.length})`);
1749
+ return;
1750
+ }
1751
+
1752
+ // Check if agent is in editor mode - delay injection if so
1753
+ const idleResult = this.idleDetector.checkIdle();
1754
+ if (idleResult.inEditorMode) {
1755
+ this.log(` Agent in editor mode, delaying injection (queue: ${this.messageQueue.length})`);
1756
+ // Check again in 2 seconds
1757
+ setTimeout(() => this.processMessageQueue(), 2000);
1758
+ return;
1759
+ }
1760
+
1761
+ this.isInjecting = true;
1762
+ this.injectionStartTime = Date.now();
1763
+
1764
+ const msg = this.messageQueue.shift()!;
1765
+ const bodyPreview = msg.body.substring(0, 50).replace(/\n/g, '\\n');
1766
+ this.log(` Processing message from ${msg.from}: "${bodyPreview}..." (remaining=${this.messageQueue.length})`);
1767
+
1768
+ try {
1769
+ const success = await this.injectMessage(msg);
1770
+
1771
+ // Metrics are now tracked in handleInjectResult which knows about retries
1772
+ if (!success) {
1773
+ // Record failure for adaptive throttling
1774
+ this.throttle.recordFailure();
1775
+ this.logError(` Injection failed for message ${msg.messageId.substring(0, 8)}`);
1776
+ this.config.onInjectionFailed?.(msg.messageId, 'Injection failed');
1777
+ this.sendSyncAck(msg.messageId, msg.sync, 'ERROR', { error: 'injection_failed' });
1778
+ } else {
1779
+ // Record success for adaptive throttling
1780
+ this.throttle.recordSuccess();
1781
+ this.sendSyncAck(msg.messageId, msg.sync, 'OK');
1782
+ }
1783
+ } catch (err: any) {
1784
+ this.logError(` Injection error: ${err.message}`);
1785
+ // Track metrics for exceptions (not handled by handleInjectResult)
1786
+ this.injectionMetrics.failed++;
1787
+ this.injectionMetrics.total++;
1788
+ // Record failure for adaptive throttling
1789
+ this.throttle.recordFailure();
1790
+ this.sendSyncAck(msg.messageId, msg.sync, 'ERROR', { error: err.message });
1791
+ } finally {
1792
+ this.isInjecting = false;
1793
+ this.injectionStartTime = 0;
1794
+
1795
+ // Process next message after adaptive delay (faster when healthy, slower under stress)
1796
+ if (this.messageQueue.length > 0 && !this.backpressureActive) {
1797
+ const delay = this.throttle.getDelay();
1798
+ setTimeout(() => this.processMessageQueue(), delay);
1799
+ }
1800
+ }
1801
+ }
1802
+
1803
+ /**
1804
+ * Override handleIncomingMessage to trigger queue processing
1805
+ */
1806
+ protected override handleIncomingMessage(
1807
+ from: string,
1808
+ payload: SendPayload,
1809
+ messageId: string,
1810
+ meta?: SendMeta,
1811
+ originalTo?: string
1812
+ ): void {
1813
+ this.log(` === MESSAGE RECEIVED: ${messageId.substring(0, 8)} from ${from} ===`);
1814
+ this.log(` Body preview: ${payload.body?.substring(0, 100) ?? '(no body)'}...`);
1815
+ super.handleIncomingMessage(from, payload, messageId, meta, originalTo);
1816
+ this.log(` Queue length after add: ${this.messageQueue.length}`);
1817
+ this.processMessageQueue();
1818
+ }
1819
+
1820
+ /**
1821
+ * Override handleIncomingChannelMessage to trigger queue processing.
1822
+ * Without this override, channel messages would be queued but processMessageQueue()
1823
+ * would never be called, causing messages to get stuck until the queue monitor runs.
1824
+ */
1825
+ protected override handleIncomingChannelMessage(
1826
+ from: string,
1827
+ channel: string,
1828
+ body: string,
1829
+ envelope: Envelope<ChannelMessagePayload>
1830
+ ): void {
1831
+ this.log(` === CHANNEL MESSAGE RECEIVED: ${envelope.id.substring(0, 8)} from ${from} on ${channel} ===`);
1832
+ this.log(` Body preview: ${body?.substring(0, 100) ?? '(no body)'}...`);
1833
+ super.handleIncomingChannelMessage(from, channel, body, envelope);
1834
+ this.log(` Queue length after add: ${this.messageQueue.length}`);
1835
+ this.processMessageQueue();
1836
+ }
1837
+
1838
+ // =========================================================================
1839
+ // Queue monitor - Detect and process stuck messages
1840
+ // =========================================================================
1841
+
1842
+ /**
1843
+ * Start the queue monitor to periodically check for stuck messages.
1844
+ * This ensures messages don't get orphaned in the queue when the agent is idle.
1845
+ */
1846
+ private startQueueMonitor(): void {
1847
+ if (this.queueMonitorTimer) {
1848
+ return; // Already started
1849
+ }
1850
+
1851
+ this.log(` Starting queue monitor (interval: ${this.QUEUE_MONITOR_INTERVAL_MS}ms)`);
1852
+
1853
+ this.queueMonitorTimer = setInterval(() => {
1854
+ this.checkForStuckQueue();
1855
+ }, this.QUEUE_MONITOR_INTERVAL_MS);
1856
+
1857
+ // Don't keep process alive just for queue monitoring
1858
+ this.queueMonitorTimer.unref?.();
1859
+ }
1860
+
1861
+ /**
1862
+ * Stop the queue monitor.
1863
+ */
1864
+ private stopQueueMonitor(): void {
1865
+ if (this.queueMonitorTimer) {
1866
+ clearInterval(this.queueMonitorTimer);
1867
+ this.queueMonitorTimer = undefined;
1868
+ this.log(` Queue monitor stopped`);
1869
+ }
1870
+ }
1871
+
1872
+ // =========================================================================
1873
+ // Protocol monitoring (detect agent mistakes like empty AGENT_RELAY_NAME)
1874
+ // =========================================================================
1875
+
1876
+ /**
1877
+ * Start watching for protocol issues in the outbox directory.
1878
+ * Detects common mistakes like:
1879
+ * - Empty AGENT_RELAY_NAME causing files at outbox//
1880
+ * - Files created directly in outbox/ instead of agent subdirectory
1881
+ */
1882
+ private startProtocolMonitor(): void {
1883
+ // Get the outbox parent directory (one level up from agent's outbox)
1884
+ const parentDir = dirname(this._canonicalOutboxPath);
1885
+
1886
+ // Ensure parent directory exists
1887
+ try {
1888
+ if (!existsSync(parentDir)) {
1889
+ mkdirSync(parentDir, { recursive: true });
1890
+ }
1891
+ } catch {
1892
+ // Ignore - directory may already exist
1893
+ }
1894
+
1895
+ try {
1896
+ this.protocolWatcher = watch(parentDir, (eventType, filename) => {
1897
+ if (eventType === 'rename' && filename) {
1898
+ // Check for files directly in parent (not in agent subdirectory)
1899
+ // This happens when $AGENT_RELAY_NAME is empty
1900
+ const fullPath = join(parentDir, filename);
1901
+ try {
1902
+ // If it's a file (not directory) directly in the parent, that's an issue
1903
+ if (existsSync(fullPath) && !lstatSync(fullPath).isDirectory()) {
1904
+ this.handleProtocolIssue('file_in_root', filename);
1905
+ }
1906
+ // Check for empty-named directory (double slash symptom)
1907
+ if (filename === '' || filename.startsWith('/')) {
1908
+ this.handleProtocolIssue('empty_agent_name', filename);
1909
+ }
1910
+ } catch {
1911
+ // Ignore stat errors
1912
+ }
1913
+ }
1914
+ });
1915
+
1916
+ // Don't keep process alive just for protocol monitoring
1917
+ this.protocolWatcher.unref?.();
1918
+ this.log(` Protocol monitor started on ${parentDir}`);
1919
+ } catch (err: any) {
1920
+ // Don't fail start() if protocol monitoring fails
1921
+ this.logError(` Failed to start protocol monitor: ${err.message}`);
1922
+ }
1923
+
1924
+ // Also do an initial scan for existing issues
1925
+ this.scanForProtocolIssues();
1926
+ }
1927
+
1928
+ /**
1929
+ * Stop the protocol monitor.
1930
+ */
1931
+ private stopProtocolMonitor(): void {
1932
+ if (this.protocolWatcher) {
1933
+ this.protocolWatcher.close();
1934
+ this.protocolWatcher = undefined;
1935
+ this.log(` Protocol monitor stopped`);
1936
+ }
1937
+ }
1938
+
1939
+ /**
1940
+ * Scan for existing protocol issues (called once at startup).
1941
+ */
1942
+ private scanForProtocolIssues(): void {
1943
+ const parentDir = dirname(this._canonicalOutboxPath);
1944
+ try {
1945
+ if (!existsSync(parentDir)) return;
1946
+
1947
+ const entries = readdirSync(parentDir);
1948
+ for (const entry of entries) {
1949
+ const fullPath = join(parentDir, entry);
1950
+ try {
1951
+ // Check for files directly in parent (should only be directories)
1952
+ if (!lstatSync(fullPath).isDirectory()) {
1953
+ this.handleProtocolIssue('file_in_root', entry);
1954
+ break; // Only report once
1955
+ }
1956
+ } catch {
1957
+ // Ignore stat errors
1958
+ }
1959
+ }
1960
+ } catch {
1961
+ // Ignore scan errors
1962
+ }
1963
+ }
1964
+
1965
+ /**
1966
+ * Handle a detected protocol issue by injecting a helpful reminder.
1967
+ */
1968
+ private handleProtocolIssue(issue: 'empty_agent_name' | 'file_in_root', filename: string): void {
1969
+ const now = Date.now();
1970
+
1971
+ // Respect cooldown to avoid spamming
1972
+ if (now - this.protocolReminderCooldown < this.PROTOCOL_REMINDER_COOLDOWN_MS) {
1973
+ return;
1974
+ }
1975
+ this.protocolReminderCooldown = now;
1976
+
1977
+ this.log(` Protocol issue detected: ${issue} (${filename})`);
1978
+
1979
+ const reminders: Record<string, string> = {
1980
+ empty_agent_name: `⚠️ **Protocol Issue Detected**
1981
+
1982
+ Your \`$AGENT_RELAY_NAME\` environment variable appears to be empty or unset.
1983
+ Your agent name is: **${this.config.name}**
1984
+
1985
+ Correct outbox path: \`$AGENT_RELAY_OUTBOX\`
1986
+
1987
+ When writing relay files, use:
1988
+ \`\`\`bash
1989
+ cat > $AGENT_RELAY_OUTBOX/msg << 'EOF'
1990
+ TO: TargetAgent
1991
+
1992
+ Your message here
1993
+ EOF
1994
+ \`\`\`
1995
+ Then output: \`->relay-file:msg\``,
1996
+
1997
+ file_in_root: `⚠️ **Protocol Issue Detected**
1998
+
1999
+ Found file "${filename}" directly in the outbox root instead of using the proper path.
2000
+ Your agent name is: **${this.config.name}**
2001
+
2002
+ The \`$AGENT_RELAY_OUTBOX\` path already points to your agent's directory.
2003
+ Write files directly inside it:
2004
+
2005
+ \`\`\`bash
2006
+ cat > $AGENT_RELAY_OUTBOX/msg << 'EOF'
2007
+ TO: TargetAgent
2008
+
2009
+ Your message here
2010
+ EOF
2011
+ \`\`\`
2012
+ Then output: \`->relay-file:msg\``,
2013
+ };
2014
+
2015
+ const reminder = reminders[issue];
2016
+ if (reminder) {
2017
+ this.injectProtocolReminder(reminder);
2018
+ }
2019
+ }
2020
+
2021
+ /**
2022
+ * Inject a protocol reminder message to the agent.
2023
+ */
2024
+ private injectProtocolReminder(message: string): void {
2025
+ const queuedMsg: QueuedMessage = {
2026
+ from: 'system',
2027
+ body: message,
2028
+ messageId: `protocol-reminder-${Date.now()}`,
2029
+ importance: 2, // Higher priority
2030
+ };
2031
+
2032
+ this.messageQueue.unshift(queuedMsg); // Add to front of queue
2033
+ this.log(` Queued protocol reminder (queue size: ${this.messageQueue.length})`);
2034
+
2035
+ // Trigger processing if not already in progress
2036
+ if (!this.isInjecting && this.readyForMessages) {
2037
+ this.processMessageQueue();
2038
+ }
2039
+ }
2040
+
2041
+ // =========================================================================
2042
+ // Periodic protocol reminders (for long sessions where agents forget protocol)
2043
+ // =========================================================================
2044
+
2045
+ /**
2046
+ * Start sending periodic protocol reminders.
2047
+ * Agents in long sessions sometimes forget the relay protocol - these
2048
+ * reminders help them stay on track without user intervention.
2049
+ */
2050
+ private startPeriodicReminder(): void {
2051
+ this.sessionStartTime = Date.now();
2052
+
2053
+ this.periodicReminderTimer = setInterval(() => {
2054
+ this.sendPeriodicProtocolReminder();
2055
+ }, this.PERIODIC_REMINDER_INTERVAL_MS);
2056
+
2057
+ // Don't keep process alive just for reminders
2058
+ this.periodicReminderTimer.unref?.();
2059
+
2060
+ const intervalMinutes = Math.round(this.PERIODIC_REMINDER_INTERVAL_MS / 60000);
2061
+ this.log(` Periodic protocol reminder started (interval: ${intervalMinutes} minutes)`);
2062
+ }
2063
+
2064
+ /**
2065
+ * Stop periodic protocol reminders.
2066
+ */
2067
+ private stopPeriodicReminder(): void {
2068
+ if (this.periodicReminderTimer) {
2069
+ clearInterval(this.periodicReminderTimer);
2070
+ this.periodicReminderTimer = undefined;
2071
+ this.log(` Periodic protocol reminder stopped`);
2072
+ }
2073
+ }
2074
+
2075
+ /**
2076
+ * Send a periodic protocol reminder to the agent.
2077
+ * This reminds agents about proper relay communication format after long sessions.
2078
+ */
2079
+ private sendPeriodicProtocolReminder(): void {
2080
+ // Don't send if not ready
2081
+ if (!this.running || !this.readyForMessages) {
2082
+ return;
2083
+ }
2084
+
2085
+ const sessionDurationMinutes = Math.round((Date.now() - this.sessionStartTime) / 60000);
2086
+
2087
+ const reminder = `📋 **Protocol Reminder** (Session: ${sessionDurationMinutes} minutes)
2088
+
2089
+ You are **${this.config.name}** in a multi-agent relay system. Here's how to communicate:
2090
+
2091
+ **Sending Messages:**
2092
+ \`\`\`bash
2093
+ cat > $AGENT_RELAY_OUTBOX/msg << 'EOF'
2094
+ TO: *
2095
+
2096
+ Your message here
2097
+ EOF
2098
+ \`\`\`
2099
+ Then output: \`->relay-file:msg\`
2100
+
2101
+ Use \`TO: *\` to broadcast to all agents, or \`TO: AgentName\` for a specific agent.
2102
+
2103
+ **Spawning Agents:**
2104
+ \`\`\`bash
2105
+ cat > $AGENT_RELAY_OUTBOX/spawn << 'EOF'
2106
+ KIND: spawn
2107
+ NAME: WorkerName
2108
+ CLI: claude
2109
+
2110
+ Task description here
2111
+ EOF
2112
+ \`\`\`
2113
+ Then output: \`->relay-file:spawn\`
2114
+
2115
+ **Message Format:**
2116
+ - \`TO: AgentName\` for direct messages
2117
+ - \`TO: *\` to broadcast to all agents
2118
+ - \`TO: #channel\` for channel messages
2119
+
2120
+ 📖 See **AGENTS.md** in the project root for full protocol documentation.`;
2121
+
2122
+ this.log(` Sending periodic protocol reminder (session: ${sessionDurationMinutes}m)`);
2123
+ this.injectProtocolReminder(reminder);
2124
+ }
2125
+
2126
+ /**
2127
+ * Check for messages stuck in the queue and process them if the agent is idle.
2128
+ *
2129
+ * This handles cases where:
2130
+ * 1. Messages arrived while the agent was busy and the retry mechanism failed
2131
+ * 2. Socket disconnection/reconnection left messages orphaned
2132
+ * 3. Injection timeouts occurred without proper queue resumption
2133
+ */
2134
+ private checkForStuckQueue(): void {
2135
+ // Skip if not ready for messages
2136
+ if (!this.readyForMessages || !this.running) {
2137
+ return;
2138
+ }
2139
+
2140
+ // Skip if queue is empty
2141
+ if (this.messageQueue.length === 0) {
2142
+ return;
2143
+ }
2144
+
2145
+ // Check if currently injecting
2146
+ if (this.isInjecting) {
2147
+ // Check if injection has been stuck for too long
2148
+ const stuckDuration = Date.now() - this.injectionStartTime;
2149
+ if (stuckDuration > this.MAX_INJECTION_STUCK_MS) {
2150
+ this.logError(` ⚠️ Injection stuck for ${Math.round(stuckDuration / 1000)}s - force resetting`);
2151
+ this.isInjecting = false;
2152
+ this.injectionStartTime = 0;
2153
+ // Clear any pending injections that might be stuck
2154
+ for (const [id, pending] of this.pendingInjections) {
2155
+ clearTimeout(pending.timeout);
2156
+ this.logError(` Clearing stuck pending injection: ${id.substring(0, 8)}`);
2157
+ }
2158
+ this.pendingInjections.clear();
2159
+ // Continue to process the queue below
2160
+ } else {
2161
+ return; // Still within normal injection time
2162
+ }
2163
+ }
2164
+
2165
+ // Skip if backpressure is active
2166
+ if (this.backpressureActive) {
2167
+ return;
2168
+ }
2169
+
2170
+ // Check if the agent is idle (high confidence)
2171
+ const idleResult = this.idleDetector.checkIdle({ minSilenceMs: 2000 });
2172
+ if (!idleResult.isIdle) {
2173
+ // Agent is still working, let it finish
2174
+ return;
2175
+ }
2176
+
2177
+ // We have messages in the queue, agent is idle, not currently injecting
2178
+ // This is a stuck queue situation - trigger processing
2179
+ const senders = [...new Set(this.messageQueue.map(m => m.from))];
2180
+ this.log(` ⚠️ Queue monitor: Found ${this.messageQueue.length} stuck message(s) from [${senders.join(', ')}]`);
2181
+ this.log(` ⚠️ Agent is idle (confidence: ${(idleResult.confidence * 100).toFixed(0)}%), triggering queue processing`);
2182
+
2183
+ // Process the queue
2184
+ this.processMessageQueue();
2185
+ }
2186
+
2187
+ // =========================================================================
2188
+ // Output parsing
2189
+ // =========================================================================
2190
+
2191
+ /**
2192
+ * Parse relay commands from output
2193
+ */
2194
+ private parseRelayCommands(): void {
2195
+ const cleanContent = stripAnsi(this.rawBuffer);
2196
+
2197
+ if (cleanContent.length <= this.lastParsedLength) {
2198
+ return;
2199
+ }
2200
+
2201
+ // Parse new content with lookback for fenced messages
2202
+ const lookbackStart = Math.max(0, this.lastParsedLength - 500);
2203
+ const contentToParse = cleanContent.substring(lookbackStart);
2204
+
2205
+ // Parse fenced messages
2206
+ this.parseFencedMessages(contentToParse);
2207
+
2208
+ // Parse single-line messages
2209
+ this.parseSingleLineMessages(contentToParse);
2210
+
2211
+ // Parse spawn/release commands
2212
+ this.parseSpawnReleaseCommands(contentToParse);
2213
+
2214
+ this.lastParsedLength = cleanContent.length;
2215
+ }
2216
+
2217
+ /**
2218
+ * Parse fenced multi-line messages
2219
+ */
2220
+ private parseFencedMessages(content: string): void {
2221
+ const escapedPrefix = this.relayPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2222
+ const fencePattern = new RegExp(
2223
+ `${escapedPrefix}(\\S+)(?:\\s+\\[thread:([\\w-]+)\\])?\\s*<<<([\\s\\S]*?)>>>`,
2224
+ 'g'
2225
+ );
2226
+
2227
+ let match;
2228
+ while ((match = fencePattern.exec(content)) !== null) {
2229
+ const target = match[1];
2230
+ const thread = match[2];
2231
+ const body = match[3].trim();
2232
+
2233
+ if (!body || target === 'spawn' || target === 'release') {
2234
+ continue;
2235
+ }
2236
+
2237
+ this.sendRelayCommand({
2238
+ to: target,
2239
+ kind: 'message',
2240
+ body,
2241
+ thread,
2242
+ raw: match[0],
2243
+ });
2244
+ }
2245
+ }
2246
+
2247
+ /**
2248
+ * Parse single-line messages
2249
+ */
2250
+ private parseSingleLineMessages(content: string): void {
2251
+ const lines = content.split('\n');
2252
+ const escapedPrefix = this.relayPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2253
+ const pattern = new RegExp(`${escapedPrefix}(\\S+)(?:\\s+\\[thread:([\\w-]+)\\])?\\s+(.+)$`);
2254
+
2255
+ for (const line of lines) {
2256
+ // Skip fenced messages
2257
+ if (line.includes('<<<') || line.includes('>>>')) {
2258
+ continue;
2259
+ }
2260
+
2261
+ const match = line.match(pattern);
2262
+ if (!match) {
2263
+ continue;
2264
+ }
2265
+
2266
+ const target = match[1];
2267
+ const thread = match[2];
2268
+ const body = match[3].trim();
2269
+
2270
+ if (!body || target === 'spawn' || target === 'release') {
2271
+ continue;
2272
+ }
2273
+
2274
+ this.sendRelayCommand({
2275
+ to: target,
2276
+ kind: 'message',
2277
+ body,
2278
+ thread,
2279
+ raw: line,
2280
+ });
2281
+ }
2282
+ }
2283
+
2284
+ // =========================================================================
2285
+ // Summary and session end detection
2286
+ // =========================================================================
2287
+
2288
+ /**
2289
+ * Check for [[SUMMARY]] blocks
2290
+ */
2291
+ private checkForSummary(content: string): void {
2292
+ const result = parseSummaryWithDetails(content);
2293
+ if (!result.found || !result.valid) {
2294
+ return;
2295
+ }
2296
+
2297
+ if (result.rawContent === this.lastSummaryRawContent) {
2298
+ return;
2299
+ }
2300
+ this.lastSummaryRawContent = result.rawContent ?? '';
2301
+
2302
+ this.emit('summary', {
2303
+ agentName: this.config.name,
2304
+ summary: result.summary,
2305
+ });
2306
+ }
2307
+
2308
+ /**
2309
+ * Check for [[SESSION_END]] blocks
2310
+ */
2311
+ private checkForSessionEnd(content: string): void {
2312
+ if (this.sessionEndProcessed) {
2313
+ return;
2314
+ }
2315
+
2316
+ const sessionEnd = parseSessionEndFromOutput(content);
2317
+ if (!sessionEnd) {
2318
+ return;
2319
+ }
2320
+
2321
+ this.sessionEndProcessed = true;
2322
+ this.emit('session-end', {
2323
+ agentName: this.config.name,
2324
+ marker: sessionEnd,
2325
+ });
2326
+ }
2327
+
2328
+ // =========================================================================
2329
+ // Public API
2330
+ // =========================================================================
2331
+
2332
+ /**
2333
+ * Query status from relay-pty
2334
+ */
2335
+ async queryStatus(): Promise<StatusResponse | null> {
2336
+ if (!this.socket || !this.socketConnected) {
2337
+ return null;
2338
+ }
2339
+
2340
+ try {
2341
+ await this.sendSocketRequest({ type: 'status' });
2342
+ // Response will come asynchronously via handleSocketResponse
2343
+ // For now, return null - could implement request/response matching
2344
+ return null;
2345
+ } catch {
2346
+ return null;
2347
+ }
2348
+ }
2349
+
2350
+ /**
2351
+ * Wait for the CLI to be ready to receive messages.
2352
+ * This waits for:
2353
+ * 1. The CLI to produce at least one output (it has started)
2354
+ * 2. The CLI to become idle (it's ready for input)
2355
+ *
2356
+ * This is more reliable than a random sleep because it waits for
2357
+ * actual signals from the CLI rather than guessing how long it takes to start.
2358
+ *
2359
+ * @param timeoutMs Maximum time to wait (default: 30s)
2360
+ * @param pollMs Polling interval (default: 100ms)
2361
+ * @returns true if CLI is ready, false if timeout
2362
+ */
2363
+ async waitUntilCliReady(timeoutMs = 30000, pollMs = 100): Promise<boolean> {
2364
+ const startTime = Date.now();
2365
+ this.log(` Waiting for CLI to be ready (timeout: ${timeoutMs}ms)`);
2366
+
2367
+ // In interactive mode, stdout is inherited (not captured), so hasReceivedOutput
2368
+ // will never be set. Trust that the process is ready if it's running.
2369
+ if (this.isInteractive) {
2370
+ this.log(` Interactive mode - trusting process is ready`);
2371
+ // Give a brief moment for the CLI to initialize its TUI.
2372
+ // 500ms is a conservative estimate based on typical CLI startup times:
2373
+ // - Claude CLI: ~200-300ms to show initial prompt
2374
+ // - Codex/Gemini: ~300-400ms
2375
+ // This delay is only used in interactive mode where we can't detect output.
2376
+ // In non-interactive mode, we poll for actual output instead.
2377
+ await sleep(500);
2378
+ return this.running;
2379
+ }
2380
+
2381
+ // Phase 1: Wait for first output (CLI has started)
2382
+ while (Date.now() - startTime < timeoutMs) {
2383
+ if (this.hasReceivedOutput) {
2384
+ this.log(` CLI has started producing output`);
2385
+ break;
2386
+ }
2387
+ await sleep(pollMs);
2388
+ }
2389
+
2390
+ if (!this.hasReceivedOutput) {
2391
+ this.log(` Timeout waiting for CLI to produce output`);
2392
+ return false;
2393
+ }
2394
+
2395
+ // Phase 2: Wait for idle state (CLI is ready for input)
2396
+ const remainingTime = timeoutMs - (Date.now() - startTime);
2397
+ if (remainingTime <= 0) {
2398
+ return false;
2399
+ }
2400
+
2401
+ const idleResult = await this.waitForIdleState(remainingTime, pollMs);
2402
+ if (idleResult.isIdle) {
2403
+ this.log(` CLI is idle and ready (confidence: ${idleResult.confidence.toFixed(2)})`);
2404
+ return true;
2405
+ }
2406
+
2407
+ this.log(` Timeout waiting for CLI to become idle`);
2408
+ return false;
2409
+ }
2410
+
2411
+ /**
2412
+ * Check if the CLI has produced any output yet.
2413
+ * Useful for checking if the CLI has started without blocking.
2414
+ * In interactive mode, returns true if process is running (output isn't captured).
2415
+ */
2416
+ hasCliStarted(): boolean {
2417
+ // In interactive mode, stdout isn't captured so hasReceivedOutput is never set
2418
+ if (this.isInteractive) {
2419
+ return this.running;
2420
+ }
2421
+ return this.hasReceivedOutput;
2422
+ }
2423
+
2424
+ /**
2425
+ * Check if the orchestrator is ready to receive and inject messages.
2426
+ * This requires:
2427
+ * 1. relay-pty process spawned
2428
+ * 2. Socket connected to relay-pty
2429
+ * 3. running flag set
2430
+ *
2431
+ * Use this to verify the agent can actually receive injected messages,
2432
+ * not just that the CLI is running.
2433
+ */
2434
+ isReadyForMessages(): boolean {
2435
+ return this.readyForMessages && this.running && this.socketConnected;
2436
+ }
2437
+
2438
+ /**
2439
+ * Wait until the orchestrator is ready to receive and inject messages.
2440
+ * This is more comprehensive than waitUntilCliReady because it ensures:
2441
+ * 1. CLI is ready (has output and is idle)
2442
+ * 2. Orchestrator is ready (socket connected, can inject)
2443
+ *
2444
+ * @param timeoutMs Maximum time to wait (default: 30s)
2445
+ * @param pollMs Polling interval (default: 100ms)
2446
+ * @returns true if ready, false if timeout
2447
+ */
2448
+ async waitUntilReadyForMessages(timeoutMs = 30000, pollMs = 100): Promise<boolean> {
2449
+ const startTime = Date.now();
2450
+ this.log(` Waiting for orchestrator to be ready for messages (timeout: ${timeoutMs}ms)`);
2451
+
2452
+ // First wait for CLI to be ready (output + idle)
2453
+ const cliReady = await this.waitUntilCliReady(timeoutMs, pollMs);
2454
+ if (!cliReady) {
2455
+ this.log(` CLI not ready within timeout`);
2456
+ return false;
2457
+ }
2458
+
2459
+ // Then wait for readyForMessages flag
2460
+ const remainingTime = timeoutMs - (Date.now() - startTime);
2461
+ if (remainingTime <= 0) {
2462
+ this.log(` No time remaining to wait for readyForMessages`);
2463
+ return this.isReadyForMessages();
2464
+ }
2465
+
2466
+ while (Date.now() - startTime < timeoutMs) {
2467
+ if (this.isReadyForMessages()) {
2468
+ this.log(` Orchestrator is ready for messages`);
2469
+ return true;
2470
+ }
2471
+ await sleep(pollMs);
2472
+ }
2473
+
2474
+ this.log(` Timeout waiting for orchestrator to be ready for messages`);
2475
+ return false;
2476
+ }
2477
+
2478
+ /**
2479
+ * Get raw output buffer
2480
+ */
2481
+ getRawOutput(): string {
2482
+ return this.rawBuffer;
2483
+ }
2484
+
2485
+ /**
2486
+ * Check if backpressure is active
2487
+ */
2488
+ isBackpressureActive(): boolean {
2489
+ return this.backpressureActive;
2490
+ }
2491
+
2492
+ /**
2493
+ * Get the socket path
2494
+ */
2495
+ getSocketPath(): string {
2496
+ return this.socketPath;
2497
+ }
2498
+
2499
+ /**
2500
+ * Get the relay-pty process PID
2501
+ */
2502
+ get pid(): number | undefined {
2503
+ return this.relayPtyProcess?.pid;
2504
+ }
2505
+
2506
+ /**
2507
+ * Get the log file path (not used by relay-pty, returns undefined)
2508
+ */
2509
+ get logPath(): string | undefined {
2510
+ return this._logPath;
2511
+ }
2512
+
2513
+ /**
2514
+ * Kill the process forcefully
2515
+ */
2516
+ async kill(): Promise<void> {
2517
+ this.isGracefulStop = true; // Mark as intentional to prevent crash broadcast
2518
+ if (this.socketReconnectTimer) {
2519
+ clearTimeout(this.socketReconnectTimer);
2520
+ this.socketReconnectTimer = undefined;
2521
+ }
2522
+ if (this.relayPtyProcess && !this.relayPtyProcess.killed) {
2523
+ this.relayPtyProcess.kill('SIGKILL');
2524
+ }
2525
+ this.running = false;
2526
+ this.disconnectSocket();
2527
+ this.destroyClient();
2528
+ }
2529
+
2530
+ /**
2531
+ * Get output lines (for compatibility with PtyWrapper)
2532
+ * @param limit Maximum number of lines to return
2533
+ */
2534
+ getOutput(limit?: number): string[] {
2535
+ const lines = this.rawBuffer.split('\n');
2536
+ if (limit && limit > 0) {
2537
+ return lines.slice(-limit);
2538
+ }
2539
+ return lines;
2540
+ }
2541
+
2542
+ /**
2543
+ * Write data directly to the process stdin
2544
+ * @param data Data to write
2545
+ */
2546
+ async write(data: string | Buffer): Promise<void> {
2547
+ if (!this.relayPtyProcess || !this.relayPtyProcess.stdin) {
2548
+ throw new Error('Process not running');
2549
+ }
2550
+ const buffer = typeof data === 'string' ? Buffer.from(data) : data;
2551
+ this.relayPtyProcess.stdin.write(buffer);
2552
+ }
2553
+
2554
+ /**
2555
+ * Inject a task using the socket-based injection system with verification.
2556
+ * This is the preferred method for spawned agent task delivery.
2557
+ *
2558
+ * @param task The task text to inject
2559
+ * @param from The sender name (default: "spawner")
2560
+ * @returns Promise resolving to true if injection succeeded, false otherwise
2561
+ */
2562
+ async injectTask(task: string, from = 'spawner'): Promise<boolean> {
2563
+ if (!this.socket || !this.socketConnected) {
2564
+ this.log(` Socket not connected for task injection, falling back to stdin write`);
2565
+ // Fallback to direct write if socket not available
2566
+ try {
2567
+ await this.write(task + '\n');
2568
+ return true;
2569
+ } catch (err: any) {
2570
+ this.logError(` Stdin write fallback failed: ${err.message}`);
2571
+ return false;
2572
+ }
2573
+ }
2574
+
2575
+ const messageId = `task-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
2576
+ const shortId = messageId.substring(0, 8);
2577
+
2578
+ this.log(` Injecting task via socket: ${shortId}`);
2579
+
2580
+ // Create request
2581
+ const request: InjectRequest = {
2582
+ type: 'inject',
2583
+ id: messageId,
2584
+ from,
2585
+ body: task,
2586
+ priority: 0, // High priority for initial task
2587
+ };
2588
+
2589
+ // Send with timeout and verification
2590
+ return new Promise<boolean>((resolve) => {
2591
+ const timeout = setTimeout(() => {
2592
+ this.logError(` Task inject timeout for ${shortId} after 30s`);
2593
+ this.pendingInjections.delete(messageId);
2594
+ resolve(false);
2595
+ }, 30000);
2596
+
2597
+ this.pendingInjections.set(messageId, {
2598
+ resolve,
2599
+ reject: () => resolve(false),
2600
+ timeout,
2601
+ from,
2602
+ shortId,
2603
+ retryCount: 0,
2604
+ originalBody: task,
2605
+ });
2606
+
2607
+ this.sendSocketRequest(request)
2608
+ .then(() => {
2609
+ this.log(` Task inject request sent: ${shortId}`);
2610
+ })
2611
+ .catch((err) => {
2612
+ this.logError(` Task inject socket request failed: ${err.message}`);
2613
+ clearTimeout(timeout);
2614
+ this.pendingInjections.delete(messageId);
2615
+ resolve(false);
2616
+ });
2617
+ });
2618
+ }
2619
+
2620
+ /**
2621
+ * Get the agent ID (from continuity if available)
2622
+ */
2623
+ getAgentId(): string | undefined {
2624
+ return this.agentId;
2625
+ }
2626
+ }