@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,15 +1,13 @@
1
- /**
2
- * Tests for CodexAdapter
3
- */ import * as fs from 'fs';
4
- import * as path from 'path';
5
- import { CodexAdapter } from '../../adapters/CodexAdapter.js';
6
- import { AgentStatus } from '../../adapters/AgentAdapter.js';
7
- import { AgentRegistry } from '../../utils/AgentRegistry.js';
8
- import { listAgentProcesses, enrichProcesses, captureProcessSnapshot } from '../../utils/process.js';
9
- import { batchGetSessionFileBirthtimes } from '../../utils/session.js';
10
- import { matchProcessesToSessions, generateAgentName } from '../../utils/matching.js';
11
- import * as os from 'os';
12
- vi.mock('../../utils/process.js', async (importOriginal)=>{
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import { CodexAdapter } from "../../providers/codex/CodexAdapter.js";
5
+ import { AgentStatus } from "../../adapters/AgentAdapter.js";
6
+ import { AgentRegistry } from "../../utils/AgentRegistry.js";
7
+ import { batchGetSessionFileBirthtimes } from "../../utils/session.js";
8
+ import { captureProcessSnapshot, enrichProcesses, listAgentProcesses } from "../../utils/process.js";
9
+ import { generateAgentName, matchProcessesToSessions } from "../../utils/matching.js";
10
+ vi.mock("../../utils/process.js", async (importOriginal)=>{
13
11
  const actual = await importOriginal();
14
12
  return {
15
13
  ...actual,
@@ -18,14 +16,14 @@ vi.mock('../../utils/process.js', async (importOriginal)=>{
18
16
  captureProcessSnapshot: vi.fn()
19
17
  };
20
18
  });
21
- vi.mock('../../utils/session.js', async ()=>{
22
- const actual = await vi.importActual('../../utils/session');
19
+ vi.mock("../../utils/session.js", async ()=>{
20
+ const actual = await vi.importActual("../../utils/session");
23
21
  return {
24
22
  ...actual,
25
23
  batchGetSessionFileBirthtimes: vi.fn()
26
24
  };
27
25
  });
28
- vi.mock('../../utils/matching.js', ()=>({
26
+ vi.mock("../../utils/matching.js", ()=>({
29
27
  matchProcessesToSessions: vi.fn(),
30
28
  generateAgentName: vi.fn()
31
29
  }));
@@ -35,2349 +33,352 @@ const mockedCaptureProcessSnapshot = captureProcessSnapshot;
35
33
  const mockedBatchGetSessionFileBirthtimes = batchGetSessionFileBirthtimes;
36
34
  const mockedMatchProcessesToSessions = matchProcessesToSessions;
37
35
  const mockedGenerateAgentName = generateAgentName;
38
- describe('CodexAdapter', ()=>{
36
+ describe("CodexAdapter", ()=>{
37
+ let originalHome;
38
+ let tmpHome;
39
39
  let adapter;
40
40
  beforeEach(()=>{
41
- adapter = new CodexAdapter();
41
+ originalHome = process.env.HOME;
42
+ tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "codex-adapter-"));
43
+ process.env.HOME = tmpHome;
44
+ adapter = new CodexAdapter(new AgentRegistry(path.join(tmpHome, "agents.json")));
42
45
  mockedListAgentProcesses.mockReset();
43
46
  mockedEnrichProcesses.mockReset();
44
47
  mockedCaptureProcessSnapshot.mockReset();
45
48
  mockedBatchGetSessionFileBirthtimes.mockReset();
46
49
  mockedMatchProcessesToSessions.mockReset();
47
50
  mockedGenerateAgentName.mockReset();
48
- // Default: enrichProcesses returns what it receives
49
- mockedEnrichProcesses.mockImplementation((procs)=>procs);
50
- // Compatibility shim for standalone adapter discovery; the manager captures once and slices by name.
51
+ mockedEnrichProcesses.mockImplementation((processes)=>processes);
51
52
  mockedCaptureProcessSnapshot.mockImplementation(async (names)=>enrichProcesses(names.flatMap((name)=>listAgentProcesses(name))));
52
- // Default: generateAgentName returns "folder (pid)"
53
- mockedGenerateAgentName.mockImplementation((cwd, pid)=>{
54
- const folder = path.basename(cwd) || 'unknown';
55
- return `${folder} (${pid})`;
56
- });
53
+ mockedGenerateAgentName.mockImplementation((cwd, pid)=>`${path.basename(cwd) || "unknown"} (${pid})`);
57
54
  });
58
- describe('initialization', ()=>{
59
- it('should expose codex type', ()=>{
60
- expect(adapter.type).toBe('codex');
55
+ afterEach(()=>{
56
+ process.env.HOME = originalHome;
57
+ fs.rmSync(tmpHome, {
58
+ recursive: true,
59
+ force: true
61
60
  });
62
61
  });
63
- describe('canHandle', ()=>{
64
- it('should return true for codex commands', ()=>{
65
- expect(adapter.canHandle({
66
- pid: 1,
67
- command: 'codex',
68
- cwd: '/repo',
69
- tty: 'ttys001'
70
- })).toBe(true);
71
- });
72
- it('should return true for codex with full path (case-insensitive)', ()=>{
73
- expect(adapter.canHandle({
74
- pid: 2,
75
- command: '/usr/local/bin/CODEX --sandbox workspace-write',
76
- cwd: '/repo',
77
- tty: 'ttys002'
78
- })).toBe(true);
79
- });
80
- it('should return false for non-codex processes', ()=>{
81
- expect(adapter.canHandle({
82
- pid: 3,
83
- command: 'node app.js',
84
- cwd: '/repo',
85
- tty: 'ttys003'
86
- })).toBe(false);
87
- });
88
- it('should return false for processes with "codex" only in path arguments', ()=>{
89
- expect(adapter.canHandle({
90
- pid: 4,
91
- command: 'node /worktrees/feature-codex-adapter-agent-manager-package/node_modules/nx/src/daemon/server/start.js',
92
- cwd: '/repo',
93
- tty: 'ttys004'
94
- })).toBe(false);
95
- });
62
+ it("exposes codex type and handles Codex executable names only", ()=>{
63
+ expect(adapter.type).toBe("codex");
64
+ expect(adapter.canHandle({
65
+ pid: 1,
66
+ command: "codex",
67
+ cwd: "/repo",
68
+ tty: "ttys001"
69
+ })).toBe(true);
70
+ expect(adapter.canHandle({
71
+ pid: 2,
72
+ command: "/usr/local/bin/CODEX --sandbox workspace-write",
73
+ cwd: "/repo",
74
+ tty: "ttys002"
75
+ })).toBe(true);
76
+ expect(adapter.canHandle({
77
+ pid: 3,
78
+ command: "node app.js",
79
+ cwd: "/repo",
80
+ tty: "ttys003"
81
+ })).toBe(false);
82
+ expect(adapter.canHandle({
83
+ pid: 4,
84
+ command: "node /worktrees/feature-codex-adapter-agent-manager-package/server.js",
85
+ cwd: "/repo",
86
+ tty: "ttys004"
87
+ })).toBe(false);
96
88
  });
97
- describe('detectAgents', ()=>{
98
- async function useRealSessionMatcher() {
99
- const actualMatching = await vi.importActual('../../utils/matching.js');
100
- mockedMatchProcessesToSessions.mockImplementation(actualMatching.matchProcessesToSessions);
101
- }
102
- it('should return empty list when no codex process is running', async ()=>{
103
- mockedListAgentProcesses.mockReturnValue([]);
104
- const agents = await adapter.detectAgents();
105
- expect(agents).toEqual([]);
106
- expect(mockedListAgentProcesses).toHaveBeenCalledWith('codex');
107
- });
108
- it('should return process-only agents when no sessions discovered', async ()=>{
109
- const processes = [
110
- {
111
- pid: 100,
112
- command: 'codex',
113
- cwd: '/repo-a',
114
- tty: 'ttys001'
115
- }
116
- ];
117
- mockedListAgentProcesses.mockReturnValue(processes);
118
- mockedEnrichProcesses.mockReturnValue(processes);
119
- // No sessions dir → discoverSessions returns []
120
- adapter.codexSessionsDir = '/nonexistent/sessions';
121
- const agents = await adapter.detectAgents();
122
- expect(agents).toHaveLength(1);
123
- expect(agents[0]).toMatchObject({
124
- type: 'codex',
125
- status: AgentStatus.RUNNING,
89
+ it("returns no agents when no Codex process is running", async ()=>{
90
+ mockedListAgentProcesses.mockReturnValue([]);
91
+ await expect(adapter.detectAgents()).resolves.toEqual([]);
92
+ expect(mockedListAgentProcesses).toHaveBeenCalledWith("codex");
93
+ });
94
+ it("returns process-only agents when no session files are discovered", async ()=>{
95
+ const processes = [
96
+ {
126
97
  pid: 100,
127
- projectPath: '/repo-a',
128
- sessionId: 'pid-100',
129
- summary: 'Codex process running'
130
- });
131
- });
132
- it('should detect agents with matched sessions', async ()=>{
133
- const processes = [
134
- {
135
- pid: 100,
136
- command: 'codex',
137
- cwd: '/repo-a',
138
- tty: 'ttys001',
139
- startTime: new Date('2026-03-18T15:00:00.000Z')
140
- }
141
- ];
142
- mockedListAgentProcesses.mockReturnValue(processes);
143
- mockedEnrichProcesses.mockReturnValue(processes);
144
- // Set up sessions dir with date directory
145
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-test-'));
146
- const sessionsDir = path.join(tmpDir, 'sessions');
147
- const dateDir = path.join(sessionsDir, '2026', '03', '18');
148
- fs.mkdirSync(dateDir, {
149
- recursive: true
150
- });
151
- // Create session file with recent timestamps so status isn't idle
152
- const now = new Date();
153
- const recentTs = now.toISOString();
154
- const sessionFile = path.join(dateDir, 'sess-abc.jsonl');
155
- fs.writeFileSync(sessionFile, [
156
- JSON.stringify({
157
- type: 'session_meta',
158
- payload: {
159
- id: 'sess-abc',
160
- timestamp: recentTs,
161
- cwd: '/repo-a'
162
- }
163
- }),
164
- JSON.stringify({
165
- type: 'event',
166
- timestamp: recentTs,
167
- payload: {
168
- type: 'token_count',
169
- message: 'Implement adapter flow'
170
- }
171
- })
172
- ].join('\n'));
173
- adapter.codexSessionsDir = sessionsDir;
174
- const sessionFiles = [
175
- {
176
- sessionId: 'sess-abc',
177
- filePath: sessionFile,
178
- projectDir: dateDir,
179
- birthtimeMs: new Date('2026-03-18T15:00:05Z').getTime(),
180
- resolvedCwd: ''
181
- }
182
- ];
183
- mockedBatchGetSessionFileBirthtimes.mockReturnValue(sessionFiles);
184
- const matches = [
185
- {
186
- process: processes[0],
187
- session: {
188
- ...sessionFiles[0],
189
- resolvedCwd: '/repo-a'
190
- },
191
- deltaMs: 5000
192
- }
193
- ];
194
- mockedMatchProcessesToSessions.mockReturnValue(matches);
195
- const agents = await adapter.detectAgents();
196
- expect(agents).toHaveLength(1);
197
- expect(agents[0]).toMatchObject({
198
- type: 'codex',
98
+ command: "codex",
99
+ cwd: "/repo-a",
100
+ tty: "ttys001"
101
+ }
102
+ ];
103
+ mockedListAgentProcesses.mockReturnValue(processes);
104
+ await expect(adapter.detectAgents()).resolves.toMatchObject([
105
+ {
106
+ type: "codex",
199
107
  status: AgentStatus.RUNNING,
200
108
  pid: 100,
201
- projectPath: '/repo-a',
202
- sessionId: 'sess-abc',
203
- summary: 'Implement adapter flow'
204
- });
205
- fs.rmSync(tmpDir, {
206
- recursive: true,
207
- force: true
208
- });
209
- });
210
- it('should match when session_meta timestamp and file birthtime both align with process start', async ()=>{
211
- await useRealSessionMatcher();
212
- const processStart = new Date('2026-03-18T15:00:00.000Z');
213
- const sessionTimestamp = '2026-03-18T15:00:05.000Z';
214
- const processes = [
215
- {
216
- pid: 101,
217
- command: 'codex',
218
- cwd: '/repo-a',
219
- tty: 'ttys001',
220
- startTime: processStart
221
- }
222
- ];
223
- mockedListAgentProcesses.mockReturnValue(processes);
224
- mockedEnrichProcesses.mockReturnValue(processes);
225
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-test-'));
226
- const sessionsDir = path.join(tmpDir, 'sessions');
227
- const dateDir = path.join(sessionsDir, '2026', '03', '18');
228
- fs.mkdirSync(dateDir, {
229
- recursive: true
230
- });
231
- const sessionFile = path.join(dateDir, 'sess-aligned.jsonl');
232
- fs.writeFileSync(sessionFile, [
233
- JSON.stringify({
234
- type: 'session_meta',
235
- payload: {
236
- id: 'sess-aligned',
237
- timestamp: sessionTimestamp,
238
- cwd: '/repo-a'
239
- }
240
- }),
241
- JSON.stringify({
242
- type: 'event',
243
- timestamp: sessionTimestamp,
244
- payload: {
245
- type: 'token_count',
246
- message: 'Aligned session'
247
- }
248
- })
249
- ].join('\n'));
250
- adapter.codexSessionsDir = sessionsDir;
251
- mockedBatchGetSessionFileBirthtimes.mockReturnValue([
252
- {
253
- sessionId: 'sess-aligned',
254
- filePath: sessionFile,
255
- projectDir: dateDir,
256
- birthtimeMs: new Date(sessionTimestamp).getTime(),
257
- resolvedCwd: ''
258
- }
259
- ]);
260
- const agents = await adapter.detectAgents();
261
- expect(agents).toHaveLength(1);
262
- expect(agents[0]).toMatchObject({
263
- pid: 101,
264
- sessionId: 'sess-aligned',
265
- summary: 'Aligned session'
266
- });
267
- fs.rmSync(tmpDir, {
268
- recursive: true,
269
- force: true
270
- });
271
- });
272
- it('should match late-created session files using session_meta timestamp', async ()=>{
273
- await useRealSessionMatcher();
274
- const processStart = new Date('2026-03-18T15:00:00.000Z');
275
- const sessionTimestamp = '2026-03-18T15:00:10.000Z';
276
- const lateBirthtime = new Date('2026-03-18T15:05:30.000Z').getTime();
277
- const processes = [
278
- {
279
- pid: 102,
280
- command: 'codex',
281
- cwd: '/repo-a',
282
- tty: 'ttys001',
283
- startTime: processStart
284
- }
285
- ];
286
- mockedListAgentProcesses.mockReturnValue(processes);
287
- mockedEnrichProcesses.mockReturnValue(processes);
288
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-test-'));
289
- const sessionsDir = path.join(tmpDir, 'sessions');
290
- const dateDir = path.join(sessionsDir, '2026', '03', '18');
291
- fs.mkdirSync(dateDir, {
292
- recursive: true
293
- });
294
- const sessionFile = path.join(dateDir, 'sess-late.jsonl');
295
- fs.writeFileSync(sessionFile, [
296
- JSON.stringify({
297
- type: 'session_meta',
298
- payload: {
299
- id: 'sess-late',
300
- timestamp: sessionTimestamp,
301
- cwd: '/repo-a'
302
- }
303
- }),
304
- JSON.stringify({
305
- type: 'event',
306
- timestamp: sessionTimestamp,
307
- payload: {
308
- type: 'token_count',
309
- message: 'Late file session'
310
- }
311
- })
312
- ].join('\n'));
313
- adapter.codexSessionsDir = sessionsDir;
314
- mockedBatchGetSessionFileBirthtimes.mockReturnValue([
315
- {
316
- sessionId: 'sess-late',
317
- filePath: sessionFile,
318
- projectDir: dateDir,
319
- birthtimeMs: lateBirthtime,
320
- resolvedCwd: ''
321
- }
322
- ]);
323
- const agents = await adapter.detectAgents();
324
- expect(agents).toHaveLength(1);
325
- expect(agents[0]).toMatchObject({
326
- pid: 102,
327
- sessionId: 'sess-late',
328
- summary: 'Late file session'
329
- });
330
- expect(mockedMatchProcessesToSessions.mock.calls[0][1][0].birthtimeMs).toBe(new Date(sessionTimestamp).getTime());
331
- fs.rmSync(tmpDir, {
332
- recursive: true,
333
- force: true
334
- });
335
- });
336
- it.each([
337
- [
338
- 'missing',
339
- undefined
340
- ],
341
- [
342
- 'invalid',
343
- 'not-a-date'
344
- ]
345
- ])('should fall back to file birthtime when session_meta timestamp is %s', async (_label, metaTimestamp)=>{
346
- await useRealSessionMatcher();
347
- const processStart = new Date('2026-03-18T15:00:00.000Z');
348
- const fileBirthtime = new Date('2026-03-18T15:00:20.000Z').getTime();
349
- const processes = [
350
- {
351
- pid: 103,
352
- command: 'codex',
353
- cwd: '/repo-a',
354
- tty: 'ttys001',
355
- startTime: processStart
356
- }
357
- ];
358
- mockedListAgentProcesses.mockReturnValue(processes);
359
- mockedEnrichProcesses.mockReturnValue(processes);
360
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-test-'));
361
- const sessionsDir = path.join(tmpDir, 'sessions');
362
- const dateDir = path.join(sessionsDir, '2026', '03', '18');
363
- fs.mkdirSync(dateDir, {
364
- recursive: true
365
- });
366
- const payload = {
367
- id: `sess-${_label}`,
368
- cwd: '/repo-a'
369
- };
370
- if (metaTimestamp !== undefined) {
371
- payload.timestamp = metaTimestamp;
109
+ projectPath: "/repo-a",
110
+ sessionId: "pid-100",
111
+ summary: "Codex process running"
372
112
  }
373
- const sessionFile = path.join(dateDir, `sess-${_label}.jsonl`);
374
- fs.writeFileSync(sessionFile, [
375
- JSON.stringify({
376
- type: 'session_meta',
377
- payload
378
- }),
379
- JSON.stringify({
380
- type: 'event',
381
- timestamp: '2026-03-18T15:00:30.000Z',
382
- payload: {
383
- type: 'token_count',
384
- message: `${_label} timestamp session`
385
- }
386
- })
387
- ].join('\n'));
388
- adapter.codexSessionsDir = sessionsDir;
389
- mockedBatchGetSessionFileBirthtimes.mockReturnValue([
390
- {
391
- sessionId: `sess-${_label}`,
392
- filePath: sessionFile,
393
- projectDir: dateDir,
394
- birthtimeMs: fileBirthtime,
395
- resolvedCwd: ''
396
- }
397
- ]);
398
- const agents = await adapter.detectAgents();
399
- expect(agents).toHaveLength(1);
400
- expect(agents[0].sessionId).toBe(`sess-${_label}`);
401
- expect(mockedMatchProcessesToSessions.mock.calls[0][1][0].birthtimeMs).toBe(fileBirthtime);
402
- fs.rmSync(tmpDir, {
403
- recursive: true,
404
- force: true
405
- });
406
- });
407
- it('should fall back to process-only for unmatched processes', async ()=>{
408
- const processes = [
409
- {
410
- pid: 100,
411
- command: 'codex',
412
- cwd: '/repo-a',
413
- tty: 'ttys001',
414
- startTime: new Date()
415
- },
416
- {
417
- pid: 200,
418
- command: 'codex',
419
- cwd: '/repo-b',
420
- tty: 'ttys002',
421
- startTime: new Date()
422
- }
423
- ];
424
- mockedListAgentProcesses.mockReturnValue(processes);
425
- mockedEnrichProcesses.mockReturnValue(processes);
426
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-test-'));
427
- const sessionsDir = path.join(tmpDir, 'sessions');
428
- const now = new Date();
429
- const dateDir = path.join(sessionsDir, String(now.getFullYear()), String(now.getMonth() + 1).padStart(2, '0'), String(now.getDate()).padStart(2, '0'));
430
- fs.mkdirSync(dateDir, {
431
- recursive: true
432
- });
433
- const sessionFile = path.join(dateDir, 'only-session.jsonl');
434
- fs.writeFileSync(sessionFile, JSON.stringify({
435
- type: 'session_meta',
113
+ ]);
114
+ });
115
+ it("maps a running Codex process through legacy session matching", async ()=>{
116
+ const processInfo = {
117
+ pid: 100,
118
+ command: "codex",
119
+ cwd: "/repo-a",
120
+ tty: "ttys001",
121
+ startTime: new Date("2026-03-18T15:00:00.000Z")
122
+ };
123
+ const { sessionFile, dateDir } = writeSession("sess-abc", "/repo-a", [
124
+ {
125
+ type: "event",
126
+ timestamp: new Date().toISOString(),
436
127
  payload: {
437
- id: 'only-session',
438
- timestamp: now.toISOString(),
439
- cwd: '/repo-a'
440
- }
441
- }));
442
- adapter.codexSessionsDir = sessionsDir;
443
- const sessionFiles = [
444
- {
445
- sessionId: 'only-session',
446
- filePath: sessionFile,
447
- projectDir: dateDir,
448
- birthtimeMs: Date.now(),
449
- resolvedCwd: ''
128
+ type: "token_count",
129
+ message: "Implement adapter flow"
450
130
  }
451
- ];
452
- mockedBatchGetSessionFileBirthtimes.mockReturnValue(sessionFiles);
453
- // Only process 100 matches
454
- const matches = [
455
- {
456
- process: processes[0],
457
- session: {
458
- ...sessionFiles[0],
459
- resolvedCwd: '/repo-a'
460
- },
461
- deltaMs: 5000
462
- }
463
- ];
464
- mockedMatchProcessesToSessions.mockReturnValue(matches);
465
- const agents = await adapter.detectAgents();
466
- expect(agents).toHaveLength(2);
467
- const matched = agents.find((a)=>a.pid === 100);
468
- const unmatched = agents.find((a)=>a.pid === 200);
469
- expect(matched?.sessionId).toBe('only-session');
470
- expect(unmatched?.sessionId).toBe('pid-200');
471
- expect(unmatched?.status).toBe(AgentStatus.RUNNING);
472
- fs.rmSync(tmpDir, {
473
- recursive: true,
474
- force: true
475
- });
476
- });
477
- it('should match resumed sessions by id even when the JSONL predates the process', async ()=>{
478
- const sessionId = '019eabed-4079-7071-9531-b853ddd9914e';
479
- const processes = [
480
- {
481
- pid: 88018,
482
- command: `codex resume ${sessionId}`,
483
- cwd: '/repo-a',
484
- tty: 'ttys001',
485
- startTime: new Date('2026-06-10T12:00:00.000Z')
486
- }
487
- ];
488
- mockedListAgentProcesses.mockReturnValue(processes);
489
- mockedEnrichProcesses.mockReturnValue(processes);
490
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-resume-'));
491
- const sessionsDir = path.join(tmpDir, 'sessions');
492
- const dateDir = path.join(sessionsDir, '2026', '06', '09');
493
- fs.mkdirSync(dateDir, {
494
- recursive: true
495
- });
496
- const recentTs = new Date().toISOString();
497
- const sessionFile = path.join(dateDir, `rollout-2026-06-09T12-28-33-${sessionId}.jsonl`);
498
- fs.writeFileSync(sessionFile, [
499
- JSON.stringify({
500
- type: 'session_meta',
501
- payload: {
502
- id: sessionId,
503
- timestamp: recentTs,
504
- cwd: '/repo-a'
505
- }
506
- }),
507
- JSON.stringify({
508
- type: 'event',
509
- timestamp: recentTs,
510
- payload: {
511
- type: 'agent_message',
512
- message: 'resumed codex conversation'
513
- }
514
- })
515
- ].join('\n'));
516
- adapter.codexSessionsDir = sessionsDir;
517
- mockedBatchGetSessionFileBirthtimes.mockReturnValue([]);
518
- const collectAllSpy = vi.spyOn(adapter, 'collectAllSessionFiles');
519
- try {
520
- const agents = await adapter.detectAgents();
521
- expect(mockedBatchGetSessionFileBirthtimes).not.toHaveBeenCalled();
522
- expect(mockedMatchProcessesToSessions).not.toHaveBeenCalled();
523
- expect(collectAllSpy).not.toHaveBeenCalled();
524
- expect(agents).toHaveLength(1);
525
- expect(agents[0]).toMatchObject({
526
- type: 'codex',
527
- pid: 88018,
528
- sessionId,
529
- projectPath: '/repo-a',
530
- sessionFilePath: sessionFile
531
- });
532
- expect(agents[0].summary).toBe('resumed codex conversation');
533
- } finally{
534
- collectAllSpy.mockRestore();
535
- fs.rmSync(tmpDir, {
536
- recursive: true,
537
- force: true
538
- });
539
131
  }
540
- });
541
- it('should fall back to all session files for non-time-sortable resume ids', async ()=>{
542
- const sessionId = 'aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee';
543
- const processes = [
544
- {
545
- pid: 88020,
546
- command: `codex resume ${sessionId}`,
547
- cwd: '/repo-a',
548
- tty: 'ttys001',
549
- startTime: new Date('2026-06-10T12:00:00.000Z')
550
- }
551
- ];
552
- mockedListAgentProcesses.mockReturnValue(processes);
553
- mockedEnrichProcesses.mockReturnValue(processes);
554
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-resume-v4-'));
555
- const sessionsDir = path.join(tmpDir, 'sessions');
556
- const dateDir = path.join(sessionsDir, '2026', '01', '02');
557
- fs.mkdirSync(dateDir, {
558
- recursive: true
559
- });
560
- const recentTs = new Date().toISOString();
561
- const sessionFile = path.join(dateDir, `${sessionId}.jsonl`);
562
- fs.writeFileSync(sessionFile, [
563
- JSON.stringify({
564
- type: 'session_meta',
565
- payload: {
566
- id: sessionId,
567
- timestamp: recentTs,
568
- cwd: '/repo-a'
569
- }
570
- }),
571
- JSON.stringify({
572
- type: 'event',
573
- timestamp: recentTs,
574
- payload: {
575
- type: 'agent_message',
576
- message: 'legacy id conversation'
577
- }
578
- })
579
- ].join('\n'));
580
- adapter.codexSessionsDir = sessionsDir;
581
- mockedBatchGetSessionFileBirthtimes.mockReturnValue([]);
582
- const collectAllSpy = vi.spyOn(adapter, 'collectAllSessionFiles');
583
- try {
584
- const agents = await adapter.detectAgents();
585
- expect(collectAllSpy).toHaveBeenCalledOnce();
586
- expect(agents).toHaveLength(1);
587
- expect(agents[0]).toMatchObject({
588
- pid: 88020,
589
- sessionId,
590
- sessionFilePath: sessionFile
591
- });
592
- } finally{
593
- collectAllSpy.mockRestore();
594
- fs.rmSync(tmpDir, {
595
- recursive: true,
596
- force: true
597
- });
132
+ ]);
133
+ const candidate = {
134
+ sessionId: "sess-abc",
135
+ filePath: sessionFile,
136
+ projectDir: dateDir,
137
+ birthtimeMs: new Date("2026-03-18T15:00:05Z").getTime(),
138
+ resolvedCwd: ""
139
+ };
140
+ const matches = [
141
+ {
142
+ process: processInfo,
143
+ session: {
144
+ ...candidate,
145
+ resolvedCwd: "/repo-a"
146
+ },
147
+ deltaMs: 5000
598
148
  }
149
+ ];
150
+ mockedListAgentProcesses.mockReturnValue([
151
+ processInfo
152
+ ]);
153
+ mockedBatchGetSessionFileBirthtimes.mockReturnValue([
154
+ candidate
155
+ ]);
156
+ mockedMatchProcessesToSessions.mockReturnValue(matches);
157
+ const agents = await adapter.detectAgents();
158
+ expect(agents).toHaveLength(1);
159
+ expect(agents[0]).toMatchObject({
160
+ type: "codex",
161
+ status: AgentStatus.RUNNING,
162
+ pid: 100,
163
+ projectPath: "/repo-a",
164
+ sessionId: "sess-abc",
165
+ summary: "Implement adapter flow"
599
166
  });
600
- it('should fall back to process-only when a resumed session becomes unreadable after direct matching', async ()=>{
601
- const sessionId = '019eabed-4079-7071-9531-b853ddd9914e';
602
- const processes = [
603
- {
604
- pid: 88019,
605
- command: `codex resume ${sessionId}`,
606
- cwd: '/repo-a',
607
- tty: 'ttys001',
608
- startTime: new Date('2026-06-10T12:00:00.000Z')
167
+ });
168
+ it("maps resumed sessions directly by id", async ()=>{
169
+ const sessionId = "019eabed-4079-7071-9531-b853ddd9914e";
170
+ const processInfo = {
171
+ pid: 88018,
172
+ command: `codex resume ${sessionId}`,
173
+ cwd: "/repo-a",
174
+ tty: "ttys001",
175
+ startTime: new Date("2026-06-10T12:00:00.000Z")
176
+ };
177
+ const { sessionFile } = writeSession(sessionId, "/repo-a", [
178
+ {
179
+ type: "event",
180
+ timestamp: new Date().toISOString(),
181
+ payload: {
182
+ type: "agent_message",
183
+ message: "resumed codex conversation"
609
184
  }
610
- ];
611
- mockedListAgentProcesses.mockReturnValue(processes);
612
- mockedEnrichProcesses.mockReturnValue(processes);
613
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-resume-gone-'));
614
- const sessionsDir = path.join(tmpDir, 'sessions');
615
- const dateDir = path.join(sessionsDir, '2026', '06', '09');
616
- fs.mkdirSync(dateDir, {
617
- recursive: true
618
- });
619
- const sessionFile = path.join(dateDir, `rollout-2026-06-09T12-28-33-${sessionId}.jsonl`);
620
- adapter.codexSessionsDir = sessionsDir;
621
- const findSpy = vi.spyOn(adapter, 'findSessionFileById').mockReturnValue({
622
- sessionId,
623
- filePath: sessionFile,
624
- projectDir: dateDir,
625
- birthtimeMs: Date.now(),
626
- resolvedCwd: '/repo-a'
627
- });
628
- mockedBatchGetSessionFileBirthtimes.mockReturnValue([]);
629
- try {
630
- const agents = await adapter.detectAgents();
631
- expect(agents).toHaveLength(1);
632
- expect(agents[0]).toMatchObject({
633
- type: 'codex',
634
- pid: 88019,
635
- sessionId: 'pid-88019',
636
- projectPath: '/repo-a'
637
- });
638
- expect(agents[0].sessionFilePath).toBeUndefined();
639
- } finally{
640
- findSpy.mockRestore();
641
- fs.rmSync(tmpDir, {
642
- recursive: true,
643
- force: true
644
- });
645
- }
646
- });
647
- it('should map a running Codex process from the hook session mapping', async ()=>{
648
- const originalHome = process.env.HOME;
649
- const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-mapping-'));
650
- process.env.HOME = tmpHome;
651
- try {
652
- const mappedAdapter = new CodexAdapter(new AgentRegistry(path.join(tmpHome, 'agents.json')));
653
- const sessionsDir = path.join(tmpHome, '.codex', 'sessions');
654
- const dateDir = path.join(sessionsDir, '2026', '06', '26');
655
- const mappingPath = path.join(tmpHome, '.codex', 'ai-devkit', 'sessions.json');
656
- const sessionFile = path.join(dateDir, 'rollout-2026-06-26T09-56-12-sess-mapped.jsonl');
657
- const recentTs = new Date().toISOString();
658
- fs.mkdirSync(dateDir, {
659
- recursive: true
660
- });
661
- fs.mkdirSync(path.dirname(mappingPath), {
662
- recursive: true
663
- });
664
- fs.writeFileSync(sessionFile, [
665
- JSON.stringify({
666
- type: 'session_meta',
667
- payload: {
668
- id: 'sess-mapped',
669
- timestamp: recentTs,
670
- cwd: '/repo-mapped'
671
- }
672
- }),
673
- JSON.stringify({
674
- type: 'event',
675
- timestamp: recentTs,
676
- payload: {
677
- type: 'agent_message',
678
- message: 'mapped conversation'
679
- }
680
- })
681
- ].join('\n'));
682
- fs.writeFileSync(mappingPath, JSON.stringify({
683
- 5151: sessionFile
684
- }));
685
- const processes = [
686
- {
687
- pid: 5151,
688
- command: 'codex',
689
- cwd: '/repo-mapped',
690
- tty: 'ttys001',
691
- startTime: new Date('2026-06-26T09:56:12.000Z')
692
- }
693
- ];
694
- mockedListAgentProcesses.mockReturnValue(processes);
695
- mockedEnrichProcesses.mockReturnValue(processes);
696
- const agents = await mappedAdapter.detectAgents();
697
- expect(agents).toHaveLength(1);
698
- expect(agents[0]).toMatchObject({
699
- type: 'codex',
700
- pid: 5151,
701
- sessionId: 'sess-mapped',
702
- projectPath: '/repo-mapped',
703
- summary: 'mapped conversation',
704
- sessionFilePath: sessionFile
705
- });
706
- expect(mockedBatchGetSessionFileBirthtimes).not.toHaveBeenCalled();
707
- expect(mockedMatchProcessesToSessions).not.toHaveBeenCalled();
708
- } finally{
709
- process.env.HOME = originalHome;
710
- fs.rmSync(tmpHome, {
711
- recursive: true,
712
- force: true
713
- });
714
- }
715
- });
716
- it('should fall back to legacy matching when mapped session file cannot be parsed', async ()=>{
717
- const originalHome = process.env.HOME;
718
- const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-mapping-fallback-'));
719
- process.env.HOME = tmpHome;
720
- try {
721
- const mappedAdapter = new CodexAdapter(new AgentRegistry(path.join(tmpHome, 'agents.json')));
722
- const sessionsDir = path.join(tmpHome, '.codex', 'sessions');
723
- const dateDir = path.join(sessionsDir, '2026', '06', '26');
724
- const mappingPath = path.join(tmpHome, '.codex', 'ai-devkit', 'sessions.json');
725
- const badSessionFile = path.join(dateDir, 'bad.jsonl');
726
- const fallbackSessionFile = path.join(dateDir, 'fallback.jsonl');
727
- const recentTs = new Date().toISOString();
728
- fs.mkdirSync(dateDir, {
729
- recursive: true
730
- });
731
- fs.mkdirSync(path.dirname(mappingPath), {
732
- recursive: true
733
- });
734
- fs.writeFileSync(badSessionFile, '{not-json');
735
- fs.writeFileSync(fallbackSessionFile, [
736
- JSON.stringify({
737
- type: 'session_meta',
738
- payload: {
739
- id: 'sess-fallback',
740
- timestamp: recentTs,
741
- cwd: '/repo-fallback'
742
- }
743
- }),
744
- JSON.stringify({
745
- type: 'event',
746
- timestamp: recentTs,
747
- payload: {
748
- type: 'token_count',
749
- message: 'fallback conversation'
750
- }
751
- })
752
- ].join('\n'));
753
- fs.writeFileSync(mappingPath, JSON.stringify({
754
- 6161: badSessionFile
755
- }));
756
- const processes = [
757
- {
758
- pid: 6161,
759
- command: 'codex',
760
- cwd: '/repo-fallback',
761
- tty: 'ttys001',
762
- startTime: new Date('2026-06-26T09:56:12.000Z')
763
- }
764
- ];
765
- const fallbackSession = {
766
- sessionId: 'sess-fallback',
767
- filePath: fallbackSessionFile,
768
- projectDir: dateDir,
769
- birthtimeMs: new Date('2026-06-26T09:56:15.000Z').getTime(),
770
- resolvedCwd: ''
771
- };
772
- mockedListAgentProcesses.mockReturnValue(processes);
773
- mockedEnrichProcesses.mockReturnValue(processes);
774
- mockedBatchGetSessionFileBirthtimes.mockReturnValue([
775
- fallbackSession
776
- ]);
777
- mockedMatchProcessesToSessions.mockReturnValue([
778
- {
779
- process: processes[0],
780
- session: {
781
- ...fallbackSession,
782
- resolvedCwd: '/repo-fallback'
783
- },
784
- deltaMs: 3000
785
- }
786
- ]);
787
- const agents = await mappedAdapter.detectAgents();
788
- expect(agents).toHaveLength(1);
789
- expect(agents[0]).toMatchObject({
790
- pid: 6161,
791
- sessionId: 'sess-fallback',
792
- summary: 'fallback conversation',
793
- sessionFilePath: fallbackSessionFile
794
- });
795
- expect(mockedMatchProcessesToSessions).toHaveBeenCalledWith([
796
- processes[0]
797
- ], expect.arrayContaining([
798
- expect.objectContaining({
799
- filePath: fallbackSessionFile
800
- })
801
- ]));
802
- } finally{
803
- process.env.HOME = originalHome;
804
- fs.rmSync(tmpHome, {
805
- recursive: true,
806
- force: true
807
- });
808
185
  }
186
+ ], "2026/06/09");
187
+ mockedListAgentProcesses.mockReturnValue([
188
+ processInfo
189
+ ]);
190
+ mockedBatchGetSessionFileBirthtimes.mockReturnValue([]);
191
+ const agents = await adapter.detectAgents();
192
+ expect(mockedBatchGetSessionFileBirthtimes).not.toHaveBeenCalled();
193
+ expect(mockedMatchProcessesToSessions).not.toHaveBeenCalled();
194
+ expect(agents).toHaveLength(1);
195
+ expect(agents[0]).toMatchObject({
196
+ type: "codex",
197
+ pid: 88018,
198
+ sessionId,
199
+ projectPath: "/repo-a",
200
+ sessionFilePath: sessionFile,
201
+ summary: "resumed codex conversation"
809
202
  });
810
203
  });
811
- describe('detectAgents registry cache short-circuit', ()=>{
812
- let tmpDir;
813
- let regPath;
814
- let registry;
815
- let cachedAdapter;
816
- let sessionFilePath;
817
- function registerEntry(over = {}) {
818
- registry.register({
819
- name: 'codex-100',
820
- type: 'codex',
821
- pid: 100,
822
- tmuxSession: '',
823
- cwd: '/repo-a',
824
- startedAt: '2026-05-30T00:00:00.000Z',
825
- sessionId: 'sess-cached',
826
- sessionFilePath,
827
- ...over
828
- });
829
- }
830
- beforeEach(()=>{
831
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-cache-'));
832
- regPath = path.join(tmpDir, 'agents.json');
833
- registry = new AgentRegistry(regPath);
834
- cachedAdapter = new CodexAdapter(registry);
835
- const recentTs = new Date().toISOString();
836
- sessionFilePath = path.join(tmpDir, 'sess-cached.jsonl');
837
- fs.writeFileSync(sessionFilePath, [
838
- JSON.stringify({
839
- type: 'session_meta',
840
- payload: {
841
- id: 'sess-cached',
842
- timestamp: recentTs,
843
- cwd: '/repo-a'
844
- }
845
- }),
846
- JSON.stringify({
847
- type: 'event',
848
- timestamp: recentTs,
849
- payload: {
850
- type: 'token_count',
851
- message: 'Hello from cache'
852
- }
853
- })
854
- ].join('\n'));
855
- });
856
- afterEach(()=>{
857
- fs.rmSync(tmpDir, {
858
- recursive: true,
859
- force: true
860
- });
861
- });
862
- it('short-circuits matching when registry has a valid entry', async ()=>{
863
- registerEntry();
864
- const processes = [
865
- {
866
- pid: 100,
867
- command: 'codex',
868
- cwd: '/repo-a',
869
- tty: 'ttys001',
870
- startTime: new Date()
871
- }
872
- ];
873
- mockedListAgentProcesses.mockReturnValue(processes);
874
- mockedEnrichProcesses.mockReturnValue(processes);
875
- const agents = await cachedAdapter.detectAgents();
876
- expect(agents).toHaveLength(1);
877
- expect(agents[0]).toMatchObject({
878
- type: 'codex',
879
- pid: 100,
880
- sessionId: 'sess-cached',
881
- sessionFilePath,
882
- summary: 'Hello from cache'
883
- });
884
- expect(mockedMatchProcessesToSessions).not.toHaveBeenCalled();
885
- expect(mockedBatchGetSessionFileBirthtimes).not.toHaveBeenCalled();
886
- });
887
- it('uses the hook session mapping instead of a stale registry entry for the same live pid', async ()=>{
888
- registerEntry();
889
- const sessionsDir = path.join(tmpDir, '.codex', 'sessions');
890
- const dateDir = path.join(sessionsDir, '2026', '06', '26');
891
- const mappingPath = path.join(tmpDir, '.codex', 'ai-devkit', 'sessions.json');
892
- const currentSessionFile = path.join(dateDir, 'sess-current.jsonl');
893
- const recentTs = new Date().toISOString();
894
- fs.mkdirSync(dateDir, {
895
- recursive: true
896
- });
897
- fs.mkdirSync(path.dirname(mappingPath), {
898
- recursive: true
899
- });
900
- fs.writeFileSync(currentSessionFile, [
901
- JSON.stringify({
902
- type: 'session_meta',
903
- payload: {
904
- id: 'sess-current',
905
- timestamp: recentTs,
906
- cwd: '/repo-current'
907
- }
908
- }),
909
- JSON.stringify({
910
- type: 'event',
911
- timestamp: recentTs,
912
- payload: {
913
- type: 'token_count',
914
- message: 'Hello from current mapping'
915
- }
916
- })
917
- ].join('\n'));
918
- fs.writeFileSync(mappingPath, JSON.stringify({
919
- 100: currentSessionFile
920
- }));
921
- cachedAdapter.codexSessionsDir = sessionsDir;
922
- cachedAdapter.sessionMappingPath = mappingPath;
923
- const processes = [
924
- {
925
- pid: 100,
926
- command: 'codex',
927
- cwd: '/repo-current',
928
- tty: 'ttys001',
929
- startTime: new Date()
204
+ it("uses hook session mapping before a stale registry entry for the same pid", async ()=>{
205
+ const registry = new AgentRegistry(path.join(tmpHome, "agents.json"));
206
+ const staleSession = path.join(tmpHome, "stale.jsonl");
207
+ fs.writeFileSync(staleSession, [
208
+ JSON.stringify({
209
+ type: "session_meta",
210
+ payload: {
211
+ id: "stale",
212
+ timestamp: new Date().toISOString(),
213
+ cwd: "/repo-stale"
214
+ }
215
+ }),
216
+ JSON.stringify({
217
+ type: "event",
218
+ timestamp: new Date().toISOString(),
219
+ payload: {
220
+ type: "agent_message",
221
+ message: "stale"
222
+ }
223
+ })
224
+ ].join("\n"));
225
+ registry.register(registryEntry({
226
+ sessionFilePath: staleSession
227
+ }));
228
+ const mappedAdapter = new CodexAdapter(registry);
229
+ const { sessionFile } = writeSession("current", "/repo-current", [
230
+ {
231
+ type: "event",
232
+ timestamp: new Date().toISOString(),
233
+ payload: {
234
+ type: "agent_message",
235
+ message: "Hello from current mapping"
930
236
  }
931
- ];
932
- mockedListAgentProcesses.mockReturnValue(processes);
933
- mockedEnrichProcesses.mockReturnValue(processes);
934
- const agents = await cachedAdapter.detectAgents();
935
- expect(agents).toHaveLength(1);
936
- expect(agents[0]).toMatchObject({
937
- type: 'codex',
237
+ }
238
+ ], "2026/06/26");
239
+ fs.mkdirSync(path.join(tmpHome, ".codex", "ai-devkit"), {
240
+ recursive: true
241
+ });
242
+ fs.writeFileSync(path.join(tmpHome, ".codex", "ai-devkit", "sessions.json"), JSON.stringify({
243
+ 100: sessionFile
244
+ }));
245
+ mockedListAgentProcesses.mockReturnValue([
246
+ {
938
247
  pid: 100,
939
- sessionId: 'sess-current',
940
- sessionFilePath: currentSessionFile,
941
- summary: 'Hello from current mapping'
942
- });
943
- expect(mockedMatchProcessesToSessions).not.toHaveBeenCalled();
944
- expect(mockedBatchGetSessionFileBirthtimes).not.toHaveBeenCalled();
945
- });
946
- it('falls through when no registry entry exists for the pid', async ()=>{
947
- const processes = [
948
- {
949
- pid: 100,
950
- command: 'codex',
951
- cwd: '/repo-a',
952
- tty: 'ttys001',
953
- startTime: new Date()
954
- }
955
- ];
956
- mockedListAgentProcesses.mockReturnValue(processes);
957
- mockedEnrichProcesses.mockReturnValue(processes);
958
- cachedAdapter.codexSessionsDir = '/nonexistent';
959
- const agents = await cachedAdapter.detectAgents();
960
- expect(agents[0].sessionId).toBe('pid-100');
961
- });
962
- it('falls through when registry entry type does not match', async ()=>{
963
- registerEntry({
964
- type: 'claude'
965
- });
966
- const processes = [
967
- {
968
- pid: 100,
969
- command: 'codex',
970
- cwd: '/repo-a',
971
- tty: 'ttys001',
972
- startTime: new Date()
973
- }
974
- ];
975
- mockedListAgentProcesses.mockReturnValue(processes);
976
- mockedEnrichProcesses.mockReturnValue(processes);
977
- cachedAdapter.codexSessionsDir = '/nonexistent';
978
- const agents = await cachedAdapter.detectAgents();
979
- expect(agents[0].sessionId).toBe('pid-100');
980
- expect(mockedMatchProcessesToSessions).not.toHaveBeenCalled();
981
- });
982
- it('falls through when the cached session file no longer exists', async ()=>{
983
- registerEntry({
984
- sessionFilePath: path.join(tmpDir, 'deleted.jsonl')
985
- });
986
- const processes = [
987
- {
988
- pid: 100,
989
- command: 'codex',
990
- cwd: '/repo-a',
991
- tty: 'ttys001',
992
- startTime: new Date()
993
- }
994
- ];
995
- mockedListAgentProcesses.mockReturnValue(processes);
996
- mockedEnrichProcesses.mockReturnValue(processes);
997
- cachedAdapter.codexSessionsDir = '/nonexistent';
998
- const agents = await cachedAdapter.detectAgents();
999
- expect(agents[0].sessionId).toBe('pid-100');
1000
- });
248
+ command: "codex",
249
+ cwd: "/repo-current",
250
+ tty: "ttys001",
251
+ startTime: new Date()
252
+ }
253
+ ]);
254
+ const agents = await mappedAdapter.detectAgents();
255
+ expect(agents).toHaveLength(1);
256
+ expect(agents[0]).toMatchObject({
257
+ pid: 100,
258
+ sessionId: "current",
259
+ sessionFilePath: sessionFile,
260
+ summary: "Hello from current mapping"
261
+ });
262
+ expect(mockedBatchGetSessionFileBirthtimes).not.toHaveBeenCalled();
263
+ expect(mockedMatchProcessesToSessions).not.toHaveBeenCalled();
1001
264
  });
1002
- describe('discoverSessions', ()=>{
1003
- let tmpDir;
1004
- beforeEach(()=>{
1005
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-test-'));
1006
- });
1007
- afterEach(()=>{
1008
- fs.rmSync(tmpDir, {
1009
- recursive: true,
1010
- force: true
1011
- });
1012
- });
1013
- it('should return empty when sessions dir does not exist', ()=>{
1014
- adapter.codexSessionsDir = path.join(tmpDir, 'nonexistent');
1015
- const discoverSessions = adapter.discoverSessions.bind(adapter);
1016
- const { sessions } = discoverSessions([
1017
- {
1018
- pid: 1,
1019
- command: 'codex',
1020
- cwd: '/repo',
1021
- tty: '',
1022
- startTime: new Date()
1023
- }
1024
- ]);
1025
- expect(sessions).toEqual([]);
1026
- });
1027
- it('should scan date directories based on process start times', ()=>{
1028
- const sessionsDir = path.join(tmpDir, 'sessions');
1029
- adapter.codexSessionsDir = sessionsDir;
1030
- const discoverSessions = adapter.discoverSessions.bind(adapter);
1031
- // Create date dir for 2026-03-18
1032
- const dateDir = path.join(sessionsDir, '2026', '03', '18');
1033
- fs.mkdirSync(dateDir, {
1034
- recursive: true
1035
- });
1036
- // Create session file with meta
1037
- const sessionFile = path.join(dateDir, 'sess1.jsonl');
1038
- fs.writeFileSync(sessionFile, JSON.stringify({
1039
- type: 'session_meta',
265
+ it("uses a valid registry cache entry before scanning sessions", async ()=>{
266
+ const registry = new AgentRegistry(path.join(tmpHome, "agents.json"));
267
+ const { sessionFile } = writeSession("cached", "/repo-a", [
268
+ {
269
+ type: "event",
270
+ timestamp: new Date().toISOString(),
1040
271
  payload: {
1041
- id: 'sess1',
1042
- cwd: '/repo-a'
1043
- }
1044
- }));
1045
- const mockFiles = [
1046
- {
1047
- sessionId: 'sess1',
1048
- filePath: sessionFile,
1049
- projectDir: dateDir,
1050
- birthtimeMs: 1710800324000,
1051
- resolvedCwd: ''
1052
- }
1053
- ];
1054
- mockedBatchGetSessionFileBirthtimes.mockReturnValue(mockFiles);
1055
- const processes = [
1056
- {
1057
- pid: 1,
1058
- command: 'codex',
1059
- cwd: '/repo-a',
1060
- tty: '',
1061
- startTime: new Date('2026-03-18T15:00:00Z')
272
+ type: "token_count",
273
+ message: "Hello from cache"
1062
274
  }
1063
- ];
1064
- const { sessions, contentCache } = discoverSessions(processes);
1065
- expect(sessions).toHaveLength(1);
1066
- expect(sessions[0].resolvedCwd).toBe('/repo-a');
1067
- expect(contentCache.has(sessionFile)).toBe(true);
1068
- expect(mockedBatchGetSessionFileBirthtimes).toHaveBeenCalledTimes(1);
1069
- });
1070
- it('should scan ±1 day window around process start time', ()=>{
1071
- const sessionsDir = path.join(tmpDir, 'sessions');
1072
- adapter.codexSessionsDir = sessionsDir;
1073
- const discoverSessions = adapter.discoverSessions.bind(adapter);
1074
- // Create date dirs for 17, 18, 19
1075
- for (const day of [
1076
- '17',
1077
- '18',
1078
- '19'
1079
- ]){
1080
- fs.mkdirSync(path.join(sessionsDir, '2026', '03', day), {
1081
- recursive: true
1082
- });
1083
275
  }
1084
- mockedBatchGetSessionFileBirthtimes.mockReturnValue([]);
1085
- const processes = [
1086
- {
1087
- pid: 1,
1088
- command: 'codex',
1089
- cwd: '/repo',
1090
- tty: '',
1091
- startTime: new Date('2026-03-18T15:00:00Z')
1092
- }
1093
- ];
1094
- discoverSessions(processes);
1095
- expect(mockedBatchGetSessionFileBirthtimes).toHaveBeenCalledTimes(1);
1096
- // Should scan all 3 date dirs
1097
- const dirs = mockedBatchGetSessionFileBirthtimes.mock.calls[0][0];
1098
- expect(dirs).toHaveLength(3);
1099
- });
1100
- it('should handle session files without session_meta', ()=>{
1101
- const sessionsDir = path.join(tmpDir, 'sessions');
1102
- adapter.codexSessionsDir = sessionsDir;
1103
- const discoverSessions = adapter.discoverSessions.bind(adapter);
1104
- const dateDir = path.join(sessionsDir, '2026', '03', '18');
1105
- fs.mkdirSync(dateDir, {
1106
- recursive: true
1107
- });
1108
- const sessionFile = path.join(dateDir, 'bad.jsonl');
1109
- fs.writeFileSync(sessionFile, JSON.stringify({
1110
- type: 'event',
1111
- payload: {}
1112
- }));
1113
- mockedBatchGetSessionFileBirthtimes.mockReturnValue([
1114
- {
1115
- sessionId: 'bad',
1116
- filePath: sessionFile,
1117
- projectDir: dateDir,
1118
- birthtimeMs: 1710800324000,
1119
- resolvedCwd: ''
1120
- }
1121
- ]);
1122
- const processes = [
1123
- {
1124
- pid: 1,
1125
- command: 'codex',
1126
- cwd: '/repo',
1127
- tty: '',
1128
- startTime: new Date('2026-03-18T15:00:00Z')
1129
- }
1130
- ];
1131
- const { sessions } = discoverSessions(processes);
1132
- expect(sessions[0].resolvedCwd).toBe('');
1133
- });
1134
- it('should use session_meta timestamp as the matching birthtime when valid', ()=>{
1135
- const sessionsDir = path.join(tmpDir, 'sessions');
1136
- adapter.codexSessionsDir = sessionsDir;
1137
- const discoverSessions = adapter.discoverSessions.bind(adapter);
1138
- const dateDir = path.join(sessionsDir, '2026', '03', '18');
1139
- fs.mkdirSync(dateDir, {
1140
- recursive: true
1141
- });
1142
- const sessionFile = path.join(dateDir, 'sess-meta-time.jsonl');
1143
- const metaTimestamp = '2026-03-18T15:00:05.000Z';
1144
- fs.writeFileSync(sessionFile, JSON.stringify({
1145
- type: 'session_meta',
1146
- payload: {
1147
- id: 'sess-meta-time',
1148
- timestamp: metaTimestamp,
1149
- cwd: '/repo-a'
1150
- }
1151
- }));
1152
- mockedBatchGetSessionFileBirthtimes.mockReturnValue([
1153
- {
1154
- sessionId: 'sess-meta-time',
1155
- filePath: sessionFile,
1156
- projectDir: dateDir,
1157
- birthtimeMs: new Date('2026-03-18T15:05:30.000Z').getTime(),
1158
- resolvedCwd: ''
1159
- }
1160
- ]);
1161
- const { sessions } = discoverSessions([
1162
- {
1163
- pid: 1,
1164
- command: 'codex',
1165
- cwd: '/repo-a',
1166
- tty: '',
1167
- startTime: new Date('2026-03-18T15:00:00Z')
1168
- }
1169
- ]);
1170
- expect(sessions[0].resolvedCwd).toBe('/repo-a');
1171
- expect(sessions[0].birthtimeMs).toBe(new Date(metaTimestamp).getTime());
1172
- });
1173
- it('should tolerate malformed session_meta and unreadable files', ()=>{
1174
- const sessionsDir = path.join(tmpDir, 'sessions');
1175
- adapter.codexSessionsDir = sessionsDir;
1176
- const discoverSessions = adapter.discoverSessions.bind(adapter);
1177
- const dateDir = path.join(sessionsDir, '2026', '03', '18');
1178
- fs.mkdirSync(dateDir, {
1179
- recursive: true
1180
- });
1181
- const malformedFile = path.join(dateDir, 'malformed.jsonl');
1182
- const missingFile = path.join(dateDir, 'missing.jsonl');
1183
- fs.writeFileSync(malformedFile, '{not valid json');
1184
- mockedBatchGetSessionFileBirthtimes.mockReturnValue([
1185
- {
1186
- sessionId: 'malformed',
1187
- filePath: malformedFile,
1188
- projectDir: dateDir,
1189
- birthtimeMs: 1710800324000,
1190
- resolvedCwd: ''
1191
- },
1192
- {
1193
- sessionId: 'missing',
1194
- filePath: missingFile,
1195
- projectDir: dateDir,
1196
- birthtimeMs: 1710800325000,
1197
- resolvedCwd: ''
1198
- }
1199
- ]);
1200
- const result = discoverSessions([
1201
- {
1202
- pid: 1,
1203
- command: 'codex',
1204
- cwd: '/repo',
1205
- tty: '',
1206
- startTime: new Date('2026-03-18T15:00:00Z')
1207
- }
1208
- ]);
1209
- expect(result.sessions).toHaveLength(2);
1210
- expect(result.sessions[0].resolvedCwd).toBe('');
1211
- expect(result.sessions[1].resolvedCwd).toBe('');
1212
- expect(result.contentCache.has(malformedFile)).toBe(true);
1213
- expect(result.contentCache.has(missingFile)).toBe(false);
1214
- });
1215
- });
1216
- describe('findSessionFileById', ()=>{
1217
- let tmpDir;
1218
- let sessionsDir;
1219
- const sessionId = 'aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee';
1220
- beforeEach(()=>{
1221
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-resume-find-'));
1222
- sessionsDir = path.join(tmpDir, 'sessions');
1223
- fs.mkdirSync(path.join(sessionsDir, '2026', '03', '18'), {
1224
- recursive: true
1225
- });
1226
- adapter.codexSessionsDir = sessionsDir;
1227
- });
1228
- afterEach(()=>{
1229
- fs.rmSync(tmpDir, {
1230
- recursive: true,
1231
- force: true
1232
- });
1233
- });
1234
- function writeResumeSession(timestamp) {
1235
- const filePath = path.join(sessionsDir, '2026', '03', '18', `${sessionId}.jsonl`);
1236
- const payload = {
1237
- id: sessionId,
1238
- cwd: '/repo-a'
1239
- };
1240
- if (timestamp !== undefined) {
1241
- payload.timestamp = timestamp;
276
+ ]);
277
+ registry.register(registryEntry({
278
+ sessionId: "cached",
279
+ sessionFilePath: sessionFile
280
+ }));
281
+ const cachedAdapter = new CodexAdapter(registry);
282
+ mockedListAgentProcesses.mockReturnValue([
283
+ {
284
+ pid: 100,
285
+ command: "codex",
286
+ cwd: "/repo-a",
287
+ tty: "ttys001",
288
+ startTime: new Date()
1242
289
  }
1243
- fs.writeFileSync(filePath, JSON.stringify({
1244
- type: 'session_meta',
1245
- payload
1246
- }));
1247
- return filePath;
1248
- }
1249
- it('should return session_meta timestamp as birthtimeMs when valid', ()=>{
1250
- const metaTimestamp = '2026-03-18T15:00:05.000Z';
1251
- writeResumeSession(metaTimestamp);
1252
- const findSessionFileById = adapter.findSessionFileById.bind(adapter);
1253
- const session = findSessionFileById(sessionId);
1254
- expect(session).toMatchObject({
1255
- sessionId,
1256
- resolvedCwd: '/repo-a',
1257
- birthtimeMs: new Date(metaTimestamp).getTime()
1258
- });
1259
- });
1260
- it.each([
1261
- [
1262
- 'missing',
1263
- undefined
1264
- ],
1265
- [
1266
- 'invalid',
1267
- 'not-a-date'
1268
- ]
1269
- ])('should fall back to stat birthtimeMs when session_meta timestamp is %s', (_label, metaTimestamp)=>{
1270
- const sessionFile = writeResumeSession(metaTimestamp);
1271
- const stat = fs.statSync(sessionFile);
1272
- const findSessionFileById = adapter.findSessionFileById.bind(adapter);
1273
- const session = findSessionFileById(sessionId);
1274
- expect(session).not.toBeNull();
1275
- expect(session.birthtimeMs).toBe(stat.birthtimeMs);
1276
- });
290
+ ]);
291
+ const agents = await cachedAdapter.detectAgents();
292
+ expect(agents).toHaveLength(1);
293
+ expect(agents[0]).toMatchObject({
294
+ type: "codex",
295
+ pid: 100,
296
+ sessionId: "cached",
297
+ sessionFilePath: sessionFile,
298
+ summary: "Hello from cache"
299
+ });
300
+ expect(mockedMatchProcessesToSessions).not.toHaveBeenCalled();
301
+ expect(mockedBatchGetSessionFileBirthtimes).not.toHaveBeenCalled();
1277
302
  });
1278
- describe('helper methods', ()=>{
1279
- describe('determineStatus', ()=>{
1280
- it('should return "waiting" for agent_message events', ()=>{
1281
- const determineStatus = adapter.determineStatus.bind(adapter);
1282
- expect(determineStatus({
1283
- lastActive: new Date(),
1284
- lastPayloadType: 'agent_message'
1285
- })).toBe(AgentStatus.WAITING);
1286
- });
1287
- it('should return "waiting" for task_complete events', ()=>{
1288
- const determineStatus = adapter.determineStatus.bind(adapter);
1289
- expect(determineStatus({
1290
- lastActive: new Date(),
1291
- lastPayloadType: 'task_complete'
1292
- })).toBe(AgentStatus.WAITING);
1293
- });
1294
- it('should return "waiting" for turn_aborted events', ()=>{
1295
- const determineStatus = adapter.determineStatus.bind(adapter);
1296
- expect(determineStatus({
1297
- lastActive: new Date(),
1298
- lastPayloadType: 'turn_aborted'
1299
- })).toBe(AgentStatus.WAITING);
1300
- });
1301
- it('should return "running" for active events', ()=>{
1302
- const determineStatus = adapter.determineStatus.bind(adapter);
1303
- expect(determineStatus({
1304
- lastActive: new Date(),
1305
- lastPayloadType: 'token_count'
1306
- })).toBe(AgentStatus.RUNNING);
1307
- });
1308
- it('should return "idle" when session exceeds threshold', ()=>{
1309
- const determineStatus = adapter.determineStatus.bind(adapter);
1310
- expect(determineStatus({
1311
- lastActive: new Date(Date.now() - 10 * 60 * 1000),
1312
- lastPayloadType: 'token_count'
1313
- })).toBe(AgentStatus.IDLE);
1314
- });
1315
- });
1316
- describe('parseSession', ()=>{
1317
- let tmpDir;
1318
- beforeEach(()=>{
1319
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-test-'));
1320
- });
1321
- afterEach(()=>{
1322
- fs.rmSync(tmpDir, {
1323
- recursive: true,
1324
- force: true
1325
- });
1326
- });
1327
- it('should parse session file with meta and events', ()=>{
1328
- const parseSession = adapter.parseSession.bind(adapter);
1329
- const filePath = path.join(tmpDir, 'session.jsonl');
1330
- fs.writeFileSync(filePath, [
1331
- JSON.stringify({
1332
- type: 'session_meta',
1333
- payload: {
1334
- id: 'sess-1',
1335
- timestamp: '2026-03-18T15:00:00Z',
1336
- cwd: '/repo'
1337
- }
1338
- }),
1339
- JSON.stringify({
1340
- type: 'event',
1341
- timestamp: '2026-03-18T15:01:00Z',
1342
- payload: {
1343
- type: 'agent_reasoning',
1344
- message: 'Working on feature'
1345
- }
1346
- })
1347
- ].join('\n'));
1348
- const session = parseSession(undefined, filePath);
1349
- expect(session).toMatchObject({
1350
- sessionId: 'sess-1',
1351
- projectPath: '/repo',
1352
- summary: 'Working on feature',
1353
- lastPayloadType: 'agent_reasoning'
1354
- });
1355
- expect(session.sessionStart.toISOString()).toBe('2026-03-18T15:00:00.000Z');
1356
- });
1357
- it('should parse from cached content without reading disk', ()=>{
1358
- const parseSession = adapter.parseSession.bind(adapter);
1359
- const content = [
1360
- JSON.stringify({
1361
- type: 'session_meta',
1362
- payload: {
1363
- id: 'cached-1',
1364
- timestamp: '2026-03-18T15:00:00Z',
1365
- cwd: '/cached'
1366
- }
1367
- }),
1368
- JSON.stringify({
1369
- type: 'event',
1370
- timestamp: '2026-03-18T15:01:00Z',
1371
- payload: {
1372
- type: 'agent_message',
1373
- message: 'Cached result'
1374
- }
1375
- })
1376
- ].join('\n');
1377
- const session = parseSession(content, '/nonexistent/path.jsonl');
1378
- expect(session).toMatchObject({
1379
- sessionId: 'cached-1',
1380
- projectPath: '/cached',
1381
- summary: 'Cached result'
1382
- });
1383
- });
1384
- it('should return null for non-existent file', ()=>{
1385
- const parseSession = adapter.parseSession.bind(adapter);
1386
- expect(parseSession(undefined, path.join(tmpDir, 'nonexistent.jsonl'))).toBeNull();
1387
- });
1388
- it('should return null when first line is not session_meta', ()=>{
1389
- const parseSession = adapter.parseSession.bind(adapter);
1390
- const filePath = path.join(tmpDir, 'bad.jsonl');
1391
- fs.writeFileSync(filePath, JSON.stringify({
1392
- type: 'event',
1393
- payload: {}
1394
- }));
1395
- expect(parseSession(undefined, filePath)).toBeNull();
1396
- });
1397
- it('should return null when session_meta has no id', ()=>{
1398
- const parseSession = adapter.parseSession.bind(adapter);
1399
- const filePath = path.join(tmpDir, 'no-id.jsonl');
1400
- fs.writeFileSync(filePath, JSON.stringify({
1401
- type: 'session_meta',
1402
- payload: {
1403
- cwd: '/repo'
1404
- }
1405
- }));
1406
- expect(parseSession(undefined, filePath)).toBeNull();
1407
- });
1408
- it('should extract summary from last event message', ()=>{
1409
- const parseSession = adapter.parseSession.bind(adapter);
1410
- const filePath = path.join(tmpDir, 'summary.jsonl');
1411
- fs.writeFileSync(filePath, [
1412
- JSON.stringify({
1413
- type: 'session_meta',
1414
- payload: {
1415
- id: 'sess-2',
1416
- timestamp: '2026-03-18T15:00:00Z',
1417
- cwd: '/repo'
1418
- }
1419
- }),
1420
- JSON.stringify({
1421
- type: 'event',
1422
- timestamp: '2026-03-18T15:01:00Z',
1423
- payload: {
1424
- type: 'agent_reasoning',
1425
- message: 'First message'
1426
- }
1427
- }),
1428
- JSON.stringify({
1429
- type: 'event',
1430
- timestamp: '2026-03-18T15:02:00Z',
1431
- payload: {
1432
- type: 'agent_message',
1433
- message: 'Last message'
1434
- }
1435
- })
1436
- ].join('\n'));
1437
- const session = parseSession(undefined, filePath);
1438
- expect(session.summary).toBe('Last message');
1439
- });
1440
- it('should extract summary from current Codex response_item messages', ()=>{
1441
- const parseSession = adapter.parseSession.bind(adapter);
1442
- const filePath = path.join(tmpDir, 'response-item-summary.jsonl');
1443
- fs.writeFileSync(filePath, [
1444
- JSON.stringify({
1445
- type: 'session_meta',
1446
- payload: {
1447
- id: 'sess-ri',
1448
- timestamp: '2026-03-18T15:00:00Z',
1449
- cwd: '/repo'
1450
- }
1451
- }),
1452
- JSON.stringify({
1453
- type: 'response_item',
1454
- timestamp: '2026-03-18T15:01:00Z',
1455
- payload: {
1456
- type: 'message',
1457
- role: 'assistant',
1458
- content: [
1459
- {
1460
- type: 'output_text',
1461
- text: 'Parsed from current schema'
1462
- }
1463
- ]
1464
- }
1465
- })
1466
- ].join('\n'));
1467
- const session = parseSession(undefined, filePath);
1468
- expect(session.summary).toBe('Parsed from current schema');
1469
- expect(session.lastPayloadType).toBe('agent_message');
1470
- });
1471
- it('should treat completed Codex AgentMessage events as waiting for list status', ()=>{
1472
- const parseSession = adapter.parseSession.bind(adapter);
1473
- const determineStatus = adapter.determineStatus.bind(adapter);
1474
- const filePath = path.join(tmpDir, 'agent-message-status.jsonl');
1475
- fs.writeFileSync(filePath, [
1476
- JSON.stringify({
1477
- type: 'session_meta',
1478
- payload: {
1479
- id: 'sess-am',
1480
- timestamp: '2026-03-18T15:00:00Z',
1481
- cwd: '/repo'
1482
- }
1483
- }),
1484
- JSON.stringify({
1485
- type: 'event_msg',
1486
- timestamp: new Date().toISOString(),
1487
- payload: {
1488
- type: 'item_completed',
1489
- item: {
1490
- type: 'AgentMessage',
1491
- content: [
1492
- {
1493
- type: 'Text',
1494
- text: 'Waiting for the user now'
1495
- }
1496
- ]
1497
- }
1498
- }
1499
- })
1500
- ].join('\n'));
1501
- const session = parseSession(undefined, filePath);
1502
- expect(session.summary).toBe('Waiting for the user now');
1503
- expect(session.lastPayloadType).toBe('agent_message');
1504
- expect(determineStatus(session)).toBe(AgentStatus.WAITING);
1505
- });
1506
- it('should handle malformed JSON lines gracefully', ()=>{
1507
- const parseSession = adapter.parseSession.bind(adapter);
1508
- const filePath = path.join(tmpDir, 'malformed.jsonl');
1509
- fs.writeFileSync(filePath, [
1510
- JSON.stringify({
1511
- type: 'session_meta',
1512
- payload: {
1513
- id: 'sess-m',
1514
- timestamp: '2026-03-18T15:00:00Z',
1515
- cwd: '/repo'
1516
- }
1517
- }),
1518
- 'not valid json',
1519
- '{"incomplete": true',
1520
- JSON.stringify({
1521
- type: 'event',
1522
- timestamp: '2026-03-18T15:01:00Z',
1523
- payload: {
1524
- type: 'agent_message',
1525
- message: 'Valid message'
1526
- }
1527
- })
1528
- ].join('\n'));
1529
- const session = parseSession(undefined, filePath);
1530
- expect(session).not.toBeNull();
1531
- expect(session.sessionId).toBe('sess-m');
1532
- expect(session.summary).toBe('Valid message');
1533
- });
1534
- it('should default summary when no messages found', ()=>{
1535
- const parseSession = adapter.parseSession.bind(adapter);
1536
- const filePath = path.join(tmpDir, 'no-msg.jsonl');
1537
- fs.writeFileSync(filePath, [
1538
- JSON.stringify({
1539
- type: 'session_meta',
1540
- payload: {
1541
- id: 'sess-3',
1542
- timestamp: '2026-03-18T15:00:00Z',
1543
- cwd: '/repo'
1544
- }
1545
- }),
1546
- JSON.stringify({
1547
- type: 'event',
1548
- timestamp: '2026-03-18T15:01:00Z',
1549
- payload: {
1550
- type: 'token_count'
1551
- }
1552
- })
1553
- ].join('\n'));
1554
- const session = parseSession(undefined, filePath);
1555
- expect(session.summary).toBe('Codex session active');
1556
- });
1557
- it('should return null for empty content', ()=>{
1558
- const parseSession = adapter.parseSession.bind(adapter);
1559
- expect(parseSession('', '/fake/path.jsonl')).toBeNull();
1560
- expect(parseSession(' \n \n ', '/fake/path.jsonl')).toBeNull();
1561
- });
1562
- it('should truncate long summary to 120 chars', ()=>{
1563
- const parseSession = adapter.parseSession.bind(adapter);
1564
- const longMsg = 'A'.repeat(200);
1565
- const content = [
1566
- JSON.stringify({
1567
- type: 'session_meta',
1568
- payload: {
1569
- id: 'sess-t',
1570
- timestamp: '2026-03-18T15:00:00Z',
1571
- cwd: '/repo'
1572
- }
1573
- }),
1574
- JSON.stringify({
1575
- type: 'event',
1576
- timestamp: '2026-03-18T15:01:00Z',
1577
- payload: {
1578
- type: 'agent_message',
1579
- message: longMsg
1580
- }
1581
- })
1582
- ].join('\n');
1583
- const session = parseSession(content, '/fake/path.jsonl');
1584
- expect(session.summary).toHaveLength(120);
1585
- expect(session.summary.endsWith('...')).toBe(true);
1586
- });
1587
- });
1588
- });
1589
- describe('getConversation', ()=>{
1590
- let tmpDir;
1591
- beforeEach(()=>{
1592
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-conv-'));
1593
- });
1594
- afterEach(()=>{
1595
- fs.rmSync(tmpDir, {
1596
- recursive: true,
1597
- force: true
1598
- });
1599
- });
1600
- function writeJsonl(lines) {
1601
- const filePath = path.join(tmpDir, 'session.jsonl');
1602
- fs.writeFileSync(filePath, lines.map((l)=>JSON.stringify(l)).join('\n'));
1603
- return filePath;
1604
- }
1605
- it('should parse user and agent messages', ()=>{
1606
- const filePath = writeJsonl([
1607
- {
1608
- type: 'session_meta',
1609
- payload: {
1610
- id: 'sess-1',
1611
- cwd: '/repo',
1612
- timestamp: '2026-03-27T10:00:00Z'
1613
- }
1614
- },
1615
- {
1616
- type: 'event',
1617
- timestamp: '2026-03-27T10:00:01Z',
1618
- payload: {
1619
- type: 'user_message',
1620
- message: 'Fix the bug'
1621
- }
1622
- },
1623
- {
1624
- type: 'event',
1625
- timestamp: '2026-03-27T10:00:05Z',
1626
- payload: {
1627
- type: 'agent_message',
1628
- message: 'I found the issue'
1629
- }
1630
- }
1631
- ]);
1632
- const messages = adapter.getConversation(filePath);
1633
- expect(messages).toHaveLength(2);
1634
- expect(messages[0]).toEqual({
1635
- role: 'user',
1636
- content: 'Fix the bug',
1637
- timestamp: '2026-03-27T10:00:01Z'
1638
- });
1639
- expect(messages[1]).toEqual({
1640
- role: 'assistant',
1641
- content: 'I found the issue',
1642
- timestamp: '2026-03-27T10:00:05Z'
1643
- });
1644
- });
1645
- it('should parse Codex response_item message records', ()=>{
1646
- const filePath = writeJsonl([
1647
- {
1648
- type: 'session_meta',
1649
- payload: {
1650
- id: 'sess-1',
1651
- cwd: '/repo',
1652
- timestamp: '2026-03-27T10:00:00Z'
1653
- }
1654
- },
1655
- {
1656
- type: 'response_item',
1657
- timestamp: '2026-03-27T10:00:01Z',
1658
- payload: {
1659
- type: 'message',
1660
- role: 'user',
1661
- content: [
1662
- {
1663
- type: 'input_text',
1664
- text: 'Fix the bug'
1665
- }
1666
- ]
1667
- }
1668
- },
1669
- {
1670
- type: 'response_item',
1671
- timestamp: '2026-03-27T10:00:05Z',
1672
- payload: {
1673
- type: 'message',
1674
- role: 'assistant',
1675
- content: [
1676
- {
1677
- type: 'output_text',
1678
- text: 'I found the issue'
1679
- }
1680
- ]
1681
- }
1682
- }
1683
- ]);
1684
- const messages = adapter.getConversation(filePath);
1685
- expect(messages).toHaveLength(2);
1686
- expect(messages[0]).toEqual({
1687
- role: 'user',
1688
- content: 'Fix the bug',
1689
- timestamp: '2026-03-27T10:00:01Z'
1690
- });
1691
- expect(messages[1]).toEqual({
1692
- role: 'assistant',
1693
- content: 'I found the issue',
1694
- timestamp: '2026-03-27T10:00:05Z'
1695
- });
1696
- });
1697
- it('should parse completed Codex AgentMessage event records', ()=>{
1698
- const filePath = writeJsonl([
1699
- {
1700
- type: 'session_meta',
1701
- payload: {
1702
- id: 'sess-1',
1703
- cwd: '/repo',
1704
- timestamp: '2026-03-27T10:00:00Z'
1705
- }
1706
- },
1707
- {
1708
- type: 'event_msg',
1709
- timestamp: '2026-03-27T10:00:05Z',
1710
- payload: {
1711
- type: 'item_completed',
1712
- item: {
1713
- type: 'AgentMessage',
1714
- content: [
1715
- {
1716
- type: 'Text',
1717
- text: 'I found the issue'
1718
- }
1719
- ]
1720
- }
1721
- }
1722
- }
1723
- ]);
1724
- const messages = adapter.getConversation(filePath);
1725
- expect(messages).toEqual([
1726
- {
1727
- role: 'assistant',
1728
- content: 'I found the issue',
1729
- timestamp: '2026-03-27T10:00:05Z'
1730
- }
1731
- ]);
1732
- });
1733
- it('should not duplicate mirrored current Codex message records', ()=>{
1734
- const filePath = writeJsonl([
1735
- {
1736
- type: 'session_meta',
1737
- payload: {
1738
- id: 'sess-1',
1739
- cwd: '/repo',
1740
- timestamp: '2026-03-27T10:00:00Z'
1741
- }
1742
- },
1743
- {
1744
- type: 'response_item',
1745
- timestamp: '2026-03-27T10:00:01Z',
1746
- payload: {
1747
- type: 'message',
1748
- role: 'user',
1749
- content: [
1750
- {
1751
- type: 'input_text',
1752
- text: 'Fix the bug'
1753
- }
1754
- ],
1755
- internal_chat_message_metadata_passthrough: {
1756
- turn_id: 'turn-1'
1757
- }
1758
- }
1759
- },
1760
- {
1761
- type: 'event_msg',
1762
- timestamp: '2026-03-27T10:00:01.001Z',
1763
- payload: {
1764
- type: 'item_completed',
1765
- turn_id: 'turn-1',
1766
- item: {
1767
- type: 'UserMessage',
1768
- content: [
1769
- {
1770
- type: 'text',
1771
- text: 'Fix the bug'
1772
- }
1773
- ]
1774
- }
1775
- }
1776
- },
1777
- {
1778
- type: 'event_msg',
1779
- timestamp: '2026-03-27T10:00:05Z',
1780
- payload: {
1781
- type: 'item_completed',
1782
- turn_id: 'turn-1',
1783
- item: {
1784
- type: 'AgentMessage',
1785
- id: 'msg-1',
1786
- content: [
1787
- {
1788
- type: 'Text',
1789
- text: 'I found the issue'
1790
- }
1791
- ]
1792
- }
1793
- }
1794
- },
1795
- {
1796
- type: 'response_item',
1797
- timestamp: '2026-03-27T10:00:05.005Z',
1798
- payload: {
1799
- type: 'message',
1800
- id: 'msg-1',
1801
- role: 'assistant',
1802
- content: [
1803
- {
1804
- type: 'output_text',
1805
- text: 'I found the issue'
1806
- }
1807
- ],
1808
- internal_chat_message_metadata_passthrough: {
1809
- turn_id: 'turn-1'
1810
- }
1811
- }
1812
- }
1813
- ]);
1814
- const messages = adapter.getConversation(filePath);
1815
- expect(messages).toEqual([
1816
- {
1817
- role: 'user',
1818
- content: 'Fix the bug',
1819
- timestamp: '2026-03-27T10:00:01Z'
1820
- },
1821
- {
1822
- role: 'assistant',
1823
- content: 'I found the issue',
1824
- timestamp: '2026-03-27T10:00:05.005Z'
1825
- }
1826
- ]);
1827
- });
1828
- it('should skip session_meta entry', ()=>{
1829
- const filePath = writeJsonl([
1830
- {
1831
- type: 'session_meta',
1832
- payload: {
1833
- id: 'sess-1',
1834
- cwd: '/repo',
1835
- timestamp: '2026-03-27T10:00:00Z'
1836
- }
1837
- },
1838
- {
1839
- type: 'event',
1840
- timestamp: '2026-03-27T10:00:01Z',
1841
- payload: {
1842
- type: 'user_message',
1843
- message: 'Hello'
1844
- }
1845
- }
1846
- ]);
1847
- const messages = adapter.getConversation(filePath);
1848
- expect(messages).toHaveLength(1);
1849
- expect(messages[0].role).toBe('user');
1850
- });
1851
- it('should map task_complete to assistant role', ()=>{
1852
- const filePath = writeJsonl([
1853
- {
1854
- type: 'session_meta',
1855
- payload: {
1856
- id: 'sess-1',
1857
- cwd: '/repo',
1858
- timestamp: '2026-03-27T10:00:00Z'
1859
- }
1860
- },
1861
- {
1862
- type: 'event',
1863
- timestamp: '2026-03-27T10:00:05Z',
1864
- payload: {
1865
- type: 'task_complete',
1866
- message: 'Task finished successfully'
1867
- }
1868
- }
1869
- ]);
1870
- const messages = adapter.getConversation(filePath);
1871
- expect(messages).toHaveLength(1);
1872
- expect(messages[0].role).toBe('assistant');
1873
- expect(messages[0].content).toBe('Task finished successfully');
1874
- });
1875
- it('should skip non-conversation types in default mode', ()=>{
1876
- const filePath = writeJsonl([
1877
- {
1878
- type: 'session_meta',
1879
- payload: {
1880
- id: 'sess-1',
1881
- cwd: '/repo',
1882
- timestamp: '2026-03-27T10:00:00Z'
1883
- }
1884
- },
1885
- {
1886
- type: 'event',
1887
- timestamp: '2026-03-27T10:00:01Z',
1888
- payload: {
1889
- type: 'user_message',
1890
- message: 'Hello'
1891
- }
1892
- },
1893
- {
1894
- type: 'event',
1895
- timestamp: '2026-03-27T10:00:02Z',
1896
- payload: {
1897
- type: 'exec_command',
1898
- message: 'Running npm test'
1899
- }
1900
- },
1901
- {
1902
- type: 'event',
1903
- timestamp: '2026-03-27T10:00:03Z',
1904
- payload: {
1905
- type: 'agent_message',
1906
- message: 'Done'
1907
- }
1908
- }
1909
- ]);
1910
- const messages = adapter.getConversation(filePath);
1911
- expect(messages).toHaveLength(2);
1912
- });
1913
- it('should include non-conversation types as system in verbose mode', ()=>{
1914
- const filePath = writeJsonl([
1915
- {
1916
- type: 'session_meta',
1917
- payload: {
1918
- id: 'sess-1',
1919
- cwd: '/repo',
1920
- timestamp: '2026-03-27T10:00:00Z'
1921
- }
1922
- },
1923
- {
1924
- type: 'event',
1925
- timestamp: '2026-03-27T10:00:02Z',
1926
- payload: {
1927
- type: 'exec_command',
1928
- message: 'Running npm test'
1929
- }
1930
- }
1931
- ]);
1932
- const messages = adapter.getConversation(filePath, {
1933
- verbose: true
1934
- });
1935
- expect(messages).toHaveLength(1);
1936
- expect(messages[0].role).toBe('system');
1937
- expect(messages[0].content).toBe('Running npm test');
1938
- });
1939
- it('should handle malformed JSON lines gracefully', ()=>{
1940
- const filePath = path.join(tmpDir, 'malformed.jsonl');
1941
- fs.writeFileSync(filePath, [
1942
- JSON.stringify({
1943
- type: 'session_meta',
1944
- payload: {
1945
- id: 'sess-1',
1946
- cwd: '/repo',
1947
- timestamp: '2026-03-27T10:00:00Z'
1948
- }
1949
- }),
1950
- 'invalid json line',
1951
- JSON.stringify({
1952
- type: 'event',
1953
- timestamp: '2026-03-27T10:00:01Z',
1954
- payload: {
1955
- type: 'user_message',
1956
- message: 'Hello'
1957
- }
1958
- })
1959
- ].join('\n'));
1960
- const messages = adapter.getConversation(filePath);
1961
- expect(messages).toHaveLength(1);
1962
- });
1963
- it('should return empty array for missing file', ()=>{
1964
- const messages = adapter.getConversation('/nonexistent/path.jsonl');
1965
- expect(messages).toEqual([]);
1966
- });
1967
- it('should return empty array for empty file', ()=>{
1968
- const filePath = path.join(tmpDir, 'empty.jsonl');
1969
- fs.writeFileSync(filePath, '');
1970
- const messages = adapter.getConversation(filePath);
1971
- expect(messages).toEqual([]);
1972
- });
1973
- it('should skip entries with empty payload message', ()=>{
1974
- const filePath = writeJsonl([
1975
- {
1976
- type: 'session_meta',
1977
- payload: {
1978
- id: 'sess-1',
1979
- cwd: '/repo',
1980
- timestamp: '2026-03-27T10:00:00Z'
1981
- }
1982
- },
1983
- {
1984
- type: 'event',
1985
- timestamp: '2026-03-27T10:00:01Z',
1986
- payload: {
1987
- type: 'user_message',
1988
- message: ''
1989
- }
1990
- },
1991
- {
1992
- type: 'event',
1993
- timestamp: '2026-03-27T10:00:02Z',
1994
- payload: {
1995
- type: 'agent_message',
1996
- message: 'Response'
1997
- }
1998
- }
1999
- ]);
2000
- const messages = adapter.getConversation(filePath);
2001
- expect(messages).toHaveLength(1);
2002
- expect(messages[0].content).toBe('Response');
2003
- });
2004
- });
2005
- describe('listSessions', ()=>{
2006
- let tmpDir;
2007
- let sessionsDir;
2008
- beforeEach(()=>{
2009
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-list-'));
2010
- sessionsDir = path.join(tmpDir, 'sessions');
2011
- fs.mkdirSync(sessionsDir, {
2012
- recursive: true
2013
- });
2014
- adapter.codexSessionsDir = sessionsDir;
2015
- });
2016
- afterEach(()=>{
2017
- fs.rmSync(tmpDir, {
2018
- recursive: true,
2019
- force: true
2020
- });
2021
- });
2022
- function writeCodexSession(dateDir, sessionId, lines) {
2023
- fs.mkdirSync(dateDir, {
2024
- recursive: true
2025
- });
2026
- const filePath = path.join(dateDir, `${sessionId}.jsonl`);
2027
- fs.writeFileSync(filePath, lines.map((l)=>JSON.stringify(l)).join('\n'));
2028
- return filePath;
2029
- }
2030
- it('returns empty when sessions dir does not exist', async ()=>{
2031
- fs.rmSync(sessionsDir, {
2032
- recursive: true,
2033
- force: true
2034
- });
2035
- const result = await adapter.listSessions();
2036
- expect(result).toEqual([]);
2037
- });
2038
- it('walks every YYYY/MM/DD dir and returns all sessions', async ()=>{
2039
- const dayA = path.join(sessionsDir, '2025', '01', '01');
2040
- const dayB = path.join(sessionsDir, '2025', '02', '03');
2041
- writeCodexSession(dayA, 'sess-a', [
2042
- {
2043
- type: 'session_meta',
2044
- payload: {
2045
- id: 'sess-a',
2046
- cwd: '/repo-a',
2047
- timestamp: '2025-01-01T00:00:00Z'
2048
- }
2049
- },
2050
- {
2051
- type: 'event',
2052
- timestamp: '2025-01-01T00:00:01Z',
2053
- payload: {
2054
- type: 'user_message',
2055
- message: 'msg-a'
2056
- }
2057
- }
2058
- ]);
2059
- writeCodexSession(dayB, 'sess-b', [
2060
- {
2061
- type: 'session_meta',
2062
- payload: {
2063
- id: 'sess-b',
2064
- cwd: '/repo-b',
2065
- timestamp: '2025-02-03T00:00:00Z'
2066
- }
2067
- },
2068
- {
2069
- type: 'event',
2070
- timestamp: '2025-02-03T00:00:01Z',
2071
- payload: {
2072
- type: 'user_message',
2073
- message: 'msg-b'
2074
- }
2075
- }
2076
- ]);
2077
- const result = await adapter.listSessions();
2078
- expect(result).toHaveLength(2);
2079
- const byId = Object.fromEntries(result.map((r)=>[
2080
- r.sessionId,
2081
- r
2082
- ]));
2083
- expect(byId['sess-a']).toMatchObject({
2084
- type: 'codex',
2085
- cwd: '/repo-a',
2086
- firstUserMessage: 'msg-a'
2087
- });
2088
- expect(byId['sess-b']).toMatchObject({
2089
- type: 'codex',
2090
- cwd: '/repo-b',
2091
- firstUserMessage: 'msg-b'
2092
- });
2093
- });
2094
- it('applies strict-equality cwd filter against session_meta cwd', async ()=>{
2095
- const day = path.join(sessionsDir, '2025', '01', '01');
2096
- writeCodexSession(day, 'keep', [
2097
- {
2098
- type: 'session_meta',
2099
- payload: {
2100
- id: 'keep',
2101
- cwd: '/repo',
2102
- timestamp: '2025-01-01T00:00:00Z'
2103
- }
2104
- },
2105
- {
2106
- type: 'event',
2107
- timestamp: '2025-01-01T00:00:01Z',
2108
- payload: {
2109
- type: 'user_message',
2110
- message: 'yes'
2111
- }
2112
- }
2113
- ]);
2114
- writeCodexSession(day, 'drop', [
2115
- {
2116
- type: 'session_meta',
2117
- payload: {
2118
- id: 'drop',
2119
- cwd: '/other',
2120
- timestamp: '2025-01-01T00:01:00Z'
2121
- }
2122
- },
2123
- {
2124
- type: 'event',
2125
- timestamp: '2025-01-01T00:01:01Z',
2126
- payload: {
2127
- type: 'user_message',
2128
- message: 'no'
2129
- }
2130
- }
2131
- ]);
2132
- const result = await adapter.listSessions({
2133
- cwd: '/repo'
2134
- });
2135
- expect(result).toHaveLength(1);
2136
- expect(result[0].sessionId).toBe('keep');
2137
- });
2138
- it('skips files without a session_meta first line', async ()=>{
2139
- const day = path.join(sessionsDir, '2025', '01', '01');
2140
- writeCodexSession(day, 'bad', [
2141
- {
2142
- type: 'event',
2143
- timestamp: '2025-01-01T00:00:00Z',
2144
- payload: {
2145
- type: 'user_message',
2146
- message: 'orphan'
2147
- }
2148
- }
2149
- ]);
2150
- writeCodexSession(day, 'good', [
2151
- {
2152
- type: 'session_meta',
2153
- payload: {
2154
- id: 'good',
2155
- cwd: '/repo',
2156
- timestamp: '2025-01-01T00:00:00Z'
2157
- }
2158
- },
2159
- {
2160
- type: 'event',
2161
- timestamp: '2025-01-01T00:00:01Z',
2162
- payload: {
2163
- type: 'user_message',
2164
- message: 'ok'
2165
- }
2166
- }
2167
- ]);
2168
- const result = await adapter.listSessions();
2169
- expect(result).toHaveLength(1);
2170
- expect(result[0].sessionId).toBe('good');
2171
- });
2172
- it('captures the first user_message as firstUserMessage', async ()=>{
2173
- const day = path.join(sessionsDir, '2025', '01', '01');
2174
- writeCodexSession(day, 's', [
2175
- {
2176
- type: 'session_meta',
2177
- payload: {
2178
- id: 's',
2179
- cwd: '/repo',
2180
- timestamp: '2025-01-01T00:00:00Z'
2181
- }
2182
- },
2183
- {
2184
- type: 'event',
2185
- timestamp: '2025-01-01T00:00:01Z',
2186
- payload: {
2187
- type: 'agent_message',
2188
- message: 'preamble'
2189
- }
2190
- },
2191
- {
2192
- type: 'event',
2193
- timestamp: '2025-01-01T00:00:02Z',
2194
- payload: {
2195
- type: 'user_message',
2196
- message: 'first user'
2197
- }
2198
- },
2199
- {
2200
- type: 'event',
2201
- timestamp: '2025-01-01T00:00:03Z',
2202
- payload: {
2203
- type: 'user_message',
2204
- message: 'second user'
2205
- }
2206
- }
2207
- ]);
2208
- const result = await adapter.listSessions({
2209
- cwd: '/repo'
2210
- });
2211
- expect(result).toHaveLength(1);
2212
- expect(result[0].firstUserMessage).toBe('first user');
2213
- });
2214
- it('captures the first current Codex response_item user message as firstUserMessage', async ()=>{
2215
- const day = path.join(sessionsDir, '2025', '01', '01');
2216
- writeCodexSession(day, 'response-item-user', [
2217
- {
2218
- type: 'session_meta',
2219
- payload: {
2220
- id: 'response-item-user',
2221
- cwd: '/repo',
2222
- timestamp: '2025-01-01T00:00:00Z'
2223
- }
2224
- },
2225
- {
2226
- type: 'response_item',
2227
- timestamp: '2025-01-01T00:00:00.500Z',
2228
- payload: {
2229
- type: 'message',
2230
- role: 'user',
2231
- content: [
2232
- {
2233
- type: 'input_text',
2234
- text: '<environment_context>\n <cwd>/repo</cwd>\n</environment_context>'
2235
- }
2236
- ]
2237
- }
2238
- },
2239
- {
2240
- type: 'response_item',
2241
- timestamp: '2025-01-01T00:00:01Z',
2242
- payload: {
2243
- type: 'message',
2244
- role: 'assistant',
2245
- content: [
2246
- {
2247
- type: 'output_text',
2248
- text: 'preamble'
2249
- }
2250
- ]
2251
- }
2252
- },
2253
- {
2254
- type: 'response_item',
2255
- timestamp: '2025-01-01T00:00:02Z',
2256
- payload: {
2257
- type: 'message',
2258
- role: 'user',
2259
- content: [
2260
- {
2261
- type: 'input_text',
2262
- text: 'first current user'
2263
- }
2264
- ]
2265
- }
2266
- },
2267
- {
2268
- type: 'response_item',
2269
- timestamp: '2025-01-01T00:00:03Z',
2270
- payload: {
2271
- type: 'message',
2272
- role: 'user',
2273
- content: [
2274
- {
2275
- type: 'input_text',
2276
- text: 'second current user'
2277
- }
2278
- ]
2279
- }
2280
- }
2281
- ]);
2282
- const result = await adapter.listSessions({
2283
- cwd: '/repo'
2284
- });
2285
- expect(result).toHaveLength(1);
2286
- expect(result[0].firstUserMessage).toBe('first current user');
2287
- });
2288
- it('captures the first current Codex event_msg user message as firstUserMessage', async ()=>{
2289
- const day = path.join(sessionsDir, '2025', '01', '01');
2290
- writeCodexSession(day, 'event-msg-user', [
2291
- {
2292
- type: 'session_meta',
2293
- payload: {
2294
- id: 'event-msg-user',
2295
- cwd: '/repo',
2296
- timestamp: '2025-01-01T00:00:00Z'
2297
- }
2298
- },
2299
- {
2300
- type: 'event_msg',
2301
- timestamp: '2025-01-01T00:00:01Z',
2302
- payload: {
2303
- type: 'item_completed',
2304
- item: {
2305
- type: 'AgentMessage',
2306
- content: [
2307
- {
2308
- type: 'Text',
2309
- text: 'preamble'
2310
- }
2311
- ]
2312
- }
2313
- }
2314
- },
2315
- {
2316
- type: 'event_msg',
2317
- timestamp: '2025-01-01T00:00:02Z',
2318
- payload: {
2319
- type: 'item_completed',
2320
- item: {
2321
- type: 'UserMessage',
2322
- content: [
2323
- {
2324
- type: 'text',
2325
- text: 'first event user'
2326
- }
2327
- ]
2328
- }
2329
- }
2330
- },
2331
- {
2332
- type: 'event_msg',
2333
- timestamp: '2025-01-01T00:00:03Z',
2334
- payload: {
2335
- type: 'item_completed',
2336
- item: {
2337
- type: 'UserMessage',
2338
- content: [
2339
- {
2340
- type: 'text',
2341
- text: 'second event user'
2342
- }
2343
- ]
2344
- }
2345
- }
303
+ it("delegates conversation parsing and historical session listing", async ()=>{
304
+ const { sessionFile } = writeSession("listed", "/repo", [
305
+ {
306
+ type: "event",
307
+ timestamp: "2025-01-01T00:00:01Z",
308
+ payload: {
309
+ type: "user_message",
310
+ message: "first user"
2346
311
  }
2347
- ]);
2348
- const result = await adapter.listSessions({
2349
- cwd: '/repo'
2350
- });
2351
- expect(result).toHaveLength(1);
2352
- expect(result[0].firstUserMessage).toBe('first event user');
2353
- });
2354
- it('returns empty firstUserMessage when no user_message exists', async ()=>{
2355
- const day = path.join(sessionsDir, '2025', '01', '01');
2356
- writeCodexSession(day, 's', [
2357
- {
2358
- type: 'session_meta',
2359
- payload: {
2360
- id: 's',
2361
- cwd: '/repo',
2362
- timestamp: '2025-01-01T00:00:00Z'
2363
- }
2364
- },
2365
- {
2366
- type: 'event',
2367
- timestamp: '2025-01-01T00:00:01Z',
2368
- payload: {
2369
- type: 'agent_message',
2370
- message: 'agent only'
2371
- }
312
+ },
313
+ {
314
+ type: "event",
315
+ timestamp: "2025-01-01T00:00:02Z",
316
+ payload: {
317
+ type: "agent_message",
318
+ message: "answer"
2372
319
  }
2373
- ]);
2374
- const result = await adapter.listSessions({
2375
- cwd: '/repo'
2376
- });
2377
- expect(result).toHaveLength(1);
2378
- expect(result[0].firstUserMessage).toBe('');
2379
- });
320
+ }
321
+ ], "2025/01/01");
322
+ expect(adapter.getConversation(sessionFile)).toEqual([
323
+ {
324
+ role: "user",
325
+ content: "first user",
326
+ timestamp: "2025-01-01T00:00:01Z"
327
+ },
328
+ {
329
+ role: "assistant",
330
+ content: "answer",
331
+ timestamp: "2025-01-01T00:00:02Z"
332
+ }
333
+ ]);
334
+ await expect(adapter.listSessions({
335
+ cwd: "/repo"
336
+ })).resolves.toMatchObject([
337
+ {
338
+ type: "codex",
339
+ sessionId: "listed",
340
+ cwd: "/repo",
341
+ firstUserMessage: "first user",
342
+ sessionFilePath: sessionFile
343
+ }
344
+ ]);
2380
345
  });
346
+ function writeSession(sessionId, cwd, entries, day = "2026/03/18") {
347
+ const dateDir = path.join(tmpHome, ".codex", "sessions", ...day.split("/"));
348
+ fs.mkdirSync(dateDir, {
349
+ recursive: true
350
+ });
351
+ const sessionFile = path.join(dateDir, `${sessionId}.jsonl`);
352
+ fs.writeFileSync(sessionFile, [
353
+ JSON.stringify({
354
+ type: "session_meta",
355
+ payload: {
356
+ id: sessionId,
357
+ timestamp: "2026-03-18T15:00:00Z",
358
+ cwd
359
+ }
360
+ }),
361
+ ...entries.map((entry)=>JSON.stringify(entry))
362
+ ].join("\n"));
363
+ return {
364
+ sessionFile,
365
+ dateDir
366
+ };
367
+ }
368
+ function registryEntry(overrides = {}) {
369
+ return {
370
+ name: "codex-100",
371
+ type: "codex",
372
+ pid: 100,
373
+ runtime: "tmux",
374
+ runtimeRef: null,
375
+ cwd: "/repo-a",
376
+ startedAt: "2026-05-30T00:00:00.000Z",
377
+ sessionId: "cached",
378
+ sessionFilePath: "",
379
+ ...overrides
380
+ };
381
+ }
2381
382
  });
2382
383
 
2383
384
  //# sourceMappingURL=CodexAdapter.test.js.map