agent-tempo 1.0.1

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 (484) hide show
  1. package/CLAUDE.md +213 -0
  2. package/LICENSE +21 -0
  3. package/README.md +289 -0
  4. package/assets/icon-32.png +0 -0
  5. package/assets/icon-512.png +0 -0
  6. package/assets/icon-64.png +0 -0
  7. package/assets/icon-dark-32.png +0 -0
  8. package/assets/icon-dark-64.png +0 -0
  9. package/assets/icon-dark.svg +9 -0
  10. package/assets/icon.svg +9 -0
  11. package/assets/logo-dark.svg +11 -0
  12. package/assets/logo-light.svg +11 -0
  13. package/dashboard/README.md +91 -0
  14. package/dashboard/dist/assets/index-CB78ToNE.css +2 -0
  15. package/dashboard/dist/assets/index-_5jV0Znu.js +62 -0
  16. package/dashboard/dist/assets/index-_5jV0Znu.js.map +1 -0
  17. package/dashboard/dist/index.html +21 -0
  18. package/dashboard/package.json +47 -0
  19. package/dist/activities/hard-terminate.d.ts +32 -0
  20. package/dist/activities/hard-terminate.js +460 -0
  21. package/dist/activities/maestro.d.ts +72 -0
  22. package/dist/activities/maestro.js +254 -0
  23. package/dist/activities/outbox.d.ts +188 -0
  24. package/dist/activities/outbox.js +849 -0
  25. package/dist/activities/resolve.d.ts +64 -0
  26. package/dist/activities/resolve.js +129 -0
  27. package/dist/activities/schedule-fire.d.ts +36 -0
  28. package/dist/activities/schedule-fire.js +147 -0
  29. package/dist/adapters/base.d.ts +426 -0
  30. package/dist/adapters/base.js +1270 -0
  31. package/dist/adapters/claude-api/adapter.d.ts +168 -0
  32. package/dist/adapters/claude-api/adapter.js +797 -0
  33. package/dist/adapters/claude-api/api-error.d.ts +96 -0
  34. package/dist/adapters/claude-api/api-error.js +191 -0
  35. package/dist/adapters/claude-api/index.d.ts +16 -0
  36. package/dist/adapters/claude-api/index.js +21 -0
  37. package/dist/adapters/claude-api/mcp-bridge.d.ts +50 -0
  38. package/dist/adapters/claude-api/mcp-bridge.js +157 -0
  39. package/dist/adapters/claude-code/adapter.d.ts +133 -0
  40. package/dist/adapters/claude-code/adapter.js +274 -0
  41. package/dist/adapters/claude-code/index.d.ts +15 -0
  42. package/dist/adapters/claude-code/index.js +20 -0
  43. package/dist/adapters/claude-code-headless/adapter.d.ts +131 -0
  44. package/dist/adapters/claude-code-headless/adapter.js +710 -0
  45. package/dist/adapters/claude-code-headless/error-mapper.d.ts +107 -0
  46. package/dist/adapters/claude-code-headless/error-mapper.js +281 -0
  47. package/dist/adapters/claude-code-headless/index.d.ts +17 -0
  48. package/dist/adapters/claude-code-headless/index.js +26 -0
  49. package/dist/adapters/claude-code-headless/pre-flight.d.ts +51 -0
  50. package/dist/adapters/claude-code-headless/pre-flight.js +207 -0
  51. package/dist/adapters/claude-code-headless/prompt.d.ts +93 -0
  52. package/dist/adapters/claude-code-headless/prompt.js +79 -0
  53. package/dist/adapters/claude-code-headless/stream-json.d.ts +242 -0
  54. package/dist/adapters/claude-code-headless/stream-json.js +208 -0
  55. package/dist/adapters/claude-code-headless/types.d.ts +28 -0
  56. package/dist/adapters/claude-code-headless/types.js +36 -0
  57. package/dist/adapters/copilot/adapter.d.ts +100 -0
  58. package/dist/adapters/copilot/adapter.js +730 -0
  59. package/dist/adapters/copilot/index.d.ts +15 -0
  60. package/dist/adapters/copilot/index.js +20 -0
  61. package/dist/adapters/index.d.ts +42 -0
  62. package/dist/adapters/index.js +115 -0
  63. package/dist/adapters/opencode/adapter.d.ts +82 -0
  64. package/dist/adapters/opencode/adapter.js +710 -0
  65. package/dist/adapters/opencode/config.d.ts +90 -0
  66. package/dist/adapters/opencode/config.js +137 -0
  67. package/dist/adapters/opencode/helpers.d.ts +40 -0
  68. package/dist/adapters/opencode/helpers.js +144 -0
  69. package/dist/adapters/opencode/index.d.ts +12 -0
  70. package/dist/adapters/opencode/index.js +17 -0
  71. package/dist/adapters/opencode/server-bridge.d.ts +124 -0
  72. package/dist/adapters/opencode/server-bridge.js +216 -0
  73. package/dist/adapters/sdk/base.d.ts +95 -0
  74. package/dist/adapters/sdk/base.js +134 -0
  75. package/dist/adapters/sdk/system-prompt.d.ts +64 -0
  76. package/dist/adapters/sdk/system-prompt.js +78 -0
  77. package/dist/adapters/terminal-error.d.ts +27 -0
  78. package/dist/adapters/terminal-error.js +39 -0
  79. package/dist/channel.d.ts +3 -0
  80. package/dist/channel.js +48 -0
  81. package/dist/cli/commands.d.ts +245 -0
  82. package/dist/cli/commands.js +2438 -0
  83. package/dist/cli/config-command.d.ts +8 -0
  84. package/dist/cli/config-command.js +254 -0
  85. package/dist/cli/daemon-command.d.ts +57 -0
  86. package/dist/cli/daemon-command.js +493 -0
  87. package/dist/cli/daemon.d.ts +217 -0
  88. package/dist/cli/daemon.js +632 -0
  89. package/dist/cli/dashboard-command.d.ts +20 -0
  90. package/dist/cli/dashboard-command.js +241 -0
  91. package/dist/cli/dev-banner.d.ts +107 -0
  92. package/dist/cli/dev-banner.js +190 -0
  93. package/dist/cli/dev-mode-bootstrap.d.ts +29 -0
  94. package/dist/cli/dev-mode-bootstrap.js +36 -0
  95. package/dist/cli/dev-verbs.d.ts +43 -0
  96. package/dist/cli/dev-verbs.js +254 -0
  97. package/dist/cli/help-text.d.ts +1 -0
  98. package/dist/cli/help-text.js +158 -0
  99. package/dist/cli/legacy-migration.d.ts +35 -0
  100. package/dist/cli/legacy-migration.js +335 -0
  101. package/dist/cli/mcp.d.ts +8 -0
  102. package/dist/cli/mcp.js +63 -0
  103. package/dist/cli/output.d.ts +12 -0
  104. package/dist/cli/output.js +37 -0
  105. package/dist/cli/preflight.d.ts +9 -0
  106. package/dist/cli/preflight.js +96 -0
  107. package/dist/cli/removed-verbs.d.ts +9 -0
  108. package/dist/cli/removed-verbs.js +78 -0
  109. package/dist/cli/sa-preflight.d.ts +99 -0
  110. package/dist/cli/sa-preflight.js +183 -0
  111. package/dist/cli/scenarios-command.d.ts +6 -0
  112. package/dist/cli/scenarios-command.js +167 -0
  113. package/dist/cli/startup.d.ts +112 -0
  114. package/dist/cli/startup.js +641 -0
  115. package/dist/cli/upgrade-command.d.ts +5 -0
  116. package/dist/cli/upgrade-command.js +240 -0
  117. package/dist/cli.d.ts +2 -0
  118. package/dist/cli.js +680 -0
  119. package/dist/client/core.d.ts +33 -0
  120. package/dist/client/core.js +1260 -0
  121. package/dist/client/ensure-conductor-spawned.d.ts +35 -0
  122. package/dist/client/ensure-conductor-spawned.js +48 -0
  123. package/dist/client/index.d.ts +32 -0
  124. package/dist/client/index.js +22 -0
  125. package/dist/client/interface.d.ts +461 -0
  126. package/dist/client/interface.js +2 -0
  127. package/dist/client/subscribe.d.ts +108 -0
  128. package/dist/client/subscribe.js +598 -0
  129. package/dist/client/with-spawn.d.ts +27 -0
  130. package/dist/client/with-spawn.js +87 -0
  131. package/dist/config.d.ts +323 -0
  132. package/dist/config.js +593 -0
  133. package/dist/connection.d.ts +7 -0
  134. package/dist/connection.js +46 -0
  135. package/dist/constants.d.ts +50 -0
  136. package/dist/constants.js +74 -0
  137. package/dist/copilot-bridge.d.ts +22 -0
  138. package/dist/copilot-bridge.js +565 -0
  139. package/dist/daemon-adapter-versions.d.ts +52 -0
  140. package/dist/daemon-adapter-versions.js +170 -0
  141. package/dist/daemon.d.ts +275 -0
  142. package/dist/daemon.js +989 -0
  143. package/dist/ensemble/agent-types.d.ts +23 -0
  144. package/dist/ensemble/agent-types.js +132 -0
  145. package/dist/ensemble/loader.d.ts +14 -0
  146. package/dist/ensemble/loader.js +140 -0
  147. package/dist/ensemble/saver.d.ts +49 -0
  148. package/dist/ensemble/saver.js +201 -0
  149. package/dist/ensemble/schema.d.ts +71 -0
  150. package/dist/ensemble/schema.js +3 -0
  151. package/dist/git-info.d.ts +4 -0
  152. package/dist/git-info.js +29 -0
  153. package/dist/http/aggregate.d.ts +319 -0
  154. package/dist/http/aggregate.js +684 -0
  155. package/dist/http/auth.d.ts +67 -0
  156. package/dist/http/auth.js +177 -0
  157. package/dist/http/body.d.ts +71 -0
  158. package/dist/http/body.js +121 -0
  159. package/dist/http/catalog.d.ts +67 -0
  160. package/dist/http/catalog.js +209 -0
  161. package/dist/http/cors.d.ts +42 -0
  162. package/dist/http/cors.js +111 -0
  163. package/dist/http/dashboard-pair.d.ts +94 -0
  164. package/dist/http/dashboard-pair.js +148 -0
  165. package/dist/http/dashboard.d.ts +20 -0
  166. package/dist/http/dashboard.js +160 -0
  167. package/dist/http/event-bus.d.ts +217 -0
  168. package/dist/http/event-bus.js +365 -0
  169. package/dist/http/event-id.d.ts +77 -0
  170. package/dist/http/event-id.js +117 -0
  171. package/dist/http/event-types.d.ts +348 -0
  172. package/dist/http/event-types.js +36 -0
  173. package/dist/http/fixtures/chat-stress.d.ts +8 -0
  174. package/dist/http/fixtures/chat-stress.js +63 -0
  175. package/dist/http/fixtures/conductor-leaving.d.ts +8 -0
  176. package/dist/http/fixtures/conductor-leaving.js +80 -0
  177. package/dist/http/fixtures/constants.d.ts +10 -0
  178. package/dist/http/fixtures/constants.js +13 -0
  179. package/dist/http/fixtures/eight-player-broadcast.d.ts +10 -0
  180. package/dist/http/fixtures/eight-player-broadcast.js +81 -0
  181. package/dist/http/fixtures/empty-ensemble.d.ts +6 -0
  182. package/dist/http/fixtures/empty-ensemble.js +26 -0
  183. package/dist/http/fixtures/index.d.ts +55 -0
  184. package/dist/http/fixtures/index.js +110 -0
  185. package/dist/http/fixtures/single-conductor.d.ts +7 -0
  186. package/dist/http/fixtures/single-conductor.js +46 -0
  187. package/dist/http/fixtures/sse-reconnect.d.ts +8 -0
  188. package/dist/http/fixtures/sse-reconnect.js +77 -0
  189. package/dist/http/index.d.ts +21 -0
  190. package/dist/http/index.js +61 -0
  191. package/dist/http/port-file.d.ts +22 -0
  192. package/dist/http/port-file.js +132 -0
  193. package/dist/http/responses.d.ts +27 -0
  194. package/dist/http/responses.js +40 -0
  195. package/dist/http/ring-buffer.d.ts +41 -0
  196. package/dist/http/ring-buffer.js +80 -0
  197. package/dist/http/server.d.ts +122 -0
  198. package/dist/http/server.js +459 -0
  199. package/dist/http/snapshot.d.ts +85 -0
  200. package/dist/http/snapshot.js +180 -0
  201. package/dist/http/sse-handler.d.ts +87 -0
  202. package/dist/http/sse-handler.js +294 -0
  203. package/dist/http/writes.d.ts +55 -0
  204. package/dist/http/writes.js +240 -0
  205. package/dist/palette/index.d.ts +138 -0
  206. package/dist/palette/index.js +221 -0
  207. package/dist/reconcile/orphans.d.ts +255 -0
  208. package/dist/reconcile/orphans.js +340 -0
  209. package/dist/scripts/258-spotcheck.js +303 -0
  210. package/dist/scripts/check-components-css-sync.js +199 -0
  211. package/dist/scripts/run-shard.js +121 -0
  212. package/dist/scripts/verify-daemon-isolation-guard.js +128 -0
  213. package/dist/server-tools.d.ts +87 -0
  214. package/dist/server-tools.js +146 -0
  215. package/dist/server.d.ts +2 -0
  216. package/dist/server.js +366 -0
  217. package/dist/spawn.d.ts +296 -0
  218. package/dist/spawn.js +747 -0
  219. package/dist/tools/agent-types.d.ts +2 -0
  220. package/dist/tools/agent-types.js +21 -0
  221. package/dist/tools/attachment-info.d.ts +4 -0
  222. package/dist/tools/attachment-info.js +48 -0
  223. package/dist/tools/broadcast.d.ts +4 -0
  224. package/dist/tools/broadcast.js +76 -0
  225. package/dist/tools/cancel-stage.d.ts +3 -0
  226. package/dist/tools/cancel-stage.js +20 -0
  227. package/dist/tools/clear-state.d.ts +3 -0
  228. package/dist/tools/clear-state.js +37 -0
  229. package/dist/tools/coat-check-evict.d.ts +4 -0
  230. package/dist/tools/coat-check-evict.js +43 -0
  231. package/dist/tools/coat-check-get.d.ts +4 -0
  232. package/dist/tools/coat-check-get.js +56 -0
  233. package/dist/tools/coat-check-list.d.ts +4 -0
  234. package/dist/tools/coat-check-list.js +60 -0
  235. package/dist/tools/coat-check-put.d.ts +4 -0
  236. package/dist/tools/coat-check-put.js +53 -0
  237. package/dist/tools/cue.d.ts +44 -0
  238. package/dist/tools/cue.js +201 -0
  239. package/dist/tools/destroy.d.ts +4 -0
  240. package/dist/tools/destroy.js +188 -0
  241. package/dist/tools/detach.d.ts +4 -0
  242. package/dist/tools/detach.js +45 -0
  243. package/dist/tools/encore.d.ts +4 -0
  244. package/dist/tools/encore.js +31 -0
  245. package/dist/tools/ensemble.d.ts +32 -0
  246. package/dist/tools/ensemble.js +198 -0
  247. package/dist/tools/evaluate-gate.d.ts +3 -0
  248. package/dist/tools/evaluate-gate.js +32 -0
  249. package/dist/tools/fetch-state.d.ts +13 -0
  250. package/dist/tools/fetch-state.js +78 -0
  251. package/dist/tools/gates.d.ts +3 -0
  252. package/dist/tools/gates.js +41 -0
  253. package/dist/tools/helpers.d.ts +21 -0
  254. package/dist/tools/helpers.js +25 -0
  255. package/dist/tools/hosts.d.ts +4 -0
  256. package/dist/tools/hosts.js +40 -0
  257. package/dist/tools/listen.d.ts +3 -0
  258. package/dist/tools/listen.js +22 -0
  259. package/dist/tools/load-lineup.d.ts +5 -0
  260. package/dist/tools/load-lineup.js +381 -0
  261. package/dist/tools/migrate.d.ts +4 -0
  262. package/dist/tools/migrate.js +60 -0
  263. package/dist/tools/pause-ensemble.d.ts +4 -0
  264. package/dist/tools/pause-ensemble.js +58 -0
  265. package/dist/tools/pause.d.ts +4 -0
  266. package/dist/tools/pause.js +36 -0
  267. package/dist/tools/play.d.ts +4 -0
  268. package/dist/tools/play.js +57 -0
  269. package/dist/tools/quality-gate.d.ts +3 -0
  270. package/dist/tools/quality-gate.js +26 -0
  271. package/dist/tools/recall.d.ts +3 -0
  272. package/dist/tools/recall.js +32 -0
  273. package/dist/tools/recruit.d.ts +38 -0
  274. package/dist/tools/recruit.js +447 -0
  275. package/dist/tools/release.d.ts +4 -0
  276. package/dist/tools/release.js +98 -0
  277. package/dist/tools/report.d.ts +3 -0
  278. package/dist/tools/report.js +29 -0
  279. package/dist/tools/resolve.d.ts +1 -0
  280. package/dist/tools/resolve.js +7 -0
  281. package/dist/tools/restart.d.ts +35 -0
  282. package/dist/tools/restart.js +131 -0
  283. package/dist/tools/restore.d.ts +4 -0
  284. package/dist/tools/restore.js +107 -0
  285. package/dist/tools/resume-ensemble.d.ts +4 -0
  286. package/dist/tools/resume-ensemble.js +79 -0
  287. package/dist/tools/save-lineup.d.ts +4 -0
  288. package/dist/tools/save-lineup.js +36 -0
  289. package/dist/tools/save-state.d.ts +3 -0
  290. package/dist/tools/save-state.js +57 -0
  291. package/dist/tools/schedule.d.ts +4 -0
  292. package/dist/tools/schedule.js +152 -0
  293. package/dist/tools/schedules.d.ts +4 -0
  294. package/dist/tools/schedules.js +54 -0
  295. package/dist/tools/set-ensemble-description.d.ts +4 -0
  296. package/dist/tools/set-ensemble-description.js +37 -0
  297. package/dist/tools/set-name.d.ts +4 -0
  298. package/dist/tools/set-name.js +45 -0
  299. package/dist/tools/set-part.d.ts +3 -0
  300. package/dist/tools/set-part.js +20 -0
  301. package/dist/tools/shutdown.d.ts +4 -0
  302. package/dist/tools/shutdown.js +54 -0
  303. package/dist/tools/stage.d.ts +3 -0
  304. package/dist/tools/stage.js +28 -0
  305. package/dist/tools/stages.d.ts +3 -0
  306. package/dist/tools/stages.js +35 -0
  307. package/dist/tools/stop.d.ts +4 -0
  308. package/dist/tools/stop.js +29 -0
  309. package/dist/tools/unschedule.d.ts +4 -0
  310. package/dist/tools/unschedule.js +35 -0
  311. package/dist/tools/who-am-i.d.ts +3 -0
  312. package/dist/tools/who-am-i.js +34 -0
  313. package/dist/tools/worktree.d.ts +4 -0
  314. package/dist/tools/worktree.js +181 -0
  315. package/dist/tui/App.d.ts +85 -0
  316. package/dist/tui/App.js +1791 -0
  317. package/dist/tui/bootstrap-types.d.ts +46 -0
  318. package/dist/tui/bootstrap-types.js +7 -0
  319. package/dist/tui/client.d.ts +6 -0
  320. package/dist/tui/client.js +9 -0
  321. package/dist/tui/commands.d.ts +71 -0
  322. package/dist/tui/commands.js +1375 -0
  323. package/dist/tui/components/ActivityLog.d.ts +16 -0
  324. package/dist/tui/components/ActivityLog.js +36 -0
  325. package/dist/tui/components/ChatView.d.ts +35 -0
  326. package/dist/tui/components/ChatView.js +54 -0
  327. package/dist/tui/components/CommandOverlay.d.ts +15 -0
  328. package/dist/tui/components/CommandOverlay.js +34 -0
  329. package/dist/tui/components/CommandPalette.d.ts +21 -0
  330. package/dist/tui/components/CommandPalette.js +67 -0
  331. package/dist/tui/components/ConductorChat.d.ts +16 -0
  332. package/dist/tui/components/ConductorChat.js +32 -0
  333. package/dist/tui/components/ConversationStream.d.ts +114 -0
  334. package/dist/tui/components/ConversationStream.js +307 -0
  335. package/dist/tui/components/CreateEnsembleWizard.d.ts +19 -0
  336. package/dist/tui/components/CreateEnsembleWizard.js +223 -0
  337. package/dist/tui/components/DestroyConfirmModal.d.ts +17 -0
  338. package/dist/tui/components/DestroyConfirmModal.js +62 -0
  339. package/dist/tui/components/EnsembleListView.d.ts +14 -0
  340. package/dist/tui/components/EnsembleListView.js +32 -0
  341. package/dist/tui/components/EnsemblePanel.d.ts +12 -0
  342. package/dist/tui/components/EnsemblePanel.js +40 -0
  343. package/dist/tui/components/ErrorView.d.ts +31 -0
  344. package/dist/tui/components/ErrorView.js +129 -0
  345. package/dist/tui/components/HomeView.d.ts +54 -0
  346. package/dist/tui/components/HomeView.js +306 -0
  347. package/dist/tui/components/InputBar.d.ts +13 -0
  348. package/dist/tui/components/InputBar.js +58 -0
  349. package/dist/tui/components/LoadLineupModal.d.ts +18 -0
  350. package/dist/tui/components/LoadLineupModal.js +79 -0
  351. package/dist/tui/components/MainView.d.ts +21 -0
  352. package/dist/tui/components/MainView.js +107 -0
  353. package/dist/tui/components/NewEnsembleModal.d.ts +9 -0
  354. package/dist/tui/components/NewEnsembleModal.js +73 -0
  355. package/dist/tui/components/Picker.d.ts +23 -0
  356. package/dist/tui/components/Picker.js +70 -0
  357. package/dist/tui/components/PlayerDetailView.d.ts +26 -0
  358. package/dist/tui/components/PlayerDetailView.js +118 -0
  359. package/dist/tui/components/PromptArea.d.ts +50 -0
  360. package/dist/tui/components/PromptArea.js +303 -0
  361. package/dist/tui/components/RecruitWizard.d.ts +17 -0
  362. package/dist/tui/components/RecruitWizard.js +221 -0
  363. package/dist/tui/components/RestoreConfirmModal.d.ts +18 -0
  364. package/dist/tui/components/RestoreConfirmModal.js +71 -0
  365. package/dist/tui/components/ScheduleOverlay.d.ts +13 -0
  366. package/dist/tui/components/ScheduleOverlay.js +113 -0
  367. package/dist/tui/components/ScheduleWizard.d.ts +19 -0
  368. package/dist/tui/components/ScheduleWizard.js +259 -0
  369. package/dist/tui/components/Splash.d.ts +23 -0
  370. package/dist/tui/components/Splash.js +221 -0
  371. package/dist/tui/components/StatusBar.d.ts +48 -0
  372. package/dist/tui/components/StatusBar.js +128 -0
  373. package/dist/tui/components/StatusOverlay.d.ts +15 -0
  374. package/dist/tui/components/StatusOverlay.js +76 -0
  375. package/dist/tui/components/TitleBar.d.ts +10 -0
  376. package/dist/tui/components/TitleBar.js +21 -0
  377. package/dist/tui/components/TopBar.d.ts +12 -0
  378. package/dist/tui/components/TopBar.js +15 -0
  379. package/dist/tui/core-api.d.ts +26 -0
  380. package/dist/tui/core-api.js +67 -0
  381. package/dist/tui/hooks/useEnsembleDiscovery.d.ts +3 -0
  382. package/dist/tui/hooks/useEnsembleDiscovery.js +30 -0
  383. package/dist/tui/hooks/useMaestroPoller.d.ts +3 -0
  384. package/dist/tui/hooks/useMaestroPoller.js +36 -0
  385. package/dist/tui/hooks/useSendCommand.d.ts +7 -0
  386. package/dist/tui/hooks/useSendCommand.js +29 -0
  387. package/dist/tui/index.d.ts +15 -0
  388. package/dist/tui/index.js +156 -0
  389. package/dist/tui/ink-context.d.ts +18 -0
  390. package/dist/tui/ink-context.js +59 -0
  391. package/dist/tui/ink-loader.d.ts +26 -0
  392. package/dist/tui/ink-loader.js +42 -0
  393. package/dist/tui/removed-commands.d.ts +9 -0
  394. package/dist/tui/removed-commands.js +22 -0
  395. package/dist/tui/sse-handler.d.ts +52 -0
  396. package/dist/tui/sse-handler.js +157 -0
  397. package/dist/tui/store.d.ts +598 -0
  398. package/dist/tui/store.js +753 -0
  399. package/dist/tui/utils/format.d.ts +56 -0
  400. package/dist/tui/utils/format.js +155 -0
  401. package/dist/tui/utils/fullscreen.d.ts +23 -0
  402. package/dist/tui/utils/fullscreen.js +71 -0
  403. package/dist/tui/utils/history.d.ts +10 -0
  404. package/dist/tui/utils/history.js +85 -0
  405. package/dist/tui/utils/platform.d.ts +45 -0
  406. package/dist/tui/utils/platform.js +258 -0
  407. package/dist/tui/utils/theme.d.ts +21 -0
  408. package/dist/tui/utils/theme.js +24 -0
  409. package/dist/types.d.ts +1020 -0
  410. package/dist/types.js +39 -0
  411. package/dist/utils/attachment-format.d.ts +22 -0
  412. package/dist/utils/attachment-format.js +32 -0
  413. package/dist/utils/default-part.d.ts +43 -0
  414. package/dist/utils/default-part.js +104 -0
  415. package/dist/utils/duration.d.ts +30 -0
  416. package/dist/utils/duration.js +69 -0
  417. package/dist/utils/ensemble-ops.d.ts +61 -0
  418. package/dist/utils/ensemble-ops.js +77 -0
  419. package/dist/utils/format-hosts.d.ts +21 -0
  420. package/dist/utils/format-hosts.js +73 -0
  421. package/dist/utils/hosts.d.ts +113 -0
  422. package/dist/utils/hosts.js +265 -0
  423. package/dist/utils/parent-death-watchdog.d.ts +1 -0
  424. package/dist/utils/parent-death-watchdog.js +47 -0
  425. package/dist/utils/query-timeout.d.ts +103 -0
  426. package/dist/utils/query-timeout.js +113 -0
  427. package/dist/utils/recall-format.d.ts +78 -0
  428. package/dist/utils/recall-format.js +105 -0
  429. package/dist/utils/restore-format.d.ts +49 -0
  430. package/dist/utils/restore-format.js +91 -0
  431. package/dist/utils/safe-path.d.ts +10 -0
  432. package/dist/utils/safe-path.js +43 -0
  433. package/dist/utils/sdk-probe.d.ts +9 -0
  434. package/dist/utils/sdk-probe.js +45 -0
  435. package/dist/utils/search-attributes.d.ts +76 -0
  436. package/dist/utils/search-attributes.js +86 -0
  437. package/dist/utils/validation.d.ts +113 -0
  438. package/dist/utils/validation.js +163 -0
  439. package/dist/utils/visibility-deadline.d.ts +186 -0
  440. package/dist/utils/visibility-deadline.js +158 -0
  441. package/dist/utils/worktree.d.ts +103 -0
  442. package/dist/utils/worktree.js +327 -0
  443. package/dist/worker.d.ts +14 -0
  444. package/dist/worker.js +146 -0
  445. package/dist/workflows/attachment-math.d.ts +56 -0
  446. package/dist/workflows/attachment-math.js +47 -0
  447. package/dist/workflows/index.d.ts +3 -0
  448. package/dist/workflows/index.js +11 -0
  449. package/dist/workflows/maestro-signals.d.ts +217 -0
  450. package/dist/workflows/maestro-signals.js +155 -0
  451. package/dist/workflows/maestro.d.ts +3 -0
  452. package/dist/workflows/maestro.js +812 -0
  453. package/dist/workflows/scheduler-signals.d.ts +10 -0
  454. package/dist/workflows/scheduler-signals.js +14 -0
  455. package/dist/workflows/scheduler.d.ts +17 -0
  456. package/dist/workflows/scheduler.js +143 -0
  457. package/dist/workflows/session.d.ts +2 -0
  458. package/dist/workflows/session.js +1638 -0
  459. package/dist/workflows/signals.d.ts +297 -0
  460. package/dist/workflows/signals.js +239 -0
  461. package/examples/agents/tempo-composer.md +56 -0
  462. package/examples/agents/tempo-conductor.md +117 -0
  463. package/examples/agents/tempo-critic.md +73 -0
  464. package/examples/agents/tempo-improv.md +74 -0
  465. package/examples/agents/tempo-liner.md +75 -0
  466. package/examples/agents/tempo-roadie.md +61 -0
  467. package/examples/agents/tempo-soloist.md +71 -0
  468. package/examples/agents/tempo-tuner.md +94 -0
  469. package/examples/ensembles/tempo-big-band.yaml +146 -0
  470. package/examples/ensembles/tempo-dev-team.yaml +58 -0
  471. package/examples/ensembles/tempo-headless-jam.yaml +77 -0
  472. package/examples/ensembles/tempo-jam-session.yaml +41 -0
  473. package/examples/ensembles/tempo-mock-jam.yaml +79 -0
  474. package/examples/ensembles/tempo-review-squad.yaml +32 -0
  475. package/package.json +172 -0
  476. package/packaging/launchd/com.agent.tempo.plist +46 -0
  477. package/packaging/systemd/agent-tempo.service +32 -0
  478. package/packaging/windows/install-task.ps1 +71 -0
  479. package/scenarios/conductor-recruit-mock.yaml +33 -0
  480. package/scenarios/echo-roundtrip.yaml +15 -0
  481. package/scenarios/multi-player-handoff.yaml +38 -0
  482. package/scenarios/recruit-cascade.yaml +38 -0
  483. package/scenarios/two-player-conversation.yaml +33 -0
  484. package/workflow-bundle.js +14146 -0
@@ -0,0 +1,1270 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AdapterRegistry = exports.BaseAttachment = void 0;
4
+ exports.buildProcessTerminatingFrame = buildProcessTerminatingFrame;
5
+ exports.installProcessLifecycleTelemetry = installProcessLifecycleTelemetry;
6
+ exports._resetProcessLifecycleTelemetryForTest = _resetProcessLifecycleTelemetryForTest;
7
+ exports._liveAdaptersForTest = _liveAdaptersForTest;
8
+ const signals_1 = require("../workflows/signals");
9
+ const terminal_error_1 = require("./terminal-error");
10
+ const log = (...args) => console.error('[agent-tempo:adapter]', ...args);
11
+ // ── Hypothesis A telemetry (#258 follow-up) ─────────────────────────────
12
+ //
13
+ // The structured `terminal fire` log shipped in #258 made the next
14
+ // adapter-silence incident self-describing — but only for cases where
15
+ // `fireTerminal` actually fires. Hypothesis A (process death — crash, OOM,
16
+ // Windows sleep, terminal close, SIGKILL) wouldn't produce that log
17
+ // because the process never reached the code path. The handlers below
18
+ // close that gap: a future #258 recurrence with no `fireTerminal` log AND
19
+ // no `adapter-process-terminating` log narrows to a distinct hypothesis
20
+ // (likely SIGKILL / abrupt OS termination — file separately).
21
+ //
22
+ // Design tenets:
23
+ // - **Idempotent registration**: a module-level boolean ensures multiple
24
+ // adapter instances spawning in the same process never double-register
25
+ // handlers. Repeated `installProcessLifecycleTelemetry()` calls no-op.
26
+ // - **Additive only**: every `process.on(...)` call appends; nothing
27
+ // calls `removeAllListeners`. Coexists with the test-cleanup chain in
28
+ // `test/helpers.ts` (#312) and the daemon's own SIGTERM/SIGINT
29
+ // shutdown function.
30
+ // - **Synchronous logging on terminal signals**: process termination
31
+ // doesn't await async log flushes. `console.error` to stderr is
32
+ // synchronous on POSIX + Windows, which is enough.
33
+ // - **No behavior change on uncaughtException**: we register
34
+ // `uncaughtExceptionMonitor` (Node 13.7+) to telemeter without
35
+ // suppressing Node's default crash. If the runtime predates that
36
+ // event, we fall back to `uncaughtException` + `process.exit(1)`
37
+ // which preserves "don't swallow."
38
+ // - **Test gating**: mocha defines `it` globally (vitest with
39
+ // `globals: false` does not). Skip auto-install whenever the test
40
+ // framework signal is present so we don't fight the existing zombie
41
+ // reap in `test/helpers.ts`. The unit tests for these handlers spawn
42
+ // a dedicated child Node process where the gate doesn't fire.
43
+ /**
44
+ * Live adapters in this process. Populated by `startV2Lifecycle` after a
45
+ * successful claim; emptied on `stopV2Lifecycle` and `fireTerminal`.
46
+ * Each lifecycle handler iterates this set to build the per-adapter
47
+ * snapshot in the structured log.
48
+ */
49
+ const liveAdapters = new Set();
50
+ let processLifecycleTelemetryInstalled = false;
51
+ let processLifecycleHandlerRefs = [];
52
+ /**
53
+ * Should `installProcessLifecycleTelemetry()` actually wire up handlers?
54
+ *
55
+ * - Forced on by `AGENT_TEMPO_LIFECYCLE_TELEMETRY=1` (used by the
56
+ * child-process tests for these handlers — see
57
+ * `test/adapter-process-lifecycle-telemetry.test.ts`).
58
+ * - Forced off by `AGENT_TEMPO_LIFECYCLE_TELEMETRY=0`.
59
+ * - Off when running under mocha (detected via `globalThis.it` —
60
+ * mocha defines this; vitest with `globals: false` does not).
61
+ * - Off when `NODE_ENV === 'test'` — belt and suspenders.
62
+ * - Otherwise on.
63
+ */
64
+ function shouldInstallLifecycleTelemetry(force) {
65
+ if (force)
66
+ return true;
67
+ const flag = process.env.AGENT_TEMPO_LIFECYCLE_TELEMETRY;
68
+ if (flag === '1' || flag === 'true')
69
+ return true;
70
+ if (flag === '0' || flag === 'false')
71
+ return false;
72
+ // Mocha exposes BDD globals (`it`, `describe`, …) on the global object;
73
+ // our vitest config opts out of globals so it doesn't trigger this gate.
74
+ if (typeof globalThis.it === 'function')
75
+ return false;
76
+ if (process.env.NODE_ENV === 'test')
77
+ return false;
78
+ return true;
79
+ }
80
+ function snapshotLiveAdapters() {
81
+ const out = [];
82
+ for (const adapter of liveAdapters) {
83
+ out.push(adapter._captureTelemetrySnapshot());
84
+ }
85
+ return out;
86
+ }
87
+ /**
88
+ * Build the structured frame emitted by every lifecycle handler. Pure
89
+ * function — exposed for unit tests that don't want to spawn a child
90
+ * process.
91
+ */
92
+ function buildProcessTerminatingFrame(signal, errorMessage, snapshot = snapshotLiveAdapters()) {
93
+ return JSON.stringify({
94
+ event: 'adapter-process-terminating',
95
+ signal,
96
+ ...(errorMessage !== undefined ? { errorMessage } : {}),
97
+ adapterCount: snapshot.length,
98
+ adapters: snapshot,
99
+ });
100
+ }
101
+ function emitTerminatingLog(signal, errorMessage) {
102
+ // `console.error` synchronously writes to stderr on POSIX + Windows.
103
+ // The `[agent-tempo:adapter]` prefix matches the rest of the adapter
104
+ // logs so a single grep surfaces both the existing `terminal fire`
105
+ // line and these new lifecycle lines for the same incident.
106
+ log(`adapter-process-terminating: ${buildProcessTerminatingFrame(signal, errorMessage)}`);
107
+ }
108
+ /**
109
+ * Install the process-lifecycle telemetry handlers. Idempotent. Skipped
110
+ * by default in test environments (see {@link shouldInstallLifecycleTelemetry}).
111
+ *
112
+ * Production callers (and the first `startV2Lifecycle()` call on any
113
+ * adapter) invoke without arguments. Unit tests pass `{ force: true }`
114
+ * to bypass the env gate.
115
+ */
116
+ function installProcessLifecycleTelemetry(opts = {}) {
117
+ if (processLifecycleTelemetryInstalled)
118
+ return;
119
+ if (!shouldInstallLifecycleTelemetry(opts.force === true))
120
+ return;
121
+ processLifecycleTelemetryInstalled = true;
122
+ const handlers = [];
123
+ // `exit` — synchronous, last chance. Don't do async work; just log.
124
+ const onExit = () => emitTerminatingLog('exit');
125
+ process.on('exit', onExit);
126
+ handlers.push({ event: 'exit', handler: onExit });
127
+ // `beforeExit` — event loop is empty but Node hasn't exited yet.
128
+ const onBeforeExit = () => emitTerminatingLog('beforeExit');
129
+ process.on('beforeExit', onBeforeExit);
130
+ handlers.push({ event: 'beforeExit', handler: onBeforeExit });
131
+ // SIGTERM — graceful termination request (kill, supervisord, systemd).
132
+ const onSigterm = () => emitTerminatingLog('SIGTERM');
133
+ process.on('SIGTERM', onSigterm);
134
+ handlers.push({ event: 'SIGTERM', handler: onSigterm });
135
+ // SIGINT — Ctrl+C from a controlling terminal.
136
+ const onSigint = () => emitTerminatingLog('SIGINT');
137
+ process.on('SIGINT', onSigint);
138
+ handlers.push({ event: 'SIGINT', handler: onSigint });
139
+ // `uncaughtExceptionMonitor` lets us telemeter without suppressing
140
+ // Node's default crash behavior. The default action runs unchanged:
141
+ // print stack, exit non-zero. The codebase's `engines` requirement
142
+ // (Node 20+) guarantees this event is available — Node 13.7+.
143
+ const onUncaughtMonitor = (err) => {
144
+ emitTerminatingLog('uncaughtException', err instanceof Error ? err.message : String(err));
145
+ };
146
+ process.on('uncaughtExceptionMonitor', onUncaughtMonitor);
147
+ handlers.push({ event: 'uncaughtExceptionMonitor', handler: onUncaughtMonitor });
148
+ // `unhandledRejection` — log only. Adding a listener prevents Node's
149
+ // default crash on unhandled promise rejections (Node 15+); that's
150
+ // intentional per the brief ("log + don't crash").
151
+ const onUnhandled = (reason) => {
152
+ const msg = reason instanceof Error ? reason.message : String(reason);
153
+ emitTerminatingLog('unhandledRejection', msg);
154
+ };
155
+ process.on('unhandledRejection', onUnhandled);
156
+ handlers.push({ event: 'unhandledRejection', handler: onUnhandled });
157
+ processLifecycleHandlerRefs = handlers;
158
+ }
159
+ /** Test-only — uninstall handlers + reset state. */
160
+ function _resetProcessLifecycleTelemetryForTest() {
161
+ for (const { event, handler } of processLifecycleHandlerRefs) {
162
+ process.off(event, handler);
163
+ }
164
+ processLifecycleHandlerRefs = [];
165
+ processLifecycleTelemetryInstalled = false;
166
+ liveAdapters.clear();
167
+ }
168
+ /** Test-only — direct access to the live-adapter set. */
169
+ function _liveAdaptersForTest() {
170
+ return liveAdapters;
171
+ }
172
+ /** Backoff tuning for the heartbeat + phase-watcher loops on transient errors. */
173
+ const LOOP_BACKOFF_FACTOR = 1.5;
174
+ const LOOP_BACKOFF_MAX_MS = 30_000;
175
+ /**
176
+ * Emit a periodic `heartbeats-delivered=N` / `phase-ticks=N` summary every N
177
+ * successful ticks (#249). Chosen so a live claude-code adapter (60s cadence)
178
+ * logs roughly once every 10 minutes and an SDK adapter (30s cadence) once
179
+ * every 5 — rare enough to stay quiet, frequent enough that a 2-hour incident
180
+ * window always contains at least one breadcrumb.
181
+ */
182
+ const LOOP_SUMMARY_EVERY = 10;
183
+ /**
184
+ * Reconnect tuning (#201). The loop retries `claimAttachment` with exponential backoff
185
+ * bounded by a total elapsed-time budget. Elapsed-time bounds beat retry-count bounds
186
+ * because they map cleanly to user-facing mental models ("waits ~15 min then gives up")
187
+ * and don't drift when the backoff curve changes. 15 min catches the long tail of
188
+ * laptop-sleep events without leaving zombie pollers running forever.
189
+ */
190
+ const RECONNECT_TOTAL_BUDGET_MS = 15 * 60_000;
191
+ const RECONNECT_BASE_MS = 10_000;
192
+ const RECONNECT_MAX_MS = 60_000;
193
+ const RECONNECT_BACKOFF_FACTOR = 1.5;
194
+ /**
195
+ * #258: tiebreaker timeout for the `describe()` confirmation that gates
196
+ * `fireTerminal('destroy')` from the reconnect-loop pre-check. The Temporal
197
+ * SDK's per-call default is conservative (10s+); we'd rather conclude
198
+ * "describe is hung, treat as terminal" in 3s than freeze the reconnect
199
+ * loop on a slow visibility-API call.
200
+ */
201
+ const DESCRIBE_TIMEOUT_MS = 3_000;
202
+ /**
203
+ * Workflow execution statuses that are unambiguously terminal — used by
204
+ * the #258 `describe()` tiebreaker to decide whether a transient
205
+ * pre-check error reflects a genuinely-gone workflow (fire destroy) or a
206
+ * transient blip (continue the loop). Anything not in this set, including
207
+ * `RUNNING`, `PAUSED`, `UNSPECIFIED`, and `UNKNOWN`, is treated as
208
+ * non-terminal — conservatively keeps the loop alive when classification
209
+ * is ambiguous.
210
+ */
211
+ const TERMINAL_WORKFLOW_STATUSES = new Set([
212
+ 'COMPLETED',
213
+ 'FAILED',
214
+ 'CANCELLED',
215
+ 'TERMINATED',
216
+ 'CONTINUED_AS_NEW',
217
+ 'TIMED_OUT',
218
+ ]);
219
+ /**
220
+ * Abstract base class for session adapters.
221
+ *
222
+ * Concrete adapters (`InteractiveAttachment`, `CopilotSdkAttachment`) own
223
+ * their own top-level delivery loop. The base class owns the V2 attachment
224
+ * lifecycle: claim, heartbeat at `descriptor.heartbeatMs`, phase-watcher
225
+ * loop, `WorkflowGone` classifier, graceful `adapterExited` on teardown.
226
+ * Subclasses must call `startV2Lifecycle()` before their delivery loop and
227
+ * `stopV2Lifecycle()` on shutdown.
228
+ *
229
+ * PR-H (#132): the `AGENT_TEMPO_LIFECYCLE_V2` flag and the legacy V1 poll-
230
+ * only path it gated have been removed. The V2 attachment-lease path is
231
+ * now the only path.
232
+ */
233
+ class BaseAttachment {
234
+ /** Populated at construction for InteractiveAttachment; lazily via `configureV2()` for subprocess adapters (Copilot bridge). */
235
+ client;
236
+ host;
237
+ /** V2 state — populated by `startV2Lifecycle()`, null on legacy path. */
238
+ token = null;
239
+ /** Handle pinned to the runId returned by `claimAttachment`. Never resolve by ID alone (§6.3). */
240
+ pinnedHandle = null;
241
+ heartbeatTimer = null;
242
+ phaseWatcherTimer = null;
243
+ heartbeatBackoff = 0;
244
+ phaseBackoff = 0;
245
+ stopped = false;
246
+ terminalFired = false;
247
+ knownPhase = null;
248
+ /**
249
+ * `true` once a heartbeat has successfully landed on the current attachment (or rebind).
250
+ * Cleared on `startV2Lifecycle`, reconnect-loop success, and CAN rebind so each freshly
251
+ * live attachment emits its own `heartbeat#1 delivered` diagnostic. Added in #249 to
252
+ * distinguish "claim OK but heartbeat loop died" from "adapter just hasn't ticked yet."
253
+ */
254
+ firstHeartbeatLogged = false;
255
+ /**
256
+ * Monotonic heartbeat counter for the current attachment cycle. Reset on
257
+ * claim/reconnect/CAN-rebind. Emitted periodically (every {@link LOOP_SUMMARY_EVERY}
258
+ * ticks) so a long-running session leaves breadcrumbs in the log proving the loop is
259
+ * alive — operators can `grep 'heartbeats-delivered='` to confirm health without
260
+ * parsing Temporal history. Added in #249.
261
+ */
262
+ heartbeatsSent = 0;
263
+ /**
264
+ * Mirror of {@link heartbeatsSent} for the phase-watcher loop. Same emission cadence,
265
+ * same rationale — the watcher is the only self-heal surface when the heartbeat loop
266
+ * dies silently, so a summary log line proves it's still live too.
267
+ */
268
+ phaseTicksDone = 0;
269
+ phaseChangeListeners = [];
270
+ leaseRevokedListeners = [];
271
+ terminalListeners = [];
272
+ /**
273
+ * Pending `abortableSleep` cancellers (#201). `stopV2Lifecycle` iterates and invokes
274
+ * each so any in-flight reconnect backoff rejects immediately and the loop unwinds
275
+ * instead of stalling teardown by up to `RECONNECT_MAX_MS`.
276
+ */
277
+ sleepAborters = new Set();
278
+ /**
279
+ * `true` while `runReconnectLoop` is active. Prevents concurrent reconnect attempts
280
+ * (e.g. if both the heartbeat and phase-watcher loops observe the same lease expiry
281
+ * at nearly the same time) and gates heartbeat/watcher ticks from firing new terminals
282
+ * while the reconnect pre-check is still deciding.
283
+ */
284
+ reconnecting = false;
285
+ /** Reconnect loop timing — production constants unless overridden for tests. */
286
+ reconnectBaseMs;
287
+ reconnectMaxMs;
288
+ reconnectBudgetMs;
289
+ reconnectBackoffFactor;
290
+ constructor(options = {}) {
291
+ this.client = options.client;
292
+ this.host = options.host;
293
+ const t = options.reconnectTiming ?? {};
294
+ this.reconnectBaseMs = t.baseMs ?? RECONNECT_BASE_MS;
295
+ this.reconnectMaxMs = t.maxMs ?? RECONNECT_MAX_MS;
296
+ this.reconnectBudgetMs = t.budgetMs ?? RECONNECT_TOTAL_BUDGET_MS;
297
+ this.reconnectBackoffFactor = t.backoffFactor ?? RECONNECT_BACKOFF_FACTOR;
298
+ }
299
+ /**
300
+ * Lazily populate the V2-path dependencies (Temporal client, host). Used by
301
+ * adapters whose subprocess constructs the client inside `run()` rather
302
+ * than receiving it from the outer process (Copilot bridge). Must be called
303
+ * BEFORE `startV2Lifecycle()`.
304
+ *
305
+ * C3 (PR-C dual-QA follow-up): rejects late reconfiguration — once a claim
306
+ * token has been issued, swapping the client out silently would leave the
307
+ * pinned handle pointing at the previous connection. Future adapters that
308
+ * mis-order the calls fail loudly instead of drifting.
309
+ */
310
+ configureV2(client, host) {
311
+ if (this.token) {
312
+ throw new Error('configureV2() called after startV2Lifecycle; configuration must happen before claim');
313
+ }
314
+ this.client = client;
315
+ this.host = host;
316
+ }
317
+ /** Subscribe to `attachmentInfo.phase` changes observed by the watcher. */
318
+ onPhaseChange(listener) {
319
+ this.phaseChangeListeners.push(listener);
320
+ return () => {
321
+ const i = this.phaseChangeListeners.indexOf(listener);
322
+ if (i >= 0)
323
+ this.phaseChangeListeners.splice(i, 1);
324
+ };
325
+ }
326
+ /** Subscribe to lease-revocation events (§9.3 split-brain resolution). */
327
+ onLeaseRevoked(listener) {
328
+ this.leaseRevokedListeners.push(listener);
329
+ return () => {
330
+ const i = this.leaseRevokedListeners.indexOf(listener);
331
+ if (i >= 0)
332
+ this.leaseRevokedListeners.splice(i, 1);
333
+ };
334
+ }
335
+ /**
336
+ * Hypothesis A telemetry — capture the adapter state included in
337
+ * process-lifecycle log frames. Public so the module-level
338
+ * `snapshotLiveAdapters()` helper can read private fields without an
339
+ * `any` cast; consumers other than the telemetry path should not call it.
340
+ */
341
+ _captureTelemetrySnapshot() {
342
+ return {
343
+ attachmentId: this.token?.attachmentId ?? null,
344
+ workflowId: this.pinnedHandle?.workflowId ?? null,
345
+ runId: this.token?.runId ?? null,
346
+ heartbeatsSent: this.heartbeatsSent,
347
+ phaseTicksDone: this.phaseTicksDone,
348
+ };
349
+ }
350
+ /**
351
+ * Subscribe to terminal events — `WorkflowNotFound` (§9.4) and phase `gone`.
352
+ * Terminal fires at most once per instance. Subclasses stop delivery + exit.
353
+ */
354
+ onTerminal(listener) {
355
+ this.terminalListeners.push(listener);
356
+ return () => {
357
+ const i = this.terminalListeners.indexOf(listener);
358
+ if (i >= 0)
359
+ this.terminalListeners.splice(i, 1);
360
+ };
361
+ }
362
+ /**
363
+ * V2 lifecycle entry point. Claims (or renews) the attachment, pins the handle by runId,
364
+ * and starts the heartbeat + phase watcher loops.
365
+ *
366
+ * @param workflowId Target session workflow id.
367
+ * @param expectedAttachmentId
368
+ * PR-D renewal path. When present, the adapter was spawned by `restart` or `migrate`
369
+ * — the workflow has already created an `Attachment` with this id and is
370
+ * expecting the new adapter to take over. Passing it through to `claimAttachment`
371
+ * selects the renewal branch in §9.2 (refresh lease in place, idempotent on retry)
372
+ * instead of the fresh-claim branch. Fresh spawn (first recruit) omits this arg.
373
+ * @returns Pinned `WorkflowHandle` — subclass delivery loop MUST use this for every
374
+ * subsequent query/signal (never resolve by id alone).
375
+ * @throws Re-throws `claimAttachment` rejections (`AttachmentConflict`, `WorkflowGone`).
376
+ */
377
+ async startV2Lifecycle(workflowId, expectedAttachmentId) {
378
+ if (!this.client) {
379
+ throw new Error('BaseAttachment V2 path requires a Temporal client — pass via constructor options');
380
+ }
381
+ if (!this.host) {
382
+ throw new Error('BaseAttachment V2 path requires a host — pass via constructor options');
383
+ }
384
+ const unpinned = this.client.workflow.getHandle(workflowId);
385
+ this.token = await unpinned.executeUpdate(signals_1.claimAttachmentUpdate, {
386
+ args: [{
387
+ host: this.host,
388
+ adapterId: this.descriptor.adapterId,
389
+ adapterClass: this.descriptor.adapterClass,
390
+ leaseMs: 3 * this.descriptor.heartbeatMs,
391
+ ...(expectedAttachmentId ? { expectedAttachmentId } : {}),
392
+ }],
393
+ });
394
+ this.pinnedHandle = this.client.workflow.getHandle(workflowId, this.token.runId);
395
+ // Hypothesis A telemetry — register this adapter so a future process-
396
+ // lifecycle handler (exit / SIGTERM / uncaughtException / …) can
397
+ // include its state in the structured log. `installProcessLifecycleTelemetry`
398
+ // is idempotent + env-gated; first call wires the handlers, subsequent
399
+ // calls no-op.
400
+ liveAdapters.add(this);
401
+ installProcessLifecycleTelemetry();
402
+ // #249: reset the per-attachment diagnostic counters so the next tick emits
403
+ // `heartbeat#1 delivered` on the freshly live lease. Without this reset a
404
+ // renewal path (e.g. restart → renewed claim) would never re-log first-heartbeat.
405
+ this.firstHeartbeatLogged = false;
406
+ this.heartbeatsSent = 0;
407
+ this.phaseTicksDone = 0;
408
+ log(`${expectedAttachmentId ? 'renewed' : 'attached to'} ${workflowId} ` +
409
+ `(attachmentId=${this.token.attachmentId}, runId=${this.token.runId}); ` +
410
+ `first heartbeat scheduled in ${this.descriptor.heartbeatMs}ms`);
411
+ this.scheduleHeartbeat();
412
+ this.schedulePhaseWatcher();
413
+ return this.pinnedHandle;
414
+ }
415
+ /**
416
+ * Tear down V2 machinery. Idempotent. Called by subclass on stop, on terminal
417
+ * events, and on graceful detach completion.
418
+ *
419
+ * When `graceful=true` (detach owner) we fire `adapterExited` so the workflow
420
+ * collapses `draining → detached` immediately per §11.1.
421
+ */
422
+ async stopV2Lifecycle(reason = 'user-stop', graceful = false) {
423
+ if (this.stopped)
424
+ return;
425
+ this.stopped = true;
426
+ // Hypothesis A telemetry — keep `liveAdapters` accurate so a subsequent
427
+ // process-lifecycle handler firing after stop doesn't include a
428
+ // already-torn-down adapter in its frame.
429
+ liveAdapters.delete(this);
430
+ if (this.heartbeatTimer) {
431
+ clearTimeout(this.heartbeatTimer);
432
+ this.heartbeatTimer = null;
433
+ }
434
+ if (this.phaseWatcherTimer) {
435
+ clearTimeout(this.phaseWatcherTimer);
436
+ this.phaseWatcherTimer = null;
437
+ }
438
+ // #201: a user-initiated stop must abort any in-flight reconnect backoff
439
+ // BEFORE awaiting `adapterExited`, otherwise teardown stalls up to
440
+ // `RECONNECT_MAX_MS` while the sleep timer runs out naturally.
441
+ this.abortSleepers();
442
+ if (graceful && this.pinnedHandle && this.token) {
443
+ try {
444
+ await this.pinnedHandle.signal(signals_1.adapterExitedSignal, {
445
+ attachmentId: this.token.attachmentId,
446
+ reason,
447
+ });
448
+ }
449
+ catch (err) {
450
+ // Best-effort — workflow may already have reaped us. Don't fail shutdown.
451
+ log(`adapterExited signal suppressed error: ${err?.message ?? err}`);
452
+ }
453
+ }
454
+ }
455
+ scheduleHeartbeat() {
456
+ const delay = this.heartbeatBackoff || this.descriptor.heartbeatMs;
457
+ this.heartbeatTimer = setTimeout(() => { void this.tickHeartbeat(); }, delay);
458
+ }
459
+ /**
460
+ * Emit a loud diagnostic when a tick early-returns via one of its guard paths (#249).
461
+ * Pre-#249 these returns were silent — the only observable effect was "heartbeats stop
462
+ * arriving." Now operators can grep `adapter.*guard tripped` to confirm or rule out
463
+ * tick-orphan as a failure mode without needing workflow history.
464
+ *
465
+ * `terminalFired=true` / `stopped=true` guards are load-bearing on the terminal path
466
+ * (don't want to re-enter terminal) so they're expected during teardown; we still log
467
+ * them but at the same level — operators can correlate timestamps against the preceding
468
+ * `terminal (...) — stopping delivery poll permanently` line.
469
+ */
470
+ logGuardTrip(loop) {
471
+ log(`${loop} guard tripped:`, JSON.stringify({
472
+ stopped: this.stopped,
473
+ reconnecting: this.reconnecting,
474
+ hasHandle: this.pinnedHandle !== null,
475
+ hasToken: this.token !== null,
476
+ terminalFired: this.terminalFired,
477
+ }));
478
+ }
479
+ /**
480
+ * Single tick of the heartbeat loop. Try/finally scaffolding (#249) guarantees
481
+ * reschedule in every path except genuinely terminal state (`stopped`,
482
+ * `terminalFired`) or when the reconnect loop has taken ownership of scheduling
483
+ * (`reconnecting`). Pre-#249 the three early-return paths at the top + the
484
+ * handled-terminal-error path silently orphaned the timer forever; a transient
485
+ * `reconnecting=true` window or a null-handle race was enough to kill the loop
486
+ * with no log and no teardown.
487
+ *
488
+ * Handled terminals (CAN rebind, destroy) still short-circuit via `return` —
489
+ * the `finally` block re-checks `reconnecting` / `terminalFired` before
490
+ * rescheduling, so the reconnect/terminal machinery keeps ownership of
491
+ * whatever comes next.
492
+ */
493
+ async tickHeartbeat() {
494
+ try {
495
+ if (this.stopped || this.terminalFired) {
496
+ this.logGuardTrip('heartbeat');
497
+ return;
498
+ }
499
+ if (this.reconnecting) {
500
+ // Reconnect loop owns reschedule; this tick was queued before the guard
501
+ // flipped. Dropping it is correct — the reconnect path will rearm.
502
+ this.logGuardTrip('heartbeat');
503
+ return;
504
+ }
505
+ if (!this.pinnedHandle || !this.token) {
506
+ // Should be unreachable after `startV2Lifecycle` success — surface loudly
507
+ // if we ever hit it instead of silently orphaning (the pre-#249 behavior).
508
+ this.logGuardTrip('heartbeat');
509
+ return;
510
+ }
511
+ try {
512
+ await this.pinnedHandle.signal(signals_1.heartbeatSignal, {
513
+ attachmentId: this.token.attachmentId,
514
+ at: new Date().toISOString(),
515
+ });
516
+ this.heartbeatBackoff = 0;
517
+ this.heartbeatsSent++;
518
+ if (!this.firstHeartbeatLogged) {
519
+ this.firstHeartbeatLogged = true;
520
+ log(`heartbeat#1 delivered (attachmentId=${this.token.attachmentId}, runId=${this.token.runId})`);
521
+ }
522
+ else if (this.heartbeatsSent % LOOP_SUMMARY_EVERY === 0) {
523
+ log(`heartbeats-delivered=${this.heartbeatsSent} (attachmentId=${this.token.attachmentId})`);
524
+ }
525
+ }
526
+ catch (err) {
527
+ if (await this.handleRunEndError(err))
528
+ return;
529
+ this.heartbeatBackoff = Math.min(this.heartbeatBackoff ? this.heartbeatBackoff * LOOP_BACKOFF_FACTOR : this.descriptor.heartbeatMs, LOOP_BACKOFF_MAX_MS);
530
+ log(`heartbeat transient error (retry in ${Math.round(this.heartbeatBackoff)}ms):`, err?.message ?? err);
531
+ }
532
+ }
533
+ finally {
534
+ if (!this.stopped && !this.reconnecting && !this.terminalFired) {
535
+ this.scheduleHeartbeat();
536
+ }
537
+ }
538
+ }
539
+ schedulePhaseWatcher() {
540
+ // §3.2 item 6: relaxed poll — once per 5 heartbeat intervals.
541
+ const base = this.descriptor.heartbeatMs * 5;
542
+ const delay = this.phaseBackoff || base;
543
+ this.phaseWatcherTimer = setTimeout(() => { void this.tickPhaseWatcher(); }, delay);
544
+ }
545
+ /**
546
+ * Single tick of the phase-watcher loop. Same orphan-resistance scaffolding as
547
+ * {@link tickHeartbeat} (#249): try/finally reschedule, unconditional unless
548
+ * `stopped` / `terminalFired` / `reconnecting`. When the heartbeat loop dies
549
+ * silently, the watcher is the only remaining self-heal surface — losing it
550
+ * too meant the adapter had no path back to a healthy state short of process
551
+ * restart.
552
+ */
553
+ async tickPhaseWatcher() {
554
+ try {
555
+ if (this.stopped || this.terminalFired) {
556
+ this.logGuardTrip('phase-watcher');
557
+ return;
558
+ }
559
+ if (this.reconnecting) {
560
+ this.logGuardTrip('phase-watcher');
561
+ return;
562
+ }
563
+ if (!this.pinnedHandle || !this.token) {
564
+ this.logGuardTrip('phase-watcher');
565
+ return;
566
+ }
567
+ try {
568
+ const info = await this.pinnedHandle.query(signals_1.attachmentInfoQuery);
569
+ this.phaseBackoff = 0;
570
+ this.phaseTicksDone++;
571
+ if (this.phaseTicksDone % LOOP_SUMMARY_EVERY === 0) {
572
+ log(`phase-ticks=${this.phaseTicksDone} (phase=${info.phase}, attachmentId=${this.token.attachmentId})`);
573
+ }
574
+ // #249: if the workflow-side attachment record shows our last heartbeat landed
575
+ // more than 2 * heartbeatMs ago, the heartbeat loop is drifting (or has
576
+ // silently died) even though the lease hasn't yet expired. Loud warning so
577
+ // operators can catch degradation before the reaper fires. Baseline is
578
+ // `claimedAt` on cycles before the first post-claim heartbeat lands.
579
+ if (info.currentAttachment && info.currentAttachment.attachmentId === this.token.attachmentId) {
580
+ const lastBeatMs = new Date(info.currentAttachment.lastHeartbeatAt || info.currentAttachment.claimedAt).getTime();
581
+ const ageMs = Date.now() - lastBeatMs;
582
+ if (ageMs > 2 * this.descriptor.heartbeatMs) {
583
+ log(`WARNING: heartbeat staleness — lastHeartbeatAt=${info.currentAttachment.lastHeartbeatAt} ` +
584
+ `age=${ageMs}ms exceeds 2× heartbeatMs (${2 * this.descriptor.heartbeatMs}ms); ` +
585
+ `lease may be about to reap (expiresAt=${info.currentAttachment.expiresAt})`);
586
+ }
587
+ }
588
+ if (this.knownPhase !== info.phase) {
589
+ this.knownPhase = info.phase;
590
+ for (const l of this.phaseChangeListeners) {
591
+ try {
592
+ l(info.phase);
593
+ }
594
+ catch (err) {
595
+ log('phase listener threw:', err);
596
+ }
597
+ }
598
+ }
599
+ // Lease revocation (§9.3) — another claimant took over.
600
+ if (info.currentAttachment &&
601
+ info.currentAttachment.attachmentId !== this.token.attachmentId) {
602
+ log(`lease revoked: attachmentId ${info.currentAttachment.attachmentId} does not match ours ${this.token.attachmentId}`);
603
+ for (const l of this.leaseRevokedListeners) {
604
+ try {
605
+ l('superseded');
606
+ }
607
+ catch (err) {
608
+ log('leaseRevoked listener threw:', err);
609
+ }
610
+ }
611
+ this.fireTerminalOrReconnect('superseded');
612
+ return;
613
+ }
614
+ // #201: the workflow side reaped our lease (main-loop §9.5.a) without anyone
615
+ // else claiming. This is the laptop-sleep failure mode — `phase=detached` with
616
+ // `currentAttachment=undefined`. Before #201 this branch was silent and the
617
+ // poller kept querying a workflow that had already evicted us, so no cues were
618
+ // delivered until manual `restart`. Now we surface it as a recoverable terminal
619
+ // that the subclass can choose to reconnect through.
620
+ if (info.phase === 'detached' && !info.currentAttachment) {
621
+ log(`lease reaped workflow-side (phase=detached, no current attachment)`);
622
+ for (const l of this.leaseRevokedListeners) {
623
+ try {
624
+ l('heartbeat-timeout');
625
+ }
626
+ catch (err) {
627
+ log('leaseRevoked listener threw:', err);
628
+ }
629
+ }
630
+ this.fireTerminalOrReconnect('heartbeat-timeout');
631
+ return;
632
+ }
633
+ // Phase `gone` is terminal — workflow destroyed. Never recoverable.
634
+ if (info.phase === 'gone') {
635
+ this.fireTerminal('destroy', 'tickPhaseWatcher:phase-gone');
636
+ return;
637
+ }
638
+ }
639
+ catch (err) {
640
+ if (await this.handleRunEndError(err))
641
+ return;
642
+ this.phaseBackoff = Math.min(this.phaseBackoff ? this.phaseBackoff * LOOP_BACKOFF_FACTOR : this.descriptor.heartbeatMs, LOOP_BACKOFF_MAX_MS);
643
+ log(`phase watcher transient error (retry in ${Math.round(this.phaseBackoff)}ms):`, err?.message ?? err);
644
+ }
645
+ }
646
+ finally {
647
+ if (!this.stopped && !this.reconnecting && !this.terminalFired) {
648
+ this.schedulePhaseWatcher();
649
+ }
650
+ }
651
+ }
652
+ /**
653
+ * Shared error-classification path for the heartbeat + phase-watcher ticks (#226).
654
+ *
655
+ * Returns `true` if the error was a terminal-class (handled inline: CAN rebind
656
+ * kicked off, or destroy fired). Returns `false` when the caller should treat
657
+ * the error as transient and continue its backoff.
658
+ *
659
+ * Always consults `fetchHistory` on any terminal-class error, because the
660
+ * Temporal SDK can't distinguish CAN-close from true-complete at the error
661
+ * level — see {@link isTerminalWorkflowError}. The history lookup is cheap
662
+ * (only runs on terminal, so at most once per adapter lifetime per terminal)
663
+ * and safer than re-querying by workflow id (which could race a fresh session
664
+ * reusing the id).
665
+ */
666
+ async handleRunEndError(err) {
667
+ if (!(0, terminal_error_1.isTerminalWorkflowError)(err))
668
+ return false;
669
+ // Always try to find a CAN successor — the Temporal SDK's error shape is
670
+ // ambiguous between CAN and true-destroy, so history is the only reliable
671
+ // disambiguator (option 1 from the #226 design brief).
672
+ const successorRunId = await this.findCanSuccessorRunId();
673
+ if (successorRunId) {
674
+ this.fireTerminalOrReconnect('continued-as-new', successorRunId);
675
+ return true;
676
+ }
677
+ // No CAN event in the closed run's history → truly terminal (COMPLETED /
678
+ // TERMINATED / FAILED / workflow-id GC'd).
679
+ this.fireTerminal('destroy', 'handleRunEndError:no-can-successor');
680
+ return true;
681
+ }
682
+ /**
683
+ * Fetch the closed pinned run's history and return the runId of a CAN successor
684
+ * if present, else `null`. Scoped to the pinned (old) run via `this.pinnedHandle`,
685
+ * so it can't be fooled by a fresh session that happens to reuse the workflow id.
686
+ *
687
+ * Called only on the terminal path from {@link handleRunEndError}, so the cost
688
+ * of `fetchHistory` (a full event stream for the closed run) is paid at most
689
+ * once per terminal — not on every tick.
690
+ */
691
+ async findCanSuccessorRunId() {
692
+ if (!this.pinnedHandle)
693
+ return null;
694
+ try {
695
+ const history = await this.pinnedHandle.fetchHistory();
696
+ const events = history?.events ?? [];
697
+ for (const ev of events) {
698
+ const attrs = ev.workflowExecutionContinuedAsNewEventAttributes;
699
+ const newRunId = attrs?.newExecutionRunId;
700
+ if (newRunId)
701
+ return newRunId;
702
+ }
703
+ return null;
704
+ }
705
+ catch (err) {
706
+ log('findCanSuccessorRunId: fetchHistory failed:', err?.message ?? err);
707
+ return null;
708
+ }
709
+ }
710
+ /**
711
+ * Fire the terminal hook — the adapter is going dark and won't recover.
712
+ *
713
+ * #258: emits a structured log line on every fire so the next post-CAN
714
+ * silence incident is unambiguous in logs. Pre-#258, a `fireTerminal`
715
+ * from an unexpected source (the root cause was a silent destroy from
716
+ * the reconnect-loop pre-check on a transient terminal-class error) was
717
+ * indistinguishable from process death in workflow history — both produced
718
+ * "no further heartbeats." The structured log includes:
719
+ *
720
+ * - `reason` — the existing DetachReason
721
+ * - `callsite` — the calling function or rationale (passed by every
722
+ * callsite so the source is grep-able without parsing stack traces)
723
+ * - `attachmentId` / `workflowId` / `runId` — for cross-referencing
724
+ * against workflow history when bisecting an incident
725
+ * - `heartbeatsSent` / `phaseTicksDone` — the existing #249 counters
726
+ * so an operator can correlate "loop alive at N heartbeats, then
727
+ * terminal fired at this callsite" without external context
728
+ *
729
+ * Idempotent — repeat calls (e.g. reconnect-exhausted re-fires after
730
+ * destroy) early-return without re-logging. The first fire wins.
731
+ */
732
+ fireTerminal(reason, callsite = 'unspecified') {
733
+ if (this.terminalFired)
734
+ return;
735
+ this.terminalFired = true;
736
+ this.stopped = true;
737
+ // Hypothesis A telemetry — same reasoning as `stopV2Lifecycle`.
738
+ liveAdapters.delete(this);
739
+ log(`terminal fire:`, JSON.stringify({
740
+ reason,
741
+ callsite,
742
+ attachmentId: this.token?.attachmentId ?? null,
743
+ workflowId: this.pinnedHandle?.workflowId ?? null,
744
+ runId: this.token?.runId ?? null,
745
+ heartbeatsSent: this.heartbeatsSent,
746
+ phaseTicksDone: this.phaseTicksDone,
747
+ }));
748
+ if (this.heartbeatTimer) {
749
+ clearTimeout(this.heartbeatTimer);
750
+ this.heartbeatTimer = null;
751
+ }
752
+ if (this.phaseWatcherTimer) {
753
+ clearTimeout(this.phaseWatcherTimer);
754
+ this.phaseWatcherTimer = null;
755
+ }
756
+ this.abortSleepers();
757
+ for (const l of this.terminalListeners) {
758
+ try {
759
+ l(reason);
760
+ }
761
+ catch (err) {
762
+ log('terminal listener threw:', err);
763
+ }
764
+ }
765
+ }
766
+ /**
767
+ * #258 tiebreaker: confirm whether a workflow is genuinely terminal after
768
+ * the reconnect-loop pre-check threw a terminal-class error. Used to
769
+ * distinguish a real workflow-gone state from a transient gRPC /
770
+ * visibility-API blip that classified as terminal.
771
+ *
772
+ * Returns:
773
+ * - `{ kind: 'running', statusName }` — workflow is alive (any
774
+ * non-terminal status). Caller should treat the original error as
775
+ * transient and continue the reconnect loop.
776
+ * - `{ kind: 'terminal', statusName }` — workflow is in a terminal
777
+ * status (`COMPLETED` / `FAILED` / `CANCELLED` / `TERMINATED` /
778
+ * `CONTINUED_AS_NEW` / `TIMED_OUT`). Caller should fire destroy.
779
+ * - `{ kind: 'describe-threw' }` — `describe()` itself failed. Treat
780
+ * as terminal (fire destroy) — consistent with pre-#258 semantics
781
+ * when classification is ambiguous, and avoids spinning forever on
782
+ * a workflow we can't reach.
783
+ * - `{ kind: 'timed-out' }` — `describe()` exceeded
784
+ * {@link DESCRIBE_TIMEOUT_MS}. Treat as terminal (fire destroy) —
785
+ * same rationale: prefer clean shutdown to a hung loop.
786
+ *
787
+ * The unpinned handle follows any CAN chain to the latest run, so
788
+ * `desc.status.name === 'CONTINUED_AS_NEW'` here means the workflow
789
+ * id itself is closed (no successor) — genuinely terminal.
790
+ */
791
+ async confirmWorkflowTerminal(unpinned) {
792
+ let timer = null;
793
+ try {
794
+ const desc = await Promise.race([
795
+ unpinned.describe(),
796
+ new Promise((resolve) => {
797
+ timer = setTimeout(() => resolve('timeout'), DESCRIBE_TIMEOUT_MS);
798
+ }),
799
+ ]);
800
+ if (desc === 'timeout')
801
+ return { kind: 'timed-out' };
802
+ const statusName = desc.status?.name ?? 'UNKNOWN';
803
+ if (TERMINAL_WORKFLOW_STATUSES.has(statusName)) {
804
+ return { kind: 'terminal', statusName };
805
+ }
806
+ return { kind: 'running', statusName };
807
+ }
808
+ catch {
809
+ return { kind: 'describe-threw' };
810
+ }
811
+ finally {
812
+ if (timer)
813
+ clearTimeout(timer);
814
+ }
815
+ }
816
+ // ───────────────────────────────────────────────────────────────────────────
817
+ // #201 reconnect machinery. Subclasses opt in by overriding `shouldReconnect`.
818
+ // ───────────────────────────────────────────────────────────────────────────
819
+ /**
820
+ * Opt-in reconnect policy. Default: return `false` — the base class behaves
821
+ * exactly as it did before #201 (fire terminal, tear down). Subclasses that
822
+ * can safely replay delivery on a fresh lease should override and return
823
+ * `true` for recoverable reasons (typically `heartbeat-timeout` and
824
+ * `superseded`; never `destroy`).
825
+ *
826
+ * Why opt-in: SDK adapters (e.g. Copilot bridge) have their own subprocess
827
+ * restart logic; double-reconnecting would race their native poller and
828
+ * produce duplicate `pendingMessages` queries. Keep them on the old
829
+ * behavior until we've proven reconnect is safe there.
830
+ */
831
+ shouldReconnect(_reason) {
832
+ return false;
833
+ }
834
+ /**
835
+ * Called once, just before the reconnect loop enters its first backoff sleep.
836
+ * Subclasses should tear down any delivery loops that are still polling the
837
+ * stale pinned handle (it may succeed but `markDelivered` will be ignored by
838
+ * the workflow because our `attachmentId` is no longer current). The default
839
+ * is a no-op.
840
+ */
841
+ async onReconnectStart(_reason) {
842
+ // Default: nothing to tear down.
843
+ }
844
+ /**
845
+ * Called once on a successful re-claim, with the freshly pinned handle.
846
+ * Subclasses should restart their delivery loop against `handle`. Runs
847
+ * before the base class reschedules its own heartbeat + phase-watcher
848
+ * loops, so the subclass sees a quiescent state.
849
+ *
850
+ * Note: the runId returned by `claimAttachment` may differ from the previous
851
+ * pinned handle's runId (the workflow may have `continueAsNew`'d during the
852
+ * outage), so subclasses MUST use the `handle` argument — never cache a
853
+ * handle from before the reconnect.
854
+ */
855
+ async onReconnected(_handle) {
856
+ // Default: nothing to restart.
857
+ }
858
+ /**
859
+ * Sleep `ms` milliseconds, resolving cleanly on timer and rejecting with
860
+ * `aborted:stopped` if `stopV2Lifecycle` or `fireTerminal` fires mid-wait.
861
+ * The canonical pattern for any blocking wait inside an adapter loop —
862
+ * never use bare `setTimeout` + `Promise` in loop code, or teardown stalls.
863
+ */
864
+ async abortableSleep(ms) {
865
+ if (this.stopped)
866
+ throw new Error('aborted:stopped');
867
+ await new Promise((resolve, reject) => {
868
+ let aborter = null;
869
+ const timer = setTimeout(() => {
870
+ if (aborter)
871
+ this.sleepAborters.delete(aborter);
872
+ resolve();
873
+ }, ms);
874
+ aborter = () => {
875
+ clearTimeout(timer);
876
+ if (aborter)
877
+ this.sleepAborters.delete(aborter);
878
+ reject(new Error('aborted:stopped'));
879
+ };
880
+ this.sleepAborters.add(aborter);
881
+ });
882
+ }
883
+ /** Reject every in-flight `abortableSleep`. Called on stop + terminal. */
884
+ abortSleepers() {
885
+ // Snapshot then clear — each aborter mutates the set during iteration.
886
+ const aborters = [...this.sleepAborters];
887
+ this.sleepAborters.clear();
888
+ for (const abort of aborters) {
889
+ try {
890
+ abort();
891
+ }
892
+ catch (err) {
893
+ log('sleep aborter threw:', err);
894
+ }
895
+ }
896
+ }
897
+ /**
898
+ * Consult {@link shouldReconnect}; if true, kick off the reconnect loop in
899
+ * the background (fire-and-forget), otherwise fire terminal synchronously.
900
+ * Called by the heartbeat / phase-watcher ticks instead of `fireTerminal`
901
+ * when the reason is potentially recoverable.
902
+ */
903
+ fireTerminalOrReconnect(reason, canSuccessorRunId) {
904
+ if (this.stopped || this.terminalFired || this.reconnecting)
905
+ return;
906
+ if (!this.shouldReconnect(reason)) {
907
+ this.fireTerminal(reason, 'fireTerminalOrReconnect:not-recoverable');
908
+ return;
909
+ }
910
+ // Pause the heartbeat + watcher loops for the duration of the reconnect.
911
+ this.reconnecting = true;
912
+ if (this.heartbeatTimer) {
913
+ clearTimeout(this.heartbeatTimer);
914
+ this.heartbeatTimer = null;
915
+ }
916
+ if (this.phaseWatcherTimer) {
917
+ clearTimeout(this.phaseWatcherTimer);
918
+ this.phaseWatcherTimer = null;
919
+ }
920
+ log(`reconnect requested (reason=${reason})`);
921
+ // #226: CAN takes the short-circuit rebind path (no backoff, no re-claim —
922
+ // the workflow's §2.3 CAN-boundary lease extension keeps the lease alive
923
+ // across the transition). Every other recoverable reason goes through the
924
+ // full #201 budget-bounded re-claim loop.
925
+ if (reason === 'continued-as-new' && canSuccessorRunId) {
926
+ void this.runCanRebind(canSuccessorRunId).catch((err) => {
927
+ log(`CAN rebind crashed:`, err?.message ?? err);
928
+ this.reconnecting = false;
929
+ this.fireTerminal('reconnect-exhausted', 'runCanRebind:crashed');
930
+ });
931
+ return;
932
+ }
933
+ void this.runReconnectLoop(reason).catch((err) => {
934
+ log(`reconnect loop crashed:`, err?.message ?? err);
935
+ this.reconnecting = false;
936
+ this.fireTerminal('reconnect-exhausted', 'runReconnectLoop:crashed');
937
+ });
938
+ }
939
+ /**
940
+ * #226 CAN rebind. Transparently repoints `pinnedHandle` at the successor run,
941
+ * keeps the existing `attachmentId` / `leaseMs` (the workflow extended the lease
942
+ * by one heartbeat interval during the CAN transition per §2.3, so the lease is
943
+ * still live on the new run), notifies the subclass to restart its delivery
944
+ * loop, and resumes heartbeat + phase-watcher.
945
+ *
946
+ * Why this is safe without re-claiming:
947
+ * - The new run carries forward `currentAttachment` verbatim from the old run.
948
+ * - The adapter's `attachmentId` still matches, so the next `heartbeat` /
949
+ * `markDelivered` / `adapterExited` signal on the new pinned handle will be
950
+ * accepted unchanged by the workflow's handlers.
951
+ * - If the lease actually did expire before we got here (e.g. adapter was
952
+ * offline through multiple CAN cycles), the next phase-watcher tick on the
953
+ * new pinned handle will see `phase=detached` + no current attachment and
954
+ * fall through to the existing #201 reclaim path — belt-and-suspenders.
955
+ */
956
+ async runCanRebind(newRunId) {
957
+ try {
958
+ if (!this.client || !this.pinnedHandle || !this.token) {
959
+ log('runCanRebind: missing client/handle/token — firing terminal');
960
+ this.fireTerminal('reconnect-exhausted', 'runCanRebind:missing-state');
961
+ return;
962
+ }
963
+ const workflowId = this.pinnedHandle.workflowId;
964
+ const oldRunId = this.token.runId;
965
+ try {
966
+ // Tear down any subclass-owned stream against the stale pinned handle
967
+ // before repointing, so the subclass doesn't race itself on the rebuild.
968
+ await this.onReconnectStart('continued-as-new');
969
+ }
970
+ catch (err) {
971
+ log('onReconnectStart threw:', err?.message ?? err);
972
+ }
973
+ const newHandle = this.client.workflow.getHandle(workflowId, newRunId);
974
+ this.pinnedHandle = newHandle;
975
+ // Keep attachmentId + leaseMs (lease carried across CAN); refresh runId so
976
+ // diagnostic logging and any token-based debug output reflect the live run.
977
+ this.token = { ...this.token, runId: newRunId };
978
+ this.knownPhase = null; // force next phase-watcher tick to re-emit phaseChange
979
+ this.heartbeatBackoff = 0;
980
+ this.phaseBackoff = 0;
981
+ // #249: reset per-attachment diagnostic counters so the first post-rebind
982
+ // heartbeat re-logs `heartbeat#1 delivered`. Without this a rebind could
983
+ // mask a dead loop on the successor run — we'd never see the confirmation
984
+ // that heartbeats resumed.
985
+ this.firstHeartbeatLogged = false;
986
+ this.heartbeatsSent = 0;
987
+ this.phaseTicksDone = 0;
988
+ log(`rebound ${workflowId} to CAN successor ` +
989
+ `(attachmentId=${this.token.attachmentId}, oldRunId=${oldRunId}, newRunId=${newRunId})`);
990
+ try {
991
+ await this.onReconnected(newHandle);
992
+ }
993
+ catch (err) {
994
+ log('onReconnected threw:', err?.message ?? err);
995
+ }
996
+ // Clear reconnecting BEFORE rescheduling so the first tick after rebind
997
+ // doesn't short-circuit on its own reconnecting-guard. Mirrors the pattern
998
+ // in `runReconnectLoop`'s success path (#206).
999
+ this.reconnecting = false;
1000
+ if (!this.stopped) {
1001
+ this.scheduleHeartbeat();
1002
+ this.schedulePhaseWatcher();
1003
+ }
1004
+ }
1005
+ finally {
1006
+ this.reconnecting = false;
1007
+ }
1008
+ }
1009
+ /**
1010
+ * Budget-bounded reconnect loop.
1011
+ *
1012
+ * Strategy:
1013
+ * 1. Sleep (abortable) with exponential backoff from {@link RECONNECT_BASE_MS}
1014
+ * up to {@link RECONNECT_MAX_MS}, capped by an elapsed-time budget of
1015
+ * {@link RECONNECT_TOTAL_BUDGET_MS}.
1016
+ * 2. Query `attachmentInfo` via a fresh unpinned handle:
1017
+ * • workflow gone → fire `destroy`, exit.
1018
+ * • phase `gone` → fire `destroy`, exit.
1019
+ * • someone else holds the lease → fire `superseded`, exit (architect §1).
1020
+ * • phase `draining` → wait another tick (lease about to reap).
1021
+ * • otherwise → attempt fresh `claimAttachment`.
1022
+ * 3. On successful claim: rebuild `this.pinnedHandle` from the **new** token's
1023
+ * `runId` (workflow may have `continueAsNew`'d during outage), reset loop
1024
+ * state, call subclass hooks, restart heartbeat + watcher.
1025
+ *
1026
+ * Fires `reconnect-exhausted` on budget exhaustion. Exits silently (without
1027
+ * firing terminal) on abort — `stopV2Lifecycle` owns teardown messaging.
1028
+ */
1029
+ async runReconnectLoop(initialReason) {
1030
+ // Single try/finally so `reconnecting` always resets no matter how we exit
1031
+ // — success path, any fireTerminal, abort-during-sleep, or an unexpected
1032
+ // throw. #206 fixed the prior abort-catch path that leaked `reconnecting=true`
1033
+ // if `stopV2Lifecycle` aborted the backoff sleep.
1034
+ try {
1035
+ if (!this.client || !this.host || !this.token || !this.pinnedHandle) {
1036
+ log('runReconnectLoop: missing client/host/token/handle — aborting');
1037
+ this.fireTerminal('reconnect-exhausted', 'runReconnectLoop:missing-state');
1038
+ return;
1039
+ }
1040
+ const workflowId = this.pinnedHandle.workflowId;
1041
+ const oldAttachmentId = this.token.attachmentId;
1042
+ try {
1043
+ await this.onReconnectStart(initialReason);
1044
+ }
1045
+ catch (err) {
1046
+ log('onReconnectStart threw:', err?.message ?? err);
1047
+ }
1048
+ const deadline = Date.now() + this.reconnectBudgetMs;
1049
+ let backoff = this.reconnectBaseMs;
1050
+ let attempt = 0;
1051
+ while (!this.stopped && Date.now() < deadline) {
1052
+ attempt++;
1053
+ log(`reconnect attempt ${attempt} (sleep ${Math.round(backoff)}ms)`);
1054
+ try {
1055
+ await this.abortableSleep(backoff);
1056
+ }
1057
+ catch {
1058
+ // User-initiated stop during sleep — teardown already owns the rest.
1059
+ // The finally block still resets `reconnecting` so a subsequent
1060
+ // reclaim attempt (hypothetical — stop normally ends the adapter) would
1061
+ // find clean state. #206.
1062
+ log('reconnect aborted by stop during backoff');
1063
+ return;
1064
+ }
1065
+ if (this.stopped)
1066
+ return;
1067
+ // §Pre-check (architect §1): query attachmentInfo via a fresh unpinned handle.
1068
+ // The old pinned handle's runId may be stale after a continueAsNew.
1069
+ const unpinned = this.client.workflow.getHandle(workflowId);
1070
+ let info;
1071
+ try {
1072
+ info = await unpinned.query(signals_1.attachmentInfoQuery);
1073
+ }
1074
+ catch (err) {
1075
+ if ((0, terminal_error_1.isTerminalWorkflowError)(err)) {
1076
+ // #258: ONE terminal-class pre-check error is not enough evidence
1077
+ // to destroy the adapter. The classifier matches phrasings
1078
+ // (`WorkflowNotFound`, `NOT_FOUND`, "workflow execution already
1079
+ // completed") that can ALSO surface from transient gRPC blips and
1080
+ // momentary visibility-API hiccups. Pre-#258, this branch fired
1081
+ // `fireTerminal('destroy')` immediately — a single transient
1082
+ // error orphaned the adapter for the rest of the session
1083
+ // (heartbeat + watcher dead via `terminalFired`, poller torn
1084
+ // down by `onReconnectStart` + `onTerminal` listener).
1085
+ //
1086
+ // Tiebreaker: confirm with `describe()` against the same unpinned
1087
+ // handle. If the workflow is genuinely gone, `describe()` will
1088
+ // either return a closed status (COMPLETED/TERMINATED/...) or
1089
+ // itself throw — fire destroy with confidence. If it returns
1090
+ // RUNNING (or any non-terminal status), the original error was
1091
+ // transient — log and continue the loop. Bounded by
1092
+ // `DESCRIBE_TIMEOUT_MS` so a slow visibility-API call can't hang
1093
+ // the reconnect path indefinitely.
1094
+ const errClass = err?.name ?? 'unknown';
1095
+ const errMsg = err?.message ?? String(err);
1096
+ const tiebreak = await this.confirmWorkflowTerminal(unpinned);
1097
+ if (tiebreak.kind === 'running') {
1098
+ log(`reconnect: pre-check threw ${errClass} but describe() shows ` +
1099
+ `${tiebreak.statusName} — treating as transient, continuing loop ` +
1100
+ `(originalError="${errMsg}")`);
1101
+ backoff = Math.min(backoff * this.reconnectBackoffFactor, this.reconnectMaxMs);
1102
+ continue;
1103
+ }
1104
+ const confirmDesc = tiebreak.kind === 'terminal'
1105
+ ? `describe() confirmed ${tiebreak.statusName}`
1106
+ : `describe() ${tiebreak.kind === 'describe-threw' ? 'threw' : 'timed out'}`;
1107
+ log(`reconnect: pre-check terminal (${errClass}) and ${confirmDesc} — firing destroy ` +
1108
+ `(originalError="${errMsg}")`);
1109
+ this.fireTerminal('destroy', 'runReconnectLoop:precheck-terminal-confirmed');
1110
+ return;
1111
+ }
1112
+ backoff = Math.min(backoff * this.reconnectBackoffFactor, this.reconnectMaxMs);
1113
+ log(`reconnect pre-check transient error (next backoff ${Math.round(backoff)}ms):`, err?.message ?? err);
1114
+ continue;
1115
+ }
1116
+ if (info.phase === 'gone') {
1117
+ log('reconnect: phase=gone — giving up');
1118
+ this.fireTerminal('destroy', 'runReconnectLoop:phase-gone');
1119
+ return;
1120
+ }
1121
+ if (info.currentAttachment && info.currentAttachment.attachmentId !== oldAttachmentId) {
1122
+ log(`reconnect: another adapter holds the lease (${info.currentAttachment.attachmentId}) — bailing`);
1123
+ this.fireTerminal('superseded', 'runReconnectLoop:other-holder');
1124
+ return;
1125
+ }
1126
+ if (info.phase === 'draining') {
1127
+ // About to reap — give the workflow one more tick to finish collapsing.
1128
+ backoff = Math.min(backoff * this.reconnectBackoffFactor, this.reconnectMaxMs);
1129
+ log(`reconnect: phase=draining, waiting (next backoff ${Math.round(backoff)}ms)`);
1130
+ continue;
1131
+ }
1132
+ // §Claim: attempt a fresh `claimAttachment` (no expectedAttachmentId — our
1133
+ // previous lease is revoked, this is a fresh claim from the workflow's POV).
1134
+ try {
1135
+ const newToken = await unpinned.executeUpdate(signals_1.claimAttachmentUpdate, {
1136
+ args: [{
1137
+ host: this.host,
1138
+ adapterId: this.descriptor.adapterId,
1139
+ adapterClass: this.descriptor.adapterClass,
1140
+ leaseMs: 3 * this.descriptor.heartbeatMs,
1141
+ }],
1142
+ });
1143
+ // Success — rebuild pinned handle from the NEW runId and hand it to the subclass.
1144
+ this.token = newToken;
1145
+ this.pinnedHandle = this.client.workflow.getHandle(workflowId, newToken.runId);
1146
+ this.knownPhase = null; // force the next phase-watcher tick to re-emit phaseChange
1147
+ this.heartbeatBackoff = 0;
1148
+ this.phaseBackoff = 0;
1149
+ // #249: reset per-attachment diagnostic counters so the first post-reconnect
1150
+ // heartbeat re-logs `heartbeat#1 delivered`. Parity with CAN rebind path.
1151
+ this.firstHeartbeatLogged = false;
1152
+ this.heartbeatsSent = 0;
1153
+ this.phaseTicksDone = 0;
1154
+ log(`reconnected to ${workflowId} after ${attempt} attempt(s) ` +
1155
+ `(new attachmentId=${newToken.attachmentId}, runId=${newToken.runId})`);
1156
+ try {
1157
+ await this.onReconnected(this.pinnedHandle);
1158
+ }
1159
+ catch (err) {
1160
+ log('onReconnected threw:', err?.message ?? err);
1161
+ }
1162
+ // Clear the reconnecting flag BEFORE rescheduling so the first
1163
+ // heartbeat/watcher tick after reconnect doesn't short-circuit on
1164
+ // its own `this.reconnecting` guard. The finally block reasserts
1165
+ // `reconnecting=false` after return; this early assignment is the
1166
+ // one that matters for loop wiring.
1167
+ this.reconnecting = false;
1168
+ if (!this.stopped) {
1169
+ this.scheduleHeartbeat();
1170
+ this.schedulePhaseWatcher();
1171
+ }
1172
+ return;
1173
+ }
1174
+ catch (err) {
1175
+ if ((0, terminal_error_1.isTerminalWorkflowError)(err)) {
1176
+ log('reconnect: workflow gone during claim');
1177
+ this.fireTerminal('destroy', 'runReconnectLoop:claim-terminal');
1178
+ return;
1179
+ }
1180
+ backoff = Math.min(backoff * this.reconnectBackoffFactor, this.reconnectMaxMs);
1181
+ log(`reconnect claim failed (next backoff ${Math.round(backoff)}ms):`, err?.message ?? err);
1182
+ }
1183
+ }
1184
+ // Budget exhausted — give up cleanly.
1185
+ log(`reconnect budget exhausted after ${attempt} attempt(s)`);
1186
+ this.fireTerminal('reconnect-exhausted', 'runReconnectLoop:budget-exhausted');
1187
+ }
1188
+ finally {
1189
+ // Guarantee state reset regardless of which path we exited on. Safe to
1190
+ // assign unconditionally — a successful reconnect also ends up here after
1191
+ // the early assignment inside the success path (the early one is needed
1192
+ // so tick reschedulers see `reconnecting=false`; this one is belt-and-
1193
+ // suspenders for the abort/throw/terminal paths).
1194
+ this.reconnecting = false;
1195
+ }
1196
+ }
1197
+ }
1198
+ exports.BaseAttachment = BaseAttachment;
1199
+ /**
1200
+ * Registry of adapter descriptors keyed by `adapterId`.
1201
+ *
1202
+ * Look up the descriptor for a given session by `SessionMetadata.adapterId` (or
1203
+ * fall back to `'claude-code'` for pre-v0.25 sessions that have no adapterId set).
1204
+ * `src/adapters/index.ts` creates the singleton `registry` and registers all
1205
+ * shipped adapters at import time.
1206
+ */
1207
+ class AdapterRegistry {
1208
+ byId = new Map();
1209
+ /** Register an adapter descriptor. Replaces any existing entry with the same id. */
1210
+ register(desc) {
1211
+ this.byId.set(desc.adapterId, desc);
1212
+ }
1213
+ /**
1214
+ * Fetch the descriptor for `adapterId`. Throws if unregistered.
1215
+ *
1216
+ * Callers resolving from possibly-undefined metadata should coalesce first:
1217
+ * `registry.get(metadata.adapterId ?? 'claude-code')`.
1218
+ */
1219
+ get(adapterId) {
1220
+ const desc = this.byId.get(adapterId);
1221
+ if (!desc) {
1222
+ const known = [...this.byId.keys()].join(', ') || '(none registered)';
1223
+ throw new Error(`Unknown adapter "${adapterId}". Registered: ${known}`);
1224
+ }
1225
+ return desc;
1226
+ }
1227
+ /** `true` if `adapterId` is registered. */
1228
+ has(adapterId) {
1229
+ return this.byId.has(adapterId);
1230
+ }
1231
+ /** Snapshot of all registered descriptors. */
1232
+ all() {
1233
+ return [...this.byId.values()];
1234
+ }
1235
+ /**
1236
+ * Resolve an `adapterId` from the legacy `agent` field on {@link SessionMetadata}.
1237
+ * Maps `'claude'` → `'claude-code'`, `'copilot'` → `'copilot'`.
1238
+ *
1239
+ * Used as a fallback when `adapterId` is not yet populated on the session metadata
1240
+ * (e.g. sessions started before PR-B landed). PR-D removes this mapping when the
1241
+ * legacy `AgentType` enum is retired.
1242
+ */
1243
+ resolveFromAgentType(agent) {
1244
+ if (agent === 'copilot')
1245
+ return 'copilot';
1246
+ // ADR 0014 §4.1 — mock adapter resolves to its own descriptor. The
1247
+ // descriptor is only registered when `isDevMode()` (gate 2); a stale
1248
+ // metadata.agentType='mock' in a production build would land here and
1249
+ // then `registry.get('mock')` would throw the documented "Unknown
1250
+ // adapter" error — which is the correct safety behaviour.
1251
+ if (agent === 'mock')
1252
+ return 'mock';
1253
+ // #131 Phase C — headless Claude API adapter. Descriptor lives in
1254
+ // src/adapters/claude-api; opt-in via `recruit({ agent: 'claude-api' })`.
1255
+ if (agent === 'claude-api')
1256
+ return 'claude-api';
1257
+ // #449 Phase C — headless multi-provider OpenCode adapter. Descriptor
1258
+ // lives in src/adapters/opencode; opt-in via `recruit({ agent: 'opencode' })`.
1259
+ if (agent === 'opencode')
1260
+ return 'opencode';
1261
+ // #520 — headless Claude Code adapter (per-turn `claude -p` subprocess).
1262
+ // Descriptor lives in src/adapters/claude-code-headless; opt-in via
1263
+ // `recruit({ agent: 'claude-code-headless' })`. Uses the host's existing
1264
+ // Claude Code OAuth login so turns bill against subscription extra-usage.
1265
+ if (agent === 'claude-code-headless')
1266
+ return 'claude-code-headless';
1267
+ return 'claude-code';
1268
+ }
1269
+ }
1270
+ exports.AdapterRegistry = AdapterRegistry;