@ai-devkit/agent-manager 0.32.0 → 0.33.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 (519) hide show
  1. package/dist/AgentManager.d.ts +3 -3
  2. package/dist/AgentManager.d.ts.map +1 -1
  3. package/dist/AgentManager.js +80 -79
  4. package/dist/AgentManager.js.map +1 -1
  5. package/dist/__tests__/AgentManager.test.js +413 -388
  6. package/dist/__tests__/AgentManager.test.js.map +1 -1
  7. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js +642 -642
  8. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js.map +1 -1
  9. package/dist/__tests__/adapters/CodexAdapter.test.js +322 -2321
  10. package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -1
  11. package/dist/__tests__/adapters/CopilotAdapter.test.js +254 -248
  12. package/dist/__tests__/adapters/CopilotAdapter.test.js.map +1 -1
  13. package/dist/__tests__/adapters/GeminiCliAdapter.test.js +564 -558
  14. package/dist/__tests__/adapters/GeminiCliAdapter.test.js.map +1 -1
  15. package/dist/__tests__/adapters/GrokCliAdapter.test.js +114 -114
  16. package/dist/__tests__/adapters/GrokCliAdapter.test.js.map +1 -1
  17. package/dist/__tests__/adapters/OpenCodeAdapter.test.js +169 -169
  18. package/dist/__tests__/adapters/OpenCodeAdapter.test.js.map +1 -1
  19. package/dist/__tests__/capacity/codex.test.js +93 -93
  20. package/dist/__tests__/capacity/codex.test.js.map +1 -1
  21. package/dist/__tests__/capacity/index.test.js +17 -17
  22. package/dist/__tests__/capacity/index.test.js.map +1 -1
  23. package/dist/__tests__/durable/CodexCliProbe.test.js +60 -60
  24. package/dist/__tests__/durable/CodexCliProbe.test.js.map +1 -1
  25. package/dist/__tests__/durable/CodexDurableAgentRepository.test.js +28 -28
  26. package/dist/__tests__/durable/CodexDurableAgentRepository.test.js.map +1 -1
  27. package/dist/__tests__/durable/CodexPrintAgent.integration.test.js +30 -30
  28. package/dist/__tests__/durable/CodexPrintAgent.integration.test.js.map +1 -1
  29. package/dist/__tests__/durable/CodexPrintAgentService.test.js +71 -71
  30. package/dist/__tests__/durable/CodexPrintAgentService.test.js.map +1 -1
  31. package/dist/__tests__/durable/CodexPrintRunner.test.js +125 -125
  32. package/dist/__tests__/durable/CodexPrintRunner.test.js.map +1 -1
  33. package/dist/__tests__/print/ClaudeCliProbe.test.js +20 -20
  34. package/dist/__tests__/print/ClaudeCliProbe.test.js.map +1 -1
  35. package/dist/__tests__/print/ClaudePrintAgent.integration.test.js +20 -20
  36. package/dist/__tests__/print/ClaudePrintAgent.integration.test.js.map +1 -1
  37. package/dist/__tests__/print/ClaudePrintAgentService.test.js +58 -58
  38. package/dist/__tests__/print/ClaudePrintAgentService.test.js.map +1 -1
  39. package/dist/__tests__/print/ClaudePrintRunner.test.js +55 -55
  40. package/dist/__tests__/print/ClaudePrintRunner.test.js.map +1 -1
  41. package/dist/__tests__/print/DurableAgent.test.js +9 -9
  42. package/dist/__tests__/print/DurableAgent.test.js.map +1 -1
  43. package/dist/__tests__/print/DurableAgentRepository.sqlite.test.js +40 -40
  44. package/dist/__tests__/print/DurableAgentRepository.sqlite.test.js.map +1 -1
  45. package/dist/__tests__/print/DurableAgentRepository.test.js +66 -66
  46. package/dist/__tests__/print/DurableAgentRepository.test.js.map +1 -1
  47. package/dist/__tests__/providers/claude/ClaudeAgentMapper.test.js +34 -34
  48. package/dist/__tests__/providers/claude/ClaudeAgentMapper.test.js.map +1 -1
  49. package/dist/__tests__/providers/claude/ClaudeSessionLocator.test.js +132 -132
  50. package/dist/__tests__/providers/claude/ClaudeSessionLocator.test.js.map +1 -1
  51. package/dist/__tests__/providers/codex/CodexSessionLocator.test.js +194 -0
  52. package/dist/__tests__/providers/codex/CodexSessionLocator.test.js.map +1 -0
  53. package/dist/__tests__/providers/codex/CodexSessionMapping.test.js +86 -0
  54. package/dist/__tests__/providers/codex/CodexSessionMapping.test.js.map +1 -0
  55. package/dist/__tests__/providers/codex/CodexSessionParser.test.js +254 -0
  56. package/dist/__tests__/providers/codex/CodexSessionParser.test.js.map +1 -0
  57. package/dist/__tests__/providers/pi/PiAdapter.test.js +337 -0
  58. package/dist/__tests__/providers/pi/PiAdapter.test.js.map +1 -0
  59. package/dist/__tests__/providers/pi/PiSessionParser.test.js +246 -0
  60. package/dist/__tests__/providers/pi/PiSessionParser.test.js.map +1 -0
  61. package/dist/__tests__/providers/pi/PiSessionTracker.test.js +108 -0
  62. package/dist/__tests__/providers/pi/PiSessionTracker.test.js.map +1 -0
  63. package/dist/__tests__/providers/pi/durable/PiCliProbe.test.js +33 -33
  64. package/dist/__tests__/providers/pi/durable/PiCliProbe.test.js.map +1 -1
  65. package/dist/__tests__/providers/pi/durable/PiDurableAgentRepository.test.js +16 -16
  66. package/dist/__tests__/providers/pi/durable/PiDurableAgentRepository.test.js.map +1 -1
  67. package/dist/__tests__/providers/pi/durable/PiPrintAgentService.test.js +52 -52
  68. package/dist/__tests__/providers/pi/durable/PiPrintAgentService.test.js.map +1 -1
  69. package/dist/__tests__/providers/pi/durable/PiPrintProtocol.test.js +44 -44
  70. package/dist/__tests__/providers/pi/durable/PiPrintProtocol.test.js.map +1 -1
  71. package/dist/__tests__/providers/pi/durable/PiPrintRunner.test.js +126 -126
  72. package/dist/__tests__/providers/pi/durable/PiPrintRunner.test.js.map +1 -1
  73. package/dist/__tests__/providers/pi/durable/PiStreamParser.test.js +14 -14
  74. package/dist/__tests__/providers/pi/durable/PiStreamParser.test.js.map +1 -1
  75. package/dist/__tests__/readiness/AgentReadiness.test.js +134 -134
  76. package/dist/__tests__/readiness/AgentReadiness.test.js.map +1 -1
  77. package/dist/__tests__/runtime/AgentRuntime.test.js +13 -0
  78. package/dist/__tests__/runtime/AgentRuntime.test.js.map +1 -0
  79. package/dist/__tests__/runtime/HerdrAgentRuntime.test.js +452 -0
  80. package/dist/__tests__/runtime/HerdrAgentRuntime.test.js.map +1 -0
  81. package/dist/__tests__/runtime/ManagedAgentRuntime.test.js +588 -0
  82. package/dist/__tests__/runtime/ManagedAgentRuntime.test.js.map +1 -0
  83. package/dist/__tests__/terminal/TerminalFocusManager.test.js +100 -100
  84. package/dist/__tests__/terminal/TerminalFocusManager.test.js.map +1 -1
  85. package/dist/__tests__/terminal/TmuxManager.test.js +101 -101
  86. package/dist/__tests__/terminal/TmuxManager.test.js.map +1 -1
  87. package/dist/__tests__/terminal/TtyWriter.test.js +185 -185
  88. package/dist/__tests__/terminal/TtyWriter.test.js.map +1 -1
  89. package/dist/__tests__/utils/AgentRegistry.test.js +335 -217
  90. package/dist/__tests__/utils/AgentRegistry.test.js.map +1 -1
  91. package/dist/__tests__/utils/ClaudeSessionParser.test.js +103 -101
  92. package/dist/__tests__/utils/ClaudeSessionParser.test.js.map +1 -1
  93. package/dist/__tests__/utils/agent-requests.test.js +43 -43
  94. package/dist/__tests__/utils/agent-requests.test.js.map +1 -1
  95. package/dist/__tests__/utils/agents.test.js +17 -17
  96. package/dist/__tests__/utils/agents.test.js.map +1 -1
  97. package/dist/__tests__/utils/matching.test.js +92 -92
  98. package/dist/__tests__/utils/matching.test.js.map +1 -1
  99. package/dist/__tests__/utils/process.test.js +108 -108
  100. package/dist/__tests__/utils/process.test.js.map +1 -1
  101. package/dist/__tests__/utils/session.test.js +53 -53
  102. package/dist/__tests__/utils/session.test.js.map +1 -1
  103. package/dist/adapters/AgentAdapter.d.ts +2 -2
  104. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  105. package/dist/adapters/AgentAdapter.js +1 -1
  106. package/dist/adapters/AgentAdapter.js.map +1 -1
  107. package/dist/adapters/CopilotAdapter.d.ts +2 -2
  108. package/dist/adapters/CopilotAdapter.d.ts.map +1 -1
  109. package/dist/adapters/CopilotAdapter.js +51 -51
  110. package/dist/adapters/CopilotAdapter.js.map +1 -1
  111. package/dist/adapters/GeminiCliAdapter.d.ts +2 -2
  112. package/dist/adapters/GeminiCliAdapter.d.ts.map +1 -1
  113. package/dist/adapters/GeminiCliAdapter.js +86 -86
  114. package/dist/adapters/GeminiCliAdapter.js.map +1 -1
  115. package/dist/adapters/GrokCliAdapter.d.ts +1 -1
  116. package/dist/adapters/GrokCliAdapter.d.ts.map +1 -1
  117. package/dist/adapters/GrokCliAdapter.js +80 -80
  118. package/dist/adapters/GrokCliAdapter.js.map +1 -1
  119. package/dist/adapters/OpenCodeAdapter.d.ts +1 -1
  120. package/dist/adapters/OpenCodeAdapter.d.ts.map +1 -1
  121. package/dist/adapters/OpenCodeAdapter.js +37 -36
  122. package/dist/adapters/OpenCodeAdapter.js.map +1 -1
  123. package/dist/adapters/index.d.ts +9 -9
  124. package/dist/adapters/index.d.ts.map +1 -1
  125. package/dist/adapters/index.js +8 -8
  126. package/dist/adapters/index.js.map +1 -1
  127. package/dist/capacity/codex.d.ts +3 -3
  128. package/dist/capacity/codex.d.ts.map +1 -1
  129. package/dist/capacity/codex.js +57 -57
  130. package/dist/capacity/codex.js.map +1 -1
  131. package/dist/capacity/index.d.ts +3 -3
  132. package/dist/capacity/index.d.ts.map +1 -1
  133. package/dist/capacity/index.js +8 -8
  134. package/dist/capacity/index.js.map +1 -1
  135. package/dist/capacity/types.d.ts +1 -1
  136. package/dist/capacity/types.js.map +1 -1
  137. package/dist/database/connection.d.ts +1 -1
  138. package/dist/database/connection.d.ts.map +1 -1
  139. package/dist/database/connection.js +15 -15
  140. package/dist/database/connection.js.map +1 -1
  141. package/dist/database/index.d.ts +3 -3
  142. package/dist/database/index.d.ts.map +1 -1
  143. package/dist/database/index.js +2 -2
  144. package/dist/database/index.js.map +1 -1
  145. package/dist/database/migrations/005_agent_runtime.sql +2 -0
  146. package/dist/database/schema.d.ts +1 -1
  147. package/dist/database/schema.d.ts.map +1 -1
  148. package/dist/database/schema.js +7 -7
  149. package/dist/database/schema.js.map +1 -1
  150. package/dist/durable/DurableAgent.d.ts +9 -9
  151. package/dist/durable/DurableAgent.d.ts.map +1 -1
  152. package/dist/durable/DurableAgent.js +16 -16
  153. package/dist/durable/DurableAgent.js.map +1 -1
  154. package/dist/durable/DurableAgentRepository.d.ts +4 -4
  155. package/dist/durable/DurableAgentRepository.d.ts.map +1 -1
  156. package/dist/durable/DurableAgentRepository.js +49 -48
  157. package/dist/durable/DurableAgentRepository.js.map +1 -1
  158. package/dist/durable/process.d.ts +1 -1
  159. package/dist/durable/process.d.ts.map +1 -1
  160. package/dist/durable/process.js +14 -14
  161. package/dist/durable/process.js.map +1 -1
  162. package/dist/durable/run.d.ts +2 -2
  163. package/dist/durable/run.d.ts.map +1 -1
  164. package/dist/durable/run.js +6 -6
  165. package/dist/durable/run.js.map +1 -1
  166. package/dist/durable/utils.d.ts +1 -1
  167. package/dist/durable/utils.d.ts.map +1 -1
  168. package/dist/durable/utils.js +4 -4
  169. package/dist/durable/utils.js.map +1 -1
  170. package/dist/index.d.ts +46 -43
  171. package/dist/index.d.ts.map +1 -1
  172. package/dist/index.js +27 -26
  173. package/dist/index.js.map +1 -1
  174. package/dist/providers/claude/ClaudeAgentMapper.d.ts +4 -4
  175. package/dist/providers/claude/ClaudeAgentMapper.d.ts.map +1 -1
  176. package/dist/providers/claude/ClaudeAgentMapper.js +10 -10
  177. package/dist/providers/claude/ClaudeAgentMapper.js.map +1 -1
  178. package/dist/providers/claude/ClaudeCodeAdapter.d.ts +1 -1
  179. package/dist/providers/claude/ClaudeCodeAdapter.d.ts.map +1 -1
  180. package/dist/providers/claude/ClaudeCodeAdapter.js +14 -14
  181. package/dist/providers/claude/ClaudeCodeAdapter.js.map +1 -1
  182. package/dist/providers/claude/ClaudeSessionLocator.d.ts +4 -4
  183. package/dist/providers/claude/ClaudeSessionLocator.d.ts.map +1 -1
  184. package/dist/providers/claude/ClaudeSessionLocator.js +79 -79
  185. package/dist/providers/claude/ClaudeSessionLocator.js.map +1 -1
  186. package/dist/providers/claude/ClaudeSessionParser.d.ts +2 -2
  187. package/dist/providers/claude/ClaudeSessionParser.d.ts.map +1 -1
  188. package/dist/providers/claude/ClaudeSessionParser.js +99 -99
  189. package/dist/providers/claude/ClaudeSessionParser.js.map +1 -1
  190. package/dist/providers/claude/durable/ClaudeCliProbe.d.ts.map +1 -1
  191. package/dist/providers/claude/durable/ClaudeCliProbe.js +16 -16
  192. package/dist/providers/claude/durable/ClaudeCliProbe.js.map +1 -1
  193. package/dist/providers/claude/durable/ClaudePrintAgentService.d.ts +5 -5
  194. package/dist/providers/claude/durable/ClaudePrintAgentService.d.ts.map +1 -1
  195. package/dist/providers/claude/durable/ClaudePrintAgentService.js +10 -10
  196. package/dist/providers/claude/durable/ClaudePrintAgentService.js.map +1 -1
  197. package/dist/providers/claude/durable/ClaudePrintRunner.d.ts +4 -4
  198. package/dist/providers/claude/durable/ClaudePrintRunner.d.ts.map +1 -1
  199. package/dist/providers/claude/durable/ClaudePrintRunner.js +18 -18
  200. package/dist/providers/claude/durable/ClaudePrintRunner.js.map +1 -1
  201. package/dist/providers/claude/durable/ClaudeStreamParser.d.ts +2 -2
  202. package/dist/providers/claude/durable/ClaudeStreamParser.d.ts.map +1 -1
  203. package/dist/providers/claude/durable/ClaudeStreamParser.js +17 -17
  204. package/dist/providers/claude/durable/ClaudeStreamParser.js.map +1 -1
  205. package/dist/providers/codex/CodexAdapter.d.ts +27 -0
  206. package/dist/providers/codex/CodexAdapter.d.ts.map +1 -0
  207. package/dist/providers/codex/CodexAdapter.js +162 -0
  208. package/dist/providers/codex/CodexAdapter.js.map +1 -0
  209. package/dist/providers/codex/CodexAgentMapper.d.ts +9 -0
  210. package/dist/providers/codex/CodexAgentMapper.d.ts.map +1 -0
  211. package/dist/providers/codex/CodexAgentMapper.js +35 -0
  212. package/dist/providers/codex/CodexAgentMapper.js.map +1 -0
  213. package/dist/providers/codex/CodexSessionLocator.d.ts +44 -0
  214. package/dist/providers/codex/CodexSessionLocator.d.ts.map +1 -0
  215. package/dist/providers/codex/CodexSessionLocator.js +207 -0
  216. package/dist/providers/codex/CodexSessionLocator.js.map +1 -0
  217. package/dist/providers/codex/CodexSessionMapping.d.ts +23 -0
  218. package/dist/providers/codex/CodexSessionMapping.d.ts.map +1 -0
  219. package/dist/providers/codex/CodexSessionMapping.js +68 -0
  220. package/dist/providers/codex/CodexSessionMapping.js.map +1 -0
  221. package/dist/providers/codex/CodexSessionParser.d.ts +58 -0
  222. package/dist/providers/codex/CodexSessionParser.d.ts.map +1 -0
  223. package/dist/providers/codex/CodexSessionParser.js +262 -0
  224. package/dist/providers/codex/CodexSessionParser.js.map +1 -0
  225. package/dist/providers/codex/durable/CodexCliProbe.d.ts.map +1 -1
  226. package/dist/providers/codex/durable/CodexCliProbe.js +22 -22
  227. package/dist/providers/codex/durable/CodexCliProbe.js.map +1 -1
  228. package/dist/providers/codex/durable/CodexPrintAgentService.d.ts +6 -6
  229. package/dist/providers/codex/durable/CodexPrintAgentService.d.ts.map +1 -1
  230. package/dist/providers/codex/durable/CodexPrintAgentService.js +10 -10
  231. package/dist/providers/codex/durable/CodexPrintAgentService.js.map +1 -1
  232. package/dist/providers/codex/durable/CodexPrintRunner.d.ts +4 -4
  233. package/dist/providers/codex/durable/CodexPrintRunner.d.ts.map +1 -1
  234. package/dist/providers/codex/durable/CodexPrintRunner.js +20 -20
  235. package/dist/providers/codex/durable/CodexPrintRunner.js.map +1 -1
  236. package/dist/providers/codex/durable/CodexStreamParser.d.ts +2 -2
  237. package/dist/providers/codex/durable/CodexStreamParser.d.ts.map +1 -1
  238. package/dist/providers/codex/durable/CodexStreamParser.js +21 -21
  239. package/dist/providers/codex/durable/CodexStreamParser.js.map +1 -1
  240. package/dist/providers/pi/PiAdapter.d.ts +28 -0
  241. package/dist/providers/pi/PiAdapter.d.ts.map +1 -0
  242. package/dist/providers/pi/PiAdapter.js +153 -0
  243. package/dist/providers/pi/PiAdapter.js.map +1 -0
  244. package/dist/providers/pi/PiAgentMapper.d.ts +9 -0
  245. package/dist/providers/pi/PiAgentMapper.d.ts.map +1 -0
  246. package/dist/providers/pi/PiAgentMapper.js +37 -0
  247. package/dist/providers/pi/PiAgentMapper.js.map +1 -0
  248. package/dist/providers/pi/PiSessionLocator.d.ts +23 -0
  249. package/dist/providers/pi/PiSessionLocator.d.ts.map +1 -0
  250. package/dist/providers/pi/PiSessionLocator.js +79 -0
  251. package/dist/providers/pi/PiSessionLocator.js.map +1 -0
  252. package/dist/providers/pi/PiSessionParser.d.ts +36 -0
  253. package/dist/providers/pi/PiSessionParser.d.ts.map +1 -0
  254. package/dist/providers/pi/PiSessionParser.js +191 -0
  255. package/dist/providers/pi/PiSessionParser.js.map +1 -0
  256. package/dist/providers/pi/PiSessionTracker.d.ts +23 -0
  257. package/dist/providers/pi/PiSessionTracker.d.ts.map +1 -0
  258. package/dist/providers/pi/PiSessionTracker.js +69 -0
  259. package/dist/providers/pi/PiSessionTracker.js.map +1 -0
  260. package/dist/providers/pi/durable/PiCliProbe.d.ts.map +1 -1
  261. package/dist/providers/pi/durable/PiCliProbe.js +15 -15
  262. package/dist/providers/pi/durable/PiCliProbe.js.map +1 -1
  263. package/dist/providers/pi/durable/PiPrintAgentService.d.ts +6 -6
  264. package/dist/providers/pi/durable/PiPrintAgentService.d.ts.map +1 -1
  265. package/dist/providers/pi/durable/PiPrintAgentService.js +10 -10
  266. package/dist/providers/pi/durable/PiPrintAgentService.js.map +1 -1
  267. package/dist/providers/pi/durable/PiPrintProtocol.d.ts.map +1 -1
  268. package/dist/providers/pi/durable/PiPrintProtocol.js +18 -18
  269. package/dist/providers/pi/durable/PiPrintProtocol.js.map +1 -1
  270. package/dist/providers/pi/durable/PiPrintRunner.d.ts +4 -4
  271. package/dist/providers/pi/durable/PiPrintRunner.d.ts.map +1 -1
  272. package/dist/providers/pi/durable/PiPrintRunner.js +15 -15
  273. package/dist/providers/pi/durable/PiPrintRunner.js.map +1 -1
  274. package/dist/providers/pi/durable/PiStreamParser.d.ts +1 -1
  275. package/dist/providers/pi/durable/PiStreamParser.d.ts.map +1 -1
  276. package/dist/providers/pi/durable/PiStreamParser.js +16 -16
  277. package/dist/providers/pi/durable/PiStreamParser.js.map +1 -1
  278. package/dist/readiness/AgentReadiness.d.ts +6 -6
  279. package/dist/readiness/AgentReadiness.d.ts.map +1 -1
  280. package/dist/readiness/AgentReadiness.js +130 -130
  281. package/dist/readiness/AgentReadiness.js.map +1 -1
  282. package/dist/runtime/ManagedAgentRuntime.d.ts +69 -0
  283. package/dist/runtime/ManagedAgentRuntime.d.ts.map +1 -0
  284. package/dist/runtime/ManagedAgentRuntime.js +156 -0
  285. package/dist/runtime/ManagedAgentRuntime.js.map +1 -0
  286. package/dist/runtime/RuntimeFactory.d.ts +4 -0
  287. package/dist/runtime/RuntimeFactory.d.ts.map +1 -0
  288. package/dist/runtime/RuntimeFactory.js +11 -0
  289. package/dist/runtime/RuntimeFactory.js.map +1 -0
  290. package/dist/runtime/errors.d.ts +27 -0
  291. package/dist/runtime/errors.d.ts.map +1 -0
  292. package/dist/runtime/errors.js +42 -0
  293. package/dist/runtime/errors.js.map +1 -0
  294. package/dist/runtime/herdr/HerdrAgentRuntime.d.ts +31 -0
  295. package/dist/runtime/herdr/HerdrAgentRuntime.d.ts.map +1 -0
  296. package/dist/runtime/herdr/HerdrAgentRuntime.js +184 -0
  297. package/dist/runtime/herdr/HerdrAgentRuntime.js.map +1 -0
  298. package/dist/runtime/herdr/HerdrCliClient.d.ts +13 -0
  299. package/dist/runtime/herdr/HerdrCliClient.d.ts.map +1 -0
  300. package/dist/runtime/herdr/HerdrCliClient.js +60 -0
  301. package/dist/runtime/herdr/HerdrCliClient.js.map +1 -0
  302. package/dist/runtime/herdr/HerdrErrors.d.ts +7 -0
  303. package/dist/runtime/herdr/HerdrErrors.d.ts.map +1 -0
  304. package/dist/runtime/herdr/HerdrErrors.js +10 -0
  305. package/dist/runtime/herdr/HerdrErrors.js.map +1 -0
  306. package/dist/runtime/herdr/HerdrResponseParsers.d.ts +14 -0
  307. package/dist/runtime/herdr/HerdrResponseParsers.d.ts.map +1 -0
  308. package/dist/runtime/herdr/HerdrResponseParsers.js +96 -0
  309. package/dist/runtime/herdr/HerdrResponseParsers.js.map +1 -0
  310. package/dist/runtime/herdr/HerdrRuntimeRef.d.ts +9 -0
  311. package/dist/runtime/herdr/HerdrRuntimeRef.d.ts.map +1 -0
  312. package/dist/runtime/herdr/HerdrRuntimeRef.js +33 -0
  313. package/dist/runtime/herdr/HerdrRuntimeRef.js.map +1 -0
  314. package/dist/runtime/tmux/PidPolling.d.ts +5 -0
  315. package/dist/runtime/tmux/PidPolling.d.ts.map +1 -0
  316. package/dist/runtime/tmux/PidPolling.js +24 -0
  317. package/dist/runtime/tmux/PidPolling.js.map +1 -0
  318. package/dist/runtime/tmux/TmuxAgentRuntime.d.ts +17 -0
  319. package/dist/runtime/tmux/TmuxAgentRuntime.d.ts.map +1 -0
  320. package/dist/runtime/tmux/TmuxAgentRuntime.js +80 -0
  321. package/dist/runtime/tmux/TmuxAgentRuntime.js.map +1 -0
  322. package/dist/runtime/tmux/TmuxRuntimeRef.d.ts +5 -0
  323. package/dist/runtime/tmux/TmuxRuntimeRef.d.ts.map +1 -0
  324. package/dist/runtime/tmux/TmuxRuntimeRef.js +9 -0
  325. package/dist/runtime/tmux/TmuxRuntimeRef.js.map +1 -0
  326. package/dist/runtime/types.d.ts +54 -0
  327. package/dist/runtime/types.d.ts.map +1 -0
  328. package/dist/runtime/types.js +3 -0
  329. package/dist/runtime/types.js.map +1 -0
  330. package/dist/terminal/TerminalFocusManager.d.ts.map +1 -1
  331. package/dist/terminal/TerminalFocusManager.js +50 -50
  332. package/dist/terminal/TerminalFocusManager.js.map +1 -1
  333. package/dist/terminal/TmuxManager.d.ts.map +1 -1
  334. package/dist/terminal/TmuxManager.js +47 -47
  335. package/dist/terminal/TmuxManager.js.map +1 -1
  336. package/dist/terminal/TtyWriter.d.ts +1 -1
  337. package/dist/terminal/TtyWriter.d.ts.map +1 -1
  338. package/dist/terminal/TtyWriter.js +89 -89
  339. package/dist/terminal/TtyWriter.js.map +1 -1
  340. package/dist/terminal/index.d.ts +4 -4
  341. package/dist/terminal/index.js +3 -3
  342. package/dist/terminal/index.js.map +1 -1
  343. package/dist/utils/AgentRegistry.d.ts +10 -2
  344. package/dist/utils/AgentRegistry.d.ts.map +1 -1
  345. package/dist/utils/AgentRegistry.js +57 -24
  346. package/dist/utils/AgentRegistry.js.map +1 -1
  347. package/dist/utils/agent-requests.d.ts.map +1 -1
  348. package/dist/utils/agent-requests.js +5 -5
  349. package/dist/utils/agent-requests.js.map +1 -1
  350. package/dist/utils/agents.d.ts +2 -2
  351. package/dist/utils/agents.d.ts.map +1 -1
  352. package/dist/utils/agents.js +15 -15
  353. package/dist/utils/agents.js.map +1 -1
  354. package/dist/utils/applescript.js +1 -1
  355. package/dist/utils/applescript.js.map +1 -1
  356. package/dist/utils/index.d.ts +6 -6
  357. package/dist/utils/index.d.ts.map +1 -1
  358. package/dist/utils/index.js +4 -4
  359. package/dist/utils/index.js.map +1 -1
  360. package/dist/utils/matching.d.ts +2 -2
  361. package/dist/utils/matching.d.ts.map +1 -1
  362. package/dist/utils/matching.js +4 -4
  363. package/dist/utils/matching.js.map +1 -1
  364. package/dist/utils/process.d.ts +1 -1
  365. package/dist/utils/process.d.ts.map +1 -1
  366. package/dist/utils/process.js +74 -74
  367. package/dist/utils/process.js.map +1 -1
  368. package/dist/utils/session.d.ts +1 -1
  369. package/dist/utils/session.d.ts.map +1 -1
  370. package/dist/utils/session.js +7 -7
  371. package/dist/utils/session.js.map +1 -1
  372. package/dist/utils/sortAgents.d.ts +2 -2
  373. package/dist/utils/sortAgents.d.ts.map +1 -1
  374. package/dist/utils/sortAgents.js +5 -5
  375. package/dist/utils/sortAgents.js.map +1 -1
  376. package/package.json +18 -19
  377. package/src/AgentManager.ts +283 -281
  378. package/src/__tests__/AgentManager.test.ts +917 -907
  379. package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +1657 -1368
  380. package/src/__tests__/adapters/CodexAdapter.test.ts +365 -1698
  381. package/src/__tests__/adapters/CopilotAdapter.test.ts +576 -481
  382. package/src/__tests__/adapters/GeminiCliAdapter.test.ts +1189 -1157
  383. package/src/__tests__/adapters/GrokCliAdapter.test.ts +325 -272
  384. package/src/__tests__/adapters/OpenCodeAdapter.test.ts +510 -455
  385. package/src/__tests__/capacity/codex.test.ts +178 -91
  386. package/src/__tests__/capacity/index.test.ts +27 -18
  387. package/src/__tests__/durable/CodexCliProbe.test.ts +75 -53
  388. package/src/__tests__/durable/CodexDurableAgentRepository.test.ts +51 -40
  389. package/src/__tests__/durable/CodexPrintAgent.integration.test.ts +89 -64
  390. package/src/__tests__/durable/CodexPrintAgentService.test.ts +196 -118
  391. package/src/__tests__/durable/CodexPrintRunner.test.ts +282 -162
  392. package/src/__tests__/fixtures/fake-claude.cjs +20 -13
  393. package/src/__tests__/fixtures/fake-codex.cjs +41 -30
  394. package/src/__tests__/print/ClaudeCliProbe.test.ts +27 -21
  395. package/src/__tests__/print/ClaudePrintAgent.integration.test.ts +48 -40
  396. package/src/__tests__/print/ClaudePrintAgentService.test.ts +118 -67
  397. package/src/__tests__/print/ClaudePrintRunner.test.ts +136 -83
  398. package/src/__tests__/print/DurableAgent.test.ts +19 -16
  399. package/src/__tests__/print/DurableAgentRepository.sqlite.test.ts +109 -86
  400. package/src/__tests__/print/DurableAgentRepository.test.ts +170 -137
  401. package/src/__tests__/providers/claude/ClaudeAgentMapper.test.ts +65 -65
  402. package/src/__tests__/providers/claude/ClaudeSessionLocator.test.ts +400 -377
  403. package/src/__tests__/providers/codex/CodexSessionLocator.test.ts +182 -0
  404. package/src/__tests__/providers/codex/CodexSessionMapping.test.ts +63 -0
  405. package/src/__tests__/providers/codex/CodexSessionParser.test.ts +192 -0
  406. package/src/__tests__/providers/pi/PiAdapter.test.ts +310 -0
  407. package/src/__tests__/providers/pi/PiSessionParser.test.ts +190 -0
  408. package/src/__tests__/providers/pi/PiSessionTracker.test.ts +79 -0
  409. package/src/__tests__/providers/pi/durable/PiCliProbe.test.ts +48 -32
  410. package/src/__tests__/providers/pi/durable/PiDurableAgentRepository.test.ts +24 -23
  411. package/src/__tests__/providers/pi/durable/PiPrintAgentService.test.ts +131 -59
  412. package/src/__tests__/providers/pi/durable/PiPrintProtocol.test.ts +66 -28
  413. package/src/__tests__/providers/pi/durable/PiPrintRunner.test.ts +236 -90
  414. package/src/__tests__/providers/pi/durable/PiStreamParser.test.ts +20 -18
  415. package/src/__tests__/readiness/AgentReadiness.test.ts +217 -169
  416. package/src/__tests__/runtime/AgentRuntime.test.ts +8 -0
  417. package/src/__tests__/runtime/HerdrAgentRuntime.test.ts +358 -0
  418. package/src/__tests__/runtime/ManagedAgentRuntime.test.ts +555 -0
  419. package/src/__tests__/terminal/TerminalFocusManager.test.ts +254 -253
  420. package/src/__tests__/terminal/TmuxManager.test.ts +157 -157
  421. package/src/__tests__/terminal/TtyWriter.test.ts +423 -419
  422. package/src/__tests__/utils/AgentRegistry.test.ts +506 -402
  423. package/src/__tests__/utils/ClaudeSessionParser.test.ts +276 -242
  424. package/src/__tests__/utils/agent-requests.test.ts +76 -56
  425. package/src/__tests__/utils/agents.test.ts +20 -20
  426. package/src/__tests__/utils/matching.test.ts +206 -171
  427. package/src/__tests__/utils/process.test.ts +276 -262
  428. package/src/__tests__/utils/session.test.ts +133 -139
  429. package/src/adapters/AgentAdapter.ts +134 -126
  430. package/src/adapters/CopilotAdapter.ts +405 -395
  431. package/src/adapters/GeminiCliAdapter.ts +598 -600
  432. package/src/adapters/GrokCliAdapter.ts +353 -342
  433. package/src/adapters/OpenCodeAdapter.ts +285 -259
  434. package/src/adapters/index.ts +15 -9
  435. package/src/capacity/codex.ts +137 -84
  436. package/src/capacity/index.ts +18 -13
  437. package/src/capacity/types.ts +1 -1
  438. package/src/database/connection.ts +73 -68
  439. package/src/database/index.ts +6 -6
  440. package/src/database/migrations/005_agent_runtime.sql +2 -0
  441. package/src/database/schema.ts +40 -38
  442. package/src/durable/DurableAgent.ts +85 -85
  443. package/src/durable/DurableAgentRepository.ts +409 -260
  444. package/src/durable/process.ts +22 -21
  445. package/src/durable/run.ts +61 -51
  446. package/src/durable/utils.ts +46 -41
  447. package/src/index.ts +144 -103
  448. package/src/providers/claude/ClaudeAgentMapper.ts +51 -50
  449. package/src/providers/claude/ClaudeCodeAdapter.ts +144 -136
  450. package/src/providers/claude/ClaudeSessionLocator.ts +291 -293
  451. package/src/providers/claude/ClaudeSessionParser.ts +322 -338
  452. package/src/providers/claude/durable/ClaudeCliProbe.ts +42 -37
  453. package/src/providers/claude/durable/ClaudePrintAgentService.ts +63 -50
  454. package/src/providers/claude/durable/ClaudePrintRunner.ts +79 -69
  455. package/src/providers/claude/durable/ClaudeStreamParser.ts +91 -62
  456. package/src/providers/codex/CodexAdapter.ts +205 -0
  457. package/src/providers/codex/CodexAgentMapper.ts +35 -0
  458. package/src/providers/codex/CodexSessionLocator.ts +279 -0
  459. package/src/providers/codex/CodexSessionMapping.ts +89 -0
  460. package/src/providers/codex/CodexSessionParser.ts +385 -0
  461. package/src/providers/codex/durable/CodexCliProbe.ts +50 -42
  462. package/src/providers/codex/durable/CodexPrintAgentService.ts +63 -49
  463. package/src/providers/codex/durable/CodexPrintRunner.ts +79 -69
  464. package/src/providers/codex/durable/CodexStreamParser.ts +97 -84
  465. package/src/providers/pi/PiAdapter.ts +183 -0
  466. package/src/providers/pi/PiAgentMapper.ts +36 -0
  467. package/src/providers/pi/PiSessionLocator.ts +95 -0
  468. package/src/providers/pi/PiSessionParser.ts +284 -0
  469. package/src/providers/pi/PiSessionTracker.ts +87 -0
  470. package/src/providers/pi/durable/PiCliProbe.ts +42 -34
  471. package/src/providers/pi/durable/PiPrintAgentService.ts +61 -53
  472. package/src/providers/pi/durable/PiPrintProtocol.ts +31 -29
  473. package/src/providers/pi/durable/PiPrintRunner.ts +75 -68
  474. package/src/providers/pi/durable/PiStreamParser.ts +82 -72
  475. package/src/readiness/AgentReadiness.ts +585 -513
  476. package/src/runtime/ManagedAgentRuntime.ts +266 -0
  477. package/src/runtime/RuntimeFactory.ts +13 -0
  478. package/src/runtime/errors.ts +50 -0
  479. package/src/runtime/herdr/HerdrAgentRuntime.ts +190 -0
  480. package/src/runtime/herdr/HerdrCliClient.ts +78 -0
  481. package/src/runtime/herdr/HerdrErrors.ts +9 -0
  482. package/src/runtime/herdr/HerdrResponseParsers.ts +76 -0
  483. package/src/runtime/herdr/HerdrRuntimeRef.ts +41 -0
  484. package/src/runtime/tmux/PidPolling.ts +34 -0
  485. package/src/runtime/tmux/TmuxAgentRuntime.ts +113 -0
  486. package/src/runtime/tmux/TmuxRuntimeRef.ts +9 -0
  487. package/src/runtime/types.ts +56 -0
  488. package/src/terminal/TerminalFocusManager.ts +215 -212
  489. package/src/terminal/TmuxManager.ts +99 -92
  490. package/src/terminal/TtyWriter.ts +203 -194
  491. package/src/terminal/index.ts +4 -4
  492. package/src/utils/AgentRegistry.ts +299 -238
  493. package/src/utils/agent-requests.ts +16 -16
  494. package/src/utils/agents.ts +41 -38
  495. package/src/utils/applescript.ts +4 -4
  496. package/src/utils/index.ts +12 -6
  497. package/src/utils/matching.ts +43 -43
  498. package/src/utils/process.ts +261 -258
  499. package/src/utils/session.ts +66 -66
  500. package/src/utils/sortAgents.ts +35 -33
  501. package/tsconfig.json +2 -8
  502. package/vitest.config.ts +16 -16
  503. package/.eslintrc.json +0 -31
  504. package/dist/__tests__/adapters/PiAdapter.test.js +0 -618
  505. package/dist/__tests__/adapters/PiAdapter.test.js.map +0 -1
  506. package/dist/__tests__/database/DurableAgentsDatabase.test.js +0 -128
  507. package/dist/__tests__/database/DurableAgentsDatabase.test.js.map +0 -1
  508. package/dist/adapters/CodexAdapter.d.ts +0 -109
  509. package/dist/adapters/CodexAdapter.d.ts.map +0 -1
  510. package/dist/adapters/CodexAdapter.js +0 -710
  511. package/dist/adapters/CodexAdapter.js.map +0 -1
  512. package/dist/adapters/PiAdapter.d.ts +0 -63
  513. package/dist/adapters/PiAdapter.d.ts.map +0 -1
  514. package/dist/adapters/PiAdapter.js +0 -453
  515. package/dist/adapters/PiAdapter.js.map +0 -1
  516. package/src/__tests__/adapters/PiAdapter.test.ts +0 -458
  517. package/src/__tests__/database/DurableAgentsDatabase.test.ts +0 -102
  518. package/src/adapters/CodexAdapter.ts +0 -923
  519. package/src/adapters/PiAdapter.ts +0 -598
@@ -1,418 +1,522 @@
1
- import fs from 'fs';
2
- import os from 'os';
3
- import path from 'path';
4
- import Database from 'better-sqlite3';
5
- import { AgentRegistry, RenameNotFoundError, RenameConflictError, type RegistryEntry } from '../../utils/AgentRegistry.js';
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import Database from "better-sqlite3";
5
+ import {
6
+ AgentRegistry,
7
+ RenameNotFoundError,
8
+ RenameConflictError,
9
+ type RegistryEntry,
10
+ } from "../../utils/AgentRegistry.js";
6
11
 
7
12
  function makeEntry(over: Partial<RegistryEntry> = {}): RegistryEntry {
8
- return {
9
- name: 'agent1',
10
- type: 'claude',
11
- pid: process.pid,
12
- tmuxSession: 'agent1',
13
- cwd: '/tmp',
14
- startedAt: '2026-05-30T00:00:00.000Z',
15
- sessionId: 'sid-1',
16
- sessionFilePath: '/tmp/session.jsonl',
17
- pinned: false,
18
- ...over,
19
- };
13
+ return {
14
+ name: "agent1",
15
+ type: "claude",
16
+ pid: process.pid,
17
+ cwd: "/tmp",
18
+ startedAt: "2026-05-30T00:00:00.000Z",
19
+ sessionId: "sid-1",
20
+ sessionFilePath: "/tmp/session.jsonl",
21
+ pinned: false,
22
+ runtime: "tmux",
23
+ runtimeRef: { session: "agent1" },
24
+ ...over,
25
+ };
20
26
  }
21
27
 
22
- describe('AgentRegistry', () => {
23
- let tmpDir: string;
24
- let regPath: string;
25
- let registry: AgentRegistry;
26
-
27
- beforeEach(() => {
28
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-registry-'));
29
- regPath = path.join(tmpDir, 'nested', 'agents.json');
30
- registry = new AgentRegistry(regPath);
31
- });
32
-
33
- afterEach(() => {
34
- vi.restoreAllMocks();
35
- fs.rmSync(tmpDir, { recursive: true, force: true });
36
- });
37
-
38
- describe('register', () => {
39
- it('creates the SQLite database and parent directory if missing', () => {
40
- registry.register(makeEntry());
41
- expect(fs.existsSync(regPath.replace(/\.json$/, '.db'))).toBe(true);
42
- expect(registry.list()[0].name).toBe('agent1');
43
- });
44
-
45
- it('appends a new entry when name is unique', () => {
46
- registry.register(makeEntry({ name: 'a' }));
47
- registry.register(makeEntry({ name: 'b', pid: process.ppid }));
48
- expect(registry.list()).toHaveLength(2);
49
- });
50
-
51
- it('upserts in place when type and pid already exist', () => {
52
- registry.register(makeEntry({ name: 'a', pid: process.pid }));
53
- registry.register(makeEntry({ name: 'fallback', pid: process.pid, tmuxSession: '' }));
54
- const all = registry.list();
55
- expect(all).toHaveLength(1);
56
- expect(all[0].pid).toBe(process.pid);
57
- expect(all[0].name).toBe('a');
58
- });
59
-
60
- it('does not write through the legacy fixed .tmp path', () => {
61
- registry.register(makeEntry());
62
- expect(fs.existsSync(`${regPath}.tmp`)).toBe(false);
63
- });
64
-
65
- it('persists session fields', () => {
66
- registry.register(makeEntry({ sessionId: 'sid-xyz', sessionFilePath: '/foo/bar.jsonl' }));
67
- const saved = registry.list()[0];
68
- expect(saved.sessionId).toBe('sid-xyz');
69
- expect(saved.sessionFilePath).toBe('/foo/bar.jsonl');
70
- });
71
-
72
- it('preserves existing tmuxSession when incoming is empty string', () => {
73
- registry.register(makeEntry({ name: 'a', tmuxSession: 'pinned' }));
74
- registry.register(makeEntry({ name: 'fallback', tmuxSession: '', pid: process.pid }));
75
- const saved = registry.lookup('a');
76
- expect(saved?.tmuxSession).toBe('pinned');
77
- expect(saved?.pid).toBe(process.pid);
78
- });
79
-
80
- it('lets a managed start entry replace a generated fallback for the same pid', () => {
81
- registry.register(makeEntry({ name: `ai-devkit-${process.pid}`, tmuxSession: '' }));
82
- registry.register(makeEntry({ name: 'custom-name', tmuxSession: 'custom-name' }));
83
- expect(registry.lookup('custom-name')?.tmuxSession).toBe('custom-name');
84
- expect(registry.lookup(`ai-devkit-${process.pid}`)).toBeNull();
85
- expect(registry.list()).toHaveLength(1);
86
- });
87
-
88
- it('preserves an existing name conflict when its probe fails with EPERM', () => {
89
- registry.register(makeEntry({ name: 'claimed-name', pid: process.pid }));
90
- vi.spyOn(process, 'kill').mockImplementation(() => {
91
- throw Object.assign(new Error('operation not permitted'), { code: 'EPERM' });
92
- });
93
-
94
- expect(() => registry.register(makeEntry({
95
- name: 'claimed-name',
96
- pid: process.pid + 1,
97
- }))).toThrow();
98
- expect(registry.lookup('claimed-name')?.pid).toBe(process.pid);
99
- });
100
- });
101
-
102
- describe('registerBatch', () => {
103
- it('is a no-op on empty array', () => {
104
- registry.registerBatch([]);
105
- expect(fs.existsSync(regPath)).toBe(false);
106
- });
107
-
108
- it('upserts multiple entries in a single batch', () => {
109
- registry.registerBatch([
110
- makeEntry({ name: 'a' }),
111
- makeEntry({ name: 'b', pid: process.pid + 1 }),
112
- makeEntry({ name: 'c', pid: process.pid + 2 }),
113
- ]);
114
- expect(registry.list()).toHaveLength(3);
115
- });
116
-
117
- it('applies the tmuxSession merge per entry', () => {
118
- registry.register(makeEntry({ name: 'a', tmuxSession: 'pinned' }));
119
- registry.registerBatch([
120
- makeEntry({ name: 'fallback', tmuxSession: '', pid: process.pid }),
121
- makeEntry({ name: 'b', tmuxSession: '', pid: process.pid + 1 }),
122
- ]);
123
- expect(registry.lookup('a')?.tmuxSession).toBe('pinned');
124
- expect(registry.lookup('a')?.pid).toBe(process.pid);
125
- expect(registry.lookup('b')?.tmuxSession).toBe('');
126
- });
127
-
128
- it('handles concurrent registry instances without duplicate pid rows', () => {
129
- const other = new AgentRegistry(regPath);
130
- registry.register(makeEntry({ name: `ai-devkit-${process.pid}`, tmuxSession: '' }));
131
- other.register(makeEntry({ name: 'custom-name', tmuxSession: 'custom-name' }));
132
- registry.register(makeEntry({ name: `ai-devkit-${process.pid}`, tmuxSession: '' }));
133
-
134
- expect(registry.list()).toHaveLength(1);
135
- expect(registry.lookup('custom-name')?.pid).toBe(process.pid);
136
- });
137
-
138
- it('cleans up a cross-type row when its pid has been reused', () => {
139
- registry.register(makeEntry({ name: 'old-claude', type: 'claude', pid: process.pid }));
140
-
141
- registry.register(makeEntry({
142
- name: 'new-codex',
143
- type: 'codex',
144
- pid: process.pid,
145
- tmuxSession: '',
146
- }));
147
-
148
- expect(registry.lookup('old-claude')).toBeNull();
149
- expect(registry.lookup('new-codex')).toMatchObject({ type: 'codex', pid: process.pid });
150
- expect(registry.list()).toHaveLength(1);
151
- });
152
-
153
- it('rolls back the whole batch when a live name conflict rejects one entry', () => {
154
- registry.register(makeEntry({ name: 'taken', pid: process.pid }));
155
-
156
- expect(() => registry.registerBatch([
157
- makeEntry({ name: 'fresh', pid: 999998 }),
158
- makeEntry({ name: 'taken', type: 'codex', pid: 999997 }),
159
- ])).toThrow(/UNIQUE constraint failed/);
160
-
161
- expect(registry.lookup('fresh')).toBeNull();
162
- expect(registry.lookup('taken')?.pid).toBe(process.pid);
163
- });
164
- });
165
-
166
- describe('lookup', () => {
167
- it('returns null when name not found', () => {
168
- expect(registry.lookup('missing')).toBeNull();
169
- });
170
-
171
- it('returns the entry when name matches', () => {
172
- registry.register(makeEntry({ name: 'a' }));
173
- expect(registry.lookup('a')?.name).toBe('a');
174
- });
175
- });
176
-
177
- describe('pinning', () => {
178
- it('defaults new rows to unpinned and toggles the persisted state', () => {
179
- registry.register(makeEntry());
180
-
181
- expect(registry.lookup('agent1')?.pinned).toBe(false);
182
- expect(registry.togglePin('claude', process.pid)).toBe(true);
183
- expect(registry.lookup('agent1')?.pinned).toBe(true);
184
- expect(registry.togglePin('claude', process.pid)).toBe(false);
185
- expect(registry.lookup('agent1')?.pinned).toBe(false);
186
- });
187
-
188
- it('updates existing recency when toggled', () => {
189
- let now = new Date('2026-08-16T10:00:00.000Z');
190
- const clocked = new AgentRegistry(regPath, { now: () => now });
191
- clocked.register(makeEntry());
192
- now = new Date('2026-08-16T10:01:00.000Z');
193
-
194
- clocked.togglePin('claude', process.pid);
195
-
196
- expect(clocked.lookup('agent1')?.updatedAt).toBe(now.toISOString());
197
- const db = new Database(regPath.replace(/\.json$/, '.db'), { readonly: true });
198
- const row = db.prepare('SELECT updated_at FROM agents WHERE type = ? AND pid = ?')
199
- .get('claude', process.pid) as { updated_at: string };
200
- db.close();
201
- expect(row.updated_at).toBe(now.toISOString());
202
- });
203
-
204
- it('returns null when the process row has disappeared', () => {
205
- expect(registry.togglePin('claude', 999999)).toBeNull();
206
- });
207
-
208
- it('preserves a pin when poll registration updates the row', () => {
209
- registry.register(makeEntry({ sessionId: 'before' }));
210
- registry.togglePin('claude', process.pid);
211
-
212
- registry.register(makeEntry({ sessionId: 'after' }));
213
-
214
- expect(registry.lookup('agent1')).toMatchObject({ sessionId: 'after', pinned: true });
215
- });
216
-
217
- it('preserves a pin through rename', () => {
218
- registry.register(makeEntry({ name: 'before' }));
219
- registry.togglePin('claude', process.pid);
220
-
221
- registry.rename('before', 'after');
222
-
223
- expect(registry.lookup('after')?.pinned).toBe(true);
224
- });
225
-
226
- it('removes the pin with a pruned process row', () => {
227
- registry.register(makeEntry({ pid: 999999 }));
228
- registry.togglePin('claude', 999999);
229
-
230
- registry.prune();
231
-
232
- expect(registry.lookup('agent1')).toBeNull();
233
- });
28
+ describe("AgentRegistry", () => {
29
+ let tmpDir: string;
30
+ let regPath: string;
31
+ let registry: AgentRegistry;
32
+
33
+ beforeEach(() => {
34
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-registry-"));
35
+ regPath = path.join(tmpDir, "nested", "agents.json");
36
+ registry = new AgentRegistry(regPath);
37
+ });
38
+
39
+ afterEach(() => {
40
+ vi.restoreAllMocks();
41
+ fs.rmSync(tmpDir, { recursive: true, force: true });
42
+ });
43
+
44
+ describe("register", () => {
45
+ it("creates the SQLite database and parent directory if missing", () => {
46
+ registry.register(makeEntry());
47
+ expect(fs.existsSync(regPath.replace(/\.json$/, ".db"))).toBe(true);
48
+ expect(registry.list()[0].name).toBe("agent1");
49
+ });
50
+
51
+ it("appends a new entry when name is unique", () => {
52
+ registry.register(makeEntry({ name: "a" }));
53
+ registry.register(makeEntry({ name: "b", pid: process.ppid }));
54
+ expect(registry.list()).toHaveLength(2);
55
+ });
56
+
57
+ it("upserts in place when type and pid already exist", () => {
58
+ registry.register(makeEntry({ name: "a", pid: process.pid }));
59
+ registry.register(makeEntry({ name: "fallback", pid: process.pid, runtimeRef: null }));
60
+ const all = registry.list();
61
+ expect(all).toHaveLength(1);
62
+ expect(all[0].pid).toBe(process.pid);
63
+ expect(all[0].name).toBe("a");
64
+ });
65
+
66
+ it("does not write through the legacy fixed .tmp path", () => {
67
+ registry.register(makeEntry());
68
+ expect(fs.existsSync(`${regPath}.tmp`)).toBe(false);
69
+ });
70
+
71
+ it("persists session fields", () => {
72
+ registry.register(makeEntry({ sessionId: "sid-xyz", sessionFilePath: "/foo/bar.jsonl" }));
73
+ const saved = registry.list()[0];
74
+ expect(saved.sessionId).toBe("sid-xyz");
75
+ expect(saved.sessionFilePath).toBe("/foo/bar.jsonl");
76
+ });
77
+
78
+ it("persists runtime metadata for Herdr-backed entries", () => {
79
+ const runtimeRef = { session: "default", paneId: "w1:p2", agentName: "reviewer" };
80
+
81
+ registry.register(
82
+ makeEntry({
83
+ name: "reviewer",
84
+ runtime: "herdr",
85
+ runtimeRef,
86
+ }),
87
+ );
88
+
89
+ expect(registry.lookup("reviewer")).toMatchObject({
90
+ runtime: "herdr",
91
+ runtimeRef,
92
+ });
93
+ });
94
+
95
+ it("lets a Herdr managed start replace an existing same-pid name", () => {
96
+ const runtimeRef = { session: "default", paneId: "w1:p2", agentName: "requested-name" };
97
+
98
+ registry.register(makeEntry({ name: "old-name", pid: process.pid }));
99
+ registry.register(
100
+ makeEntry({
101
+ name: "requested-name",
102
+ pid: process.pid,
103
+ runtime: "herdr",
104
+ runtimeRef,
105
+ }),
106
+ );
107
+
108
+ expect(registry.lookup("old-name")).toBeNull();
109
+ expect(registry.lookup("requested-name")).toMatchObject({
110
+ pid: process.pid,
111
+ runtime: "herdr",
112
+ runtimeRef,
113
+ });
114
+ });
115
+
116
+ it("preserves existing runtime ref when incoming runtime ref is empty", () => {
117
+ registry.register(makeEntry({ name: "a", runtimeRef: { session: "pinned" } }));
118
+ registry.register(makeEntry({ name: "fallback", runtimeRef: null, pid: process.pid }));
119
+ const saved = registry.lookup("a");
120
+ expect(saved?.runtimeRef).toEqual({ session: "pinned" });
121
+ expect(saved?.pid).toBe(process.pid);
122
+ });
123
+
124
+ it("defaults an empty tmux runtime ref from the tmux session", () => {
125
+ registry.register(makeEntry({ name: "a", runtimeRef: null }));
126
+
127
+ expect(registry.lookup("a")?.runtimeRef).toBeNull();
128
+ });
129
+
130
+ it("lets a managed start entry replace a generated fallback for the same pid", () => {
131
+ registry.register(makeEntry({ name: `ai-devkit-${process.pid}`, runtimeRef: null }));
132
+ registry.register(makeEntry({ name: "custom-name", runtimeRef: { session: "custom-name" } }));
133
+ expect(registry.lookup("custom-name")?.runtimeRef).toEqual({ session: "custom-name" });
134
+ expect(registry.lookup(`ai-devkit-${process.pid}`)).toBeNull();
135
+ expect(registry.list()).toHaveLength(1);
136
+ });
137
+
138
+ it("preserves an existing name conflict when its probe fails with EPERM", () => {
139
+ registry.register(makeEntry({ name: "claimed-name", pid: process.pid }));
140
+ vi.spyOn(process, "kill").mockImplementation(() => {
141
+ throw Object.assign(new Error("operation not permitted"), { code: "EPERM" });
142
+ });
143
+
144
+ expect(() =>
145
+ registry.register(
146
+ makeEntry({
147
+ name: "claimed-name",
148
+ pid: process.pid + 1,
149
+ }),
150
+ ),
151
+ ).toThrow();
152
+ expect(registry.lookup("claimed-name")?.pid).toBe(process.pid);
153
+ });
154
+ });
155
+
156
+ describe("registerBatch", () => {
157
+ it("is a no-op on empty array", () => {
158
+ registry.registerBatch([]);
159
+ expect(fs.existsSync(regPath)).toBe(false);
160
+ });
161
+
162
+ it("upserts multiple entries in a single batch", () => {
163
+ registry.registerBatch([
164
+ makeEntry({ name: "a" }),
165
+ makeEntry({ name: "b", pid: process.pid + 1 }),
166
+ makeEntry({ name: "c", pid: process.pid + 2 }),
167
+ ]);
168
+ expect(registry.list()).toHaveLength(3);
169
+ });
170
+
171
+ it("applies the tmux runtime ref merge per entry", () => {
172
+ registry.register(makeEntry({ name: "a", runtimeRef: { session: "pinned" } }));
173
+ registry.registerBatch([
174
+ makeEntry({ name: "fallback", runtimeRef: null, pid: process.pid }),
175
+ makeEntry({ name: "b", runtimeRef: null, pid: process.pid + 1 }),
176
+ ]);
177
+ expect(registry.lookup("a")?.runtimeRef).toEqual({ session: "pinned" });
178
+ expect(registry.lookup("a")?.pid).toBe(process.pid);
179
+ expect(registry.lookup("b")?.runtimeRef).toBeNull();
180
+ });
181
+
182
+ it("handles concurrent registry instances without duplicate pid rows", () => {
183
+ const other = new AgentRegistry(regPath);
184
+ registry.register(makeEntry({ name: `ai-devkit-${process.pid}`, runtimeRef: null }));
185
+ other.register(makeEntry({ name: "custom-name", runtimeRef: { session: "custom-name" } }));
186
+ registry.register(makeEntry({ name: `ai-devkit-${process.pid}`, runtimeRef: null }));
187
+
188
+ expect(registry.list()).toHaveLength(1);
189
+ expect(registry.lookup("custom-name")?.pid).toBe(process.pid);
190
+ });
191
+
192
+ it("cleans up a cross-type row when its pid has been reused", () => {
193
+ registry.register(makeEntry({ name: "old-claude", type: "claude", pid: process.pid }));
194
+
195
+ registry.register(
196
+ makeEntry({
197
+ name: "new-codex",
198
+ type: "codex",
199
+ pid: process.pid,
200
+ runtimeRef: null,
201
+ }),
202
+ );
203
+
204
+ expect(registry.lookup("old-claude")).toBeNull();
205
+ expect(registry.lookup("new-codex")).toMatchObject({ type: "codex", pid: process.pid });
206
+ expect(registry.list()).toHaveLength(1);
207
+ });
208
+
209
+ it("rolls back the whole batch when a live name conflict rejects one entry", () => {
210
+ registry.register(makeEntry({ name: "taken", pid: process.pid }));
211
+
212
+ expect(() =>
213
+ registry.registerBatch([
214
+ makeEntry({ name: "fresh", pid: 999998 }),
215
+ makeEntry({ name: "taken", type: "codex", pid: 999997 }),
216
+ ]),
217
+ ).toThrow(/UNIQUE constraint failed/);
218
+
219
+ expect(registry.lookup("fresh")).toBeNull();
220
+ expect(registry.lookup("taken")?.pid).toBe(process.pid);
221
+ });
222
+ });
223
+
224
+ describe("lookup", () => {
225
+ it("returns null when name not found", () => {
226
+ expect(registry.lookup("missing")).toBeNull();
227
+ });
228
+
229
+ it("returns the entry when name matches", () => {
230
+ registry.register(makeEntry({ name: "a" }));
231
+ expect(registry.lookup("a")?.name).toBe("a");
232
+ });
233
+ });
234
+
235
+ describe("pinning", () => {
236
+ it("defaults new rows to unpinned and toggles the persisted state", () => {
237
+ registry.register(makeEntry());
238
+
239
+ expect(registry.lookup("agent1")?.pinned).toBe(false);
240
+ expect(registry.togglePin("claude", process.pid)).toBe(true);
241
+ expect(registry.lookup("agent1")?.pinned).toBe(true);
242
+ expect(registry.togglePin("claude", process.pid)).toBe(false);
243
+ expect(registry.lookup("agent1")?.pinned).toBe(false);
244
+ });
245
+
246
+ it("updates existing recency when toggled", () => {
247
+ let now = new Date("2026-08-16T10:00:00.000Z");
248
+ const clocked = new AgentRegistry(regPath, { now: () => now });
249
+ clocked.register(makeEntry());
250
+ now = new Date("2026-08-16T10:01:00.000Z");
251
+
252
+ clocked.togglePin("claude", process.pid);
253
+
254
+ expect(clocked.lookup("agent1")?.updatedAt).toBe(now.toISOString());
255
+ const db = new Database(regPath.replace(/\.json$/, ".db"), { readonly: true });
256
+ const row = db
257
+ .prepare("SELECT updated_at FROM agents WHERE type = ? AND pid = ?")
258
+ .get("claude", process.pid) as { updated_at: string };
259
+ db.close();
260
+ expect(row.updated_at).toBe(now.toISOString());
261
+ });
262
+
263
+ it("returns null when the process row has disappeared", () => {
264
+ expect(registry.togglePin("claude", 999999)).toBeNull();
265
+ });
266
+
267
+ it("preserves a pin when poll registration updates the row", () => {
268
+ registry.register(makeEntry({ sessionId: "before" }));
269
+ registry.togglePin("claude", process.pid);
270
+
271
+ registry.register(makeEntry({ sessionId: "after" }));
272
+
273
+ expect(registry.lookup("agent1")).toMatchObject({ sessionId: "after", pinned: true });
274
+ });
275
+
276
+ it("preserves a pin through rename", () => {
277
+ registry.register(makeEntry({ name: "before" }));
278
+ registry.togglePin("claude", process.pid);
279
+
280
+ registry.rename("before", "after");
281
+
282
+ expect(registry.lookup("after")?.pinned).toBe(true);
283
+ });
284
+
285
+ it("removes the pin with a pruned process row", () => {
286
+ registry.register(makeEntry({ pid: 999999 }));
287
+ registry.togglePin("claude", 999999);
288
+
289
+ registry.prune();
234
290
 
235
- it('reports a clear error when a readonly registry toggles a pin', () => {
236
- registry.register(makeEntry());
237
- const readonlyRegistry = new AgentRegistry(regPath, { readonly: true });
291
+ expect(registry.lookup("agent1")).toBeNull();
292
+ });
293
+
294
+ it("reports a clear error when a readonly registry toggles a pin", () => {
295
+ registry.register(makeEntry());
296
+ const readonlyRegistry = new AgentRegistry(regPath, { readonly: true });
297
+
298
+ expect(() => readonlyRegistry.togglePin("claude", process.pid)).toThrow(/readonly/i);
299
+ });
300
+ });
238
301
 
239
- expect(() => readonlyRegistry.togglePin('claude', process.pid)).toThrow(/readonly/i);
240
- });
302
+ describe("list", () => {
303
+ it("returns empty array when database does not contain entries", () => {
304
+ expect(registry.list()).toEqual([]);
241
305
  });
242
306
 
243
- describe('list', () => {
244
- it('returns empty array when database does not contain entries', () => {
245
- expect(registry.list()).toEqual([]);
246
- });
307
+ it("ignores existing legacy agents.json entries", () => {
308
+ const legacyEntry = makeEntry({ name: "legacy", runtimeRef: { session: "legacy" } });
309
+ fs.mkdirSync(path.dirname(regPath), { recursive: true });
310
+ fs.writeFileSync(regPath, JSON.stringify({ entries: [legacyEntry] }), "utf8");
247
311
 
248
- it('ignores existing legacy agents.json entries', () => {
249
- const legacyEntry = makeEntry({ name: 'legacy', tmuxSession: 'legacy' });
250
- fs.mkdirSync(path.dirname(regPath), { recursive: true });
251
- fs.writeFileSync(regPath, JSON.stringify({ entries: [legacyEntry] }), 'utf8');
252
-
253
- const legacyRegistry = new AgentRegistry(regPath);
254
-
255
- expect(legacyRegistry.lookup('legacy')).toBeNull();
256
- expect(legacyRegistry.list()).toEqual([]);
257
- expect(fs.existsSync(regPath.replace(/\.json$/, '.db'))).toBe(true);
258
- });
312
+ const legacyRegistry = new AgentRegistry(regPath);
313
+
314
+ expect(legacyRegistry.lookup("legacy")).toBeNull();
315
+ expect(legacyRegistry.list()).toEqual([]);
316
+ expect(fs.existsSync(regPath.replace(/\.json$/, ".db"))).toBe(true);
259
317
  });
260
318
 
261
- describe('isAlive', () => {
262
- it('returns true for the current process', () => {
263
- expect(registry.isAlive(makeEntry({ pid: process.pid }))).toBe(true);
264
- });
319
+ it("reads pre-runtime SQLite rows as tmux-backed records", () => {
320
+ const dbPath = regPath.replace(/\.json$/, ".db");
321
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
322
+ fs.rmSync(dbPath, { force: true });
323
+ fs.rmSync(`${dbPath}-wal`, { force: true });
324
+ fs.rmSync(`${dbPath}-shm`, { force: true });
325
+ const db = new Database(dbPath);
326
+ db.exec(`
327
+ CREATE TABLE agents (
328
+ type TEXT NOT NULL,
329
+ pid INTEGER NOT NULL,
330
+ name TEXT NOT NULL UNIQUE,
331
+ tmux_session TEXT NOT NULL DEFAULT '',
332
+ cwd TEXT NOT NULL DEFAULT '',
333
+ started_at TEXT NOT NULL,
334
+ session_id TEXT NOT NULL DEFAULT '',
335
+ session_file_path TEXT NOT NULL DEFAULT '',
336
+ updated_at TEXT NOT NULL,
337
+ PRIMARY KEY (type, pid)
338
+ );
339
+ INSERT INTO agents (
340
+ type, pid, name, tmux_session, cwd, started_at, session_id, session_file_path, updated_at
341
+ ) VALUES (
342
+ 'claude', ${process.pid}, 'legacy-sqlite', 'legacy-sqlite', '/tmp',
343
+ '2026-05-30T00:00:00.000Z', 'sid-1', '/tmp/session.jsonl', '2026-05-30T00:00:00.000Z'
344
+ );
345
+ PRAGMA user_version = 1;
346
+ `);
347
+ db.close();
348
+
349
+ const migratedRegistry = new AgentRegistry(regPath);
350
+
351
+ expect(migratedRegistry.lookup("legacy-sqlite")).toMatchObject({
352
+ runtime: "tmux",
353
+ runtimeRef: { session: "legacy-sqlite" },
354
+ });
355
+ });
356
+ });
265
357
 
266
- it('returns false for a PID that does not exist', () => {
267
- expect(registry.isAlive(makeEntry({ pid: 999999 }))).toBe(false);
268
- });
358
+ describe("isAlive", () => {
359
+ it("returns true for the current process", () => {
360
+ expect(registry.isAlive(makeEntry({ pid: process.pid }))).toBe(true);
361
+ });
269
362
 
270
- it('returns true when the process probe is forbidden with EPERM', () => {
271
- vi.spyOn(process, 'kill').mockImplementation(() => {
272
- throw Object.assign(new Error('operation not permitted'), { code: 'EPERM' });
273
- });
363
+ it("returns false for a PID that does not exist", () => {
364
+ expect(registry.isAlive(makeEntry({ pid: 999999 }))).toBe(false);
365
+ });
366
+
367
+ it("returns true when the process probe is forbidden with EPERM", () => {
368
+ vi.spyOn(process, "kill").mockImplementation(() => {
369
+ throw Object.assign(new Error("operation not permitted"), { code: "EPERM" });
370
+ });
371
+
372
+ expect(registry.isAlive(makeEntry())).toBe(true);
373
+ });
374
+
375
+ it("returns false when the process probe reports ESRCH", () => {
376
+ vi.spyOn(process, "kill").mockImplementation(() => {
377
+ throw Object.assign(new Error("no such process"), { code: "ESRCH" });
378
+ });
379
+
380
+ expect(registry.isAlive(makeEntry())).toBe(false);
381
+ });
382
+
383
+ it("returns true when the process probe fails without a definitive error code", () => {
384
+ vi.spyOn(process, "kill").mockImplementation(() => {
385
+ throw new Error("indeterminate probe failure");
386
+ });
387
+
388
+ expect(registry.isAlive(makeEntry())).toBe(true);
389
+ });
390
+ });
391
+
392
+ describe("prune", () => {
393
+ it("removes entries whose PIDs are dead", () => {
394
+ registry.register(makeEntry({ name: "alive", pid: process.pid }));
395
+ registry.register(makeEntry({ name: "dead", pid: 999999 }));
396
+ registry.prune();
397
+ const remaining = registry.list();
398
+ expect(remaining).toHaveLength(1);
399
+ expect(remaining[0].name).toBe("alive");
400
+ });
401
+
402
+ it("is a no-op when all entries are alive", () => {
403
+ registry.register(makeEntry({ pid: process.pid }));
404
+ const before = registry.list();
405
+ registry.prune();
406
+ const after = registry.list();
407
+ expect(after).toEqual(before);
408
+ });
409
+
410
+ it("preserves entries when liveness probing fails with EPERM", () => {
411
+ registry.register(makeEntry({ name: "custom-name", runtimeRef: { session: "tmux-custom" } }));
412
+ vi.spyOn(process, "kill").mockImplementation(() => {
413
+ throw Object.assign(new Error("operation not permitted"), { code: "EPERM" });
414
+ });
415
+
416
+ registry.prune();
417
+
418
+ expect(registry.lookup("custom-name")).toMatchObject({
419
+ name: "custom-name",
420
+ runtimeRef: { session: "tmux-custom" },
421
+ });
422
+ });
423
+
424
+ it("removes entries when liveness probing fails with ESRCH", () => {
425
+ registry.register(makeEntry({ name: "dead" }));
426
+ vi.spyOn(process, "kill").mockImplementation(() => {
427
+ throw Object.assign(new Error("no such process"), { code: "ESRCH" });
428
+ });
429
+
430
+ registry.prune();
431
+
432
+ expect(registry.lookup("dead")).toBeNull();
433
+ });
434
+
435
+ it("does nothing when file is missing", () => {
436
+ expect(() => registry.prune()).not.toThrow();
437
+ });
438
+
439
+ it("keeps forced prune available before the passive cadence is due", () => {
440
+ let nowMs = Date.parse("2026-08-14T10:00:00.000Z");
441
+ const clocked = new AgentRegistry(regPath, {
442
+ now: () => new Date(nowMs),
443
+ pruneIntervalMs: 30_000,
444
+ });
445
+ clocked.register(makeEntry({ name: "forced", pid: process.pid }));
446
+ const alive = vi.spyOn(clocked, "isAlive").mockReturnValue(true);
447
+ clocked.pruneIfDue();
448
+ alive.mockReturnValue(false);
449
+ nowMs += 1;
450
+
451
+ clocked.prune();
452
+
453
+ expect(alive).toHaveBeenCalledTimes(2);
454
+ expect(clocked.lookup("forced")).toBeNull();
455
+ });
456
+ });
457
+
458
+ describe("default()", () => {
459
+ it("returns a singleton instance", () => {
460
+ expect(AgentRegistry.default()).toBe(AgentRegistry.default());
461
+ });
462
+ });
463
+
464
+ describe("rename", () => {
465
+ it("updates the name of an existing entry", () => {
466
+ registry.register(makeEntry({ name: "old-name", pid: process.pid }));
467
+ registry.rename("old-name", "new-name");
468
+ expect(registry.lookup("new-name")?.name).toBe("new-name");
469
+ expect(registry.lookup("old-name")).toBeNull();
470
+ });
471
+
472
+ it("preserves all other fields on the renamed entry", () => {
473
+ registry.register(
474
+ makeEntry({
475
+ name: "old-name",
476
+ pid: process.pid,
477
+ runtimeRef: { session: "old-name" },
478
+ cwd: "/my/cwd",
479
+ }),
480
+ );
481
+ registry.rename("old-name", "new-name");
482
+ const entry = registry.lookup("new-name");
483
+ expect(entry?.runtimeRef).toEqual({ session: "old-name" });
484
+ expect(entry?.cwd).toBe("/my/cwd");
485
+ expect(entry?.pid).toBe(process.pid);
486
+ });
487
+
488
+ it("throws RenameNotFoundError when current name does not exist", () => {
489
+ expect(() => registry.rename("ghost", "new-name")).toThrow(RenameNotFoundError);
490
+ });
491
+
492
+ it("throws RenameConflictError when new name is already in use by a live entry", () => {
493
+ registry.register(makeEntry({ name: "agent-a", pid: process.pid }));
494
+ registry.register(makeEntry({ name: "agent-b", pid: process.ppid }));
495
+ expect(() => registry.rename("agent-a", "agent-b")).toThrow(RenameConflictError);
496
+ });
497
+
498
+ it("throws RenameConflictError when the conflicting entry probe fails with EPERM", () => {
499
+ registry.register(makeEntry({ name: "agent-a", pid: process.pid }));
500
+ registry.register(makeEntry({ name: "agent-b", pid: process.ppid }));
501
+ vi.spyOn(process, "kill").mockImplementation(() => {
502
+ throw Object.assign(new Error("operation not permitted"), { code: "EPERM" });
503
+ });
504
+
505
+ expect(() => registry.rename("agent-a", "agent-b")).toThrow(RenameConflictError);
506
+ expect(registry.lookup("agent-b")?.pid).toBe(process.ppid);
507
+ });
508
+
509
+ it("succeeds when new name exists only as a stale (dead) entry", () => {
510
+ registry.register(makeEntry({ name: "agent-a", pid: process.pid }));
511
+ registry.register(makeEntry({ name: "agent-b", pid: 999999 }));
512
+ expect(() => registry.rename("agent-a", "agent-b")).not.toThrow();
513
+ expect(registry.lookup("agent-b")?.pid).toBe(process.pid);
514
+ });
274
515
 
275
- expect(registry.isAlive(makeEntry())).toBe(true);
276
- });
277
-
278
- it('returns false when the process probe reports ESRCH', () => {
279
- vi.spyOn(process, 'kill').mockImplementation(() => {
280
- throw Object.assign(new Error('no such process'), { code: 'ESRCH' });
281
- });
282
-
283
- expect(registry.isAlive(makeEntry())).toBe(false);
284
- });
285
-
286
- it('returns true when the process probe fails without a definitive error code', () => {
287
- vi.spyOn(process, 'kill').mockImplementation(() => {
288
- throw new Error('indeterminate probe failure');
289
- });
290
-
291
- expect(registry.isAlive(makeEntry())).toBe(true);
292
- });
293
- });
294
-
295
- describe('prune', () => {
296
- it('removes entries whose PIDs are dead', () => {
297
- registry.register(makeEntry({ name: 'alive', pid: process.pid }));
298
- registry.register(makeEntry({ name: 'dead', pid: 999999 }));
299
- registry.prune();
300
- const remaining = registry.list();
301
- expect(remaining).toHaveLength(1);
302
- expect(remaining[0].name).toBe('alive');
303
- });
304
-
305
- it('is a no-op when all entries are alive', () => {
306
- registry.register(makeEntry({ pid: process.pid }));
307
- const before = registry.list();
308
- registry.prune();
309
- const after = registry.list();
310
- expect(after).toEqual(before);
311
- });
312
-
313
- it('preserves entries when liveness probing fails with EPERM', () => {
314
- registry.register(makeEntry({ name: 'custom-name', tmuxSession: 'tmux-custom' }));
315
- vi.spyOn(process, 'kill').mockImplementation(() => {
316
- throw Object.assign(new Error('operation not permitted'), { code: 'EPERM' });
317
- });
318
-
319
- registry.prune();
320
-
321
- expect(registry.lookup('custom-name')).toMatchObject({
322
- name: 'custom-name',
323
- tmuxSession: 'tmux-custom',
324
- });
325
- });
326
-
327
- it('removes entries when liveness probing fails with ESRCH', () => {
328
- registry.register(makeEntry({ name: 'dead' }));
329
- vi.spyOn(process, 'kill').mockImplementation(() => {
330
- throw Object.assign(new Error('no such process'), { code: 'ESRCH' });
331
- });
332
-
333
- registry.prune();
334
-
335
- expect(registry.lookup('dead')).toBeNull();
336
- });
337
-
338
- it('does nothing when file is missing', () => {
339
- expect(() => registry.prune()).not.toThrow();
340
- });
341
-
342
- it('keeps forced prune available before the passive cadence is due', () => {
343
- let nowMs = Date.parse('2026-08-14T10:00:00.000Z');
344
- const clocked = new AgentRegistry(regPath, {
345
- now: () => new Date(nowMs),
346
- pruneIntervalMs: 30_000,
347
- });
348
- clocked.register(makeEntry({ name: 'forced', pid: process.pid }));
349
- const alive = vi.spyOn(clocked, 'isAlive').mockReturnValue(true);
350
- clocked.pruneIfDue();
351
- alive.mockReturnValue(false);
352
- nowMs += 1;
353
-
354
- clocked.prune();
355
-
356
- expect(alive).toHaveBeenCalledTimes(2);
357
- expect(clocked.lookup('forced')).toBeNull();
358
- });
359
- });
360
-
361
- describe('default()', () => {
362
- it('returns a singleton instance', () => {
363
- expect(AgentRegistry.default()).toBe(AgentRegistry.default());
364
- });
365
- });
366
-
367
- describe('rename', () => {
368
- it('updates the name of an existing entry', () => {
369
- registry.register(makeEntry({ name: 'old-name', pid: process.pid }));
370
- registry.rename('old-name', 'new-name');
371
- expect(registry.lookup('new-name')?.name).toBe('new-name');
372
- expect(registry.lookup('old-name')).toBeNull();
373
- });
374
-
375
- it('preserves all other fields on the renamed entry', () => {
376
- registry.register(makeEntry({ name: 'old-name', pid: process.pid, tmuxSession: 'old-name', cwd: '/my/cwd' }));
377
- registry.rename('old-name', 'new-name');
378
- const entry = registry.lookup('new-name');
379
- expect(entry?.tmuxSession).toBe('old-name');
380
- expect(entry?.cwd).toBe('/my/cwd');
381
- expect(entry?.pid).toBe(process.pid);
382
- });
383
-
384
- it('throws RenameNotFoundError when current name does not exist', () => {
385
- expect(() => registry.rename('ghost', 'new-name')).toThrow(RenameNotFoundError);
386
- });
387
-
388
- it('throws RenameConflictError when new name is already in use by a live entry', () => {
389
- registry.register(makeEntry({ name: 'agent-a', pid: process.pid }));
390
- registry.register(makeEntry({ name: 'agent-b', pid: process.ppid }));
391
- expect(() => registry.rename('agent-a', 'agent-b')).toThrow(RenameConflictError);
392
- });
393
-
394
- it('throws RenameConflictError when the conflicting entry probe fails with EPERM', () => {
395
- registry.register(makeEntry({ name: 'agent-a', pid: process.pid }));
396
- registry.register(makeEntry({ name: 'agent-b', pid: process.ppid }));
397
- vi.spyOn(process, 'kill').mockImplementation(() => {
398
- throw Object.assign(new Error('operation not permitted'), { code: 'EPERM' });
399
- });
400
-
401
- expect(() => registry.rename('agent-a', 'agent-b')).toThrow(RenameConflictError);
402
- expect(registry.lookup('agent-b')?.pid).toBe(process.ppid);
403
- });
404
-
405
- it('succeeds when new name exists only as a stale (dead) entry', () => {
406
- registry.register(makeEntry({ name: 'agent-a', pid: process.pid }));
407
- registry.register(makeEntry({ name: 'agent-b', pid: 999999 }));
408
- expect(() => registry.rename('agent-a', 'agent-b')).not.toThrow();
409
- expect(registry.lookup('agent-b')?.pid).toBe(process.pid);
410
- });
411
-
412
- it('does not create the legacy fixed .tmp path on rename', () => {
413
- registry.register(makeEntry({ name: 'old-name', pid: process.pid }));
414
- registry.rename('old-name', 'new-name');
415
- expect(fs.existsSync(`${regPath}.tmp`)).toBe(false);
416
- });
516
+ it("does not create the legacy fixed .tmp path on rename", () => {
517
+ registry.register(makeEntry({ name: "old-name", pid: process.pid }));
518
+ registry.rename("old-name", "new-name");
519
+ expect(fs.existsSync(`${regPath}.tmp`)).toBe(false);
417
520
  });
521
+ });
418
522
  });