agent-relay 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (490) hide show
  1. package/README.md +1 -1
  2. package/dist/bridge/spawner.d.ts +53 -0
  3. package/dist/bridge/spawner.d.ts.map +1 -1
  4. package/dist/bridge/spawner.js +203 -19
  5. package/dist/bridge/spawner.js.map +1 -1
  6. package/dist/bridge/types.d.ts +12 -0
  7. package/dist/bridge/types.d.ts.map +1 -1
  8. package/dist/cli/index.js +401 -5
  9. package/dist/cli/index.js.map +1 -1
  10. package/dist/cloud/api/auth.d.ts +3 -2
  11. package/dist/cloud/api/auth.d.ts.map +1 -1
  12. package/dist/cloud/api/auth.js +10 -98
  13. package/dist/cloud/api/auth.js.map +1 -1
  14. package/dist/cloud/api/cli-pty-runner.d.ts +54 -0
  15. package/dist/cloud/api/cli-pty-runner.d.ts.map +1 -0
  16. package/dist/cloud/api/cli-pty-runner.js +119 -0
  17. package/dist/cloud/api/cli-pty-runner.js.map +1 -0
  18. package/dist/cloud/api/generic-webhooks.d.ts +8 -0
  19. package/dist/cloud/api/generic-webhooks.d.ts.map +1 -0
  20. package/dist/cloud/api/generic-webhooks.js +129 -0
  21. package/dist/cloud/api/generic-webhooks.js.map +1 -0
  22. package/dist/cloud/api/git.d.ts +8 -0
  23. package/dist/cloud/api/git.d.ts.map +1 -0
  24. package/dist/cloud/api/git.js +131 -0
  25. package/dist/cloud/api/git.js.map +1 -0
  26. package/dist/cloud/api/github-app.d.ts +11 -0
  27. package/dist/cloud/api/github-app.d.ts.map +1 -0
  28. package/dist/cloud/api/github-app.js +189 -0
  29. package/dist/cloud/api/github-app.js.map +1 -0
  30. package/dist/cloud/api/middleware/planLimits.d.ts +7 -0
  31. package/dist/cloud/api/middleware/planLimits.d.ts.map +1 -1
  32. package/dist/cloud/api/middleware/planLimits.js +39 -1
  33. package/dist/cloud/api/middleware/planLimits.js.map +1 -1
  34. package/dist/cloud/api/monitoring.d.ts +11 -0
  35. package/dist/cloud/api/monitoring.d.ts.map +1 -0
  36. package/dist/cloud/api/monitoring.js +578 -0
  37. package/dist/cloud/api/monitoring.js.map +1 -0
  38. package/dist/cloud/api/nango-auth.d.ts +9 -0
  39. package/dist/cloud/api/nango-auth.d.ts.map +1 -0
  40. package/dist/cloud/api/nango-auth.js +377 -0
  41. package/dist/cloud/api/nango-auth.js.map +1 -0
  42. package/dist/cloud/api/onboarding.d.ts +8 -1
  43. package/dist/cloud/api/onboarding.d.ts.map +1 -1
  44. package/dist/cloud/api/onboarding.js +300 -119
  45. package/dist/cloud/api/onboarding.js.map +1 -1
  46. package/dist/cloud/api/policy.d.ts +8 -0
  47. package/dist/cloud/api/policy.d.ts.map +1 -0
  48. package/dist/cloud/api/policy.js +229 -0
  49. package/dist/cloud/api/policy.js.map +1 -0
  50. package/dist/cloud/api/providers.js +114 -42
  51. package/dist/cloud/api/providers.js.map +1 -1
  52. package/dist/cloud/api/test-helpers.d.ts +10 -0
  53. package/dist/cloud/api/test-helpers.d.ts.map +1 -0
  54. package/dist/cloud/api/test-helpers.js +575 -0
  55. package/dist/cloud/api/test-helpers.js.map +1 -0
  56. package/dist/cloud/api/webhooks.d.ts +7 -0
  57. package/dist/cloud/api/webhooks.d.ts.map +1 -0
  58. package/dist/cloud/api/webhooks.js +496 -0
  59. package/dist/cloud/api/webhooks.js.map +1 -0
  60. package/dist/cloud/api/workspaces.js +225 -8
  61. package/dist/cloud/api/workspaces.js.map +1 -1
  62. package/dist/cloud/billing/plans.d.ts.map +1 -1
  63. package/dist/cloud/billing/plans.js +13 -0
  64. package/dist/cloud/billing/plans.js.map +1 -1
  65. package/dist/cloud/billing/types.d.ts +9 -3
  66. package/dist/cloud/billing/types.d.ts.map +1 -1
  67. package/dist/cloud/config.d.ts +9 -2
  68. package/dist/cloud/config.d.ts.map +1 -1
  69. package/dist/cloud/config.js +13 -4
  70. package/dist/cloud/config.js.map +1 -1
  71. package/dist/cloud/db/drizzle.d.ts +84 -1
  72. package/dist/cloud/db/drizzle.d.ts.map +1 -1
  73. package/dist/cloud/db/drizzle.js +470 -0
  74. package/dist/cloud/db/drizzle.js.map +1 -1
  75. package/dist/cloud/db/index.d.ts +9 -4
  76. package/dist/cloud/db/index.d.ts.map +1 -1
  77. package/dist/cloud/db/index.js +11 -3
  78. package/dist/cloud/db/index.js.map +1 -1
  79. package/dist/cloud/db/schema.d.ts +3283 -556
  80. package/dist/cloud/db/schema.d.ts.map +1 -1
  81. package/dist/cloud/db/schema.js +314 -1
  82. package/dist/cloud/db/schema.js.map +1 -1
  83. package/dist/cloud/index.d.ts +1 -0
  84. package/dist/cloud/index.d.ts.map +1 -1
  85. package/dist/cloud/index.js +2 -0
  86. package/dist/cloud/index.js.map +1 -1
  87. package/dist/cloud/provisioner/index.d.ts +24 -0
  88. package/dist/cloud/provisioner/index.d.ts.map +1 -1
  89. package/dist/cloud/provisioner/index.js +319 -18
  90. package/dist/cloud/provisioner/index.js.map +1 -1
  91. package/dist/cloud/server.d.ts +1 -0
  92. package/dist/cloud/server.d.ts.map +1 -1
  93. package/dist/cloud/server.js +357 -13
  94. package/dist/cloud/server.js.map +1 -1
  95. package/dist/cloud/services/auto-scaler.d.ts +152 -0
  96. package/dist/cloud/services/auto-scaler.d.ts.map +1 -0
  97. package/dist/cloud/services/auto-scaler.js +439 -0
  98. package/dist/cloud/services/auto-scaler.js.map +1 -0
  99. package/dist/cloud/services/capacity-manager.d.ts +148 -0
  100. package/dist/cloud/services/capacity-manager.d.ts.map +1 -0
  101. package/dist/cloud/services/capacity-manager.js +449 -0
  102. package/dist/cloud/services/capacity-manager.js.map +1 -0
  103. package/dist/cloud/services/ci-agent-spawner.d.ts +49 -0
  104. package/dist/cloud/services/ci-agent-spawner.d.ts.map +1 -0
  105. package/dist/cloud/services/ci-agent-spawner.js +373 -0
  106. package/dist/cloud/services/ci-agent-spawner.js.map +1 -0
  107. package/dist/cloud/services/index.d.ts +12 -0
  108. package/dist/cloud/services/index.d.ts.map +1 -0
  109. package/dist/cloud/services/index.js +15 -0
  110. package/dist/cloud/services/index.js.map +1 -0
  111. package/dist/cloud/services/mention-handler.d.ts +65 -0
  112. package/dist/cloud/services/mention-handler.d.ts.map +1 -0
  113. package/dist/cloud/services/mention-handler.js +405 -0
  114. package/dist/cloud/services/mention-handler.js.map +1 -0
  115. package/dist/cloud/services/nango.d.ts +126 -0
  116. package/dist/cloud/services/nango.d.ts.map +1 -0
  117. package/dist/cloud/services/nango.js +191 -0
  118. package/dist/cloud/services/nango.js.map +1 -0
  119. package/dist/cloud/services/persistence.d.ts +131 -0
  120. package/dist/cloud/services/persistence.d.ts.map +1 -0
  121. package/dist/cloud/services/persistence.js +200 -0
  122. package/dist/cloud/services/persistence.js.map +1 -0
  123. package/dist/cloud/services/planLimits.d.ts +15 -0
  124. package/dist/cloud/services/planLimits.d.ts.map +1 -1
  125. package/dist/cloud/services/planLimits.js +28 -0
  126. package/dist/cloud/services/planLimits.js.map +1 -1
  127. package/dist/cloud/services/scaling-orchestrator.d.ts +159 -0
  128. package/dist/cloud/services/scaling-orchestrator.d.ts.map +1 -0
  129. package/dist/cloud/services/scaling-orchestrator.js +502 -0
  130. package/dist/cloud/services/scaling-orchestrator.js.map +1 -0
  131. package/dist/cloud/services/scaling-policy.d.ts +121 -0
  132. package/dist/cloud/services/scaling-policy.d.ts.map +1 -0
  133. package/dist/cloud/services/scaling-policy.js +415 -0
  134. package/dist/cloud/services/scaling-policy.js.map +1 -0
  135. package/dist/cloud/vault/index.js +1 -1
  136. package/dist/cloud/vault/index.js.map +1 -1
  137. package/dist/cloud/webhooks/index.d.ts +24 -0
  138. package/dist/cloud/webhooks/index.d.ts.map +1 -0
  139. package/dist/cloud/webhooks/index.js +29 -0
  140. package/dist/cloud/webhooks/index.js.map +1 -0
  141. package/dist/cloud/webhooks/parsers/github.d.ts +8 -0
  142. package/dist/cloud/webhooks/parsers/github.d.ts.map +1 -0
  143. package/dist/cloud/webhooks/parsers/github.js +234 -0
  144. package/dist/cloud/webhooks/parsers/github.js.map +1 -0
  145. package/dist/cloud/webhooks/parsers/index.d.ts +23 -0
  146. package/dist/cloud/webhooks/parsers/index.d.ts.map +1 -0
  147. package/dist/cloud/webhooks/parsers/index.js +30 -0
  148. package/dist/cloud/webhooks/parsers/index.js.map +1 -0
  149. package/dist/cloud/webhooks/parsers/linear.d.ts +9 -0
  150. package/dist/cloud/webhooks/parsers/linear.d.ts.map +1 -0
  151. package/dist/cloud/webhooks/parsers/linear.js +258 -0
  152. package/dist/cloud/webhooks/parsers/linear.js.map +1 -0
  153. package/dist/cloud/webhooks/parsers/slack.d.ts +9 -0
  154. package/dist/cloud/webhooks/parsers/slack.d.ts.map +1 -0
  155. package/dist/cloud/webhooks/parsers/slack.js +214 -0
  156. package/dist/cloud/webhooks/parsers/slack.js.map +1 -0
  157. package/dist/cloud/webhooks/responders/github.d.ts +8 -0
  158. package/dist/cloud/webhooks/responders/github.d.ts.map +1 -0
  159. package/dist/cloud/webhooks/responders/github.js +73 -0
  160. package/dist/cloud/webhooks/responders/github.js.map +1 -0
  161. package/dist/cloud/webhooks/responders/index.d.ts +23 -0
  162. package/dist/cloud/webhooks/responders/index.d.ts.map +1 -0
  163. package/dist/cloud/webhooks/responders/index.js +30 -0
  164. package/dist/cloud/webhooks/responders/index.js.map +1 -0
  165. package/dist/cloud/webhooks/responders/linear.d.ts +9 -0
  166. package/dist/cloud/webhooks/responders/linear.d.ts.map +1 -0
  167. package/dist/cloud/webhooks/responders/linear.js +149 -0
  168. package/dist/cloud/webhooks/responders/linear.js.map +1 -0
  169. package/dist/cloud/webhooks/responders/slack.d.ts +20 -0
  170. package/dist/cloud/webhooks/responders/slack.d.ts.map +1 -0
  171. package/dist/cloud/webhooks/responders/slack.js +178 -0
  172. package/dist/cloud/webhooks/responders/slack.js.map +1 -0
  173. package/dist/cloud/webhooks/router.d.ts +25 -0
  174. package/dist/cloud/webhooks/router.d.ts.map +1 -0
  175. package/dist/cloud/webhooks/router.js +504 -0
  176. package/dist/cloud/webhooks/router.js.map +1 -0
  177. package/dist/cloud/webhooks/rules-engine.d.ts +24 -0
  178. package/dist/cloud/webhooks/rules-engine.d.ts.map +1 -0
  179. package/dist/cloud/webhooks/rules-engine.js +287 -0
  180. package/dist/cloud/webhooks/rules-engine.js.map +1 -0
  181. package/dist/cloud/webhooks/types.d.ts +186 -0
  182. package/dist/cloud/webhooks/types.d.ts.map +1 -0
  183. package/dist/cloud/webhooks/types.js +8 -0
  184. package/dist/cloud/webhooks/types.js.map +1 -0
  185. package/dist/continuity/formatter.d.ts +51 -0
  186. package/dist/continuity/formatter.d.ts.map +1 -0
  187. package/dist/continuity/formatter.js +313 -0
  188. package/dist/continuity/formatter.js.map +1 -0
  189. package/dist/continuity/handoff-store.d.ts +67 -0
  190. package/dist/continuity/handoff-store.d.ts.map +1 -0
  191. package/dist/continuity/handoff-store.js +472 -0
  192. package/dist/continuity/handoff-store.js.map +1 -0
  193. package/dist/continuity/index.d.ts +45 -0
  194. package/dist/continuity/index.d.ts.map +1 -0
  195. package/dist/continuity/index.js +48 -0
  196. package/dist/continuity/index.js.map +1 -0
  197. package/dist/continuity/ledger-store.d.ts +110 -0
  198. package/dist/continuity/ledger-store.d.ts.map +1 -0
  199. package/dist/continuity/ledger-store.js +500 -0
  200. package/dist/continuity/ledger-store.js.map +1 -0
  201. package/dist/continuity/manager.d.ts +178 -0
  202. package/dist/continuity/manager.d.ts.map +1 -0
  203. package/dist/continuity/manager.js +562 -0
  204. package/dist/continuity/manager.js.map +1 -0
  205. package/dist/continuity/parser.d.ts +76 -0
  206. package/dist/continuity/parser.d.ts.map +1 -0
  207. package/dist/continuity/parser.js +579 -0
  208. package/dist/continuity/parser.js.map +1 -0
  209. package/dist/continuity/types.d.ts +180 -0
  210. package/dist/continuity/types.d.ts.map +1 -0
  211. package/dist/continuity/types.js +9 -0
  212. package/dist/continuity/types.js.map +1 -0
  213. package/dist/daemon/agent-manager.d.ts +27 -0
  214. package/dist/daemon/agent-manager.d.ts.map +1 -1
  215. package/dist/daemon/agent-manager.js +107 -6
  216. package/dist/daemon/agent-manager.js.map +1 -1
  217. package/dist/daemon/agent-registry.d.ts +32 -0
  218. package/dist/daemon/agent-registry.d.ts.map +1 -1
  219. package/dist/daemon/agent-registry.js +42 -2
  220. package/dist/daemon/agent-registry.js.map +1 -1
  221. package/dist/daemon/api.d.ts +12 -0
  222. package/dist/daemon/api.d.ts.map +1 -1
  223. package/dist/daemon/api.js +131 -2
  224. package/dist/daemon/api.js.map +1 -1
  225. package/dist/daemon/cli-auth.d.ts +67 -0
  226. package/dist/daemon/cli-auth.d.ts.map +1 -0
  227. package/dist/daemon/cli-auth.js +537 -0
  228. package/dist/daemon/cli-auth.js.map +1 -0
  229. package/dist/daemon/cloud-sync.d.ts.map +1 -1
  230. package/dist/daemon/cloud-sync.js +9 -7
  231. package/dist/daemon/cloud-sync.js.map +1 -1
  232. package/dist/daemon/orchestrator.d.ts.map +1 -1
  233. package/dist/daemon/orchestrator.js +30 -0
  234. package/dist/daemon/orchestrator.js.map +1 -1
  235. package/dist/daemon/router.d.ts +5 -0
  236. package/dist/daemon/router.d.ts.map +1 -1
  237. package/dist/daemon/router.js +78 -26
  238. package/dist/daemon/router.js.map +1 -1
  239. package/dist/daemon/server.d.ts +5 -0
  240. package/dist/daemon/server.d.ts.map +1 -1
  241. package/dist/daemon/server.js +9 -1
  242. package/dist/daemon/server.js.map +1 -1
  243. package/dist/daemon/services/browser-testing.d.ts +88 -0
  244. package/dist/daemon/services/browser-testing.d.ts.map +1 -0
  245. package/dist/daemon/services/browser-testing.js +244 -0
  246. package/dist/daemon/services/browser-testing.js.map +1 -0
  247. package/dist/daemon/services/container-spawner.d.ts +135 -0
  248. package/dist/daemon/services/container-spawner.d.ts.map +1 -0
  249. package/dist/daemon/services/container-spawner.js +313 -0
  250. package/dist/daemon/services/container-spawner.js.map +1 -0
  251. package/dist/daemon/types.d.ts +5 -1
  252. package/dist/daemon/types.d.ts.map +1 -1
  253. package/dist/dashboard/out/404.html +1 -1
  254. package/dist/dashboard/out/_next/static/chunks/116-2502180def231162.js +1 -0
  255. package/dist/dashboard/out/_next/static/chunks/282-980c2eb8fff20123.js +1 -0
  256. package/dist/dashboard/out/_next/static/chunks/480-2d4111711d4e473c.js +1 -0
  257. package/dist/dashboard/out/_next/static/chunks/724-73c1ee5f60abe860.js +9 -0
  258. package/dist/dashboard/out/_next/static/chunks/766-c3a14283c88d815b.js +1 -0
  259. package/dist/dashboard/out/_next/static/chunks/app/app/page-7120be68bea622f3.js +1 -0
  260. package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-dc2e3a1a22478efc.js +1 -0
  261. package/dist/dashboard/out/_next/static/chunks/app/history/{page-b6edd4dde8d08194.js → page-56a8b4616a90dc43.js} +1 -1
  262. package/dist/dashboard/out/_next/static/chunks/app/login/page-3eac37ea6f5dd153.js +1 -0
  263. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-1081dd190a331a91.js +1 -0
  264. package/dist/dashboard/out/_next/static/chunks/app/page-daf87e86f783f980.js +1 -0
  265. package/dist/dashboard/out/_next/static/chunks/app/providers/page-b68a681526eb145e.js +1 -0
  266. package/dist/dashboard/out/_next/static/chunks/app/signup/page-fee4ed1709070bcd.js +1 -0
  267. package/dist/dashboard/out/_next/static/chunks/e868780c-48e5f147c90a3a41.js +18 -0
  268. package/dist/dashboard/out/_next/static/chunks/{main-c2f423b9c9f4591b.js → main-97850e03d723ea8c.js} +1 -1
  269. package/dist/dashboard/out/_next/static/chunks/webpack-1cdd8ed57114d5e1.js +1 -0
  270. package/dist/dashboard/out/_next/static/css/29852f26181969a0.css +1 -0
  271. package/dist/dashboard/out/_next/static/css/411ce23ffeae9f76.css +1 -0
  272. package/dist/dashboard/out/app.html +1 -14
  273. package/dist/dashboard/out/app.txt +2 -2
  274. package/dist/dashboard/out/connect-repos.html +1 -0
  275. package/dist/dashboard/out/connect-repos.txt +7 -0
  276. package/dist/dashboard/out/history.html +1 -1
  277. package/dist/dashboard/out/history.txt +2 -2
  278. package/dist/dashboard/out/index.html +1 -1
  279. package/dist/dashboard/out/index.txt +2 -2
  280. package/dist/dashboard/out/login.html +6 -0
  281. package/dist/dashboard/out/login.txt +7 -0
  282. package/dist/dashboard/out/metrics.html +1 -1
  283. package/dist/dashboard/out/metrics.txt +2 -2
  284. package/dist/dashboard/out/pricing.html +2 -2
  285. package/dist/dashboard/out/pricing.txt +2 -2
  286. package/dist/dashboard/out/providers.html +1 -0
  287. package/dist/dashboard/out/providers.txt +7 -0
  288. package/dist/dashboard/out/signup.html +6 -0
  289. package/dist/dashboard/out/signup.txt +7 -0
  290. package/dist/dashboard-server/server.d.ts.map +1 -1
  291. package/dist/dashboard-server/server.js +1308 -8
  292. package/dist/dashboard-server/server.js.map +1 -1
  293. package/dist/hooks/emitter.d.ts +40 -0
  294. package/dist/hooks/emitter.d.ts.map +1 -0
  295. package/dist/hooks/emitter.js +63 -0
  296. package/dist/hooks/emitter.js.map +1 -0
  297. package/dist/hooks/index.d.ts +3 -0
  298. package/dist/hooks/index.d.ts.map +1 -1
  299. package/dist/hooks/index.js +3 -0
  300. package/dist/hooks/index.js.map +1 -1
  301. package/dist/hooks/registry.d.ts +173 -0
  302. package/dist/hooks/registry.d.ts.map +1 -0
  303. package/dist/hooks/registry.js +476 -0
  304. package/dist/hooks/registry.js.map +1 -0
  305. package/dist/hooks/trajectory-hooks.d.ts +52 -0
  306. package/dist/hooks/trajectory-hooks.d.ts.map +1 -0
  307. package/dist/hooks/trajectory-hooks.js +183 -0
  308. package/dist/hooks/trajectory-hooks.js.map +1 -0
  309. package/dist/hooks/types.d.ts +141 -0
  310. package/dist/hooks/types.d.ts.map +1 -1
  311. package/dist/index.d.ts +2 -0
  312. package/dist/index.d.ts.map +1 -1
  313. package/dist/index.js +3 -0
  314. package/dist/index.js.map +1 -1
  315. package/dist/memory/adapters/index.d.ts +8 -0
  316. package/dist/memory/adapters/index.d.ts.map +1 -0
  317. package/dist/memory/adapters/index.js +8 -0
  318. package/dist/memory/adapters/index.js.map +1 -0
  319. package/dist/memory/adapters/inmemory.d.ts +59 -0
  320. package/dist/memory/adapters/inmemory.d.ts.map +1 -0
  321. package/dist/memory/adapters/inmemory.js +195 -0
  322. package/dist/memory/adapters/inmemory.js.map +1 -0
  323. package/dist/memory/adapters/supermemory.d.ts +71 -0
  324. package/dist/memory/adapters/supermemory.d.ts.map +1 -0
  325. package/dist/memory/adapters/supermemory.js +338 -0
  326. package/dist/memory/adapters/supermemory.js.map +1 -0
  327. package/dist/memory/factory.d.ts +48 -0
  328. package/dist/memory/factory.d.ts.map +1 -0
  329. package/dist/memory/factory.js +143 -0
  330. package/dist/memory/factory.js.map +1 -0
  331. package/dist/memory/index.d.ts +32 -0
  332. package/dist/memory/index.d.ts.map +1 -0
  333. package/dist/memory/index.js +32 -0
  334. package/dist/memory/index.js.map +1 -0
  335. package/dist/memory/memory-hooks.d.ts +60 -0
  336. package/dist/memory/memory-hooks.d.ts.map +1 -0
  337. package/dist/memory/memory-hooks.js +313 -0
  338. package/dist/memory/memory-hooks.js.map +1 -0
  339. package/dist/memory/service.d.ts +49 -0
  340. package/dist/memory/service.d.ts.map +1 -0
  341. package/dist/memory/service.js +146 -0
  342. package/dist/memory/service.js.map +1 -0
  343. package/dist/memory/types.d.ts +195 -0
  344. package/dist/memory/types.d.ts.map +1 -0
  345. package/dist/memory/types.js +8 -0
  346. package/dist/memory/types.js.map +1 -0
  347. package/dist/policy/agent-policy.d.ts +225 -0
  348. package/dist/policy/agent-policy.d.ts.map +1 -0
  349. package/dist/policy/agent-policy.js +665 -0
  350. package/dist/policy/agent-policy.js.map +1 -0
  351. package/dist/policy/cloud-policy-fetcher.d.ts +12 -0
  352. package/dist/policy/cloud-policy-fetcher.d.ts.map +1 -0
  353. package/dist/policy/cloud-policy-fetcher.js +64 -0
  354. package/dist/policy/cloud-policy-fetcher.js.map +1 -0
  355. package/dist/resiliency/crash-insights.d.ts +156 -0
  356. package/dist/resiliency/crash-insights.d.ts.map +1 -0
  357. package/dist/resiliency/crash-insights.js +492 -0
  358. package/dist/resiliency/crash-insights.js.map +1 -0
  359. package/dist/resiliency/gossip-health.d.ts +137 -0
  360. package/dist/resiliency/gossip-health.d.ts.map +1 -0
  361. package/dist/resiliency/gossip-health.js +241 -0
  362. package/dist/resiliency/gossip-health.js.map +1 -0
  363. package/dist/resiliency/index.d.ts +5 -0
  364. package/dist/resiliency/index.d.ts.map +1 -1
  365. package/dist/resiliency/index.js +5 -0
  366. package/dist/resiliency/index.js.map +1 -1
  367. package/dist/resiliency/leader-watchdog.d.ts +109 -0
  368. package/dist/resiliency/leader-watchdog.d.ts.map +1 -0
  369. package/dist/resiliency/leader-watchdog.js +189 -0
  370. package/dist/resiliency/leader-watchdog.js.map +1 -0
  371. package/dist/resiliency/memory-monitor.d.ts +172 -0
  372. package/dist/resiliency/memory-monitor.d.ts.map +1 -0
  373. package/dist/resiliency/memory-monitor.js +593 -0
  374. package/dist/resiliency/memory-monitor.js.map +1 -0
  375. package/dist/resiliency/stateless-lead.d.ts +149 -0
  376. package/dist/resiliency/stateless-lead.d.ts.map +1 -0
  377. package/dist/resiliency/stateless-lead.js +308 -0
  378. package/dist/resiliency/stateless-lead.js.map +1 -0
  379. package/dist/resiliency/supervisor.d.ts +38 -0
  380. package/dist/resiliency/supervisor.d.ts.map +1 -1
  381. package/dist/resiliency/supervisor.js +122 -0
  382. package/dist/resiliency/supervisor.js.map +1 -1
  383. package/dist/shared/cli-auth-config.d.ts +91 -0
  384. package/dist/shared/cli-auth-config.d.ts.map +1 -0
  385. package/dist/shared/cli-auth-config.js +264 -0
  386. package/dist/shared/cli-auth-config.js.map +1 -0
  387. package/dist/storage/adapter.d.ts +1 -1
  388. package/dist/storage/adapter.d.ts.map +1 -1
  389. package/dist/trajectory/config.d.ts +84 -0
  390. package/dist/trajectory/config.d.ts.map +1 -0
  391. package/dist/trajectory/config.js +163 -0
  392. package/dist/trajectory/config.js.map +1 -0
  393. package/dist/trajectory/index.d.ts +8 -0
  394. package/dist/trajectory/index.d.ts.map +1 -0
  395. package/dist/trajectory/index.js +8 -0
  396. package/dist/trajectory/index.js.map +1 -0
  397. package/dist/trajectory/integration.d.ts +292 -0
  398. package/dist/trajectory/integration.d.ts.map +1 -0
  399. package/dist/trajectory/integration.js +834 -0
  400. package/dist/trajectory/integration.js.map +1 -0
  401. package/dist/utils/logger.js +1 -1
  402. package/dist/utils/logger.js.map +1 -1
  403. package/dist/utils/project-namespace.d.ts +24 -0
  404. package/dist/utils/project-namespace.d.ts.map +1 -1
  405. package/dist/utils/project-namespace.js +84 -0
  406. package/dist/utils/project-namespace.js.map +1 -1
  407. package/dist/wrapper/parser.d.ts +10 -0
  408. package/dist/wrapper/parser.d.ts.map +1 -1
  409. package/dist/wrapper/parser.js +100 -33
  410. package/dist/wrapper/parser.js.map +1 -1
  411. package/dist/wrapper/pty-wrapper.d.ts +197 -16
  412. package/dist/wrapper/pty-wrapper.d.ts.map +1 -1
  413. package/dist/wrapper/pty-wrapper.js +943 -106
  414. package/dist/wrapper/pty-wrapper.js.map +1 -1
  415. package/dist/wrapper/shared.d.ts +165 -0
  416. package/dist/wrapper/shared.d.ts.map +1 -0
  417. package/dist/wrapper/shared.js +270 -0
  418. package/dist/wrapper/shared.js.map +1 -0
  419. package/dist/wrapper/tmux-wrapper.d.ts +73 -11
  420. package/dist/wrapper/tmux-wrapper.d.ts.map +1 -1
  421. package/dist/wrapper/tmux-wrapper.js +541 -120
  422. package/dist/wrapper/tmux-wrapper.js.map +1 -1
  423. package/docs/CLOUD-ARCHITECTURE.md +152 -0
  424. package/docs/HOOKS_API.md +394 -0
  425. package/docs/WRAPPER_EVENTS.md +358 -0
  426. package/docs/agent-policy-snippet.md +40 -0
  427. package/docs/agent-relay-protocol.md +238 -0
  428. package/docs/agent-relay-snippet.md +53 -47
  429. package/docs/archive/EXECUTIVE_SUMMARY.md +358 -0
  430. package/docs/archive/ROADMAP.md +329 -0
  431. package/docs/competitive/GASTOWN.md +451 -0
  432. package/docs/{COMPETITIVE_ANALYSIS.md → competitive/OVERVIEW.md} +1 -0
  433. package/docs/competitive/README.md +34 -0
  434. package/docs/competitive/TMUX_ORCHESTRATOR.md +605 -0
  435. package/docs/dashboard.png +0 -0
  436. package/docs/design/ci-failure-webhooks.md +812 -0
  437. package/docs/design/comprehensive-integrations.md +238 -0
  438. package/docs/design/e2b-sandbox-integration.md +504 -0
  439. package/docs/design/github-app-permissions.md +264 -0
  440. package/docs/local-testing.md +428 -0
  441. package/docs/proposals/continuous-claude-integration.md +622 -0
  442. package/docs/proposals/custom-commands.md +368 -0
  443. package/docs/tasks/global-skills-system.tasks.md +230 -0
  444. package/docs/tasks/webhook-integrations.tasks.md +184 -0
  445. package/docs/tasks/workspace-capabilities.tasks.md +121 -0
  446. package/docs/testing/RESILIENCY-TEST-PLAN-2026-01-01.md +366 -0
  447. package/package.json +16 -7
  448. package/scripts/cloud-setup.sh +96 -0
  449. package/scripts/manual-qa.sh +293 -0
  450. package/scripts/postinstall.js +60 -0
  451. package/scripts/run-cloud-qa.sh +220 -0
  452. package/scripts/test-cli-auth/Dockerfile +44 -0
  453. package/scripts/test-cli-auth/Dockerfile.real +79 -0
  454. package/scripts/test-cli-auth/README.md +286 -0
  455. package/scripts/test-cli-auth/ci-test-real-clis.ts +251 -0
  456. package/scripts/test-cli-auth/ci-test-runner.ts +263 -0
  457. package/scripts/test-cli-auth/mock-cli.sh +147 -0
  458. package/scripts/test-cli-auth/package.json +14 -0
  459. package/scripts/test-cli-auth/test-oauth-flow.ts +220 -0
  460. package/scripts/test-pty-input-auto.js +222 -0
  461. package/scripts/test-pty-input.js +150 -0
  462. package/dist/dashboard/out/_next/static/chunks/693-7b3301d8f6bc5014.js +0 -1
  463. package/dist/dashboard/out/_next/static/chunks/713-f78477eb185f1f4d.js +0 -1
  464. package/dist/dashboard/out/_next/static/chunks/766-e53e1cfe39b0b5b5.js +0 -1
  465. package/dist/dashboard/out/_next/static/chunks/900-037c64bfd797fb2a.js +0 -1
  466. package/dist/dashboard/out/_next/static/chunks/app/app/page-e3d9e1f4466b9bae.js +0 -1
  467. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-e68825a81db67ba1.js +0 -1
  468. package/dist/dashboard/out/_next/static/chunks/app/page-cc108bf68c8a657f.js +0 -1
  469. package/dist/dashboard/out/_next/static/chunks/webpack-a5acc2831d094776.js +0 -1
  470. package/dist/dashboard/out/_next/static/css/79b80143647a07d7.css +0 -1
  471. package/dist/dashboard/out/_next/static/css/8cf277370ad48cfe.css +0 -1
  472. /package/dist/dashboard/out/_next/static/{6HHWb2ZmnJ4OSm0zUP7h4 → H5aWG0udPB4iOUIl_gytz}/_buildManifest.js +0 -0
  473. /package/dist/dashboard/out/_next/static/{6HHWb2ZmnJ4OSm0zUP7h4 → H5aWG0udPB4iOUIl_gytz}/_ssgManifest.js +0 -0
  474. /package/dist/dashboard/out/_next/static/chunks/{117-b2cd8d6485aacf2b.js → 117-b100311aff8d5c61.js} +0 -0
  475. /package/dist/dashboard/out/_next/static/chunks/{648-8f3f26864ce515e5.js → 648-a13d3c2b1be45466.js} +0 -0
  476. /package/dist/dashboard/out/_next/static/chunks/app/_not-found/{page-0b990dbb71d72a98.js → page-a4973f3e3c82fb67.js} +0 -0
  477. /package/dist/dashboard/out/_next/static/chunks/app/pricing/{page-d80e03a5297f95b6.js → page-4d72d5a5d8a9b618.js} +0 -0
  478. /package/docs/{CHANGELOG.md → archive/CHANGELOG.md} +0 -0
  479. /package/docs/{CLI-SIMPLIFICATION-COMPLETE.md → archive/CLI-SIMPLIFICATION-COMPLETE.md} +0 -0
  480. /package/docs/{DESIGN_BRIDGE_STAFFING.md → archive/DESIGN_BRIDGE_STAFFING.md} +0 -0
  481. /package/docs/{DESIGN_V2.md → archive/DESIGN_V2.md} +0 -0
  482. /package/docs/{MONETIZATION.md → archive/MONETIZATION.md} +0 -0
  483. /package/docs/{PROPOSAL-trajectories.md → archive/PROPOSAL-trajectories.md} +0 -0
  484. /package/docs/{SCALING_ANALYSIS.md → archive/SCALING_ANALYSIS.md} +0 -0
  485. /package/docs/{TESTING_PRESENCE_FEATURES.md → archive/TESTING_PRESENCE_FEATURES.md} +0 -0
  486. /package/docs/{TMUX_IMPLEMENTATION_NOTES.md → archive/TMUX_IMPLEMENTATION_NOTES.md} +0 -0
  487. /package/docs/{TMUX_IMPROVEMENTS.md → archive/TMUX_IMPROVEMENTS.md} +0 -0
  488. /package/docs/{dashboard-v2-plan.md → archive/dashboard-v2-plan.md} +0 -0
  489. /package/docs/{removable-code-analysis.md → archive/removable-code-analysis.md} +0 -0
  490. /package/docs/{competitive-analysis-mcp-agent-mail.md → competitive/MCP_AGENT_MAIL.md} +0 -0
@@ -20,6 +20,11 @@ import { InboxManager } from './inbox.js';
20
20
  import { SqliteStorageAdapter } from '../storage/sqlite-adapter.js';
21
21
  import { getProjectPaths } from '../utils/project-namespace.js';
22
22
  import { getTmuxPath } from '../utils/tmux-resolver.js';
23
+ import { findAgentConfig } from '../utils/agent-config.js';
24
+ import { getTrajectoryIntegration, detectPhaseFromContent, detectToolCalls, detectErrors, getCompactTrailInstructions, getTrailEnvVars, } from '../trajectory/integration.js';
25
+ import { escapeForShell } from '../bridge/utils.js';
26
+ import { getContinuityManager, parseContinuityCommand, hasContinuityCommand, } from '../continuity/index.js';
27
+ import { stripAnsi, sleep, getDefaultRelayPrefix, createInjectionMetrics, buildInjectionString, injectWithRetry as sharedInjectWithRetry, INJECTION_CONSTANTS, CLI_QUIRKS, } from './shared.js';
23
28
  const execAsync = promisify(exec);
24
29
  const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
25
30
  // Constants for cursor stability detection in waitForClearInput
@@ -34,10 +39,10 @@ const RELAY_LOG_TRUNCATE_LENGTH = 50;
34
39
  /**
35
40
  * Get the default relay prefix for a given CLI type.
36
41
  * All agents now use '->relay:' as the unified prefix.
42
+ * @deprecated Use getDefaultRelayPrefix() from shared.js instead
37
43
  */
38
44
  export function getDefaultPrefix(_cliType) {
39
- // Unified prefix for all agent types
40
- return '->relay:';
45
+ return getDefaultRelayPrefix();
41
46
  }
42
47
  export class TmuxWrapper {
43
48
  config;
@@ -67,15 +72,25 @@ export class TmuxWrapper {
67
72
  lastSummaryHash = ''; // Dedup summary saves
68
73
  lastSummaryRawContent = ''; // Dedup invalid JSON error logging
69
74
  sessionEndProcessed = false; // Track if we've already processed session end
75
+ sessionEndData; // Store SESSION_END data for handoff
70
76
  pendingRelayCommands = [];
71
77
  queuedMessageHashes = new Set(); // For offline queue dedup
72
78
  MAX_PENDING_RELAY_COMMANDS = 50;
73
79
  processedSpawnCommands = new Set(); // Dedup spawn commands
74
80
  processedReleaseCommands = new Set(); // Dedup release commands
81
+ pendingFencedSpawn = null; // Track multi-line spawn task
75
82
  receivedMessageIdSet = new Set();
76
83
  receivedMessageIdOrder = [];
77
84
  MAX_RECEIVED_MESSAGES = 2000;
78
85
  tmuxPath; // Resolved path to tmux binary (system or bundled)
86
+ trajectory; // Trajectory tracking via trail
87
+ lastDetectedPhase; // Track last auto-detected PDERO phase
88
+ seenToolCalls = new Set(); // Dedup tool call trajectory events
89
+ seenErrors = new Set(); // Dedup error trajectory events
90
+ continuity; // Session continuity management
91
+ processedContinuityCommands = new Set(); // Dedup continuity commands
92
+ agentId; // Unique agent ID for resume functionality
93
+ injectionMetrics = createInjectionMetrics(); // Track injection reliability
79
94
  constructor(config) {
80
95
  this.config = {
81
96
  cols: process.stdout.columns || 120,
@@ -109,6 +124,9 @@ export class TmuxWrapper {
109
124
  else if (cmdLower.includes('droid')) {
110
125
  this.cliType = 'droid';
111
126
  }
127
+ else if (cmdLower.includes('opencode')) {
128
+ this.cliType = 'opencode';
129
+ }
112
130
  else {
113
131
  this.cliType = 'other';
114
132
  }
@@ -118,13 +136,22 @@ export class TmuxWrapper {
118
136
  this.sessionName = `relay-${config.name}`;
119
137
  // Resolve tmux path early so we fail fast if tmux isn't available
120
138
  this.tmuxPath = getTmuxPath();
139
+ // Auto-detect agent role from .claude/agents/ or .openagents/ if task not provided
140
+ let detectedTask = this.config.task;
141
+ if (!detectedTask) {
142
+ const agentConfig = findAgentConfig(config.name, this.config.cwd);
143
+ if (agentConfig?.description) {
144
+ detectedTask = agentConfig.description;
145
+ this.logStderr(`Auto-detected role: ${detectedTask.substring(0, 60)}...`, true);
146
+ }
147
+ }
121
148
  this.client = new RelayClient({
122
149
  agentName: config.name,
123
150
  socketPath: config.socketPath,
124
151
  cli: this.cliType,
125
152
  program: this.config.program,
126
153
  model: this.config.model,
127
- task: this.config.task,
154
+ task: detectedTask,
128
155
  workingDirectory: this.config.cwd ?? process.cwd(),
129
156
  quiet: true, // Keep stdout clean; we log to stderr via wrapper
130
157
  });
@@ -145,6 +172,10 @@ export class TmuxWrapper {
145
172
  this.storage = undefined;
146
173
  return false;
147
174
  });
175
+ // Initialize trajectory tracking via trail CLI
176
+ this.trajectory = getTrajectoryIntegration(projectPaths.projectId, config.name);
177
+ // Initialize continuity manager for session persistence
178
+ this.continuity = getContinuityManager({ defaultCli: this.cliType });
148
179
  // Handle incoming messages from relay
149
180
  this.client.onMessage = (from, payload, messageId, meta, originalTo) => {
150
181
  this.handleIncomingMessage(from, payload, messageId, meta, originalTo);
@@ -182,6 +213,46 @@ export class TmuxWrapper {
182
213
  // Prefix with newline to avoid corrupting tmux status line
183
214
  process.stderr.write(`\r[relay:${this.config.name}] ${msg}\n`);
184
215
  }
216
+ /**
217
+ * Detect PDERO phase from output content and auto-transition if needed.
218
+ * Also detects tool calls and errors, recording them to the trajectory.
219
+ */
220
+ detectAndTransitionPhase(content) {
221
+ if (!this.trajectory)
222
+ return;
223
+ // Detect phase transitions
224
+ const detectedPhase = detectPhaseFromContent(content);
225
+ if (detectedPhase && detectedPhase !== this.lastDetectedPhase) {
226
+ const currentPhase = this.trajectory.getPhase();
227
+ if (detectedPhase !== currentPhase) {
228
+ this.trajectory.transition(detectedPhase, 'Auto-detected from output');
229
+ this.lastDetectedPhase = detectedPhase;
230
+ this.logStderr(`Phase transition: ${currentPhase || 'none'} → ${detectedPhase}`);
231
+ }
232
+ }
233
+ // Detect and record tool calls
234
+ // Note: We deduplicate by tool+status to record each unique tool type once per session
235
+ // (e.g., "Read" started, "Read" completed). This provides a summary of tools used
236
+ // without flooding the trajectory with every individual invocation.
237
+ const tools = detectToolCalls(content);
238
+ for (const tool of tools) {
239
+ const key = `${tool.tool}:${tool.status || 'started'}`;
240
+ if (!this.seenToolCalls.has(key)) {
241
+ this.seenToolCalls.add(key);
242
+ const statusLabel = tool.status === 'completed' ? ' (completed)' : '';
243
+ this.trajectory.event(`Tool: ${tool.tool}${statusLabel}`, 'tool_call');
244
+ }
245
+ }
246
+ // Detect and record errors
247
+ const errors = detectErrors(content);
248
+ for (const error of errors) {
249
+ if (!this.seenErrors.has(error.message)) {
250
+ this.seenErrors.add(error.message);
251
+ const prefix = error.type === 'warning' ? 'Warning' : 'Error';
252
+ this.trajectory.event(`${prefix}: ${error.message}`, 'error');
253
+ }
254
+ }
255
+ }
185
256
  /**
186
257
  * Build the full command with proper quoting
187
258
  * Args containing spaces need to be quoted
@@ -286,20 +357,24 @@ export class TmuxWrapper {
286
357
  // Ignore on older tmux versions lacking these key tables
287
358
  }
288
359
  }
289
- // Set environment variables
360
+ // Set environment variables including trail/trajectory vars
361
+ const projectPaths = getProjectPaths();
362
+ const trailEnvVars = getTrailEnvVars(projectPaths.projectId, this.config.name, projectPaths.dataDir);
290
363
  for (const [key, value] of Object.entries({
291
364
  ...this.config.env,
365
+ ...trailEnvVars,
292
366
  AGENT_RELAY_NAME: this.config.name,
293
367
  TERM: 'xterm-256color',
294
368
  })) {
295
- const escaped = value.replace(/"/g, '\\"');
369
+ // Use proper shell escaping to prevent command injection via env var values
370
+ const escaped = escapeForShell(value);
296
371
  execSync(`"${this.tmuxPath}" setenv -t ${this.sessionName} ${key} "${escaped}"`);
297
372
  }
298
373
  // Wait for shell to be ready (look for prompt)
299
374
  await this.waitForShellReady();
300
375
  // Send the command to run
301
376
  await this.sendKeysLiteral(fullCommand);
302
- await this.sleep(100);
377
+ await sleep(100);
303
378
  await this.sendKeys('Enter');
304
379
  }
305
380
  catch (err) {
@@ -310,6 +385,10 @@ export class TmuxWrapper {
310
385
  this.running = true;
311
386
  this.lastActivityTime = Date.now();
312
387
  this.activityState = 'active';
388
+ // Initialize trajectory tracking (auto-start if task provided)
389
+ this.initializeTrajectory();
390
+ // Initialize continuity and get/create agentId
391
+ this.initializeAgentId();
313
392
  // Inject instructions for the agent (after a delay to let CLI initialize)
314
393
  setTimeout(() => this.injectInstructions(), 3000);
315
394
  // Start background polling (silent - no stdout writes)
@@ -319,28 +398,126 @@ export class TmuxWrapper {
319
398
  this.attachToSession();
320
399
  }
321
400
  /**
322
- * Inject usage instructions for the agent
401
+ * Initialize trajectory tracking
402
+ * Auto-starts a trajectory if task is provided in config
403
+ */
404
+ async initializeTrajectory() {
405
+ if (!this.trajectory)
406
+ return;
407
+ // Auto-start trajectory if task is provided
408
+ if (this.config.task) {
409
+ const success = await this.trajectory.initialize(this.config.task);
410
+ if (success) {
411
+ this.logStderr(`Trajectory started for task: ${this.config.task}`);
412
+ }
413
+ }
414
+ else {
415
+ // Just initialize without starting a trajectory
416
+ await this.trajectory.initialize();
417
+ }
418
+ }
419
+ /**
420
+ * Initialize agent ID for continuity/resume functionality
421
+ */
422
+ async initializeAgentId() {
423
+ if (!this.continuity)
424
+ return;
425
+ try {
426
+ let ledger;
427
+ // If resuming from a previous agent ID, try to find that ledger
428
+ if (this.config.resumeAgentId) {
429
+ ledger = await this.continuity.findLedgerByAgentId(this.config.resumeAgentId);
430
+ if (ledger) {
431
+ this.logStderr(`Resuming agent ID: ${ledger.agentId} (from previous session)`);
432
+ }
433
+ else {
434
+ this.logStderr(`Resume agent ID ${this.config.resumeAgentId} not found, creating new`, true);
435
+ }
436
+ }
437
+ // If not resuming or resume ID not found, get or create ledger
438
+ if (!ledger) {
439
+ ledger = await this.continuity.getOrCreateLedger(this.config.name, this.cliType);
440
+ this.logStderr(`Agent ID: ${ledger.agentId} (use this to resume if agent dies)`);
441
+ }
442
+ this.agentId = ledger.agentId;
443
+ }
444
+ catch (err) {
445
+ this.logStderr(`Failed to initialize agent ID: ${err.message}`, true);
446
+ }
447
+ }
448
+ /**
449
+ * Get the current agent ID
450
+ */
451
+ getAgentId() {
452
+ return this.agentId;
453
+ }
454
+ /**
455
+ * Inject usage instructions for the agent including persistence protocol
323
456
  */
324
457
  async injectInstructions() {
325
458
  if (!this.running)
326
459
  return;
327
460
  // Use escaped prefix (\->relay:) in examples to prevent parser from treating them as real commands
328
461
  const escapedPrefix = '\\' + this.relayPrefix;
329
- const instructions = [
462
+ // Build instructions including relay and trail
463
+ const relayInstructions = [
330
464
  `[Agent Relay] You are "${this.config.name}" - connected for real-time messaging.`,
331
465
  `SEND: ${escapedPrefix}AgentName message`,
332
466
  `MULTI-LINE: ${escapedPrefix}AgentName <<<(newline)content(newline)>>> - ALWAYS end with >>> on its own line!`,
333
- `RECEIVE: Messages appear as "Relay message from X [id]: content" - use "agent-relay read <id>" for long messages`,
467
+ `PERSIST: Output [[SUMMARY]]{"currentTask":"...","context":"..."}[[/SUMMARY]] after major work.`,
468
+ `END: Output [[SESSION_END]]{"summary":"..."}[[/SESSION_END]] when session complete.`,
334
469
  ].join(' | ');
470
+ // Add trail instructions if available
471
+ const trailInstructions = getCompactTrailInstructions();
335
472
  try {
336
- await this.sendKeysLiteral(instructions);
337
- await this.sleep(50);
473
+ await this.sendKeysLiteral(relayInstructions);
474
+ await sleep(50);
338
475
  await this.sendKeys('Enter');
476
+ // Inject trail instructions
477
+ if (this.trajectory?.isTrailInstalledSync()) {
478
+ await sleep(100);
479
+ await this.sendKeysLiteral(trailInstructions);
480
+ await sleep(50);
481
+ await this.sendKeys('Enter');
482
+ }
483
+ // Inject continuity context from previous session
484
+ await this.injectContinuityContext();
339
485
  }
340
486
  catch {
341
487
  // Silent fail - instructions are nice-to-have
342
488
  }
343
489
  }
490
+ /**
491
+ * Inject continuity context from previous session
492
+ */
493
+ async injectContinuityContext() {
494
+ if (!this.continuity || !this.running)
495
+ return;
496
+ try {
497
+ const context = await this.continuity.getStartupContext(this.config.name);
498
+ if (context && context.formatted) {
499
+ // Inject a brief notification about loaded context
500
+ const notification = `[Continuity] Previous session context loaded. ${context.ledger ? `Task: ${context.ledger.currentTask?.slice(0, 50) || 'unknown'}` : ''}${context.handoff ? ` | Last handoff: ${context.handoff.createdAt.toISOString().split('T')[0]}` : ''}`;
501
+ await sleep(200);
502
+ await this.sendKeysLiteral(notification);
503
+ await sleep(50);
504
+ await this.sendKeys('Enter');
505
+ // Queue the full context for injection when agent is ready
506
+ this.messageQueue.push({
507
+ from: 'system',
508
+ body: context.formatted,
509
+ messageId: `continuity-startup-${Date.now()}`,
510
+ });
511
+ this.checkForInjectionOpportunity();
512
+ if (this.config.debug) {
513
+ this.logStderr(`[CONTINUITY] Loaded context for ${this.config.name}`);
514
+ }
515
+ }
516
+ }
517
+ catch (err) {
518
+ this.logStderr(`[CONTINUITY] Failed to load context: ${err.message}`, true);
519
+ }
520
+ }
344
521
  /**
345
522
  * Wait for tmux session to be ready
346
523
  */
@@ -374,14 +551,14 @@ export class TmuxWrapper {
374
551
  if (promptPatterns.test(lastLine)) {
375
552
  this.logStderr('Shell ready');
376
553
  // Extra delay to ensure shell is fully ready
377
- await this.sleep(200);
554
+ await sleep(200);
378
555
  return;
379
556
  }
380
557
  }
381
558
  catch {
382
559
  // Session might not be ready yet
383
560
  }
384
- await this.sleep(200);
561
+ await sleep(200);
385
562
  }
386
563
  // Fallback: proceed anyway after timeout
387
564
  this.logStderr('Shell ready timeout, proceeding anyway');
@@ -435,10 +612,10 @@ export class TmuxWrapper {
435
612
  `"${this.tmuxPath}" capture-pane -t ${this.sessionName} -p -J -S - 2>/dev/null`);
436
613
  // Always parse the FULL capture for ->relay commands
437
614
  // This handles terminal UIs that rewrite content in place
438
- const cleanContent = this.stripAnsi(stdout);
615
+ const cleanContent = stripAnsi(stdout);
439
616
  // Join continuation lines that TUIs split across multiple lines
440
617
  const joinedContent = this.joinContinuationLines(cleanContent);
441
- const { commands } = this.parser.parse(joinedContent);
618
+ const { commands, output: filteredOutput } = this.parser.parse(joinedContent);
442
619
  // Debug: log relay commands being parsed
443
620
  if (commands.length > 0 && this.config.debug) {
444
621
  for (const cmd of commands) {
@@ -452,12 +629,13 @@ export class TmuxWrapper {
452
629
  this.markActivity();
453
630
  this.processedOutputLength = stdout.length;
454
631
  // Stream new output to daemon for dashboard log viewing
632
+ // Use filtered output to exclude thinking blocks and relay commands
455
633
  if (this.config.streamLogs && this.client.state === 'READY') {
456
- // Send incremental output since last log
457
- const newContent = cleanContent.substring(this.lastLoggedLength);
634
+ // Send incremental filtered output since last log
635
+ const newContent = filteredOutput.substring(this.lastLoggedLength);
458
636
  if (newContent.length > 0) {
459
637
  this.client.sendLog(newContent);
460
- this.lastLoggedLength = cleanContent.length;
638
+ this.lastLoggedLength = filteredOutput.length;
461
639
  }
462
640
  }
463
641
  }
@@ -467,6 +645,10 @@ export class TmuxWrapper {
467
645
  }
468
646
  // Check for [[SUMMARY]] blocks and save to storage
469
647
  this.parseSummaryAndSave(cleanContent);
648
+ // Detect PDERO phase transitions from output content
649
+ this.detectAndTransitionPhase(cleanContent);
650
+ // Parse and handle continuity commands (->continuity:save, ->continuity:load, etc.)
651
+ await this.parseContinuityCommands(joinedContent);
470
652
  // Check for [[SESSION_END]] blocks to explicitly close session
471
653
  this.parseSessionEndAndClose(cleanContent);
472
654
  // Check for ->relay:spawn and ->relay:release commands (any agent can spawn)
@@ -482,18 +664,6 @@ export class TmuxWrapper {
482
664
  }
483
665
  }
484
666
  }
485
- /**
486
- * Strip ANSI escape codes
487
- */
488
- stripAnsi(str) {
489
- // Strip ANSI escape sequences (with \x1B prefix)
490
- // eslint-disable-next-line no-control-regex
491
- let result = str.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
492
- // Strip orphaned CSI sequences that lost their escape byte
493
- // These look like [?25h, [?2026l, [0m, etc. at the start of content
494
- result = result.replace(/^\s*(\[\??\d*[A-Za-z])+\s*/g, '');
495
- return result;
496
- }
497
667
  /**
498
668
  * Join continuation lines after ->relay commands.
499
669
  * Claude Code and other TUIs insert real newlines in output, causing
@@ -616,6 +786,8 @@ export class TmuxWrapper {
616
786
  this.queuedMessageHashes.delete(msgHash);
617
787
  const truncatedBody = cmd.body.substring(0, Math.min(RELAY_LOG_TRUNCATE_LENGTH, cmd.body.length));
618
788
  this.logStderr(`→ ${cmd.to}: ${truncatedBody}...`);
789
+ // Record in trajectory via trail
790
+ this.trajectory?.message('sent', this.config.name, cmd.to, cmd.body);
619
791
  }
620
792
  else if (this.client.state !== 'READY') {
621
793
  // Only log failure once per state change
@@ -663,7 +835,14 @@ export class TmuxWrapper {
663
835
  if (summaryHash === this.lastSummaryHash)
664
836
  return;
665
837
  this.lastSummaryHash = summaryHash;
666
- // Wait for storage to be ready before saving
838
+ // Save to continuity ledger for session recovery
839
+ // This ensures the ledger has actual data instead of placeholders
840
+ if (this.continuity) {
841
+ this.saveSummaryToLedger(summary).catch(err => {
842
+ this.logStderr(`Failed to save summary to ledger: ${err.message}`, true);
843
+ });
844
+ }
845
+ // Wait for storage to be ready before saving to project storage
667
846
  this.storageReady.then(ready => {
668
847
  if (!ready || !this.storage) {
669
848
  this.logStderr('Cannot save summary: storage not initialized');
@@ -685,6 +864,86 @@ export class TmuxWrapper {
685
864
  });
686
865
  });
687
866
  }
867
+ /**
868
+ * Save a parsed summary to the continuity ledger.
869
+ * Maps summary fields to ledger fields for session recovery.
870
+ */
871
+ async saveSummaryToLedger(summary) {
872
+ if (!this.continuity)
873
+ return;
874
+ const updates = {};
875
+ // Map summary fields to ledger fields
876
+ if (summary.currentTask) {
877
+ updates.currentTask = summary.currentTask;
878
+ }
879
+ if (summary.completedTasks && summary.completedTasks.length > 0) {
880
+ updates.completed = summary.completedTasks;
881
+ }
882
+ if (summary.context) {
883
+ // Store context in inProgress as "next steps" hint
884
+ updates.inProgress = [summary.context];
885
+ }
886
+ if (summary.files && summary.files.length > 0) {
887
+ updates.fileContext = summary.files.map((f) => ({ path: f }));
888
+ }
889
+ // Only save if we have meaningful updates
890
+ if (Object.keys(updates).length > 0) {
891
+ await this.continuity.saveLedger(this.config.name, updates);
892
+ this.logStderr('Saved summary to continuity ledger');
893
+ }
894
+ }
895
+ /**
896
+ * Parse ->continuity: commands from output and handle them.
897
+ * Supported commands:
898
+ * ->continuity:save <<<...>>> - Save session state to ledger
899
+ * ->continuity:load - Request context injection
900
+ * ->continuity:search "query" - Search past handoffs
901
+ * ->continuity:uncertain "..." - Mark item as uncertain
902
+ * ->continuity:handoff <<<...>>> - Create explicit handoff
903
+ */
904
+ async parseContinuityCommands(content) {
905
+ if (!this.continuity)
906
+ return;
907
+ if (!hasContinuityCommand(content))
908
+ return;
909
+ const command = parseContinuityCommand(content);
910
+ if (!command)
911
+ return;
912
+ // Create a hash for deduplication
913
+ // For commands with content (save, handoff, uncertain), use content hash
914
+ // For commands without content (load, search), allow each unique call
915
+ const hasContent = command.content || command.query || command.item;
916
+ const cmdHash = hasContent
917
+ ? `${command.type}:${command.content || command.query || command.item}`
918
+ : `${command.type}:${Date.now()}`; // Allow load/search to run each time
919
+ if (hasContent && this.processedContinuityCommands.has(cmdHash))
920
+ return;
921
+ this.processedContinuityCommands.add(cmdHash);
922
+ // Limit dedup set size
923
+ if (this.processedContinuityCommands.size > 100) {
924
+ const oldest = this.processedContinuityCommands.values().next().value;
925
+ if (oldest)
926
+ this.processedContinuityCommands.delete(oldest);
927
+ }
928
+ try {
929
+ if (this.config.debug) {
930
+ this.logStderr(`[CONTINUITY] Processing ${command.type} command`);
931
+ }
932
+ const response = await this.continuity.handleCommand(this.config.name, command);
933
+ // If there's a response (e.g., from load or search), inject it
934
+ if (response) {
935
+ this.messageQueue.push({
936
+ from: 'system',
937
+ body: response,
938
+ messageId: `continuity-${Date.now()}`,
939
+ });
940
+ this.checkForInjectionOpportunity();
941
+ }
942
+ }
943
+ catch (err) {
944
+ this.logStderr(`[CONTINUITY] Error: ${err.message}`, true);
945
+ }
946
+ }
688
947
  /**
689
948
  * Parse [[SESSION_END]] blocks from output and close session explicitly.
690
949
  * Agents output this to mark their work session as complete:
@@ -692,6 +951,8 @@ export class TmuxWrapper {
692
951
  * [[SESSION_END]]
693
952
  * {"summary": "Completed auth module", "completedTasks": ["login", "logout"]}
694
953
  * [[/SESSION_END]]
954
+ *
955
+ * Also stores the data for use in autoSave to populate handoff (fixes empty handoff issue).
695
956
  */
696
957
  parseSessionEndAndClose(content) {
697
958
  if (this.sessionEndProcessed)
@@ -699,6 +960,8 @@ export class TmuxWrapper {
699
960
  const sessionEnd = parseSessionEndFromOutput(content);
700
961
  if (!sessionEnd)
701
962
  return;
963
+ // Store SESSION_END data for use in autoSave (fixes empty handoff issue)
964
+ this.sessionEndData = sessionEnd;
702
965
  // Get session ID from client connection - if not available yet, don't set flag
703
966
  // so we can retry when sessionId becomes available
704
967
  const sessionId = this.client.currentSessionId;
@@ -723,29 +986,184 @@ export class TmuxWrapper {
723
986
  });
724
987
  });
725
988
  }
989
+ /**
990
+ * Execute spawn via API (if dashboardPort set) or callback
991
+ */
992
+ async executeSpawn(name, cli, task) {
993
+ if (this.config.dashboardPort) {
994
+ // Use dashboard API for spawning (works from any context, no terminal required)
995
+ try {
996
+ const response = await fetch(`http://localhost:${this.config.dashboardPort}/api/spawn`, {
997
+ method: 'POST',
998
+ headers: { 'Content-Type': 'application/json' },
999
+ body: JSON.stringify({ name, cli, task }),
1000
+ });
1001
+ const result = await response.json();
1002
+ if (result.success) {
1003
+ this.logStderr(`Spawned ${name} via API`);
1004
+ }
1005
+ else {
1006
+ this.logStderr(`Spawn failed: ${result.error}`, true);
1007
+ }
1008
+ }
1009
+ catch (err) {
1010
+ this.logStderr(`Spawn API call failed: ${err.message}`, true);
1011
+ }
1012
+ }
1013
+ else if (this.config.onSpawn) {
1014
+ // Fall back to callback
1015
+ try {
1016
+ await this.config.onSpawn(name, cli, task);
1017
+ }
1018
+ catch (err) {
1019
+ this.logStderr(`Spawn failed: ${err.message}`, true);
1020
+ }
1021
+ }
1022
+ }
1023
+ /**
1024
+ * Execute release via API (if dashboardPort set) or callback
1025
+ */
1026
+ async executeRelease(name) {
1027
+ if (this.config.dashboardPort) {
1028
+ // Use dashboard API for release (works from any context, no terminal required)
1029
+ try {
1030
+ const response = await fetch(`http://localhost:${this.config.dashboardPort}/api/spawned/${encodeURIComponent(name)}`, {
1031
+ method: 'DELETE',
1032
+ });
1033
+ const result = await response.json();
1034
+ if (result.success) {
1035
+ this.logStderr(`Released ${name} via API`);
1036
+ }
1037
+ else {
1038
+ this.logStderr(`Release failed: ${result.error}`, true);
1039
+ }
1040
+ }
1041
+ catch (err) {
1042
+ this.logStderr(`Release API call failed: ${err.message}`, true);
1043
+ }
1044
+ }
1045
+ else if (this.config.onRelease) {
1046
+ // Fall back to callback
1047
+ try {
1048
+ await this.config.onRelease(name);
1049
+ }
1050
+ catch (err) {
1051
+ this.logStderr(`Release failed: ${err.message}`, true);
1052
+ }
1053
+ }
1054
+ }
726
1055
  /**
727
1056
  * Parse ->relay:spawn and ->relay:release commands from output.
728
- * Format:
729
- * ->relay:spawn WorkerName cli "task description"
1057
+ * Supports two formats:
1058
+ * Single-line: ->relay:spawn WorkerName cli "task description"
1059
+ * Multi-line (fenced): ->relay:spawn WorkerName cli <<<
1060
+ * task description here
1061
+ * can span multiple lines>>>
730
1062
  * ->relay:release WorkerName
731
1063
  */
732
1064
  parseSpawnReleaseCommands(content) {
733
- // Only process if callbacks are configured
734
- if (!this.config.onSpawn && !this.config.onRelease)
1065
+ // Only process if we have API or callbacks configured
1066
+ const canSpawn = this.config.dashboardPort || this.config.onSpawn;
1067
+ const canRelease = this.config.dashboardPort || this.config.onRelease;
1068
+ if (!canSpawn && !canRelease)
735
1069
  return;
736
1070
  const lines = content.split('\n');
1071
+ // Pattern to strip common line prefixes (bullets, prompts, etc.)
1072
+ // Must include ● (U+25CF BLACK CIRCLE) used by Claude's TUI
1073
+ const linePrefixPattern = /^(?:[>$%#→➜›»●•◦‣⁃\-*⏺◆◇○□■│┃┆┇┊┋╎╏✦]\s*)+/;
737
1074
  for (const line of lines) {
738
- const trimmed = line.trim();
739
- // Match ->relay:spawn WorkerName cli OR ->relay:spawn WorkerName cli "task"
740
- // Task is now optional - agents can be spawned without immediate task injection
741
- // Pattern: ->relay:spawn <name> <cli> [optional: "<task>" or '<task>']
742
- // Allow trailing whitespace and optional bullet prefixes that TUIs might add
743
- const spawnMatch = trimmed.match(/^(?:[•\-*]\s*)?->relay:spawn\s+(\S+)\s+(\S+)(?:\s+["'](.+?)["'])?\s*$/);
744
- if (spawnMatch && this.config.onSpawn) {
745
- const [, name, cli, task] = spawnMatch;
746
- const taskStr = task || ''; // Task is optional, default to empty string
1075
+ let trimmed = line.trim();
1076
+ // Strip common line prefixes (bullets, prompts) before checking for commands
1077
+ trimmed = trimmed.replace(linePrefixPattern, '');
1078
+ // If we're in fenced spawn mode, accumulate lines until we see >>>
1079
+ if (this.pendingFencedSpawn) {
1080
+ // Check for fence close (>>> at end of line or on its own line)
1081
+ const closeIdx = trimmed.indexOf('>>>');
1082
+ if (closeIdx !== -1) {
1083
+ // Add content before >>> to task
1084
+ const contentBeforeClose = trimmed.substring(0, closeIdx);
1085
+ if (contentBeforeClose) {
1086
+ this.pendingFencedSpawn.taskLines.push(contentBeforeClose);
1087
+ }
1088
+ // Execute the spawn with accumulated task
1089
+ const { name, cli, taskLines } = this.pendingFencedSpawn;
1090
+ const taskStr = taskLines.join('\n').trim();
1091
+ const spawnKey = `${name}:${cli}`;
1092
+ if (!this.processedSpawnCommands.has(spawnKey)) {
1093
+ this.processedSpawnCommands.add(spawnKey);
1094
+ if (taskStr) {
1095
+ this.logStderr(`Spawn command (fenced): ${name} (${cli}) - "${taskStr.substring(0, 50)}..."`);
1096
+ }
1097
+ else {
1098
+ this.logStderr(`Spawn command (fenced): ${name} (${cli}) - no task`);
1099
+ }
1100
+ this.executeSpawn(name, cli, taskStr);
1101
+ }
1102
+ this.pendingFencedSpawn = null;
1103
+ }
1104
+ else {
1105
+ // Accumulate line as part of task
1106
+ this.pendingFencedSpawn.taskLines.push(line);
1107
+ }
1108
+ continue;
1109
+ }
1110
+ // Check for fenced spawn start: ->relay:spawn Name [cli] <<< (CLI optional, defaults to 'claude')
1111
+ // Prefixes are stripped above, so we just look for the command at start of line
1112
+ const fencedSpawnMatch = trimmed.match(/^->relay:spawn\s+(\S+)(?:\s+(\S+))?\s+<<<(.*)$/);
1113
+ if (fencedSpawnMatch && canSpawn) {
1114
+ const [, name, cliOrUndefined, inlineContent] = fencedSpawnMatch;
1115
+ const cli = cliOrUndefined || 'claude';
1116
+ // Validate name
1117
+ if (name.length < 2) {
1118
+ this.logStderr(`Fenced spawn has invalid name, skipping: name=${name}`);
1119
+ continue;
1120
+ }
1121
+ // Check if fence closes on same line (e.g., ->relay:spawn Worker cli <<<task>>>)
1122
+ const inlineCloseIdx = inlineContent.indexOf('>>>');
1123
+ if (inlineCloseIdx !== -1) {
1124
+ // Single line fenced: extract task between <<< and >>>
1125
+ const taskStr = inlineContent.substring(0, inlineCloseIdx).trim();
1126
+ const spawnKey = `${name}:${cli}`;
1127
+ if (!this.processedSpawnCommands.has(spawnKey)) {
1128
+ this.processedSpawnCommands.add(spawnKey);
1129
+ if (taskStr) {
1130
+ this.logStderr(`Spawn command: ${name} (${cli}) - "${taskStr.substring(0, 50)}..."`);
1131
+ }
1132
+ else {
1133
+ this.logStderr(`Spawn command: ${name} (${cli}) - no task`);
1134
+ }
1135
+ this.executeSpawn(name, cli, taskStr);
1136
+ }
1137
+ }
1138
+ else {
1139
+ // Start multi-line fenced mode
1140
+ this.pendingFencedSpawn = {
1141
+ name,
1142
+ cli,
1143
+ taskLines: inlineContent.trim() ? [inlineContent.trim()] : [],
1144
+ };
1145
+ this.logStderr(`Starting fenced spawn capture: ${name} (${cli})`);
1146
+ }
1147
+ continue;
1148
+ }
1149
+ // Match single-line spawn: ->relay:spawn WorkerName [cli] ["task"]
1150
+ // CLI is optional - defaults to 'claude'. Task is also optional.
1151
+ // Prefixes are stripped above, so we just look for the command at start of line
1152
+ const spawnMatch = trimmed.match(/^->relay:spawn\s+(\S+)(?:\s+(\S+))?(?:\s+["'](.+?)["'])?\s*$/);
1153
+ if (spawnMatch && canSpawn) {
1154
+ const [, name, cliOrUndefined, task] = spawnMatch;
1155
+ const cli = cliOrUndefined || 'claude';
1156
+ // Validate the parsed values
1157
+ if (cli === '<<<' || cli === '>>>' || name === '<<<' || name === '>>>') {
1158
+ this.logStderr(`Invalid spawn command (fence markers), skipping: name=${name}, cli=${cli}`);
1159
+ continue;
1160
+ }
1161
+ if (name.length < 2) {
1162
+ this.logStderr(`Spawn command has suspiciously short name, skipping: name=${name}`);
1163
+ continue;
1164
+ }
1165
+ const taskStr = task || '';
747
1166
  const spawnKey = `${name}:${cli}`;
748
- // Dedup - only process each spawn once (keyed by name:cli, not including task)
749
1167
  if (!this.processedSpawnCommands.has(spawnKey)) {
750
1168
  this.processedSpawnCommands.add(spawnKey);
751
1169
  if (taskStr) {
@@ -754,24 +1172,19 @@ export class TmuxWrapper {
754
1172
  else {
755
1173
  this.logStderr(`Spawn command: ${name} (${cli}) - no task`);
756
1174
  }
757
- this.config.onSpawn(name, cli, taskStr).catch(err => {
758
- this.logStderr(`Spawn failed: ${err.message}`, true);
759
- });
1175
+ this.executeSpawn(name, cli, taskStr);
760
1176
  }
761
1177
  continue;
762
1178
  }
763
1179
  // Match ->relay:release WorkerName
764
- // Allow trailing whitespace and optional bullet prefixes
765
- const releaseMatch = trimmed.match(/^(?:[•\-*]\s*)?->relay:release\s+(\S+)\s*$/);
766
- if (releaseMatch && this.config.onRelease) {
1180
+ // Prefixes are stripped above, so we just look for the command at start of line
1181
+ const releaseMatch = trimmed.match(/^->relay:release\s+(\S+)\s*$/);
1182
+ if (releaseMatch && canRelease) {
767
1183
  const [, name] = releaseMatch;
768
- // Dedup - only process each release once
769
1184
  if (!this.processedReleaseCommands.has(name)) {
770
1185
  this.processedReleaseCommands.add(name);
771
1186
  this.logStderr(`Release command: ${name}`);
772
- this.config.onRelease(name).catch(err => {
773
- this.logStderr(`Release failed: ${err.message}`, true);
774
- });
1187
+ this.executeRelease(name);
775
1188
  }
776
1189
  }
777
1190
  }
@@ -789,6 +1202,8 @@ export class TmuxWrapper {
789
1202
  const truncatedBody = payload.body.substring(0, Math.min(DEBUG_LOG_TRUNCATE_LENGTH, payload.body.length));
790
1203
  const channelInfo = originalTo === '*' ? ' [broadcast]' : '';
791
1204
  this.logStderr(`← ${from}${channelInfo}: ${truncatedBody}...`);
1205
+ // Record in trajectory via trail
1206
+ this.trajectory?.message('received', from, this.config.name, payload.body);
792
1207
  // Queue for injection - include originalTo so we can inform the agent how to route responses
793
1208
  this.messageQueue.push({ from, body: payload.body, messageId, thread: payload.thread, importance: meta?.importance, data: payload.data, originalTo });
794
1209
  // Write to inbox if enabled
@@ -818,7 +1233,8 @@ export class TmuxWrapper {
818
1233
  this.injectNextMessage();
819
1234
  }
820
1235
  /**
821
- * Inject message via tmux send-keys
1236
+ * Inject message via tmux send-keys.
1237
+ * Uses shared injection logic with tmux-specific callbacks.
822
1238
  */
823
1239
  async injectNextMessage() {
824
1240
  const msg = this.messageQueue.shift();
@@ -827,33 +1243,21 @@ export class TmuxWrapper {
827
1243
  this.isInjecting = true;
828
1244
  this.logStderr(`Injecting message from ${msg.from} (cli: ${this.cliType})`);
829
1245
  try {
830
- // Strip ANSI escape sequences and orphaned control sequences from message body
831
- let sanitizedBody = this.stripAnsi(msg.body).replace(/[\r\n]+/g, ' ').trim();
832
- // Gemini interprets certain keywords (While, For, If, etc.) as shell commands
833
- // Wrap in backticks to prevent shell keyword interpretation
834
- if (this.cliType === 'gemini') {
835
- sanitizedBody = `\`${sanitizedBody.replace(/`/g, "'")}\``;
836
- }
837
- // Short message ID for display (first 8 chars)
838
1246
  const shortId = msg.messageId.substring(0, 8);
839
- // Remove message truncation to allow full messages to pass through
840
- const wasTruncated = false;
841
- // Always include message ID; add lookup hint if truncated
842
- const idTag = `[${shortId}]`;
843
- const truncationHint = wasTruncated
844
- ? ` [TRUNCATED - run "agent-relay read ${msg.messageId}"]`
845
- : '';
846
1247
  // Wait for input to be clear before injecting
1248
+ // If input is not clear (human typing), re-queue and try later - never clear forcefully!
1249
+ // Fix for agent-relay-j9z: forceful clearing destroys human input in progress
847
1250
  const waitTimeoutMs = this.config.inputWaitTimeoutMs ?? 5000;
848
1251
  const waitPollMs = this.config.inputWaitPollMs ?? 200;
849
1252
  const inputClear = await this.waitForClearInput(waitTimeoutMs, waitPollMs);
850
1253
  if (!inputClear) {
851
- // Input still has text after timeout - clear it forcefully
852
- this.logStderr('Input not clear after waiting, clearing forcefully');
853
- await this.sendKeys('Escape');
854
- await this.sleep(30);
855
- await this.sendKeys('C-u');
856
- await this.sleep(30);
1254
+ // Input still has text after timeout - DON'T clear forcefully, re-queue instead
1255
+ // This preserves any human input in progress
1256
+ this.logStderr('Input not clear after waiting, re-queuing injection to preserve human input');
1257
+ this.messageQueue.unshift(msg);
1258
+ this.isInjecting = false;
1259
+ setTimeout(() => this.checkForInjectionOpportunity(), this.config.injectRetryMs ?? 1000);
1260
+ return;
857
1261
  }
858
1262
  // Ensure pane output is stable to avoid interleaving with active generation
859
1263
  const stablePane = await this.waitForStablePane(this.config.outputStabilityTimeoutMs ?? 2000, this.config.outputStabilityPollMs ?? 200);
@@ -868,8 +1272,8 @@ export class TmuxWrapper {
868
1272
  // If at shell prompt, skip injection to avoid shell command execution
869
1273
  if (this.cliType === 'gemini') {
870
1274
  const lastLine = await this.getLastLine();
871
- const cleanLine = this.stripAnsi(lastLine).trim();
872
- if (/^\$\s*$/.test(cleanLine) || /^\s*\$\s*$/.test(cleanLine)) {
1275
+ const cleanLine = stripAnsi(lastLine).trim();
1276
+ if (CLI_QUIRKS.isShellPrompt(cleanLine)) {
873
1277
  this.logStderr('Gemini at shell prompt, skipping injection to avoid shell execution');
874
1278
  // Re-queue the message for later
875
1279
  this.messageQueue.unshift(msg);
@@ -878,33 +1282,51 @@ export class TmuxWrapper {
878
1282
  return;
879
1283
  }
880
1284
  }
881
- // Standard injection for all CLIs including Gemini
882
- // Format: Relay message from Sender [abc12345] [thread:xxx] [!] [#general]: content
883
- // Thread/importance/channel hints are compact and optional to not break TUIs
884
- const threadHint = msg.thread ? ` [thread:${msg.thread}]` : '';
885
- // Importance indicator: [!!] for high (>75), [!] for medium (>50), none for low/default
886
- const importanceHint = msg.importance !== undefined && msg.importance > 75 ? ' [!!]' :
887
- msg.importance !== undefined && msg.importance > 50 ? ' [!]' : '';
888
- // Channel indicator: [#general] for broadcasts - tells agent to reply to * not sender
889
- // This ensures responses to #general messages go back to #general, not as DMs
890
- const channelHint = msg.originalTo === '*' ? ' [#general]' : '';
891
- // Extract attachment file paths if present
892
- let attachmentHint = '';
893
- if (msg.data?.attachments && Array.isArray(msg.data.attachments)) {
894
- const filePaths = msg.data.attachments
895
- .map((att) => att.filePath)
896
- .filter((p) => typeof p === 'string');
897
- if (filePaths.length > 0) {
898
- attachmentHint = ` [Attachments: ${filePaths.join(', ')}]`;
1285
+ // Build injection string using shared utility
1286
+ let injection = buildInjectionString(msg);
1287
+ // Gemini-specific: wrap body in backticks to prevent shell keyword interpretation
1288
+ if (this.cliType === 'gemini') {
1289
+ const colonIdx = injection.indexOf(': ');
1290
+ if (colonIdx > 0) {
1291
+ const prefix = injection.substring(0, colonIdx + 2);
1292
+ const body = injection.substring(colonIdx + 2);
1293
+ injection = prefix + CLI_QUIRKS.wrapForGemini(body);
1294
+ }
1295
+ }
1296
+ // Create callbacks for shared injection logic
1297
+ const callbacks = {
1298
+ getOutput: async () => {
1299
+ try {
1300
+ const { stdout } = await execAsync(`"${this.tmuxPath}" capture-pane -t ${this.sessionName} -p -S - 2>/dev/null`);
1301
+ return stdout;
1302
+ }
1303
+ catch {
1304
+ return '';
1305
+ }
1306
+ },
1307
+ performInjection: async (inj) => {
1308
+ await this.pasteLiteral(inj);
1309
+ await sleep(INJECTION_CONSTANTS.ENTER_DELAY_MS);
1310
+ await this.sendKeys('Enter');
1311
+ },
1312
+ log: (message) => this.logStderr(message),
1313
+ logError: (message) => this.logStderr(message, true),
1314
+ getMetrics: () => this.injectionMetrics,
1315
+ };
1316
+ // Inject with retry and verification using shared logic
1317
+ const result = await sharedInjectWithRetry(injection, shortId, msg.from, callbacks);
1318
+ if (result.success) {
1319
+ this.logStderr(`Injection complete (attempt ${result.attempts})`);
1320
+ }
1321
+ else {
1322
+ // All retries failed - log and optionally fall back to inbox
1323
+ this.logStderr(`Message delivery failed after ${result.attempts} attempts: from=${msg.from} id=${shortId}`, true);
1324
+ // Write to inbox as fallback if enabled
1325
+ if (this.inbox) {
1326
+ this.inbox.addMessage(msg.from, msg.body);
1327
+ this.logStderr('Wrote message to inbox as fallback');
899
1328
  }
900
1329
  }
901
- const injection = `Relay message from ${msg.from} ${idTag}${threadHint}${importanceHint}${channelHint}${attachmentHint}: ${sanitizedBody}${truncationHint}`;
902
- // Paste message as a bracketed paste to avoid interleaving with active output
903
- await this.pasteLiteral(injection);
904
- await this.sleep(30);
905
- // Submit
906
- await this.sendKeys('Enter');
907
- this.logStderr(`Injection complete`);
908
1330
  }
909
1331
  catch (err) {
910
1332
  this.logStderr(`Injection failed: ${err.message}`, true);
@@ -912,7 +1334,7 @@ export class TmuxWrapper {
912
1334
  finally {
913
1335
  this.isInjecting = false;
914
1336
  if (this.messageQueue.length > 0) {
915
- setTimeout(() => this.checkForInjectionOpportunity(), 1000);
1337
+ setTimeout(() => this.checkForInjectionOpportunity(), INJECTION_CONSTANTS.QUEUE_PROCESS_DELAY_MS);
916
1338
  }
917
1339
  }
918
1340
  }
@@ -967,7 +1389,7 @@ export class TmuxWrapper {
967
1389
  // Set tmux buffer then paste
968
1390
  // Skip bracketed paste (-p) for CLIs that don't handle it properly (droid, other)
969
1391
  await execAsync(`"${this.tmuxPath}" set-buffer -- "${escaped}"`);
970
- const useBracketedPaste = this.cliType === 'claude' || this.cliType === 'codex' || this.cliType === 'gemini';
1392
+ const useBracketedPaste = this.cliType === 'claude' || this.cliType === 'codex' || this.cliType === 'gemini' || this.cliType === 'opencode';
971
1393
  if (useBracketedPaste) {
972
1394
  await execAsync(`"${this.tmuxPath}" paste-buffer -t ${this.sessionName} -p`);
973
1395
  }
@@ -975,9 +1397,6 @@ export class TmuxWrapper {
975
1397
  await execAsync(`"${this.tmuxPath}" paste-buffer -t ${this.sessionName}`);
976
1398
  }
977
1399
  }
978
- sleep(ms) {
979
- return new Promise(r => setTimeout(r, ms));
980
- }
981
1400
  /**
982
1401
  * Reset session-specific state for wrapper reuse.
983
1402
  * Call this when starting a new session with the same wrapper instance.
@@ -986,18 +1405,13 @@ export class TmuxWrapper {
986
1405
  this.sessionEndProcessed = false;
987
1406
  this.lastSummaryHash = '';
988
1407
  this.lastSummaryRawContent = '';
1408
+ this.sessionEndData = undefined;
989
1409
  }
990
1410
  /**
991
1411
  * Get the prompt pattern for the current CLI type.
992
1412
  */
993
1413
  getPromptPattern() {
994
- const promptPatterns = {
995
- claude: /^[>›»]\s*$/, // Claude: "> " or similar
996
- gemini: /^[>›»]\s*$/, // Gemini: "> "
997
- codex: /^[>›»]\s*$/, // Codex: "> "
998
- other: /^[>$%#➜›»]\s*$/, // Shell or other: "$ ", "> ", etc.
999
- };
1000
- return promptPatterns[this.cliType] || promptPatterns.other;
1414
+ return CLI_QUIRKS.getPromptPattern(this.cliType);
1001
1415
  }
1002
1416
  /**
1003
1417
  * Capture the last non-empty line from the tmux pane.
@@ -1016,7 +1430,7 @@ export class TmuxWrapper {
1016
1430
  * Detect if the provided line contains visible user input (beyond the prompt).
1017
1431
  */
1018
1432
  hasVisibleInput(line) {
1019
- const cleanLine = this.stripAnsi(line).trimEnd();
1433
+ const cleanLine = stripAnsi(line).trimEnd();
1020
1434
  if (cleanLine === '')
1021
1435
  return false;
1022
1436
  return !this.getPromptPattern().test(cleanLine);
@@ -1028,7 +1442,7 @@ export class TmuxWrapper {
1028
1442
  async isInputClear(lastLine) {
1029
1443
  try {
1030
1444
  const lineToCheck = lastLine ?? await this.getLastLine();
1031
- const cleanLine = this.stripAnsi(lineToCheck).trimEnd();
1445
+ const cleanLine = stripAnsi(lineToCheck).trimEnd();
1032
1446
  const isClear = this.getPromptPattern().test(cleanLine);
1033
1447
  if (this.config.debug) {
1034
1448
  const truncatedLine = cleanLine.substring(0, Math.min(DEBUG_LOG_TRUNCATE_LENGTH, cleanLine.length));
@@ -1088,7 +1502,7 @@ export class TmuxWrapper {
1088
1502
  stableCursorCount = 0;
1089
1503
  lastCursorX = cursorX;
1090
1504
  }
1091
- await this.sleep(pollIntervalMs);
1505
+ await sleep(pollIntervalMs);
1092
1506
  }
1093
1507
  this.logStderr(`waitForClearInput: timed out after ${maxWaitMs}ms`);
1094
1508
  return false;
@@ -1117,7 +1531,7 @@ export class TmuxWrapper {
1117
1531
  return false;
1118
1532
  let stableCount = 0;
1119
1533
  while (Date.now() - start < maxWaitMs) {
1120
- await this.sleep(pollIntervalMs);
1534
+ await sleep(pollIntervalMs);
1121
1535
  const sig = await this.capturePaneSignature();
1122
1536
  if (!sig)
1123
1537
  continue;
@@ -1143,6 +1557,13 @@ export class TmuxWrapper {
1143
1557
  return;
1144
1558
  this.running = false;
1145
1559
  this.activityState = 'disconnected';
1560
+ // Auto-save continuity state before shutdown (fire and forget)
1561
+ // Pass sessionEndData to populate handoff (fixes empty handoff issue)
1562
+ if (this.continuity) {
1563
+ this.continuity.autoSave(this.config.name, 'session_end', this.sessionEndData).catch((err) => {
1564
+ this.logStderr(`[CONTINUITY] Auto-save failed: ${err.message}`, true);
1565
+ });
1566
+ }
1146
1567
  // Reset session state for potential reuse
1147
1568
  this.resetSessionState();
1148
1569
  // Stop polling