@agimon-ai/doompi-team 0.0.1-alpha.2 → 0.0.1-alpha.21

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 (727) hide show
  1. package/README.md +33 -17
  2. package/dist/adapters/asyncJobTracker.cjs +2 -0
  3. package/dist/adapters/asyncJobTracker.cjs.map +1 -0
  4. package/dist/adapters/asyncJobTracker.mjs +2 -0
  5. package/dist/adapters/asyncJobTracker.mjs.map +1 -0
  6. package/dist/adapters/atomicJson.cjs +2 -0
  7. package/dist/adapters/atomicJson.cjs.map +1 -0
  8. package/dist/adapters/atomicJson.mjs +2 -0
  9. package/dist/adapters/atomicJson.mjs.map +1 -0
  10. package/dist/adapters/pi/extension.cjs +2 -0
  11. package/dist/adapters/pi/extension.cjs.map +1 -0
  12. package/dist/adapters/pi/extension.mjs +2 -0
  13. package/dist/adapters/pi/extension.mjs.map +1 -0
  14. package/dist/adapters/pollScheduler.cjs +2 -0
  15. package/dist/adapters/pollScheduler.cjs.map +1 -0
  16. package/dist/adapters/pollScheduler.mjs +2 -0
  17. package/dist/adapters/pollScheduler.mjs.map +1 -0
  18. package/dist/adapters/process/childTranscript.cjs +5 -0
  19. package/dist/adapters/process/childTranscript.cjs.map +1 -0
  20. package/dist/adapters/process/childTranscript.mjs +5 -0
  21. package/dist/adapters/process/childTranscript.mjs.map +1 -0
  22. package/dist/adapters/process/env.cjs +2 -0
  23. package/dist/adapters/process/env.cjs.map +1 -0
  24. package/dist/adapters/process/env.d.cts +69 -0
  25. package/dist/adapters/process/env.d.cts.map +1 -0
  26. package/dist/adapters/process/env.d.mts +69 -0
  27. package/dist/adapters/process/env.d.mts.map +1 -0
  28. package/dist/adapters/process/env.mjs +2 -0
  29. package/dist/adapters/process/env.mjs.map +1 -0
  30. package/dist/adapters/process/sdkRunnerEntry.cjs +2 -0
  31. package/dist/adapters/process/sdkRunnerEntry.cjs.map +1 -0
  32. package/dist/adapters/process/sdkRunnerEntry.d.cts +5 -0
  33. package/dist/adapters/process/sdkRunnerEntry.d.cts.map +1 -0
  34. package/dist/adapters/process/sdkRunnerEntry.d.mts +5 -0
  35. package/dist/adapters/process/sdkRunnerEntry.d.mts.map +1 -0
  36. package/dist/adapters/process/sdkRunnerEntry.mjs +2 -0
  37. package/dist/adapters/process/sdkRunnerEntry.mjs.map +1 -0
  38. package/dist/adapters/processTerminal.cjs +2 -0
  39. package/dist/adapters/processTerminal.cjs.map +1 -0
  40. package/dist/adapters/processTerminal.mjs +2 -0
  41. package/dist/adapters/processTerminal.mjs.map +1 -0
  42. package/dist/adapters/resultWatcher.cjs +2 -0
  43. package/dist/adapters/resultWatcher.cjs.map +1 -0
  44. package/dist/adapters/resultWatcher.mjs +2 -0
  45. package/dist/adapters/resultWatcher.mjs.map +1 -0
  46. package/dist/adapters/runIdResolver.cjs +2 -0
  47. package/dist/adapters/runIdResolver.cjs.map +1 -0
  48. package/dist/adapters/runIdResolver.mjs +2 -0
  49. package/dist/adapters/runIdResolver.mjs.map +1 -0
  50. package/dist/adapters/runRegistry.cjs +2 -0
  51. package/dist/adapters/runRegistry.cjs.map +1 -0
  52. package/dist/adapters/runRegistry.mjs +2 -0
  53. package/dist/adapters/runRegistry.mjs.map +1 -0
  54. package/dist/adapters/scopeOwner.cjs +2 -0
  55. package/dist/adapters/scopeOwner.cjs.map +1 -0
  56. package/dist/adapters/scopeOwner.mjs +2 -0
  57. package/dist/adapters/scopeOwner.mjs.map +1 -0
  58. package/dist/adapters/staleRunReconciler.cjs +2 -0
  59. package/dist/adapters/staleRunReconciler.cjs.map +1 -0
  60. package/dist/adapters/staleRunReconciler.mjs +2 -0
  61. package/dist/adapters/staleRunReconciler.mjs.map +1 -0
  62. package/dist/adapters/statusReader.cjs +2 -0
  63. package/dist/adapters/statusReader.cjs.map +1 -0
  64. package/dist/adapters/statusReader.mjs +2 -0
  65. package/dist/adapters/statusReader.mjs.map +1 -0
  66. package/dist/adapters/suspendedRuns.cjs +6 -0
  67. package/dist/adapters/suspendedRuns.cjs.map +1 -0
  68. package/dist/adapters/suspendedRuns.mjs +6 -0
  69. package/dist/adapters/suspendedRuns.mjs.map +1 -0
  70. package/dist/api/backgroundWork.cjs +1 -1
  71. package/dist/api/backgroundWork.d.cts +2 -2
  72. package/dist/api/backgroundWork.d.mts +2 -2
  73. package/dist/api/backgroundWork.mjs +1 -1
  74. package/dist/api/capabilityCeiling.cjs +1 -1
  75. package/dist/api/capabilityCeiling.d.cts +3 -3
  76. package/dist/api/capabilityCeiling.d.mts +3 -3
  77. package/dist/api/capabilityCeiling.mjs +1 -2
  78. package/dist/api/delegation.cjs +1 -1
  79. package/dist/api/delegation.d.cts +1 -1
  80. package/dist/api/delegation.d.mts +1 -1
  81. package/dist/api/delegation.mjs +1 -1
  82. package/dist/api/teamSnapshot.cjs +1 -1
  83. package/dist/api/teamSnapshot.d.cts +2 -2
  84. package/dist/api/teamSnapshot.d.mts +2 -2
  85. package/dist/api/teamSnapshot.mjs +1 -2
  86. package/dist/commands/slash/chainExpression.cjs.map +1 -0
  87. package/dist/commands/slash/chainExpression.mjs.map +1 -0
  88. package/dist/commands/slash/doctor.cjs +5 -0
  89. package/dist/commands/slash/doctor.cjs.map +1 -0
  90. package/dist/commands/slash/doctor.mjs +5 -0
  91. package/dist/{slash/doctor.cjs.map → commands/slash/doctor.mjs.map} +1 -1
  92. package/dist/{slash → commands/slash}/slashCommands.cjs +1 -1
  93. package/dist/commands/slash/slashCommands.cjs.map +1 -0
  94. package/dist/{slash → commands/slash}/slashCommands.mjs +2 -2
  95. package/dist/commands/slash/slashCommands.mjs.map +1 -0
  96. package/dist/commands/slash/spawnRequestMapping.cjs.map +1 -0
  97. package/dist/commands/slash/spawnRequestMapping.mjs.map +1 -0
  98. package/dist/commands/slash/subagentLaunch.cjs +2 -0
  99. package/dist/commands/slash/subagentLaunch.cjs.map +1 -0
  100. package/dist/commands/slash/subagentLaunch.mjs +2 -0
  101. package/dist/commands/slash/subagentLaunch.mjs.map +1 -0
  102. package/dist/container/globalBridge.cjs.map +1 -1
  103. package/dist/container/globalBridge.d.cts +1 -1
  104. package/dist/container/globalBridge.d.mts +1 -1
  105. package/dist/container/globalBridge.mjs.map +1 -1
  106. package/dist/container/index.cjs +1 -1
  107. package/dist/container/index.cjs.map +1 -1
  108. package/dist/container/index.d.cts +2 -1
  109. package/dist/container/index.d.cts.map +1 -1
  110. package/dist/container/index.d.mts +2 -1
  111. package/dist/container/index.d.mts.map +1 -1
  112. package/dist/container/index.mjs +1 -1
  113. package/dist/container/index.mjs.map +1 -1
  114. package/dist/container/modules/agents.cjs +1 -1
  115. package/dist/container/modules/agents.cjs.map +1 -1
  116. package/dist/container/modules/agents.mjs +1 -1
  117. package/dist/container/modules/agents.mjs.map +1 -1
  118. package/dist/container/modules/core.cjs +1 -1
  119. package/dist/container/modules/core.cjs.map +1 -1
  120. package/dist/container/modules/core.mjs +1 -1
  121. package/dist/container/modules/core.mjs.map +1 -1
  122. package/dist/container/modules/extensions.cjs +1 -1
  123. package/dist/container/modules/extensions.cjs.map +1 -1
  124. package/dist/container/modules/extensions.mjs +1 -1
  125. package/dist/container/modules/extensions.mjs.map +1 -1
  126. package/dist/container/modules/intercom.cjs +1 -1
  127. package/dist/container/modules/intercom.cjs.map +1 -1
  128. package/dist/container/modules/intercom.mjs +1 -1
  129. package/dist/container/modules/intercom.mjs.map +1 -1
  130. package/dist/container/modules/runs.cjs +1 -1
  131. package/dist/container/modules/runs.cjs.map +1 -1
  132. package/dist/container/modules/runs.mjs +1 -1
  133. package/dist/container/modules/runs.mjs.map +1 -1
  134. package/dist/container/types.cjs +1 -1
  135. package/dist/container/types.cjs.map +1 -1
  136. package/dist/container/types.d.cts +1 -0
  137. package/dist/container/types.d.cts.map +1 -1
  138. package/dist/container/types.d.mts +1 -0
  139. package/dist/container/types.d.mts.map +1 -1
  140. package/dist/container/types.mjs +1 -1
  141. package/dist/container/types.mjs.map +1 -1
  142. package/dist/env.cjs +1 -2
  143. package/dist/env.d.cts +2 -65
  144. package/dist/env.d.mts +2 -65
  145. package/dist/env.mjs +1 -1
  146. package/dist/env.mjs.map +1 -1
  147. package/dist/extensions/config.cjs.map +1 -1
  148. package/dist/extensions/config.mjs.map +1 -1
  149. package/dist/extensions/delegationBridge.cjs.map +1 -1
  150. package/dist/extensions/delegationBridge.mjs.map +1 -1
  151. package/dist/extensions/doom.cjs +1 -1
  152. package/dist/extensions/doom.cjs.map +1 -1
  153. package/dist/extensions/doom.mjs +1 -1
  154. package/dist/extensions/doom.mjs.map +1 -1
  155. package/dist/extensions/fablePlanBridge.cjs +1 -1
  156. package/dist/extensions/fablePlanBridge.cjs.map +1 -1
  157. package/dist/extensions/fablePlanBridge.mjs +1 -1
  158. package/dist/extensions/fablePlanBridge.mjs.map +1 -1
  159. package/dist/extensions/managementActions.cjs +1 -1
  160. package/dist/extensions/managementActions.cjs.map +1 -1
  161. package/dist/extensions/managementActions.mjs +1 -1
  162. package/dist/extensions/managementActions.mjs.map +1 -1
  163. package/dist/extensions/operationJournal.cjs +1 -1
  164. package/dist/extensions/operationJournal.cjs.map +1 -1
  165. package/dist/extensions/operationJournal.mjs +1 -1
  166. package/dist/extensions/operationJournal.mjs.map +1 -1
  167. package/dist/extensions/orchestratorPrompt.cjs +5 -6
  168. package/dist/extensions/orchestratorPrompt.cjs.map +1 -1
  169. package/dist/extensions/orchestratorPrompt.mjs +5 -6
  170. package/dist/extensions/orchestratorPrompt.mjs.map +1 -1
  171. package/dist/extensions/schemas.mjs.map +1 -1
  172. package/dist/extensions/spawnPlan.cjs +3 -2
  173. package/dist/extensions/spawnPlan.cjs.map +1 -1
  174. package/dist/extensions/spawnPlan.mjs +3 -2
  175. package/dist/extensions/spawnPlan.mjs.map +1 -1
  176. package/dist/extensions/standard.cjs +1 -1
  177. package/dist/extensions/standard.cjs.map +1 -1
  178. package/dist/extensions/standard.mjs +1 -1
  179. package/dist/extensions/standard.mjs.map +1 -1
  180. package/dist/extensions/statusViews.cjs +1 -1
  181. package/dist/extensions/statusViews.cjs.map +1 -1
  182. package/dist/extensions/statusViews.mjs +2 -2
  183. package/dist/extensions/statusViews.mjs.map +1 -1
  184. package/dist/extensions/subagentPromptRuntime.cjs +11 -11
  185. package/dist/extensions/subagentPromptRuntime.cjs.map +1 -1
  186. package/dist/extensions/subagentPromptRuntime.d.cts.map +1 -1
  187. package/dist/extensions/subagentPromptRuntime.d.mts.map +1 -1
  188. package/dist/extensions/subagentPromptRuntime.mjs +5 -5
  189. package/dist/extensions/subagentPromptRuntime.mjs.map +1 -1
  190. package/dist/extensions/subagentTool.cjs +5 -6
  191. package/dist/extensions/subagentTool.cjs.map +1 -1
  192. package/dist/extensions/subagentTool.mjs +5 -6
  193. package/dist/extensions/subagentTool.mjs.map +1 -1
  194. package/dist/extensions/toolDescription.cjs +1 -1
  195. package/dist/extensions/toolDescription.cjs.map +1 -1
  196. package/dist/extensions/toolDescription.mjs +1 -1
  197. package/dist/extensions/toolDescription.mjs.map +1 -1
  198. package/dist/index.cjs +1 -1
  199. package/dist/index.cjs.map +1 -1
  200. package/dist/index.d.cts +2 -2
  201. package/dist/index.d.cts.map +1 -1
  202. package/dist/index.d.mts +2 -2
  203. package/dist/index.d.mts.map +1 -1
  204. package/dist/index.mjs +1 -1
  205. package/dist/index.mjs.map +1 -1
  206. package/dist/runs/background/cliRunnerEntry.cjs +1 -2
  207. package/dist/runs/background/cliRunnerEntry.mjs +1 -2
  208. package/dist/runs/sdkRunnerEntry.cjs +1 -2
  209. package/dist/runs/sdkRunnerEntry.d.cts +2 -1
  210. package/dist/runs/sdkRunnerEntry.d.mts +2 -1
  211. package/dist/runs/sdkRunnerEntry.mjs +1 -2
  212. package/dist/schemas/team/backgroundWork.cjs +2 -0
  213. package/dist/schemas/team/backgroundWork.cjs.map +1 -0
  214. package/dist/{protocol → schemas/team}/backgroundWork.d.cts +2 -2
  215. package/dist/schemas/team/backgroundWork.d.cts.map +1 -0
  216. package/dist/{protocol → schemas/team}/backgroundWork.d.mts +2 -2
  217. package/dist/schemas/team/backgroundWork.d.mts.map +1 -0
  218. package/dist/schemas/team/backgroundWork.mjs +2 -0
  219. package/dist/{protocol/backgroundWork.cjs.map → schemas/team/backgroundWork.mjs.map} +1 -1
  220. package/dist/schemas/team/capabilityCeiling.cjs +2 -0
  221. package/dist/schemas/team/capabilityCeiling.cjs.map +1 -0
  222. package/dist/{protocol → schemas/team}/capabilityCeiling.d.cts +3 -3
  223. package/dist/schemas/team/capabilityCeiling.d.cts.map +1 -0
  224. package/dist/{protocol → schemas/team}/capabilityCeiling.d.mts +3 -3
  225. package/dist/schemas/team/capabilityCeiling.d.mts.map +1 -0
  226. package/dist/schemas/team/capabilityCeiling.mjs +2 -0
  227. package/dist/schemas/team/capabilityCeiling.mjs.map +1 -0
  228. package/dist/schemas/team/delegation.cjs.map +1 -0
  229. package/dist/{protocol → schemas/team}/delegation.d.cts +1 -1
  230. package/dist/schemas/team/delegation.d.cts.map +1 -0
  231. package/dist/{protocol → schemas/team}/delegation.d.mts +1 -1
  232. package/dist/schemas/team/delegation.d.mts.map +1 -0
  233. package/dist/schemas/team/delegation.mjs.map +1 -0
  234. package/dist/schemas/team/packageConfig.cjs +2 -0
  235. package/dist/schemas/team/packageConfig.cjs.map +1 -0
  236. package/dist/schemas/team/packageConfig.mjs +2 -0
  237. package/dist/schemas/team/packageConfig.mjs.map +1 -0
  238. package/dist/services/agents/discovery.cjs +2 -0
  239. package/dist/services/agents/discovery.cjs.map +1 -0
  240. package/dist/services/agents/discovery.mjs +2 -0
  241. package/dist/services/agents/discovery.mjs.map +1 -0
  242. package/dist/services/agents/frontmatter.cjs.map +1 -0
  243. package/dist/{agents → services/agents}/frontmatter.mjs +1 -1
  244. package/dist/services/agents/frontmatter.mjs.map +1 -0
  245. package/dist/services/agents/loader.cjs +2 -0
  246. package/dist/services/agents/loader.cjs.map +1 -0
  247. package/dist/services/agents/loader.mjs +2 -0
  248. package/dist/services/agents/loader.mjs.map +1 -0
  249. package/dist/services/agents/memory.cjs +3 -0
  250. package/dist/services/agents/memory.cjs.map +1 -0
  251. package/dist/{agents → services/agents}/memory.mjs +1 -1
  252. package/dist/services/agents/memory.mjs.map +1 -0
  253. package/dist/services/agents/projectRoot.cjs +2 -0
  254. package/dist/services/agents/projectRoot.cjs.map +1 -0
  255. package/dist/services/agents/projectRoot.mjs +2 -0
  256. package/dist/services/agents/projectRoot.mjs.map +1 -0
  257. package/dist/services/agents/settings.cjs +2 -0
  258. package/dist/services/agents/settings.cjs.map +1 -0
  259. package/dist/services/agents/settings.mjs +2 -0
  260. package/dist/services/agents/settings.mjs.map +1 -0
  261. package/dist/services/agents/skills.cjs +0 -0
  262. package/dist/services/agents/skills.cjs.map +1 -0
  263. package/dist/services/agents/skills.mjs +5 -0
  264. package/dist/services/agents/skills.mjs.map +1 -0
  265. package/dist/{agents → services/agents}/types.d.cts +4 -4
  266. package/dist/services/agents/types.d.cts.map +1 -0
  267. package/dist/{agents → services/agents}/types.d.mts +4 -4
  268. package/dist/services/agents/types.d.mts.map +1 -0
  269. package/dist/services/api/backgroundWork.cjs +1 -0
  270. package/dist/services/api/backgroundWork.d.cts +1 -0
  271. package/dist/services/api/backgroundWork.d.mts +1 -0
  272. package/dist/services/api/backgroundWork.mjs +1 -0
  273. package/dist/services/api/capabilityCeiling.cjs +1 -0
  274. package/dist/services/api/capabilityCeiling.d.cts +2 -0
  275. package/dist/services/api/capabilityCeiling.d.mts +2 -0
  276. package/dist/services/api/capabilityCeiling.mjs +2 -0
  277. package/dist/services/api/capabilityCeiling.mjs.map +1 -0
  278. package/dist/services/api/teamSnapshot.cjs +1 -0
  279. package/dist/services/api/teamSnapshot.d.cts +1 -0
  280. package/dist/services/api/teamSnapshot.d.mts +1 -0
  281. package/dist/services/api/teamSnapshot.mjs +2 -0
  282. package/dist/services/api/teamSnapshot.mjs.map +1 -0
  283. package/dist/services/intercom/intercomBridge.cjs.map +1 -0
  284. package/dist/services/intercom/intercomBridge.mjs.map +1 -0
  285. package/dist/services/intercom/nativeTeamChannel.cjs +5 -0
  286. package/dist/services/intercom/nativeTeamChannel.cjs.map +1 -0
  287. package/dist/{intercom → services/intercom}/nativeTeamChannel.d.cts +1 -1
  288. package/dist/services/intercom/nativeTeamChannel.d.cts.map +1 -0
  289. package/dist/{intercom → services/intercom}/nativeTeamChannel.d.mts +1 -1
  290. package/dist/services/intercom/nativeTeamChannel.d.mts.map +1 -0
  291. package/dist/services/intercom/nativeTeamChannel.mjs +5 -0
  292. package/dist/services/intercom/nativeTeamChannel.mjs.map +1 -0
  293. package/dist/services/intercom/supervisorControlChannel.cjs +2 -0
  294. package/dist/services/intercom/supervisorControlChannel.cjs.map +1 -0
  295. package/dist/{intercom → services/intercom}/supervisorControlChannel.mjs +1 -1
  296. package/dist/services/intercom/supervisorControlChannel.mjs.map +1 -0
  297. package/dist/services/runs/background/asyncExecution.cjs +2 -0
  298. package/dist/services/runs/background/asyncExecution.cjs.map +1 -0
  299. package/dist/services/runs/background/asyncExecution.mjs +2 -0
  300. package/dist/services/runs/background/asyncExecution.mjs.map +1 -0
  301. package/dist/services/runs/background/asyncJobTracker.cjs +1 -0
  302. package/dist/services/runs/background/asyncJobTracker.mjs +2 -0
  303. package/dist/services/runs/background/asyncJobTracker.mjs.map +1 -0
  304. package/dist/services/runs/background/cliRunnerEntry.cjs +2 -0
  305. package/dist/services/runs/background/cliRunnerEntry.cjs.map +1 -0
  306. package/dist/services/runs/background/cliRunnerEntry.mjs +2 -0
  307. package/dist/services/runs/background/cliRunnerEntry.mjs.map +1 -0
  308. package/dist/services/runs/background/completionBatcher.cjs.map +1 -0
  309. package/dist/services/runs/background/completionBatcher.mjs +2 -0
  310. package/dist/services/runs/background/completionBatcher.mjs.map +1 -0
  311. package/dist/{runs → services/runs}/background/notify.cjs +1 -1
  312. package/dist/services/runs/background/notify.cjs.map +1 -0
  313. package/dist/{runs → services/runs}/background/notify.mjs +1 -1
  314. package/dist/services/runs/background/notify.mjs.map +1 -0
  315. package/dist/services/runs/background/parentWatchdog.cjs.map +1 -0
  316. package/dist/services/runs/background/parentWatchdog.mjs.map +1 -0
  317. package/dist/services/runs/background/processTerminal.cjs +1 -0
  318. package/dist/services/runs/background/processTerminal.mjs +2 -0
  319. package/dist/services/runs/background/processTerminal.mjs.map +1 -0
  320. package/dist/services/runs/background/resultWatcher.cjs +1 -0
  321. package/dist/services/runs/background/resultWatcher.mjs +2 -0
  322. package/dist/services/runs/background/resultWatcher.mjs.map +1 -0
  323. package/dist/services/runs/background/runIdResolver.cjs +1 -0
  324. package/dist/services/runs/background/runIdResolver.mjs +2 -0
  325. package/dist/services/runs/background/runIdResolver.mjs.map +1 -0
  326. package/dist/services/runs/background/runnerBootstrap.cjs +2 -0
  327. package/dist/services/runs/background/runnerBootstrap.cjs.map +1 -0
  328. package/dist/services/runs/background/runnerBootstrap.mjs +2 -0
  329. package/dist/services/runs/background/runnerBootstrap.mjs.map +1 -0
  330. package/dist/services/runs/background/runnerExecution.cjs +2 -0
  331. package/dist/services/runs/background/runnerExecution.cjs.map +1 -0
  332. package/dist/services/runs/background/runnerExecution.mjs +2 -0
  333. package/dist/services/runs/background/runnerExecution.mjs.map +1 -0
  334. package/dist/{runs → services/runs}/background/runnerReporting.cjs +2 -2
  335. package/dist/services/runs/background/runnerReporting.cjs.map +1 -0
  336. package/dist/{runs → services/runs}/background/runnerReporting.mjs +2 -2
  337. package/dist/services/runs/background/runnerReporting.mjs.map +1 -0
  338. package/dist/services/runs/background/runnerTeamMembership.cjs +2 -0
  339. package/dist/services/runs/background/runnerTeamMembership.cjs.map +1 -0
  340. package/dist/services/runs/background/runnerTeamMembership.mjs +2 -0
  341. package/dist/services/runs/background/runnerTeamMembership.mjs.map +1 -0
  342. package/dist/services/runs/background/spawnHandshake.cjs +2 -0
  343. package/dist/services/runs/background/spawnHandshake.cjs.map +1 -0
  344. package/dist/services/runs/background/spawnHandshake.mjs +2 -0
  345. package/dist/services/runs/background/spawnHandshake.mjs.map +1 -0
  346. package/dist/services/runs/background/staleRunReconciler.cjs +1 -0
  347. package/dist/services/runs/background/staleRunReconciler.mjs +2 -0
  348. package/dist/services/runs/background/staleRunReconciler.mjs.map +1 -0
  349. package/dist/services/runs/background/statusReader.cjs +1 -0
  350. package/dist/services/runs/background/statusReader.mjs +2 -0
  351. package/dist/services/runs/background/statusReader.mjs.map +1 -0
  352. package/dist/services/runs/background/statusWriter.cjs +2 -0
  353. package/dist/services/runs/background/statusWriter.cjs.map +1 -0
  354. package/dist/services/runs/background/statusWriter.mjs +2 -0
  355. package/dist/services/runs/background/statusWriter.mjs.map +1 -0
  356. package/dist/services/runs/background/subagentWait.cjs +2 -0
  357. package/dist/services/runs/background/subagentWait.cjs.map +1 -0
  358. package/dist/services/runs/background/subagentWait.mjs +2 -0
  359. package/dist/services/runs/background/subagentWait.mjs.map +1 -0
  360. package/dist/{runs/background/terminalPersistence.mjs → services/runs/background/terminalPersistence.cjs} +2 -2
  361. package/dist/services/runs/background/terminalPersistence.cjs.map +1 -0
  362. package/dist/services/runs/background/terminalPersistence.mjs +2 -0
  363. package/dist/services/runs/background/terminalPersistence.mjs.map +1 -0
  364. package/dist/services/runs/extensionErrorTelemetry.cjs +2 -0
  365. package/dist/services/runs/extensionErrorTelemetry.cjs.map +1 -0
  366. package/dist/services/runs/extensionErrorTelemetry.mjs +2 -0
  367. package/dist/services/runs/extensionErrorTelemetry.mjs.map +1 -0
  368. package/dist/services/runs/registry/processGroup.cjs.map +1 -0
  369. package/dist/services/runs/registry/processGroup.mjs.map +1 -0
  370. package/dist/services/runs/registry/runRegistry.cjs +1 -0
  371. package/dist/services/runs/registry/runRegistry.mjs +2 -0
  372. package/dist/services/runs/registry/runRegistry.mjs.map +1 -0
  373. package/dist/services/runs/registry/sessionLifecycle.cjs +2 -0
  374. package/dist/services/runs/registry/sessionLifecycle.cjs.map +1 -0
  375. package/dist/services/runs/registry/sessionLifecycle.mjs +2 -0
  376. package/dist/services/runs/registry/sessionLifecycle.mjs.map +1 -0
  377. package/dist/services/runs/registry/suspendedRuns.cjs +1 -0
  378. package/dist/services/runs/registry/suspendedRuns.mjs +2 -0
  379. package/dist/services/runs/registry/suspendedRuns.mjs.map +1 -0
  380. package/dist/services/runs/sdkRunnerEntry.cjs +1 -0
  381. package/dist/services/runs/sdkRunnerEntry.mjs +1 -0
  382. package/dist/services/runs/shared/claudeFableProfile.cjs +4 -0
  383. package/dist/services/runs/shared/claudeFableProfile.cjs.map +1 -0
  384. package/dist/services/runs/shared/claudeFableProfile.mjs +4 -0
  385. package/dist/services/runs/shared/claudeFableProfile.mjs.map +1 -0
  386. package/dist/services/runs/shared/depthGuard.cjs +2 -0
  387. package/dist/services/runs/shared/depthGuard.cjs.map +1 -0
  388. package/dist/services/runs/shared/depthGuard.mjs +2 -0
  389. package/dist/services/runs/shared/depthGuard.mjs.map +1 -0
  390. package/dist/services/runs/shared/mcpDirectToolAllowlist.cjs +2 -0
  391. package/dist/services/runs/shared/mcpDirectToolAllowlist.cjs.map +1 -0
  392. package/dist/services/runs/shared/mcpDirectToolAllowlist.mjs +2 -0
  393. package/dist/services/runs/shared/mcpDirectToolAllowlist.mjs.map +1 -0
  394. package/dist/services/runs/shared/modelFallback.cjs +2 -0
  395. package/dist/services/runs/shared/modelFallback.cjs.map +1 -0
  396. package/dist/services/runs/shared/modelFallback.mjs +2 -0
  397. package/dist/services/runs/shared/modelFallback.mjs.map +1 -0
  398. package/dist/services/runs/shared/modelScope.cjs +2 -0
  399. package/dist/services/runs/shared/modelScope.cjs.map +1 -0
  400. package/dist/services/runs/shared/modelScope.mjs +2 -0
  401. package/dist/services/runs/shared/modelScope.mjs.map +1 -0
  402. package/dist/services/runs/shared/nestedPath.cjs +2 -0
  403. package/dist/services/runs/shared/nestedPath.cjs.map +1 -0
  404. package/dist/services/runs/shared/nestedPath.mjs +2 -0
  405. package/dist/services/runs/shared/nestedPath.mjs.map +1 -0
  406. package/dist/services/runs/shared/piArgs.cjs +2 -0
  407. package/dist/services/runs/shared/piArgs.cjs.map +1 -0
  408. package/dist/services/runs/shared/piArgs.mjs +2 -0
  409. package/dist/services/runs/shared/piArgs.mjs.map +1 -0
  410. package/dist/services/runs/shared/piSpawn.cjs +2 -0
  411. package/dist/services/runs/shared/piSpawn.cjs.map +1 -0
  412. package/dist/services/runs/shared/piSpawn.mjs +2 -0
  413. package/dist/services/runs/shared/piSpawn.mjs.map +1 -0
  414. package/dist/services/runs/shared/runWithConcurrency.cjs +2 -0
  415. package/dist/services/runs/shared/runWithConcurrency.cjs.map +1 -0
  416. package/dist/services/runs/shared/runWithConcurrency.d.cts +25 -0
  417. package/dist/services/runs/shared/runWithConcurrency.d.cts.map +1 -0
  418. package/dist/services/runs/shared/runWithConcurrency.d.mts +25 -0
  419. package/dist/services/runs/shared/runWithConcurrency.d.mts.map +1 -0
  420. package/dist/services/runs/shared/runWithConcurrency.mjs +2 -0
  421. package/dist/services/runs/shared/runWithConcurrency.mjs.map +1 -0
  422. package/dist/services/runs/shared/runtimeRegistry.cjs +2 -0
  423. package/dist/services/runs/shared/runtimeRegistry.cjs.map +1 -0
  424. package/dist/services/runs/shared/runtimeRegistry.mjs +2 -0
  425. package/dist/services/runs/shared/runtimeRegistry.mjs.map +1 -0
  426. package/dist/services/runs/shared/structuredOutput.cjs +2 -0
  427. package/dist/{runs → services/runs}/shared/structuredOutput.cjs.map +1 -1
  428. package/dist/services/runs/shared/structuredOutput.mjs +2 -0
  429. package/dist/{runs → services/runs}/shared/structuredOutput.mjs.map +1 -1
  430. package/dist/services/runs/shared/toolAvailability.cjs +2 -0
  431. package/dist/services/runs/shared/toolAvailability.cjs.map +1 -0
  432. package/dist/services/runs/shared/toolAvailability.mjs +2 -0
  433. package/dist/services/runs/shared/toolAvailability.mjs.map +1 -0
  434. package/dist/services/runs/shared/toolBudget.cjs +2 -0
  435. package/dist/services/runs/shared/toolBudget.cjs.map +1 -0
  436. package/dist/services/runs/shared/toolBudget.mjs +2 -0
  437. package/dist/services/runs/shared/toolBudget.mjs.map +1 -0
  438. package/dist/shared/artifacts.cjs.map +1 -1
  439. package/dist/shared/artifacts.mjs.map +1 -1
  440. package/dist/shared/atomicJson.cjs +1 -2
  441. package/dist/shared/atomicJson.mjs +1 -1
  442. package/dist/shared/atomicJson.mjs.map +1 -1
  443. package/dist/shared/childTranscript.cjs +1 -5
  444. package/dist/shared/childTranscript.mjs +1 -4
  445. package/dist/shared/childTranscript.mjs.map +1 -1
  446. package/dist/shared/fileSystemRetry.cjs +1 -1
  447. package/dist/shared/fileSystemRetry.cjs.map +1 -1
  448. package/dist/shared/fileSystemRetry.mjs +1 -1
  449. package/dist/shared/fileSystemRetry.mjs.map +1 -1
  450. package/dist/shared/formatters.cjs.map +1 -1
  451. package/dist/shared/formatters.mjs.map +1 -1
  452. package/dist/shared/paths.cjs +1 -1
  453. package/dist/shared/paths.cjs.map +1 -1
  454. package/dist/shared/paths.mjs +1 -1
  455. package/dist/shared/paths.mjs.map +1 -1
  456. package/dist/shared/pollScheduler.cjs +1 -2
  457. package/dist/shared/pollScheduler.mjs +1 -1
  458. package/dist/shared/pollScheduler.mjs.map +1 -1
  459. package/dist/shared/scopeOwner.cjs +1 -2
  460. package/dist/shared/scopeOwner.mjs +1 -1
  461. package/dist/shared/scopeOwner.mjs.map +1 -1
  462. package/dist/tui/agentCatalog.cjs +1 -1
  463. package/dist/tui/agentCatalog.cjs.map +1 -1
  464. package/dist/tui/agentCatalog.mjs +1 -1
  465. package/dist/tui/agentCatalog.mjs.map +1 -1
  466. package/dist/tui/agentResourceProjection.cjs +1 -1
  467. package/dist/tui/agentResourceProjection.cjs.map +1 -1
  468. package/dist/tui/agentResourceProjection.mjs +1 -1
  469. package/dist/tui/agentResourceProjection.mjs.map +1 -1
  470. package/dist/tui/completionNotice.cjs +1 -1
  471. package/dist/tui/completionNotice.cjs.map +1 -1
  472. package/dist/tui/completionNotice.mjs +1 -1
  473. package/dist/tui/completionNotice.mjs.map +1 -1
  474. package/dist/tui/fleet.cjs +2 -2
  475. package/dist/tui/fleet.cjs.map +1 -1
  476. package/dist/tui/fleet.mjs +2 -2
  477. package/dist/tui/fleet.mjs.map +1 -1
  478. package/dist/tui/fleetAgentView.cjs.map +1 -1
  479. package/dist/tui/fleetAgentView.mjs.map +1 -1
  480. package/dist/tui/fleetStatus.cjs +1 -1
  481. package/dist/tui/fleetStatus.cjs.map +1 -1
  482. package/dist/tui/fleetStatus.mjs +1 -1
  483. package/dist/tui/fleetStatus.mjs.map +1 -1
  484. package/dist/tui/fleetTranscript.cjs +1 -1
  485. package/dist/tui/fleetTranscript.cjs.map +1 -1
  486. package/dist/tui/fleetTranscript.mjs +1 -1
  487. package/dist/tui/fleetTranscript.mjs.map +1 -1
  488. package/dist/tui/register.cjs +1 -1
  489. package/dist/tui/register.cjs.map +1 -1
  490. package/dist/tui/register.mjs +1 -1
  491. package/dist/tui/register.mjs.map +1 -1
  492. package/dist/tui/subagentToolRender.cjs +3 -0
  493. package/dist/tui/subagentToolRender.cjs.map +1 -0
  494. package/dist/tui/subagentToolRender.mjs +3 -0
  495. package/dist/tui/subagentToolRender.mjs.map +1 -0
  496. package/dist/{types.d.cts → types/index.d.cts} +2 -2
  497. package/dist/types/index.d.cts.map +1 -0
  498. package/dist/{types.d.mts → types/index.d.mts} +2 -2
  499. package/dist/types/index.d.mts.map +1 -0
  500. package/package.json +23 -16
  501. package/dist/agents/discovery.cjs +0 -2
  502. package/dist/agents/discovery.cjs.map +0 -1
  503. package/dist/agents/discovery.mjs +0 -2
  504. package/dist/agents/discovery.mjs.map +0 -1
  505. package/dist/agents/frontmatter.cjs.map +0 -1
  506. package/dist/agents/frontmatter.mjs.map +0 -1
  507. package/dist/agents/loader.cjs +0 -2
  508. package/dist/agents/loader.cjs.map +0 -1
  509. package/dist/agents/loader.mjs +0 -2
  510. package/dist/agents/loader.mjs.map +0 -1
  511. package/dist/agents/memory.cjs +0 -3
  512. package/dist/agents/memory.cjs.map +0 -1
  513. package/dist/agents/memory.mjs.map +0 -1
  514. package/dist/agents/projectRoot.cjs +0 -2
  515. package/dist/agents/projectRoot.cjs.map +0 -1
  516. package/dist/agents/projectRoot.mjs +0 -2
  517. package/dist/agents/projectRoot.mjs.map +0 -1
  518. package/dist/agents/settings.cjs +0 -2
  519. package/dist/agents/settings.cjs.map +0 -1
  520. package/dist/agents/settings.mjs +0 -2
  521. package/dist/agents/settings.mjs.map +0 -1
  522. package/dist/agents/skills.cjs +0 -0
  523. package/dist/agents/skills.cjs.map +0 -1
  524. package/dist/agents/skills.mjs +0 -5
  525. package/dist/agents/skills.mjs.map +0 -1
  526. package/dist/agents/types.d.cts.map +0 -1
  527. package/dist/agents/types.d.mts.map +0 -1
  528. package/dist/api/capabilityCeiling.mjs.map +0 -1
  529. package/dist/api/teamSnapshot.mjs.map +0 -1
  530. package/dist/env.cjs.map +0 -1
  531. package/dist/env.d.cts.map +0 -1
  532. package/dist/env.d.mts.map +0 -1
  533. package/dist/extension.cjs +0 -2
  534. package/dist/extension.cjs.map +0 -1
  535. package/dist/extension.mjs +0 -2
  536. package/dist/extension.mjs.map +0 -1
  537. package/dist/intercom/intercomBridge.cjs.map +0 -1
  538. package/dist/intercom/intercomBridge.mjs.map +0 -1
  539. package/dist/intercom/nativeTeamChannel.cjs +0 -5
  540. package/dist/intercom/nativeTeamChannel.cjs.map +0 -1
  541. package/dist/intercom/nativeTeamChannel.d.cts.map +0 -1
  542. package/dist/intercom/nativeTeamChannel.d.mts.map +0 -1
  543. package/dist/intercom/nativeTeamChannel.mjs +0 -5
  544. package/dist/intercom/nativeTeamChannel.mjs.map +0 -1
  545. package/dist/intercom/supervisorControlChannel.cjs +0 -2
  546. package/dist/intercom/supervisorControlChannel.cjs.map +0 -1
  547. package/dist/intercom/supervisorControlChannel.mjs.map +0 -1
  548. package/dist/protocol/backgroundWork.cjs +0 -2
  549. package/dist/protocol/backgroundWork.d.cts.map +0 -1
  550. package/dist/protocol/backgroundWork.d.mts.map +0 -1
  551. package/dist/protocol/backgroundWork.mjs +0 -2
  552. package/dist/protocol/backgroundWork.mjs.map +0 -1
  553. package/dist/protocol/capabilityCeiling.cjs +0 -2
  554. package/dist/protocol/capabilityCeiling.cjs.map +0 -1
  555. package/dist/protocol/capabilityCeiling.d.cts.map +0 -1
  556. package/dist/protocol/capabilityCeiling.d.mts.map +0 -1
  557. package/dist/protocol/capabilityCeiling.mjs +0 -2
  558. package/dist/protocol/capabilityCeiling.mjs.map +0 -1
  559. package/dist/protocol/delegation.cjs.map +0 -1
  560. package/dist/protocol/delegation.d.cts.map +0 -1
  561. package/dist/protocol/delegation.d.mts.map +0 -1
  562. package/dist/protocol/delegation.mjs.map +0 -1
  563. package/dist/runs/background/asyncExecution.cjs +0 -2
  564. package/dist/runs/background/asyncExecution.cjs.map +0 -1
  565. package/dist/runs/background/asyncExecution.mjs +0 -2
  566. package/dist/runs/background/asyncExecution.mjs.map +0 -1
  567. package/dist/runs/background/asyncJobTracker.cjs +0 -2
  568. package/dist/runs/background/asyncJobTracker.cjs.map +0 -1
  569. package/dist/runs/background/asyncJobTracker.mjs +0 -2
  570. package/dist/runs/background/asyncJobTracker.mjs.map +0 -1
  571. package/dist/runs/background/cliRunnerEntry.cjs.map +0 -1
  572. package/dist/runs/background/cliRunnerEntry.mjs.map +0 -1
  573. package/dist/runs/background/completionBatcher.cjs.map +0 -1
  574. package/dist/runs/background/completionBatcher.mjs +0 -2
  575. package/dist/runs/background/completionBatcher.mjs.map +0 -1
  576. package/dist/runs/background/notify.cjs.map +0 -1
  577. package/dist/runs/background/notify.mjs.map +0 -1
  578. package/dist/runs/background/parentWatchdog.cjs.map +0 -1
  579. package/dist/runs/background/parentWatchdog.mjs.map +0 -1
  580. package/dist/runs/background/processTerminal.cjs +0 -2
  581. package/dist/runs/background/processTerminal.cjs.map +0 -1
  582. package/dist/runs/background/processTerminal.mjs +0 -2
  583. package/dist/runs/background/processTerminal.mjs.map +0 -1
  584. package/dist/runs/background/resultWatcher.cjs +0 -2
  585. package/dist/runs/background/resultWatcher.cjs.map +0 -1
  586. package/dist/runs/background/resultWatcher.mjs +0 -2
  587. package/dist/runs/background/resultWatcher.mjs.map +0 -1
  588. package/dist/runs/background/runIdResolver.cjs +0 -2
  589. package/dist/runs/background/runIdResolver.cjs.map +0 -1
  590. package/dist/runs/background/runIdResolver.mjs +0 -2
  591. package/dist/runs/background/runIdResolver.mjs.map +0 -1
  592. package/dist/runs/background/runnerBootstrap.cjs +0 -2
  593. package/dist/runs/background/runnerBootstrap.cjs.map +0 -1
  594. package/dist/runs/background/runnerBootstrap.mjs +0 -2
  595. package/dist/runs/background/runnerBootstrap.mjs.map +0 -1
  596. package/dist/runs/background/runnerExecution.cjs +0 -2
  597. package/dist/runs/background/runnerExecution.cjs.map +0 -1
  598. package/dist/runs/background/runnerExecution.mjs +0 -2
  599. package/dist/runs/background/runnerExecution.mjs.map +0 -1
  600. package/dist/runs/background/runnerReporting.cjs.map +0 -1
  601. package/dist/runs/background/runnerReporting.mjs.map +0 -1
  602. package/dist/runs/background/runnerTeamMembership.cjs +0 -2
  603. package/dist/runs/background/runnerTeamMembership.cjs.map +0 -1
  604. package/dist/runs/background/runnerTeamMembership.mjs +0 -2
  605. package/dist/runs/background/runnerTeamMembership.mjs.map +0 -1
  606. package/dist/runs/background/spawnHandshake.cjs +0 -2
  607. package/dist/runs/background/spawnHandshake.cjs.map +0 -1
  608. package/dist/runs/background/spawnHandshake.mjs +0 -2
  609. package/dist/runs/background/spawnHandshake.mjs.map +0 -1
  610. package/dist/runs/background/staleRunReconciler.cjs +0 -2
  611. package/dist/runs/background/staleRunReconciler.cjs.map +0 -1
  612. package/dist/runs/background/staleRunReconciler.mjs +0 -2
  613. package/dist/runs/background/staleRunReconciler.mjs.map +0 -1
  614. package/dist/runs/background/statusReader.cjs +0 -2
  615. package/dist/runs/background/statusReader.cjs.map +0 -1
  616. package/dist/runs/background/statusReader.mjs +0 -2
  617. package/dist/runs/background/statusReader.mjs.map +0 -1
  618. package/dist/runs/background/statusWriter.cjs +0 -2
  619. package/dist/runs/background/statusWriter.cjs.map +0 -1
  620. package/dist/runs/background/statusWriter.mjs +0 -2
  621. package/dist/runs/background/statusWriter.mjs.map +0 -1
  622. package/dist/runs/background/subagentWait.cjs +0 -2
  623. package/dist/runs/background/subagentWait.cjs.map +0 -1
  624. package/dist/runs/background/subagentWait.mjs +0 -2
  625. package/dist/runs/background/subagentWait.mjs.map +0 -1
  626. package/dist/runs/background/terminalPersistence.cjs +0 -2
  627. package/dist/runs/background/terminalPersistence.cjs.map +0 -1
  628. package/dist/runs/background/terminalPersistence.mjs.map +0 -1
  629. package/dist/runs/registry/processGroup.cjs.map +0 -1
  630. package/dist/runs/registry/processGroup.mjs.map +0 -1
  631. package/dist/runs/registry/runRegistry.cjs +0 -2
  632. package/dist/runs/registry/runRegistry.cjs.map +0 -1
  633. package/dist/runs/registry/runRegistry.mjs +0 -2
  634. package/dist/runs/registry/runRegistry.mjs.map +0 -1
  635. package/dist/runs/registry/sessionLifecycle.cjs +0 -2
  636. package/dist/runs/registry/sessionLifecycle.cjs.map +0 -1
  637. package/dist/runs/registry/sessionLifecycle.mjs +0 -2
  638. package/dist/runs/registry/sessionLifecycle.mjs.map +0 -1
  639. package/dist/runs/registry/suspendedRuns.cjs +0 -4
  640. package/dist/runs/registry/suspendedRuns.cjs.map +0 -1
  641. package/dist/runs/registry/suspendedRuns.mjs +0 -4
  642. package/dist/runs/registry/suspendedRuns.mjs.map +0 -1
  643. package/dist/runs/sdkRunnerEntry.cjs.map +0 -1
  644. package/dist/runs/sdkRunnerEntry.mjs.map +0 -1
  645. package/dist/runs/shared/claudeFableProfile.cjs +0 -4
  646. package/dist/runs/shared/claudeFableProfile.cjs.map +0 -1
  647. package/dist/runs/shared/claudeFableProfile.mjs +0 -4
  648. package/dist/runs/shared/claudeFableProfile.mjs.map +0 -1
  649. package/dist/runs/shared/depthGuard.cjs +0 -2
  650. package/dist/runs/shared/depthGuard.cjs.map +0 -1
  651. package/dist/runs/shared/depthGuard.mjs +0 -2
  652. package/dist/runs/shared/depthGuard.mjs.map +0 -1
  653. package/dist/runs/shared/mcpDirectToolAllowlist.cjs +0 -2
  654. package/dist/runs/shared/mcpDirectToolAllowlist.cjs.map +0 -1
  655. package/dist/runs/shared/mcpDirectToolAllowlist.mjs +0 -2
  656. package/dist/runs/shared/mcpDirectToolAllowlist.mjs.map +0 -1
  657. package/dist/runs/shared/modelFallback.cjs +0 -2
  658. package/dist/runs/shared/modelFallback.cjs.map +0 -1
  659. package/dist/runs/shared/modelFallback.mjs +0 -2
  660. package/dist/runs/shared/modelFallback.mjs.map +0 -1
  661. package/dist/runs/shared/modelScope.cjs +0 -2
  662. package/dist/runs/shared/modelScope.cjs.map +0 -1
  663. package/dist/runs/shared/modelScope.mjs +0 -2
  664. package/dist/runs/shared/modelScope.mjs.map +0 -1
  665. package/dist/runs/shared/nestedPath.cjs +0 -2
  666. package/dist/runs/shared/nestedPath.cjs.map +0 -1
  667. package/dist/runs/shared/nestedPath.mjs +0 -2
  668. package/dist/runs/shared/nestedPath.mjs.map +0 -1
  669. package/dist/runs/shared/piArgs.cjs +0 -2
  670. package/dist/runs/shared/piArgs.cjs.map +0 -1
  671. package/dist/runs/shared/piArgs.mjs +0 -2
  672. package/dist/runs/shared/piArgs.mjs.map +0 -1
  673. package/dist/runs/shared/piSpawn.cjs +0 -2
  674. package/dist/runs/shared/piSpawn.cjs.map +0 -1
  675. package/dist/runs/shared/piSpawn.mjs +0 -2
  676. package/dist/runs/shared/piSpawn.mjs.map +0 -1
  677. package/dist/runs/shared/runWithConcurrency.cjs +0 -2
  678. package/dist/runs/shared/runWithConcurrency.cjs.map +0 -1
  679. package/dist/runs/shared/runWithConcurrency.mjs +0 -2
  680. package/dist/runs/shared/runWithConcurrency.mjs.map +0 -1
  681. package/dist/runs/shared/runtimeRegistry.cjs +0 -2
  682. package/dist/runs/shared/runtimeRegistry.cjs.map +0 -1
  683. package/dist/runs/shared/runtimeRegistry.mjs +0 -2
  684. package/dist/runs/shared/runtimeRegistry.mjs.map +0 -1
  685. package/dist/runs/shared/structuredOutput.cjs +0 -2
  686. package/dist/runs/shared/structuredOutput.mjs +0 -2
  687. package/dist/runs/shared/toolAvailability.cjs +0 -2
  688. package/dist/runs/shared/toolAvailability.cjs.map +0 -1
  689. package/dist/runs/shared/toolAvailability.mjs +0 -2
  690. package/dist/runs/shared/toolAvailability.mjs.map +0 -1
  691. package/dist/runs/shared/toolBudget.cjs +0 -2
  692. package/dist/runs/shared/toolBudget.cjs.map +0 -1
  693. package/dist/runs/shared/toolBudget.mjs +0 -2
  694. package/dist/runs/shared/toolBudget.mjs.map +0 -1
  695. package/dist/shared/atomicJson.cjs.map +0 -1
  696. package/dist/shared/childTranscript.cjs.map +0 -1
  697. package/dist/shared/pollScheduler.cjs.map +0 -1
  698. package/dist/shared/scopeOwner.cjs.map +0 -1
  699. package/dist/slash/chainExpression.cjs.map +0 -1
  700. package/dist/slash/chainExpression.mjs.map +0 -1
  701. package/dist/slash/doctor.cjs +0 -5
  702. package/dist/slash/doctor.mjs +0 -5
  703. package/dist/slash/doctor.mjs.map +0 -1
  704. package/dist/slash/slashCommands.cjs.map +0 -1
  705. package/dist/slash/slashCommands.mjs.map +0 -1
  706. package/dist/slash/spawnRequestMapping.cjs.map +0 -1
  707. package/dist/slash/spawnRequestMapping.mjs.map +0 -1
  708. package/dist/slash/subagentLaunch.cjs +0 -2
  709. package/dist/slash/subagentLaunch.cjs.map +0 -1
  710. package/dist/slash/subagentLaunch.mjs +0 -2
  711. package/dist/slash/subagentLaunch.mjs.map +0 -1
  712. package/dist/types.d.cts.map +0 -1
  713. package/dist/types.d.mts.map +0 -1
  714. /package/dist/{slash → commands/slash}/chainExpression.cjs +0 -0
  715. /package/dist/{slash → commands/slash}/chainExpression.mjs +0 -0
  716. /package/dist/{slash → commands/slash}/spawnRequestMapping.cjs +0 -0
  717. /package/dist/{slash → commands/slash}/spawnRequestMapping.mjs +0 -0
  718. /package/dist/{protocol → schemas/team}/delegation.cjs +0 -0
  719. /package/dist/{protocol → schemas/team}/delegation.mjs +0 -0
  720. /package/dist/{agents → services/agents}/frontmatter.cjs +0 -0
  721. /package/dist/{intercom → services/intercom}/intercomBridge.cjs +0 -0
  722. /package/dist/{intercom → services/intercom}/intercomBridge.mjs +0 -0
  723. /package/dist/{runs → services/runs}/background/completionBatcher.cjs +0 -0
  724. /package/dist/{runs → services/runs}/background/parentWatchdog.cjs +0 -0
  725. /package/dist/{runs → services/runs}/background/parentWatchdog.mjs +0 -0
  726. /package/dist/{runs → services/runs}/registry/processGroup.cjs +0 -0
  727. /package/dist/{runs → services/runs}/registry/processGroup.mjs +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resultWatcher.cjs","names":["path","currentResultsDir","currentRunsDir","fs","resolveWatchPath","SUBAGENT_RUN_ID_ENV","DOOM_TEAM_TYPES"],"sources":["../../src/adapters/resultWatcher.ts"],"sourcesContent":["/**\n * Watches `currentResultsDir()` for a run's terminal result and delivers each one to\n * a consumer exactly once.\n *\n * WHY THIS EXISTS:\n * The predecessor's watcher\n * (`doom-pi-subagents/src/runs/background/resultWatcher.ts`) had four\n * separate, verified bugs. This is a redesign around the fixes, not a line\n * port - the fixes below are structural, not patches on the old control flow.\n *\n * FIX 1 - WATCH BOTH 'rename' AND 'change':\n * The predecessor only reacted to `event === 'rename'` (`resultWatcher.ts:398-399`),\n * so a result file rewritten in place - same path, new contents, no\n * create/delete/rename - was never noticed until the poll safety net happened\n * to run. `watchResultsDir()` below reacts to any `fs.watch` callback whose\n * filename matches, regardless of `eventType`, the same way\n * `SpawnHandshake.watchDirectory` already does in this package.\n *\n * FIX 2 - TTL DEDUPE:\n * The predecessor re-implemented \"is this a duplicate\" inline at its call\n * site (`resultWatcher.ts:180-195`) *and* separately inside\n * `completion-dedupe.ts`'s `markSeenWithTtl`, so the check and the record used\n * two independently maintained copies of the same expiry logic. Every check\n * and every record here goes through `hasRecentlyDelivered()` and\n * `markDelivered()` and nothing else - one implementation of the TTL policy,\n * used the same way on both sides.\n *\n * FIX 3 - TIMER TYPES:\n * The predecessor stored `setTimeout`'s return value in fields typed (or\n * used) as if they were numbers (`resultWatcher.ts:361,371,425`), which only\n * happens to work under Node's `setTimeout` and breaks the moment the return\n * value is compared, serialized, or ported somewhere `setTimeout` returns a\n * number (browsers, some polyfills). This module does not own a timer at all:\n * `fs.watch` is the fast path and `PollScheduler` is the bounded safety net\n * (see fix 5), so there is no `Timeout` handle here for the compiler to get\n * wrong in the first place.\n *\n * FIX 4 - ATOMIC CONSUME (TOCTOU):\n * The predecessor checked a result file's existence, then unlinked it, as two\n * separate steps (`resultWatcher.ts:184-194,324-332`), racing any other\n * reader of the same file between the two. Here, the *first* thing that\n * happens to a candidate result is an attempted rename into\n * `currentRunsDir()/<runId>/claimed-result.json` (`claimResult()`). A successful\n * rename is exclusive ownership: nothing else can also be holding that\n * result, because there is only ever one file at the source path and only one\n * caller's `renameSync` can move it. A failed rename (source already gone)\n * means another claimer got there first, which is `claimResult()`'s normal,\n * expected return of `false` - not a caught error.\n *\n * FIX 5 - WATCH-FIRST, POLL AS A BOUNDED SAFETY NET:\n * `start()` registers with `PollScheduler` instead of owning a timer, the\n * same pattern as every other long-lived background subscriber in this\n * directory. `fs.watch` gives low latency; the scheduler's tick is the bound\n * on how late a check can ever be on a platform or mount where watches are\n * unreliable. Neither path is trusted alone.\n *\n * DESIGN PATTERNS:\n * - A claimed result lives at `currentRunsDir()/<runId>/claimed-result.json`, inside\n * the run's own working directory rather than a subdirectory of\n * `currentResultsDir()`. Putting it under `currentResultsDir()` would make our own claims\n * visible to the very `fs.watch` this module registers, and would risk\n * colliding with `currentResultsDir()/<runId>.json` naming. `currentRunsDir()/<runId>/` is\n * already this run's own directory (see `statusWriter.ts`,\n * `terminalPersistence.ts`), so this reuses existing per-run space instead\n * of inventing a second one\n * - `run()` uses promise-based filesystem operations and is awaited by\n * `PollScheduler`; delivery remains independently asynchronous, and a\n * claim's `inFlight` flag stops it from being re-fired on the next tick\n * while the previous attempt is still outstanding\n * - `run()` reports \"work happened\" only when it claimed a new file, not when\n * it merely retried a still-pending delivery. A consumer that keeps\n * rejecting the same result forever would otherwise pin the scheduler's\n * backoff at the floor indefinitely\n * - A crash between claiming a result and delivering it leaves\n * `claimed-result.json` on disk with nothing tracking it in memory.\n * `start()` scans for these once, so a fresh process picks up an\n * interrupted delivery instead of leaving it stranded forever\n *\n * AVOID:\n * - Reading or unlinking anything at `currentResultsDir()/<runId>.json` once\n * `claimResult()` has returned true for it; ownership has moved to the\n * claimed copy, and the source name may already belong to an unrelated,\n * later result\n * - Treating a rename failure from `claimResult()` as noteworthy; the\n * `onProcessingError` path is for delivery failures and parse failures, not\n * for the ordinary case of losing a claim race\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { inject, injectable } from 'inversify';\nimport { DOOM_TEAM_TYPES } from '../container/types.ts';\nimport { resolveWatchPath } from '../shared/configDir.ts';\nimport { currentResultsDir, currentRunsDir } from '../shared/paths.ts';\nimport type { IPollScheduler } from '../shared/pollScheduler.ts';\nimport { SUBAGENT_RUN_ID_ENV } from './process/env.ts';\n\n/** Suffix a candidate result file must have in `currentResultsDir()`. */\nexport const RESULT_FILE_SUFFIX = '.json';\n/** Leaf name of a claimed result inside `currentRunsDir()/<runId>/`. */\nexport const CLAIMED_RESULT_FILE_NAME = 'claimed-result.json';\n\n/** Poll safety-net cadence, generously looser than `fs.watch`'s latency (see `PollScheduler`'s header). */\nconst DEFAULT_POLL_INTERVAL_MS = 4_000;\n/** How long a delivered run id is remembered, so a re-appearing result for it is dropped instead of redelivered. */\nconst DEFAULT_DEDUPE_TTL_MS = 60_000;\n/** Cap on tracked delivered ids, so a very long session cannot grow this without bound. */\nconst MAX_TRACKED_DELIVERIES = 256;\n\nconst POLL_SUBSCRIBER_ID = 'result-watcher';\n\n/** The parsed contents of a result file, with `runId` guaranteed present. */\nexport interface RunResultFile {\n runId: string;\n [key: string]: unknown;\n}\n\n/**\n * Handles one delivered result. Returns (or resolves to) `true` once it has\n * been durably accepted, which is what lets the claimed copy be removed;\n * `false` means \"not yet\", and the same claim is retried on a later tick.\n */\nexport type ResultConsumer = (result: RunResultFile) => boolean | Promise<boolean>;\n\nexport interface IResultWatcher {\n /** Begin watching. Calling this again resets whatever was previously tracked, same as `CoalescedStatusWriter.open`. */\n start(consumer: ResultConsumer): void;\n /** Stop watching, unregister from the scheduler, and drop all in-memory state. */\n stop(): void;\n}\n\ninterface ClaimEntry {\n /** True while `deliverClaim()` is awaiting this claim's consumer call, so `run()` does not fire it twice. */\n inFlight: boolean;\n}\n\nfunction resultPathFor(file: string): string {\n return path.join(currentResultsDir(), file);\n}\n\nfunction runDirFor(runId: string): string {\n return path.join(currentRunsDir(), runId);\n}\n\nfunction claimPathFor(runId: string): string {\n return path.join(runDirFor(runId), CLAIMED_RESULT_FILE_NAME);\n}\n\nfunction errorCode(error: unknown): string | undefined {\n return typeof error === 'object' && error !== null && 'code' in error\n ? (error as NodeJS.ErrnoException).code\n : undefined;\n}\n\nfunction isNotFound(error: unknown): boolean {\n return errorCode(error) === 'ENOENT';\n}\n\n@injectable()\nexport class ResultWatcher implements IResultWatcher {\n constructor(@inject(DOOM_TEAM_TYPES.PollScheduler) private readonly scheduler: IPollScheduler) {}\n\n /**\n * Seams for tests, as protected members rather than constructor arguments:\n * inversify cannot resolve a primitive, so a test subclass overrides these.\n */\n protected readonly pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS;\n protected readonly dedupeTtlMs: number = DEFAULT_DEDUPE_TTL_MS;\n\n protected now(): number {\n return Date.now();\n }\n\n /** Records a claim or delivery failure. Overridable; never a stdio write (see package rules). */\n protected onProcessingError(id: string, error: unknown): void {\n this.lastProcessingError = { id, error, at: this.now() };\n this.processingErrorCount += 1;\n }\n\n /** The most recent processing failure, for diagnostics. */\n lastProcessingError: { id: string; error: unknown; at: number } | undefined;\n /** How many processing failures this watcher has absorbed. */\n processingErrorCount = 0;\n\n private consumer: ResultConsumer | undefined;\n private watcher: fs.FSWatcher | undefined;\n private unregisterPoll: (() => void) | undefined;\n private readonly claims = new Map<string, ClaimEntry>();\n private readonly deliveredAt = new Map<string, number>();\n private generation = 0;\n private initializedGeneration = -1;\n\n // ---------------------------------------------------------------------\n // Filesystem seams. ESM module namespaces are frozen (`vi.spyOn(fs, ...)`\n // throws), so every `node:fs` call a test needs to redirect is wrapped here.\n // ---------------------------------------------------------------------\n\n protected listResultFiles(): string[] {\n try {\n return fs.readdirSync(currentResultsDir()).filter((entry) => entry.endsWith(RESULT_FILE_SUFFIX));\n } catch (error) {\n if (isNotFound(error)) return [];\n throw error;\n }\n }\n\n /** Run ids with a lingering claimed copy, found by scanning `currentRunsDir()` once at `start()`. */\n protected listClaimedRunIds(): string[] {\n try {\n return fs\n .readdirSync(currentRunsDir())\n .filter((entry) => this.fileExists(path.join(runDirFor(entry), CLAIMED_RESULT_FILE_NAME)));\n } catch (error) {\n if (isNotFound(error)) return [];\n throw error;\n }\n }\n\n protected async listResultFilesAsync(): Promise<string[]> {\n try {\n const entries = await fs.promises.readdir(currentResultsDir());\n return entries.filter((entry) => entry.endsWith(RESULT_FILE_SUFFIX));\n } catch (error) {\n if (isNotFound(error)) return [];\n throw error;\n }\n }\n\n protected async listClaimedRunIdsAsync(): Promise<string[]> {\n let entries: string[];\n try {\n entries = await fs.promises.readdir(currentRunsDir());\n } catch (error) {\n if (isNotFound(error)) return [];\n throw error;\n }\n const existing = await Promise.all(\n entries.map((entry) => this.fileExistsAsync(path.join(runDirFor(entry), CLAIMED_RESULT_FILE_NAME))),\n );\n return entries.filter((_entry, index) => existing[index]);\n }\n\n protected fileExists(filePath: string): boolean {\n return fs.existsSync(filePath);\n }\n\n protected readFile(filePath: string): string | undefined {\n try {\n return fs.readFileSync(filePath, 'utf-8');\n } catch (error) {\n if (isNotFound(error)) return undefined;\n throw error;\n }\n }\n\n protected ensureDir(dirPath: string): void {\n fs.mkdirSync(dirPath, { recursive: true });\n }\n\n /**\n * The atomic claim (fix 4). Returns `false`, without throwing, when the\n * source is already gone - another claimer won the race, which is the\n * expected outcome of two watchers noticing the same file.\n */\n protected renameFile(fromPath: string, toPath: string): boolean {\n try {\n fs.renameSync(fromPath, toPath);\n return true;\n } catch (error) {\n if (isNotFound(error)) return false;\n throw error;\n }\n }\n\n protected unlinkFile(filePath: string): void {\n try {\n fs.unlinkSync(filePath);\n } catch (error) {\n if (!isNotFound(error)) this.onProcessingError(filePath, error);\n }\n }\n\n protected async fileExistsAsync(filePath: string): Promise<boolean> {\n try {\n await fs.promises.access(filePath);\n return true;\n } catch (error) {\n if (isNotFound(error)) return false;\n throw error;\n }\n }\n\n protected async readFileAsync(filePath: string): Promise<string | undefined> {\n try {\n return await fs.promises.readFile(filePath, 'utf-8');\n } catch (error) {\n if (isNotFound(error)) return undefined;\n throw error;\n }\n }\n\n protected async ensureDirAsync(dirPath: string): Promise<void> {\n await fs.promises.mkdir(dirPath, { recursive: true });\n }\n\n protected async renameFileAsync(fromPath: string, toPath: string): Promise<boolean> {\n try {\n await fs.promises.rename(fromPath, toPath);\n return true;\n } catch (error) {\n if (isNotFound(error)) return false;\n throw error;\n }\n }\n\n protected async unlinkFileAsync(filePath: string): Promise<void> {\n try {\n await fs.promises.unlink(filePath);\n } catch (error) {\n if (!isNotFound(error)) this.onProcessingError(filePath, error);\n }\n }\n\n /**\n * The `fs.watch` callback's logic, factored out so it can be exercised\n * deterministically (fix 1's actual test coverage): calling this directly\n * with synthetic `eventType`/`filename` values proves the filter reacts to\n * 'change' exactly like 'rename', without depending on whether - or when -\n * the OS actually delivers a real filesystem event. `fs.watch` gives no\n * delivery guarantee at all, so a test that waits on a real one to arrive\n * is testing the OS's scheduler, not this code; see `watchResultsDir()`'s\n * only caller of this, and the poll safety net (fix 5) that exists\n * precisely because that guarantee does not hold.\n */\n protected handleWatchEvent(_eventType: fs.WatchEventType, filename: string | Buffer | null): void {\n if (!filename) return;\n if (!filename.toString().endsWith(RESULT_FILE_SUFFIX)) return;\n this.scheduler.wake();\n }\n\n /**\n * A watch that has emitted 'error' is treated as gone: `run()` checks\n * `getWatcher()` on its next tick and re-establishes it, falling back to\n * the poll safety net (fix 5) in the meantime. Factored out, like\n * `handleWatchEvent`, so a test can trigger it directly rather than by\n * provoking a real watch failure.\n */\n protected handleWatchError(): void {\n this.watcher = undefined;\n }\n\n /** The currently-held watch, if any. Exposed for tests; `run()` reads the field directly. */\n protected getWatcher(): fs.FSWatcher | undefined {\n return this.watcher;\n }\n\n /**\n * Watch `currentResultsDir()` for candidate result files. Reacts to every `fs.watch`\n * callback regardless of `eventType` (fix 1, see `handleWatchEvent`).\n * Returns `undefined`, without throwing, when the watch could not be\n * established at all - `run()` retries this on its next tick, and the poll\n * safety net (fix 5) covers the gap in the meantime.\n */\n protected watchResultsDir(): fs.FSWatcher | undefined {\n try {\n this.ensureDir(currentResultsDir());\n const watchPath = resolveWatchPath(currentResultsDir(), fs.realpathSync.native);\n const watcher = fs.watch(watchPath, (eventType, filename) => this.handleWatchEvent(eventType, filename));\n watcher.on('error', () => this.handleWatchError());\n watcher.unref?.();\n return watcher;\n } catch {\n return undefined;\n }\n }\n\n protected async watchResultsDirAsync(): Promise<fs.FSWatcher | undefined> {\n try {\n await this.ensureDirAsync(currentResultsDir());\n let watchPath = currentResultsDir();\n try {\n watchPath = await fs.promises.realpath(watchPath);\n } catch {\n // Watching the unresolved path may still work on this platform.\n }\n const watcher = fs.watch(watchPath, (eventType, filename) => this.handleWatchEvent(eventType, filename));\n watcher.on('error', () => this.handleWatchError());\n watcher.unref?.();\n return watcher;\n } catch {\n return undefined;\n }\n }\n\n // ---------------------------------------------------------------------\n // TTL dedupe (fix 2). One implementation, used by both the check and the\n // record, so they cannot drift the way the predecessor's two copies did.\n // ---------------------------------------------------------------------\n\n private pruneDeliveredAt(now: number): void {\n for (const [key, at] of this.deliveredAt) {\n if (now - at > this.dedupeTtlMs) this.deliveredAt.delete(key);\n }\n }\n\n private hasRecentlyDelivered(runId: string): boolean {\n const now = this.now();\n this.pruneDeliveredAt(now);\n return this.deliveredAt.has(runId);\n }\n\n private markDelivered(runId: string): void {\n const now = this.now();\n this.pruneDeliveredAt(now);\n this.deliveredAt.set(runId, now);\n if (this.deliveredAt.size > MAX_TRACKED_DELIVERIES) {\n const oldest = this.deliveredAt.keys().next();\n if (!oldest.done) this.deliveredAt.delete(oldest.value);\n }\n }\n\n // ---------------------------------------------------------------------\n // Claim + deliver\n // ---------------------------------------------------------------------\n\n /** Attempt the atomic claim for one candidate file. `true` only when this call won the race. */\n protected claimResult(runId: string, file: string): boolean {\n this.ensureDir(runDirFor(runId));\n return this.renameFile(resultPathFor(file), claimPathFor(runId));\n }\n\n protected async claimResultAsync(runId: string, file: string): Promise<boolean> {\n await this.ensureDirAsync(runDirFor(runId));\n return this.renameFileAsync(resultPathFor(file), claimPathFor(runId));\n }\n\n /** Claim every new candidate in `currentResultsDir()`. Returns how many were newly claimed. */\n private async discoverAndClaim(generation: number): Promise<number> {\n let claimedCount = 0;\n const ownRunId = process.env[SUBAGENT_RUN_ID_ENV]?.trim();\n for (const file of await this.listResultFilesAsync()) {\n if (generation !== this.generation) return claimedCount;\n const runId = file.slice(0, -RESULT_FILE_SUFFIX.length);\n // A nested-capable child loads the same Doom Team extension as its\n // parent. It must not claim the result for its own one-shot run.\n if (!runId || runId === ownRunId || this.claims.has(runId)) continue;\n if (await this.claimResultAsync(runId, file)) {\n if (generation !== this.generation) return claimedCount;\n this.claims.set(runId, { inFlight: false });\n claimedCount += 1;\n }\n // A `false` result means another claimer already took this file; that\n // is the normal outcome of a race, not an error (fix 4).\n }\n return claimedCount;\n }\n\n private async discardClaim(runId: string, generation: number): Promise<void> {\n if (generation !== this.generation) return;\n await this.unlinkFileAsync(claimPathFor(runId));\n if (generation === this.generation) this.claims.delete(runId);\n }\n\n private async deliverClaim(runId: string, entry: ClaimEntry, generation: number): Promise<void> {\n const claimPath = claimPathFor(runId);\n try {\n const raw = await this.readFileAsync(claimPath);\n if (generation !== this.generation) return;\n if (raw === undefined) {\n // Claimed file vanished from under us (manual cleanup, disk issue);\n // nothing left to deliver.\n this.claims.delete(runId);\n return;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (error) {\n this.onProcessingError(runId, error);\n await this.discardClaim(runId, generation);\n return;\n }\n if (typeof parsed !== 'object' || parsed === null) {\n await this.discardClaim(runId, generation);\n return;\n }\n\n const parsedRecord = parsed as Record<string, unknown>;\n const resolvedRunId = typeof parsedRecord.runId === 'string' && parsedRecord.runId ? parsedRecord.runId : runId;\n if (this.hasRecentlyDelivered(resolvedRunId)) {\n // A duplicate write for a run we already delivered inside the TTL\n // window: dropped, not redelivered (fix 2).\n await this.discardClaim(runId, generation);\n return;\n }\n\n const consumer = this.consumer;\n if (!consumer) {\n // No subscriber yet (start() raced ahead of the caller wiring one\n // in); retry once one is registered.\n entry.inFlight = false;\n return;\n }\n\n const record: RunResultFile = { ...parsedRecord, runId: resolvedRunId };\n const accepted = await consumer(record);\n if (generation !== this.generation) return;\n if (accepted) {\n this.markDelivered(resolvedRunId);\n await this.discardClaim(runId, generation);\n } else {\n entry.inFlight = false;\n }\n } catch (error) {\n if (generation !== this.generation) return;\n this.onProcessingError(runId, error);\n entry.inFlight = false;\n }\n }\n\n /**\n * One scheduler tick. Filesystem discovery is awaited by `PollScheduler`;\n * consumer delivery remains independently asynchronous. Returns `true`\n * only when a new result was claimed this tick, so a consumer that keeps\n * saying \"not yet\" cannot pin the scheduler's backoff at the floor forever.\n */\n protected async run(): Promise<boolean> {\n const generation = this.generation;\n if (!this.watcher) this.watcher = await this.watchResultsDirAsync();\n if (generation !== this.generation) return false;\n\n if (this.initializedGeneration !== generation) {\n const claimedRunIds = await this.listClaimedRunIdsAsync();\n if (generation !== this.generation) return false;\n for (const runId of claimedRunIds) this.claims.set(runId, { inFlight: false });\n this.initializedGeneration = generation;\n }\n\n const claimedCount = await this.discoverAndClaim(generation);\n if (generation !== this.generation) return false;\n\n for (const [runId, entry] of this.claims) {\n if (entry.inFlight) continue;\n entry.inFlight = true;\n void this.deliverClaim(runId, entry, generation);\n }\n\n return claimedCount > 0;\n }\n\n // ---------------------------------------------------------------------\n // Public API\n // ---------------------------------------------------------------------\n\n start(consumer: ResultConsumer): void {\n // A second start() is a reset, the same idiom as CoalescedStatusWriter.open().\n this.stop();\n this.consumer = consumer;\n this.unregisterPoll = this.scheduler.register({\n id: POLL_SUBSCRIBER_ID,\n intervalMs: this.pollIntervalMs,\n run: () => this.run(),\n });\n this.scheduler.wake();\n }\n\n stop(): void {\n this.generation += 1;\n this.initializedGeneration = -1;\n this.consumer = undefined;\n this.watcher?.close();\n this.watcher = undefined;\n this.unregisterPoll?.();\n this.unregisterPoll = undefined;\n this.claims.clear();\n this.deliveredAt.clear();\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{__esmMin as e}from"../_virtual/_rolldown/runtime.mjs";import{__decorate as t,init_decorate as n}from"../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorate.mjs";import{DOOM_TEAM_TYPES as r,init_types as i}from"../container/types.mjs";import{init_configDir as a,resolveWatchPath as o}from"../shared/configDir.mjs";import{SUBAGENT_RUN_ID_ENV as s,init_env as c}from"./process/env.mjs";import{__decorateMetadata as l,init_decorateMetadata as u}from"../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateMetadata.mjs";import{__decorateParam as d,init_decorateParam as f}from"../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateParam.mjs";import{currentResultsDir as p,currentRunsDir as m,init_paths as h}from"../shared/paths.mjs";import*as g from"node:fs";import*as _ from"node:path";import{inject as v,injectable as y}from"inversify";function b(e){return _.join(p(),e)}function x(e){return _.join(m(),e)}function S(e){return _.join(x(e),E)}function C(e){return typeof e==`object`&&e&&`code`in e?e.code:void 0}function w(e){return C(e)===`ENOENT`}var T,E,D,O,k,A,j,M=e((()=>{i(),a(),h(),c(),u(),f(),n(),T=`.json`,E=`claimed-result.json`,D=4e3,O=6e4,k=256,A=`result-watcher`,j=class{constructor(e){this.scheduler=e}pollIntervalMs=D;dedupeTtlMs=O;now(){return Date.now()}onProcessingError(e,t){this.lastProcessingError={id:e,error:t,at:this.now()},this.processingErrorCount+=1}lastProcessingError;processingErrorCount=0;consumer;watcher;unregisterPoll;claims=new Map;deliveredAt=new Map;generation=0;initializedGeneration=-1;listResultFiles(){try{return g.readdirSync(p()).filter(e=>e.endsWith(T))}catch(e){if(w(e))return[];throw e}}listClaimedRunIds(){try{return g.readdirSync(m()).filter(e=>this.fileExists(_.join(x(e),E)))}catch(e){if(w(e))return[];throw e}}async listResultFilesAsync(){try{return(await g.promises.readdir(p())).filter(e=>e.endsWith(T))}catch(e){if(w(e))return[];throw e}}async listClaimedRunIdsAsync(){let e;try{e=await g.promises.readdir(m())}catch(e){if(w(e))return[];throw e}let t=await Promise.all(e.map(e=>this.fileExistsAsync(_.join(x(e),E))));return e.filter((e,n)=>t[n])}fileExists(e){return g.existsSync(e)}readFile(e){try{return g.readFileSync(e,`utf-8`)}catch(e){if(w(e))return;throw e}}ensureDir(e){g.mkdirSync(e,{recursive:!0})}renameFile(e,t){try{return g.renameSync(e,t),!0}catch(e){if(w(e))return!1;throw e}}unlinkFile(e){try{g.unlinkSync(e)}catch(t){w(t)||this.onProcessingError(e,t)}}async fileExistsAsync(e){try{return await g.promises.access(e),!0}catch(e){if(w(e))return!1;throw e}}async readFileAsync(e){try{return await g.promises.readFile(e,`utf-8`)}catch(e){if(w(e))return;throw e}}async ensureDirAsync(e){await g.promises.mkdir(e,{recursive:!0})}async renameFileAsync(e,t){try{return await g.promises.rename(e,t),!0}catch(e){if(w(e))return!1;throw e}}async unlinkFileAsync(e){try{await g.promises.unlink(e)}catch(t){w(t)||this.onProcessingError(e,t)}}handleWatchEvent(e,t){t&&t.toString().endsWith(`.json`)&&this.scheduler.wake()}handleWatchError(){this.watcher=void 0}getWatcher(){return this.watcher}watchResultsDir(){try{this.ensureDir(p());let e=o(p(),g.realpathSync.native),t=g.watch(e,(e,t)=>this.handleWatchEvent(e,t));return t.on(`error`,()=>this.handleWatchError()),t.unref?.(),t}catch{return}}async watchResultsDirAsync(){try{await this.ensureDirAsync(p());let e=p();try{e=await g.promises.realpath(e)}catch{}let t=g.watch(e,(e,t)=>this.handleWatchEvent(e,t));return t.on(`error`,()=>this.handleWatchError()),t.unref?.(),t}catch{return}}pruneDeliveredAt(e){for(let[t,n]of this.deliveredAt)e-n>this.dedupeTtlMs&&this.deliveredAt.delete(t)}hasRecentlyDelivered(e){let t=this.now();return this.pruneDeliveredAt(t),this.deliveredAt.has(e)}markDelivered(e){let t=this.now();if(this.pruneDeliveredAt(t),this.deliveredAt.set(e,t),this.deliveredAt.size>k){let e=this.deliveredAt.keys().next();e.done||this.deliveredAt.delete(e.value)}}claimResult(e,t){return this.ensureDir(x(e)),this.renameFile(b(t),S(e))}async claimResultAsync(e,t){return await this.ensureDirAsync(x(e)),this.renameFileAsync(b(t),S(e))}async discoverAndClaim(e){let t=0,n=process.env[s]?.trim();for(let r of await this.listResultFilesAsync()){if(e!==this.generation)return t;let i=r.slice(0,-5);if(!(!i||i===n||this.claims.has(i))&&await this.claimResultAsync(i,r)){if(e!==this.generation)return t;this.claims.set(i,{inFlight:!1}),t+=1}}return t}async discardClaim(e,t){t===this.generation&&(await this.unlinkFileAsync(S(e)),t===this.generation&&this.claims.delete(e))}async deliverClaim(e,t,n){let r=S(e);try{let i=await this.readFileAsync(r);if(n!==this.generation)return;if(i===void 0){this.claims.delete(e);return}let a;try{a=JSON.parse(i)}catch(t){this.onProcessingError(e,t),await this.discardClaim(e,n);return}if(typeof a!=`object`||!a){await this.discardClaim(e,n);return}let o=a,s=typeof o.runId==`string`&&o.runId?o.runId:e;if(this.hasRecentlyDelivered(s)){await this.discardClaim(e,n);return}let c=this.consumer;if(!c){t.inFlight=!1;return}let l=await c({...o,runId:s});if(n!==this.generation)return;l?(this.markDelivered(s),await this.discardClaim(e,n)):t.inFlight=!1}catch(r){if(n!==this.generation)return;this.onProcessingError(e,r),t.inFlight=!1}}async run(){let e=this.generation;if(this.watcher||=await this.watchResultsDirAsync(),e!==this.generation)return!1;if(this.initializedGeneration!==e){let t=await this.listClaimedRunIdsAsync();if(e!==this.generation)return!1;for(let e of t)this.claims.set(e,{inFlight:!1});this.initializedGeneration=e}let t=await this.discoverAndClaim(e);if(e!==this.generation)return!1;for(let[t,n]of this.claims)n.inFlight||(n.inFlight=!0,this.deliverClaim(t,n,e));return t>0}start(e){this.stop(),this.consumer=e,this.unregisterPoll=this.scheduler.register({id:A,intervalMs:this.pollIntervalMs,run:()=>this.run()}),this.scheduler.wake()}stop(){this.generation+=1,this.initializedGeneration=-1,this.consumer=void 0,this.watcher?.close(),this.watcher=void 0,this.unregisterPoll?.(),this.unregisterPoll=void 0,this.claims.clear(),this.deliveredAt.clear()}},j=t([y(),d(0,v(r.PollScheduler)),l(`design:paramtypes`,[Object])],j)}));M();export{E as CLAIMED_RESULT_FILE_NAME,T as RESULT_FILE_SUFFIX,j as ResultWatcher,M as init_resultWatcher};
2
+ //# sourceMappingURL=resultWatcher.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resultWatcher.mjs","names":[],"sources":["../../src/adapters/resultWatcher.ts"],"sourcesContent":["/**\n * Watches `currentResultsDir()` for a run's terminal result and delivers each one to\n * a consumer exactly once.\n *\n * WHY THIS EXISTS:\n * The predecessor's watcher\n * (`doom-pi-subagents/src/runs/background/resultWatcher.ts`) had four\n * separate, verified bugs. This is a redesign around the fixes, not a line\n * port - the fixes below are structural, not patches on the old control flow.\n *\n * FIX 1 - WATCH BOTH 'rename' AND 'change':\n * The predecessor only reacted to `event === 'rename'` (`resultWatcher.ts:398-399`),\n * so a result file rewritten in place - same path, new contents, no\n * create/delete/rename - was never noticed until the poll safety net happened\n * to run. `watchResultsDir()` below reacts to any `fs.watch` callback whose\n * filename matches, regardless of `eventType`, the same way\n * `SpawnHandshake.watchDirectory` already does in this package.\n *\n * FIX 2 - TTL DEDUPE:\n * The predecessor re-implemented \"is this a duplicate\" inline at its call\n * site (`resultWatcher.ts:180-195`) *and* separately inside\n * `completion-dedupe.ts`'s `markSeenWithTtl`, so the check and the record used\n * two independently maintained copies of the same expiry logic. Every check\n * and every record here goes through `hasRecentlyDelivered()` and\n * `markDelivered()` and nothing else - one implementation of the TTL policy,\n * used the same way on both sides.\n *\n * FIX 3 - TIMER TYPES:\n * The predecessor stored `setTimeout`'s return value in fields typed (or\n * used) as if they were numbers (`resultWatcher.ts:361,371,425`), which only\n * happens to work under Node's `setTimeout` and breaks the moment the return\n * value is compared, serialized, or ported somewhere `setTimeout` returns a\n * number (browsers, some polyfills). This module does not own a timer at all:\n * `fs.watch` is the fast path and `PollScheduler` is the bounded safety net\n * (see fix 5), so there is no `Timeout` handle here for the compiler to get\n * wrong in the first place.\n *\n * FIX 4 - ATOMIC CONSUME (TOCTOU):\n * The predecessor checked a result file's existence, then unlinked it, as two\n * separate steps (`resultWatcher.ts:184-194,324-332`), racing any other\n * reader of the same file between the two. Here, the *first* thing that\n * happens to a candidate result is an attempted rename into\n * `currentRunsDir()/<runId>/claimed-result.json` (`claimResult()`). A successful\n * rename is exclusive ownership: nothing else can also be holding that\n * result, because there is only ever one file at the source path and only one\n * caller's `renameSync` can move it. A failed rename (source already gone)\n * means another claimer got there first, which is `claimResult()`'s normal,\n * expected return of `false` - not a caught error.\n *\n * FIX 5 - WATCH-FIRST, POLL AS A BOUNDED SAFETY NET:\n * `start()` registers with `PollScheduler` instead of owning a timer, the\n * same pattern as every other long-lived background subscriber in this\n * directory. `fs.watch` gives low latency; the scheduler's tick is the bound\n * on how late a check can ever be on a platform or mount where watches are\n * unreliable. Neither path is trusted alone.\n *\n * DESIGN PATTERNS:\n * - A claimed result lives at `currentRunsDir()/<runId>/claimed-result.json`, inside\n * the run's own working directory rather than a subdirectory of\n * `currentResultsDir()`. Putting it under `currentResultsDir()` would make our own claims\n * visible to the very `fs.watch` this module registers, and would risk\n * colliding with `currentResultsDir()/<runId>.json` naming. `currentRunsDir()/<runId>/` is\n * already this run's own directory (see `statusWriter.ts`,\n * `terminalPersistence.ts`), so this reuses existing per-run space instead\n * of inventing a second one\n * - `run()` uses promise-based filesystem operations and is awaited by\n * `PollScheduler`; delivery remains independently asynchronous, and a\n * claim's `inFlight` flag stops it from being re-fired on the next tick\n * while the previous attempt is still outstanding\n * - `run()` reports \"work happened\" only when it claimed a new file, not when\n * it merely retried a still-pending delivery. A consumer that keeps\n * rejecting the same result forever would otherwise pin the scheduler's\n * backoff at the floor indefinitely\n * - A crash between claiming a result and delivering it leaves\n * `claimed-result.json` on disk with nothing tracking it in memory.\n * `start()` scans for these once, so a fresh process picks up an\n * interrupted delivery instead of leaving it stranded forever\n *\n * AVOID:\n * - Reading or unlinking anything at `currentResultsDir()/<runId>.json` once\n * `claimResult()` has returned true for it; ownership has moved to the\n * claimed copy, and the source name may already belong to an unrelated,\n * later result\n * - Treating a rename failure from `claimResult()` as noteworthy; the\n * `onProcessingError` path is for delivery failures and parse failures, not\n * for the ordinary case of losing a claim race\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { inject, injectable } from 'inversify';\nimport { DOOM_TEAM_TYPES } from '../container/types.ts';\nimport { resolveWatchPath } from '../shared/configDir.ts';\nimport { currentResultsDir, currentRunsDir } from '../shared/paths.ts';\nimport type { IPollScheduler } from '../shared/pollScheduler.ts';\nimport { SUBAGENT_RUN_ID_ENV } from './process/env.ts';\n\n/** Suffix a candidate result file must have in `currentResultsDir()`. */\nexport const RESULT_FILE_SUFFIX = '.json';\n/** Leaf name of a claimed result inside `currentRunsDir()/<runId>/`. */\nexport const CLAIMED_RESULT_FILE_NAME = 'claimed-result.json';\n\n/** Poll safety-net cadence, generously looser than `fs.watch`'s latency (see `PollScheduler`'s header). */\nconst DEFAULT_POLL_INTERVAL_MS = 4_000;\n/** How long a delivered run id is remembered, so a re-appearing result for it is dropped instead of redelivered. */\nconst DEFAULT_DEDUPE_TTL_MS = 60_000;\n/** Cap on tracked delivered ids, so a very long session cannot grow this without bound. */\nconst MAX_TRACKED_DELIVERIES = 256;\n\nconst POLL_SUBSCRIBER_ID = 'result-watcher';\n\n/** The parsed contents of a result file, with `runId` guaranteed present. */\nexport interface RunResultFile {\n runId: string;\n [key: string]: unknown;\n}\n\n/**\n * Handles one delivered result. Returns (or resolves to) `true` once it has\n * been durably accepted, which is what lets the claimed copy be removed;\n * `false` means \"not yet\", and the same claim is retried on a later tick.\n */\nexport type ResultConsumer = (result: RunResultFile) => boolean | Promise<boolean>;\n\nexport interface IResultWatcher {\n /** Begin watching. Calling this again resets whatever was previously tracked, same as `CoalescedStatusWriter.open`. */\n start(consumer: ResultConsumer): void;\n /** Stop watching, unregister from the scheduler, and drop all in-memory state. */\n stop(): void;\n}\n\ninterface ClaimEntry {\n /** True while `deliverClaim()` is awaiting this claim's consumer call, so `run()` does not fire it twice. */\n inFlight: boolean;\n}\n\nfunction resultPathFor(file: string): string {\n return path.join(currentResultsDir(), file);\n}\n\nfunction runDirFor(runId: string): string {\n return path.join(currentRunsDir(), runId);\n}\n\nfunction claimPathFor(runId: string): string {\n return path.join(runDirFor(runId), CLAIMED_RESULT_FILE_NAME);\n}\n\nfunction errorCode(error: unknown): string | undefined {\n return typeof error === 'object' && error !== null && 'code' in error\n ? (error as NodeJS.ErrnoException).code\n : undefined;\n}\n\nfunction isNotFound(error: unknown): boolean {\n return errorCode(error) === 'ENOENT';\n}\n\n@injectable()\nexport class ResultWatcher implements IResultWatcher {\n constructor(@inject(DOOM_TEAM_TYPES.PollScheduler) private readonly scheduler: IPollScheduler) {}\n\n /**\n * Seams for tests, as protected members rather than constructor arguments:\n * inversify cannot resolve a primitive, so a test subclass overrides these.\n */\n protected readonly pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS;\n protected readonly dedupeTtlMs: number = DEFAULT_DEDUPE_TTL_MS;\n\n protected now(): number {\n return Date.now();\n }\n\n /** Records a claim or delivery failure. Overridable; never a stdio write (see package rules). */\n protected onProcessingError(id: string, error: unknown): void {\n this.lastProcessingError = { id, error, at: this.now() };\n this.processingErrorCount += 1;\n }\n\n /** The most recent processing failure, for diagnostics. */\n lastProcessingError: { id: string; error: unknown; at: number } | undefined;\n /** How many processing failures this watcher has absorbed. */\n processingErrorCount = 0;\n\n private consumer: ResultConsumer | undefined;\n private watcher: fs.FSWatcher | undefined;\n private unregisterPoll: (() => void) | undefined;\n private readonly claims = new Map<string, ClaimEntry>();\n private readonly deliveredAt = new Map<string, number>();\n private generation = 0;\n private initializedGeneration = -1;\n\n // ---------------------------------------------------------------------\n // Filesystem seams. ESM module namespaces are frozen (`vi.spyOn(fs, ...)`\n // throws), so every `node:fs` call a test needs to redirect is wrapped here.\n // ---------------------------------------------------------------------\n\n protected listResultFiles(): string[] {\n try {\n return fs.readdirSync(currentResultsDir()).filter((entry) => entry.endsWith(RESULT_FILE_SUFFIX));\n } catch (error) {\n if (isNotFound(error)) return [];\n throw error;\n }\n }\n\n /** Run ids with a lingering claimed copy, found by scanning `currentRunsDir()` once at `start()`. */\n protected listClaimedRunIds(): string[] {\n try {\n return fs\n .readdirSync(currentRunsDir())\n .filter((entry) => this.fileExists(path.join(runDirFor(entry), CLAIMED_RESULT_FILE_NAME)));\n } catch (error) {\n if (isNotFound(error)) return [];\n throw error;\n }\n }\n\n protected async listResultFilesAsync(): Promise<string[]> {\n try {\n const entries = await fs.promises.readdir(currentResultsDir());\n return entries.filter((entry) => entry.endsWith(RESULT_FILE_SUFFIX));\n } catch (error) {\n if (isNotFound(error)) return [];\n throw error;\n }\n }\n\n protected async listClaimedRunIdsAsync(): Promise<string[]> {\n let entries: string[];\n try {\n entries = await fs.promises.readdir(currentRunsDir());\n } catch (error) {\n if (isNotFound(error)) return [];\n throw error;\n }\n const existing = await Promise.all(\n entries.map((entry) => this.fileExistsAsync(path.join(runDirFor(entry), CLAIMED_RESULT_FILE_NAME))),\n );\n return entries.filter((_entry, index) => existing[index]);\n }\n\n protected fileExists(filePath: string): boolean {\n return fs.existsSync(filePath);\n }\n\n protected readFile(filePath: string): string | undefined {\n try {\n return fs.readFileSync(filePath, 'utf-8');\n } catch (error) {\n if (isNotFound(error)) return undefined;\n throw error;\n }\n }\n\n protected ensureDir(dirPath: string): void {\n fs.mkdirSync(dirPath, { recursive: true });\n }\n\n /**\n * The atomic claim (fix 4). Returns `false`, without throwing, when the\n * source is already gone - another claimer won the race, which is the\n * expected outcome of two watchers noticing the same file.\n */\n protected renameFile(fromPath: string, toPath: string): boolean {\n try {\n fs.renameSync(fromPath, toPath);\n return true;\n } catch (error) {\n if (isNotFound(error)) return false;\n throw error;\n }\n }\n\n protected unlinkFile(filePath: string): void {\n try {\n fs.unlinkSync(filePath);\n } catch (error) {\n if (!isNotFound(error)) this.onProcessingError(filePath, error);\n }\n }\n\n protected async fileExistsAsync(filePath: string): Promise<boolean> {\n try {\n await fs.promises.access(filePath);\n return true;\n } catch (error) {\n if (isNotFound(error)) return false;\n throw error;\n }\n }\n\n protected async readFileAsync(filePath: string): Promise<string | undefined> {\n try {\n return await fs.promises.readFile(filePath, 'utf-8');\n } catch (error) {\n if (isNotFound(error)) return undefined;\n throw error;\n }\n }\n\n protected async ensureDirAsync(dirPath: string): Promise<void> {\n await fs.promises.mkdir(dirPath, { recursive: true });\n }\n\n protected async renameFileAsync(fromPath: string, toPath: string): Promise<boolean> {\n try {\n await fs.promises.rename(fromPath, toPath);\n return true;\n } catch (error) {\n if (isNotFound(error)) return false;\n throw error;\n }\n }\n\n protected async unlinkFileAsync(filePath: string): Promise<void> {\n try {\n await fs.promises.unlink(filePath);\n } catch (error) {\n if (!isNotFound(error)) this.onProcessingError(filePath, error);\n }\n }\n\n /**\n * The `fs.watch` callback's logic, factored out so it can be exercised\n * deterministically (fix 1's actual test coverage): calling this directly\n * with synthetic `eventType`/`filename` values proves the filter reacts to\n * 'change' exactly like 'rename', without depending on whether - or when -\n * the OS actually delivers a real filesystem event. `fs.watch` gives no\n * delivery guarantee at all, so a test that waits on a real one to arrive\n * is testing the OS's scheduler, not this code; see `watchResultsDir()`'s\n * only caller of this, and the poll safety net (fix 5) that exists\n * precisely because that guarantee does not hold.\n */\n protected handleWatchEvent(_eventType: fs.WatchEventType, filename: string | Buffer | null): void {\n if (!filename) return;\n if (!filename.toString().endsWith(RESULT_FILE_SUFFIX)) return;\n this.scheduler.wake();\n }\n\n /**\n * A watch that has emitted 'error' is treated as gone: `run()` checks\n * `getWatcher()` on its next tick and re-establishes it, falling back to\n * the poll safety net (fix 5) in the meantime. Factored out, like\n * `handleWatchEvent`, so a test can trigger it directly rather than by\n * provoking a real watch failure.\n */\n protected handleWatchError(): void {\n this.watcher = undefined;\n }\n\n /** The currently-held watch, if any. Exposed for tests; `run()` reads the field directly. */\n protected getWatcher(): fs.FSWatcher | undefined {\n return this.watcher;\n }\n\n /**\n * Watch `currentResultsDir()` for candidate result files. Reacts to every `fs.watch`\n * callback regardless of `eventType` (fix 1, see `handleWatchEvent`).\n * Returns `undefined`, without throwing, when the watch could not be\n * established at all - `run()` retries this on its next tick, and the poll\n * safety net (fix 5) covers the gap in the meantime.\n */\n protected watchResultsDir(): fs.FSWatcher | undefined {\n try {\n this.ensureDir(currentResultsDir());\n const watchPath = resolveWatchPath(currentResultsDir(), fs.realpathSync.native);\n const watcher = fs.watch(watchPath, (eventType, filename) => this.handleWatchEvent(eventType, filename));\n watcher.on('error', () => this.handleWatchError());\n watcher.unref?.();\n return watcher;\n } catch {\n return undefined;\n }\n }\n\n protected async watchResultsDirAsync(): Promise<fs.FSWatcher | undefined> {\n try {\n await this.ensureDirAsync(currentResultsDir());\n let watchPath = currentResultsDir();\n try {\n watchPath = await fs.promises.realpath(watchPath);\n } catch {\n // Watching the unresolved path may still work on this platform.\n }\n const watcher = fs.watch(watchPath, (eventType, filename) => this.handleWatchEvent(eventType, filename));\n watcher.on('error', () => this.handleWatchError());\n watcher.unref?.();\n return watcher;\n } catch {\n return undefined;\n }\n }\n\n // ---------------------------------------------------------------------\n // TTL dedupe (fix 2). One implementation, used by both the check and the\n // record, so they cannot drift the way the predecessor's two copies did.\n // ---------------------------------------------------------------------\n\n private pruneDeliveredAt(now: number): void {\n for (const [key, at] of this.deliveredAt) {\n if (now - at > this.dedupeTtlMs) this.deliveredAt.delete(key);\n }\n }\n\n private hasRecentlyDelivered(runId: string): boolean {\n const now = this.now();\n this.pruneDeliveredAt(now);\n return this.deliveredAt.has(runId);\n }\n\n private markDelivered(runId: string): void {\n const now = this.now();\n this.pruneDeliveredAt(now);\n this.deliveredAt.set(runId, now);\n if (this.deliveredAt.size > MAX_TRACKED_DELIVERIES) {\n const oldest = this.deliveredAt.keys().next();\n if (!oldest.done) this.deliveredAt.delete(oldest.value);\n }\n }\n\n // ---------------------------------------------------------------------\n // Claim + deliver\n // ---------------------------------------------------------------------\n\n /** Attempt the atomic claim for one candidate file. `true` only when this call won the race. */\n protected claimResult(runId: string, file: string): boolean {\n this.ensureDir(runDirFor(runId));\n return this.renameFile(resultPathFor(file), claimPathFor(runId));\n }\n\n protected async claimResultAsync(runId: string, file: string): Promise<boolean> {\n await this.ensureDirAsync(runDirFor(runId));\n return this.renameFileAsync(resultPathFor(file), claimPathFor(runId));\n }\n\n /** Claim every new candidate in `currentResultsDir()`. Returns how many were newly claimed. */\n private async discoverAndClaim(generation: number): Promise<number> {\n let claimedCount = 0;\n const ownRunId = process.env[SUBAGENT_RUN_ID_ENV]?.trim();\n for (const file of await this.listResultFilesAsync()) {\n if (generation !== this.generation) return claimedCount;\n const runId = file.slice(0, -RESULT_FILE_SUFFIX.length);\n // A nested-capable child loads the same Doom Team extension as its\n // parent. It must not claim the result for its own one-shot run.\n if (!runId || runId === ownRunId || this.claims.has(runId)) continue;\n if (await this.claimResultAsync(runId, file)) {\n if (generation !== this.generation) return claimedCount;\n this.claims.set(runId, { inFlight: false });\n claimedCount += 1;\n }\n // A `false` result means another claimer already took this file; that\n // is the normal outcome of a race, not an error (fix 4).\n }\n return claimedCount;\n }\n\n private async discardClaim(runId: string, generation: number): Promise<void> {\n if (generation !== this.generation) return;\n await this.unlinkFileAsync(claimPathFor(runId));\n if (generation === this.generation) this.claims.delete(runId);\n }\n\n private async deliverClaim(runId: string, entry: ClaimEntry, generation: number): Promise<void> {\n const claimPath = claimPathFor(runId);\n try {\n const raw = await this.readFileAsync(claimPath);\n if (generation !== this.generation) return;\n if (raw === undefined) {\n // Claimed file vanished from under us (manual cleanup, disk issue);\n // nothing left to deliver.\n this.claims.delete(runId);\n return;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (error) {\n this.onProcessingError(runId, error);\n await this.discardClaim(runId, generation);\n return;\n }\n if (typeof parsed !== 'object' || parsed === null) {\n await this.discardClaim(runId, generation);\n return;\n }\n\n const parsedRecord = parsed as Record<string, unknown>;\n const resolvedRunId = typeof parsedRecord.runId === 'string' && parsedRecord.runId ? parsedRecord.runId : runId;\n if (this.hasRecentlyDelivered(resolvedRunId)) {\n // A duplicate write for a run we already delivered inside the TTL\n // window: dropped, not redelivered (fix 2).\n await this.discardClaim(runId, generation);\n return;\n }\n\n const consumer = this.consumer;\n if (!consumer) {\n // No subscriber yet (start() raced ahead of the caller wiring one\n // in); retry once one is registered.\n entry.inFlight = false;\n return;\n }\n\n const record: RunResultFile = { ...parsedRecord, runId: resolvedRunId };\n const accepted = await consumer(record);\n if (generation !== this.generation) return;\n if (accepted) {\n this.markDelivered(resolvedRunId);\n await this.discardClaim(runId, generation);\n } else {\n entry.inFlight = false;\n }\n } catch (error) {\n if (generation !== this.generation) return;\n this.onProcessingError(runId, error);\n entry.inFlight = false;\n }\n }\n\n /**\n * One scheduler tick. Filesystem discovery is awaited by `PollScheduler`;\n * consumer delivery remains independently asynchronous. Returns `true`\n * only when a new result was claimed this tick, so a consumer that keeps\n * saying \"not yet\" cannot pin the scheduler's backoff at the floor forever.\n */\n protected async run(): Promise<boolean> {\n const generation = this.generation;\n if (!this.watcher) this.watcher = await this.watchResultsDirAsync();\n if (generation !== this.generation) return false;\n\n if (this.initializedGeneration !== generation) {\n const claimedRunIds = await this.listClaimedRunIdsAsync();\n if (generation !== this.generation) return false;\n for (const runId of claimedRunIds) this.claims.set(runId, { inFlight: false });\n this.initializedGeneration = generation;\n }\n\n const claimedCount = await this.discoverAndClaim(generation);\n if (generation !== this.generation) return false;\n\n for (const [runId, entry] of this.claims) {\n if (entry.inFlight) continue;\n entry.inFlight = true;\n void this.deliverClaim(runId, entry, generation);\n }\n\n return claimedCount > 0;\n }\n\n // ---------------------------------------------------------------------\n // Public API\n // ---------------------------------------------------------------------\n\n start(consumer: ResultConsumer): void {\n // A second start() is a reset, the same idiom as CoalescedStatusWriter.open().\n this.stop();\n this.consumer = consumer;\n this.unregisterPoll = this.scheduler.register({\n id: POLL_SUBSCRIBER_ID,\n intervalMs: this.pollIntervalMs,\n run: () => this.run(),\n });\n this.scheduler.wake();\n }\n\n stop(): void {\n this.generation += 1;\n this.initializedGeneration = -1;\n this.consumer = undefined;\n this.watcher?.close();\n this.watcher = undefined;\n this.unregisterPoll?.();\n this.unregisterPoll = undefined;\n this.claims.clear();\n this.deliveredAt.clear();\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ const e=require(`../_virtual/_rolldown/runtime.cjs`),t=require(`../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorate.cjs`),n=require(`../shared/paths.cjs`),r=require(`./resultWatcher.cjs`);let i=require(`node:fs`);i=e.__toESM(i,1);let a=require(`node:path`);a=e.__toESM(a,1);let o=require(`inversify`);function s(e){return a.join(n.currentRunsDir(),e)}function c(e){return a.join(n.currentResultsDir(),`${e}${r.RESULT_FILE_SUFFIX}`)}function l(e){return a.join(s(e),r.CLAIMED_RESULT_FILE_NAME)}function u(e){return typeof e==`object`&&e&&`code`in e?e.code:void 0}function d(e){return u(e)===`ENOENT`}let f=class{pathExists(e){return i.existsSync(e)}runsDir(){return n.currentRunsDir()}resultsDir(){return n.currentResultsDir()}listRunDirNames(){try{return i.readdirSync(this.runsDir(),{withFileTypes:!0}).filter(e=>e.isDirectory()).map(e=>e.name)}catch(e){if(d(e))return[];throw e}}listPendingResultIds(){try{return i.readdirSync(this.resultsDir()).filter(e=>e.endsWith(r.RESULT_FILE_SUFFIX)).map(e=>e.slice(0,-r.RESULT_FILE_SUFFIX.length))}catch(e){if(d(e))return[];throw e}}resolve(e){let t=this.locate(e);if(t)return t;let n=this.candidateRunIds().filter(t=>t.startsWith(e));if(n.length>1)throw Error(`Ambiguous run id prefix '${e}' matched: ${n.join(`, `)}. Provide a longer id.`);let[r]=n;return r?this.locate(r):void 0}async resolveAsync(e){let t=await this.locateAsync(e);if(t)return t;let n=(await this.candidateRunIdsAsync()).filter(t=>t.startsWith(e));if(n.length>1)throw Error(`Ambiguous run id prefix '${e}' matched: ${n.join(`, `)}. Provide a longer id.`);let[r]=n;return r?this.locateAsync(r):void 0}candidateRunIds(){return[...new Set([...this.listRunDirNames(),...this.listPendingResultIds()])]}locate(e){let t=this.pathExists(s(e))?s(e):void 0,n=l(e);if(this.pathExists(n))return{runId:e,runDir:t,resultPath:n,claimed:!0};let r=c(e);if(this.pathExists(r))return{runId:e,runDir:t,resultPath:r,claimed:!1};if(t)return{runId:e,runDir:t,resultPath:void 0,claimed:!1}}async pathExistsAsync(e){try{return await i.promises.access(e),!0}catch(e){if(d(e))return!1;throw e}}async listRunDirNamesAsync(){try{return(await i.promises.readdir(this.runsDir(),{withFileTypes:!0})).filter(e=>e.isDirectory()).map(e=>e.name)}catch(e){if(d(e))return[];throw e}}async listPendingResultIdsAsync(){try{return(await i.promises.readdir(this.resultsDir())).filter(e=>e.endsWith(r.RESULT_FILE_SUFFIX)).map(e=>e.slice(0,-r.RESULT_FILE_SUFFIX.length))}catch(e){if(d(e))return[];throw e}}async candidateRunIdsAsync(){let[e,t]=await Promise.all([this.listRunDirNamesAsync(),this.listPendingResultIdsAsync()]);return[...new Set([...e,...t])]}async locateAsync(e){let t=s(e),n=l(e),r=c(e),[i,a,o]=await Promise.all([this.pathExistsAsync(t),this.pathExistsAsync(n),this.pathExistsAsync(r)]),u=i?t:void 0;if(a)return{runId:e,runDir:u,resultPath:n,claimed:!0};if(o)return{runId:e,runDir:u,resultPath:r,claimed:!1};if(u)return{runId:e,runDir:u,resultPath:void 0,claimed:!1}}};f=t.__decorate([(0,o.injectable)()],f),Object.defineProperty(exports,`RunIdResolver`,{enumerable:!0,get:function(){return f}});
2
+ //# sourceMappingURL=runIdResolver.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runIdResolver.cjs","names":["path","currentRunsDir","currentResultsDir","RESULT_FILE_SUFFIX","CLAIMED_RESULT_FILE_NAME","fs"],"sources":["../../src/adapters/runIdResolver.ts"],"sourcesContent":["/**\n * Resolve a possibly-abbreviated subagent run id to where its files live.\n *\n * WHY THIS IS SMALLER THAN THE PREDECESSOR:\n * `doom-pi-subagents/src/runs/background/runIdResolver.ts` disambiguates\n * three kinds of run - foreground, async, nested - because that package\n * tracks all three in `SubagentState`. This package is async-only (see\n * `spawnHandshake.ts`'s header) and has not yet ported foreground or nested\n * run tracking (that is later work; see `SubagentState` in the predecessor).\n * This resolver covers the one kind of run this package currently has: a run\n * whose state lives under `currentRunsDir()/<runId>/` and whose terminal result lives\n * in `currentResultsDir()/<runId>.json` until `ResultWatcher` claims it. It is\n * designed to grow, not to be replaced, once the other kinds land.\n *\n * DESIGN PATTERNS:\n * - Aware of `ResultWatcher`'s claim convention\n * (`currentRunsDir()/<runId>/claimed-result.json`): a run whose result has already\n * been claimed for delivery must still resolve, and to the claimed copy,\n * not to a `currentResultsDir()` entry that no longer exists\n * - Prefix matching mirrors the predecessor's ambiguity handling: an\n * unambiguous prefix resolves like an exact id, an ambiguous one throws\n * naming every match, and a prefix matching nothing returns `undefined`\n * rather than throwing - only ambiguity is exceptional, not \"not found\"\n *\n * AVOID:\n * - Reading `currentResultsDir()/<runId>.json` for a run that `pathExists` shows has\n * a claimed copy; the source file may already belong to an unrelated, later\n * run by the time this runs\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { injectable } from 'inversify';\n\nimport { currentResultsDir, currentRunsDir } from '../shared/paths.ts';\nimport { CLAIMED_RESULT_FILE_NAME, RESULT_FILE_SUFFIX } from './resultWatcher.ts';\n\nexport interface ResolvedRunLocation {\n runId: string;\n /** `currentRunsDir()/<runId>`, when it exists. */\n runDir: string | undefined;\n /** Wherever the run's result currently lives - pending or already claimed. */\n resultPath: string | undefined;\n /** True when `resultPath` is a claimed copy awaiting delivery, not the original. */\n claimed: boolean;\n}\n\nexport interface IRunIdResolver {\n /**\n * Resolve an exact id or an unambiguous prefix. `undefined` when nothing\n * matches. Throws when a prefix matches more than one run id.\n */\n resolve(id: string): ResolvedRunLocation | undefined;\n /** Promise-based directory discovery for recurring reconciliation. */\n resolveAsync?(id: string): Promise<ResolvedRunLocation | undefined>;\n}\n\nfunction runDirFor(runId: string): string {\n return path.join(currentRunsDir(), runId);\n}\n\nfunction pendingResultPathFor(runId: string): string {\n return path.join(currentResultsDir(), `${runId}${RESULT_FILE_SUFFIX}`);\n}\n\nfunction claimedResultPathFor(runId: string): string {\n return path.join(runDirFor(runId), CLAIMED_RESULT_FILE_NAME);\n}\n\nfunction errorCode(error: unknown): string | undefined {\n return typeof error === 'object' && error !== null && 'code' in error\n ? (error as NodeJS.ErrnoException).code\n : undefined;\n}\n\nfunction isNotFound(error: unknown): boolean {\n return errorCode(error) === 'ENOENT';\n}\n\n@injectable()\nexport class RunIdResolver implements IRunIdResolver {\n // ---------------------------------------------------------------------\n // Filesystem seams. ESM module namespaces are frozen (`vi.spyOn(fs, ...)`\n // throws), so every `node:fs` call a test needs to redirect is wrapped here.\n // ---------------------------------------------------------------------\n\n protected pathExists(filePath: string): boolean {\n return fs.existsSync(filePath);\n }\n\n /**\n * Seams for the two directory-listing methods below, as protected methods\n * rather than constructor arguments (inversify cannot resolve a primitive\n * like a path, and a directory is no different from `AgentDiscoveryService`'s\n * `now()`/`load()` seams in that respect). A test subclass points these at\n * a directory that does not exist, which exercises the `isNotFound` branch\n * for real without touching the actual shared `currentRunsDir()`/`currentResultsDir()` -\n * closing the only reason this class's tests would otherwise need to.\n */\n protected runsDir(): string {\n return currentRunsDir();\n }\n\n protected resultsDir(): string {\n return currentResultsDir();\n }\n\n protected listRunDirNames(): string[] {\n try {\n return fs\n .readdirSync(this.runsDir(), { withFileTypes: true })\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name);\n } catch (error) {\n if (isNotFound(error)) return [];\n throw error;\n }\n }\n\n protected listPendingResultIds(): string[] {\n try {\n return fs\n .readdirSync(this.resultsDir())\n .filter((entry) => entry.endsWith(RESULT_FILE_SUFFIX))\n .map((entry) => entry.slice(0, -RESULT_FILE_SUFFIX.length));\n } catch (error) {\n if (isNotFound(error)) return [];\n throw error;\n }\n }\n\n resolve(id: string): ResolvedRunLocation | undefined {\n const exact = this.locate(id);\n if (exact) return exact;\n\n const candidates = this.candidateRunIds().filter((candidate) => candidate.startsWith(id));\n if (candidates.length > 1) {\n throw new Error(`Ambiguous run id prefix '${id}' matched: ${candidates.join(', ')}. Provide a longer id.`);\n }\n const [onlyMatch] = candidates;\n return onlyMatch ? this.locate(onlyMatch) : undefined;\n }\n\n async resolveAsync(id: string): Promise<ResolvedRunLocation | undefined> {\n const exact = await this.locateAsync(id);\n if (exact) return exact;\n\n const candidates = (await this.candidateRunIdsAsync()).filter((candidate) => candidate.startsWith(id));\n if (candidates.length > 1) {\n throw new Error(`Ambiguous run id prefix '${id}' matched: ${candidates.join(', ')}. Provide a longer id.`);\n }\n const [onlyMatch] = candidates;\n return onlyMatch ? this.locateAsync(onlyMatch) : undefined;\n }\n\n private candidateRunIds(): string[] {\n return [...new Set([...this.listRunDirNames(), ...this.listPendingResultIds()])];\n }\n\n private locate(runId: string): ResolvedRunLocation | undefined {\n const runDir = this.pathExists(runDirFor(runId)) ? runDirFor(runId) : undefined;\n const claimedPath = claimedResultPathFor(runId);\n if (this.pathExists(claimedPath)) {\n return { runId, runDir, resultPath: claimedPath, claimed: true };\n }\n const pendingPath = pendingResultPathFor(runId);\n if (this.pathExists(pendingPath)) {\n return { runId, runDir, resultPath: pendingPath, claimed: false };\n }\n if (runDir) return { runId, runDir, resultPath: undefined, claimed: false };\n return undefined;\n }\n\n private async pathExistsAsync(filePath: string): Promise<boolean> {\n try {\n await fs.promises.access(filePath);\n return true;\n } catch (error) {\n if (isNotFound(error)) return false;\n throw error;\n }\n }\n\n private async listRunDirNamesAsync(): Promise<string[]> {\n try {\n return (await fs.promises.readdir(this.runsDir(), { withFileTypes: true }))\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name);\n } catch (error) {\n if (isNotFound(error)) return [];\n throw error;\n }\n }\n\n private async listPendingResultIdsAsync(): Promise<string[]> {\n try {\n return (await fs.promises.readdir(this.resultsDir()))\n .filter((entry) => entry.endsWith(RESULT_FILE_SUFFIX))\n .map((entry) => entry.slice(0, -RESULT_FILE_SUFFIX.length));\n } catch (error) {\n if (isNotFound(error)) return [];\n throw error;\n }\n }\n\n private async candidateRunIdsAsync(): Promise<string[]> {\n const [runs, results] = await Promise.all([this.listRunDirNamesAsync(), this.listPendingResultIdsAsync()]);\n return [...new Set([...runs, ...results])];\n }\n\n private async locateAsync(runId: string): Promise<ResolvedRunLocation | undefined> {\n const runDirectory = runDirFor(runId);\n const claimedPath = claimedResultPathFor(runId);\n const pendingPath = pendingResultPathFor(runId);\n const [hasRunDirectory, hasClaimedResult, hasPendingResult] = await Promise.all([\n this.pathExistsAsync(runDirectory),\n this.pathExistsAsync(claimedPath),\n this.pathExistsAsync(pendingPath),\n ]);\n const runDir = hasRunDirectory ? runDirectory : undefined;\n if (hasClaimedResult) return { runId, runDir, resultPath: claimedPath, claimed: true };\n if (hasPendingResult) return { runId, runDir, resultPath: pendingPath, claimed: false };\n if (runDir) return { runId, runDir, resultPath: undefined, claimed: false };\n return undefined;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{__esmMin as e}from"../_virtual/_rolldown/runtime.mjs";import{__decorate as t,init_decorate as n}from"../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorate.mjs";import{currentResultsDir as r,currentRunsDir as i,init_paths as a}from"../shared/paths.mjs";import{CLAIMED_RESULT_FILE_NAME as o,RESULT_FILE_SUFFIX as s,init_resultWatcher as c}from"./resultWatcher.mjs";import*as l from"node:fs";import*as u from"node:path";import{injectable as d}from"inversify";function f(e){return u.join(i(),e)}function p(e){return u.join(r(),`${e}${s}`)}function m(e){return u.join(f(e),o)}function h(e){return typeof e==`object`&&e&&`code`in e?e.code:void 0}function g(e){return h(e)===`ENOENT`}var _,v=e((()=>{a(),c(),n(),_=class{pathExists(e){return l.existsSync(e)}runsDir(){return i()}resultsDir(){return r()}listRunDirNames(){try{return l.readdirSync(this.runsDir(),{withFileTypes:!0}).filter(e=>e.isDirectory()).map(e=>e.name)}catch(e){if(g(e))return[];throw e}}listPendingResultIds(){try{return l.readdirSync(this.resultsDir()).filter(e=>e.endsWith(s)).map(e=>e.slice(0,-s.length))}catch(e){if(g(e))return[];throw e}}resolve(e){let t=this.locate(e);if(t)return t;let n=this.candidateRunIds().filter(t=>t.startsWith(e));if(n.length>1)throw Error(`Ambiguous run id prefix '${e}' matched: ${n.join(`, `)}. Provide a longer id.`);let[r]=n;return r?this.locate(r):void 0}async resolveAsync(e){let t=await this.locateAsync(e);if(t)return t;let n=(await this.candidateRunIdsAsync()).filter(t=>t.startsWith(e));if(n.length>1)throw Error(`Ambiguous run id prefix '${e}' matched: ${n.join(`, `)}. Provide a longer id.`);let[r]=n;return r?this.locateAsync(r):void 0}candidateRunIds(){return[...new Set([...this.listRunDirNames(),...this.listPendingResultIds()])]}locate(e){let t=this.pathExists(f(e))?f(e):void 0,n=m(e);if(this.pathExists(n))return{runId:e,runDir:t,resultPath:n,claimed:!0};let r=p(e);if(this.pathExists(r))return{runId:e,runDir:t,resultPath:r,claimed:!1};if(t)return{runId:e,runDir:t,resultPath:void 0,claimed:!1}}async pathExistsAsync(e){try{return await l.promises.access(e),!0}catch(e){if(g(e))return!1;throw e}}async listRunDirNamesAsync(){try{return(await l.promises.readdir(this.runsDir(),{withFileTypes:!0})).filter(e=>e.isDirectory()).map(e=>e.name)}catch(e){if(g(e))return[];throw e}}async listPendingResultIdsAsync(){try{return(await l.promises.readdir(this.resultsDir())).filter(e=>e.endsWith(s)).map(e=>e.slice(0,-s.length))}catch(e){if(g(e))return[];throw e}}async candidateRunIdsAsync(){let[e,t]=await Promise.all([this.listRunDirNamesAsync(),this.listPendingResultIdsAsync()]);return[...new Set([...e,...t])]}async locateAsync(e){let t=f(e),n=m(e),r=p(e),[i,a,o]=await Promise.all([this.pathExistsAsync(t),this.pathExistsAsync(n),this.pathExistsAsync(r)]),s=i?t:void 0;if(a)return{runId:e,runDir:s,resultPath:n,claimed:!0};if(o)return{runId:e,runDir:s,resultPath:r,claimed:!1};if(s)return{runId:e,runDir:s,resultPath:void 0,claimed:!1}}},_=t([d()],_)}));v();export{_ as RunIdResolver,v as init_runIdResolver};
2
+ //# sourceMappingURL=runIdResolver.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runIdResolver.mjs","names":[],"sources":["../../src/adapters/runIdResolver.ts"],"sourcesContent":["/**\n * Resolve a possibly-abbreviated subagent run id to where its files live.\n *\n * WHY THIS IS SMALLER THAN THE PREDECESSOR:\n * `doom-pi-subagents/src/runs/background/runIdResolver.ts` disambiguates\n * three kinds of run - foreground, async, nested - because that package\n * tracks all three in `SubagentState`. This package is async-only (see\n * `spawnHandshake.ts`'s header) and has not yet ported foreground or nested\n * run tracking (that is later work; see `SubagentState` in the predecessor).\n * This resolver covers the one kind of run this package currently has: a run\n * whose state lives under `currentRunsDir()/<runId>/` and whose terminal result lives\n * in `currentResultsDir()/<runId>.json` until `ResultWatcher` claims it. It is\n * designed to grow, not to be replaced, once the other kinds land.\n *\n * DESIGN PATTERNS:\n * - Aware of `ResultWatcher`'s claim convention\n * (`currentRunsDir()/<runId>/claimed-result.json`): a run whose result has already\n * been claimed for delivery must still resolve, and to the claimed copy,\n * not to a `currentResultsDir()` entry that no longer exists\n * - Prefix matching mirrors the predecessor's ambiguity handling: an\n * unambiguous prefix resolves like an exact id, an ambiguous one throws\n * naming every match, and a prefix matching nothing returns `undefined`\n * rather than throwing - only ambiguity is exceptional, not \"not found\"\n *\n * AVOID:\n * - Reading `currentResultsDir()/<runId>.json` for a run that `pathExists` shows has\n * a claimed copy; the source file may already belong to an unrelated, later\n * run by the time this runs\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { injectable } from 'inversify';\n\nimport { currentResultsDir, currentRunsDir } from '../shared/paths.ts';\nimport { CLAIMED_RESULT_FILE_NAME, RESULT_FILE_SUFFIX } from './resultWatcher.ts';\n\nexport interface ResolvedRunLocation {\n runId: string;\n /** `currentRunsDir()/<runId>`, when it exists. */\n runDir: string | undefined;\n /** Wherever the run's result currently lives - pending or already claimed. */\n resultPath: string | undefined;\n /** True when `resultPath` is a claimed copy awaiting delivery, not the original. */\n claimed: boolean;\n}\n\nexport interface IRunIdResolver {\n /**\n * Resolve an exact id or an unambiguous prefix. `undefined` when nothing\n * matches. Throws when a prefix matches more than one run id.\n */\n resolve(id: string): ResolvedRunLocation | undefined;\n /** Promise-based directory discovery for recurring reconciliation. */\n resolveAsync?(id: string): Promise<ResolvedRunLocation | undefined>;\n}\n\nfunction runDirFor(runId: string): string {\n return path.join(currentRunsDir(), runId);\n}\n\nfunction pendingResultPathFor(runId: string): string {\n return path.join(currentResultsDir(), `${runId}${RESULT_FILE_SUFFIX}`);\n}\n\nfunction claimedResultPathFor(runId: string): string {\n return path.join(runDirFor(runId), CLAIMED_RESULT_FILE_NAME);\n}\n\nfunction errorCode(error: unknown): string | undefined {\n return typeof error === 'object' && error !== null && 'code' in error\n ? (error as NodeJS.ErrnoException).code\n : undefined;\n}\n\nfunction isNotFound(error: unknown): boolean {\n return errorCode(error) === 'ENOENT';\n}\n\n@injectable()\nexport class RunIdResolver implements IRunIdResolver {\n // ---------------------------------------------------------------------\n // Filesystem seams. ESM module namespaces are frozen (`vi.spyOn(fs, ...)`\n // throws), so every `node:fs` call a test needs to redirect is wrapped here.\n // ---------------------------------------------------------------------\n\n protected pathExists(filePath: string): boolean {\n return fs.existsSync(filePath);\n }\n\n /**\n * Seams for the two directory-listing methods below, as protected methods\n * rather than constructor arguments (inversify cannot resolve a primitive\n * like a path, and a directory is no different from `AgentDiscoveryService`'s\n * `now()`/`load()` seams in that respect). A test subclass points these at\n * a directory that does not exist, which exercises the `isNotFound` branch\n * for real without touching the actual shared `currentRunsDir()`/`currentResultsDir()` -\n * closing the only reason this class's tests would otherwise need to.\n */\n protected runsDir(): string {\n return currentRunsDir();\n }\n\n protected resultsDir(): string {\n return currentResultsDir();\n }\n\n protected listRunDirNames(): string[] {\n try {\n return fs\n .readdirSync(this.runsDir(), { withFileTypes: true })\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name);\n } catch (error) {\n if (isNotFound(error)) return [];\n throw error;\n }\n }\n\n protected listPendingResultIds(): string[] {\n try {\n return fs\n .readdirSync(this.resultsDir())\n .filter((entry) => entry.endsWith(RESULT_FILE_SUFFIX))\n .map((entry) => entry.slice(0, -RESULT_FILE_SUFFIX.length));\n } catch (error) {\n if (isNotFound(error)) return [];\n throw error;\n }\n }\n\n resolve(id: string): ResolvedRunLocation | undefined {\n const exact = this.locate(id);\n if (exact) return exact;\n\n const candidates = this.candidateRunIds().filter((candidate) => candidate.startsWith(id));\n if (candidates.length > 1) {\n throw new Error(`Ambiguous run id prefix '${id}' matched: ${candidates.join(', ')}. Provide a longer id.`);\n }\n const [onlyMatch] = candidates;\n return onlyMatch ? this.locate(onlyMatch) : undefined;\n }\n\n async resolveAsync(id: string): Promise<ResolvedRunLocation | undefined> {\n const exact = await this.locateAsync(id);\n if (exact) return exact;\n\n const candidates = (await this.candidateRunIdsAsync()).filter((candidate) => candidate.startsWith(id));\n if (candidates.length > 1) {\n throw new Error(`Ambiguous run id prefix '${id}' matched: ${candidates.join(', ')}. Provide a longer id.`);\n }\n const [onlyMatch] = candidates;\n return onlyMatch ? this.locateAsync(onlyMatch) : undefined;\n }\n\n private candidateRunIds(): string[] {\n return [...new Set([...this.listRunDirNames(), ...this.listPendingResultIds()])];\n }\n\n private locate(runId: string): ResolvedRunLocation | undefined {\n const runDir = this.pathExists(runDirFor(runId)) ? runDirFor(runId) : undefined;\n const claimedPath = claimedResultPathFor(runId);\n if (this.pathExists(claimedPath)) {\n return { runId, runDir, resultPath: claimedPath, claimed: true };\n }\n const pendingPath = pendingResultPathFor(runId);\n if (this.pathExists(pendingPath)) {\n return { runId, runDir, resultPath: pendingPath, claimed: false };\n }\n if (runDir) return { runId, runDir, resultPath: undefined, claimed: false };\n return undefined;\n }\n\n private async pathExistsAsync(filePath: string): Promise<boolean> {\n try {\n await fs.promises.access(filePath);\n return true;\n } catch (error) {\n if (isNotFound(error)) return false;\n throw error;\n }\n }\n\n private async listRunDirNamesAsync(): Promise<string[]> {\n try {\n return (await fs.promises.readdir(this.runsDir(), { withFileTypes: true }))\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name);\n } catch (error) {\n if (isNotFound(error)) return [];\n throw error;\n }\n }\n\n private async listPendingResultIdsAsync(): Promise<string[]> {\n try {\n return (await fs.promises.readdir(this.resultsDir()))\n .filter((entry) => entry.endsWith(RESULT_FILE_SUFFIX))\n .map((entry) => entry.slice(0, -RESULT_FILE_SUFFIX.length));\n } catch (error) {\n if (isNotFound(error)) return [];\n throw error;\n }\n }\n\n private async candidateRunIdsAsync(): Promise<string[]> {\n const [runs, results] = await Promise.all([this.listRunDirNamesAsync(), this.listPendingResultIdsAsync()]);\n return [...new Set([...runs, ...results])];\n }\n\n private async locateAsync(runId: string): Promise<ResolvedRunLocation | undefined> {\n const runDirectory = runDirFor(runId);\n const claimedPath = claimedResultPathFor(runId);\n const pendingPath = pendingResultPathFor(runId);\n const [hasRunDirectory, hasClaimedResult, hasPendingResult] = await Promise.all([\n this.pathExistsAsync(runDirectory),\n this.pathExistsAsync(claimedPath),\n this.pathExistsAsync(pendingPath),\n ]);\n const runDir = hasRunDirectory ? runDirectory : undefined;\n if (hasClaimedResult) return { runId, runDir, resultPath: claimedPath, claimed: true };\n if (hasPendingResult) return { runId, runDir, resultPath: pendingPath, claimed: false };\n if (runDir) return { runId, runDir, resultPath: undefined, claimed: false };\n return undefined;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ const e=require(`../_virtual/_rolldown/runtime.cjs`),t=require(`./atomicJson.cjs`),n=require(`../shared/paths.cjs`),r=require(`../shared/versioned.cjs`),i=require(`./scopeOwner.cjs`);let a=require(`node:fs`);a=e.__toESM(a,1);let o=require(`node:path`);o=e.__toESM(o,1);function s(e){try{return process.kill(e,0),!0}catch(e){return e.code===`EPERM`}}function c(e){return o.join(n.sessionScopeDir(e),`registry.json`)}function l(e){let t;try{t=JSON.parse(e)}catch{return}let n=r.parseVersioned(t,[1]);if(!(!n.ok||!Array.isArray(n.value.runs)))return n.value}function u(e){try{return l(a.readFileSync(c(e),`utf-8`))}catch{return}}async function d(e){try{return l(await a.promises.readFile(c(e),`utf-8`))}catch{return}}function f(e,n){t.writeAtomicJson(c(e),{version:1,runs:n})}async function p(e,n){await t.writeAtomicJsonAsync(c(e),{version:1,runs:n})}function m(e,t){let n=(u(e)?.runs??[]).filter(e=>e.runId!==t.runId);n.push({...t,hostPid:process.pid}),f(e,n)}function h(e,t){let n=u(e)?.runs;n&&f(e,n.filter(e=>e.runId!==t))}function g(e){return u(e)?.runs??[]}async function _(e){return(await d(e))?.runs??[]}async function v(e,t=s){let n=(await d(e))?.runs;if(!n)return[];let r=n.filter(e=>!t(e.pid));if(r.length===0)return[];let i=new Set(r.map(e=>e.runId));return await p(e,n.filter(e=>!i.has(e.runId))),r.map(e=>e.runId)}async function y(e,t=s){let r;try{r=await a.promises.readdir(n.SESSIONS_ROOT_DIR)}catch{return[]}let o=[];for(let n of r){if(n===e.scopeKey)continue;let r=await i.readScopeOwnerAsync({rootSessionId:``,scopeKey:n});if(!r)continue;let a={rootSessionId:r.rootSessionId,scopeKey:n};(await _(a)).some(e=>t(e.pid))||o.push(a)}return o}async function b(e){try{return await a.promises.rm(n.sessionScopeDir(e),{recursive:!0,force:!0}),!0}catch{return n.scopeOwnerPath(e),!1}}exports.defaultPidLiveness=s,exports.findReapableScopesAsync=y,exports.listRuns=g,exports.listRunsAsync=_,exports.pruneDeadRunsAsync=v,exports.reapScopeAsync=b,exports.registerRun=m,exports.releaseRun=h;
2
+ //# sourceMappingURL=runRegistry.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runRegistry.cjs","names":["path","sessionScopeDir","parseVersioned","fs","writeAtomicJsonAsync","SESSIONS_ROOT_DIR","readScopeOwnerAsync","scopeOwnerPath"],"sources":["../../src/adapters/runRegistry.ts"],"sourcesContent":["/**\n * Which runs this session started, and which process owns them.\n *\n * WHY A FILE AND NOT THE IN-MEMORY TRACKER:\n * `AsyncJobTracker` already knows this session's runs, but only in memory, and\n * its own header refuses to scan the runs directory to work out ownership. That\n * is the right call for delivery, and useless for the one question that matters\n * at shutdown and after a crash: which pids does this scope still own? A\n * process that died cannot answer, so the answer has to be on disk.\n *\n * REAPING KEYS ON RUN LIVENESS, NOT ON `hostPid` ALONE:\n * The obvious rule - \"reap a scope whose owning process is dead\" - is wrong.\n * After `/new` the abandoned scope's `hostPid` is the still-live current\n * process, so a hostPid-only test would never reap it. A scope is reapable when\n * none of its recorded pids is alive; a dead `hostPid` only widens that to\n * scopes whose owner is gone entirely.\n *\n * MODELLED ON `doom-runner`'s `RunnerRegistry`, whose `hostPid`-for-orphan-\n * detection and `listBySession` shape this reuses. The containment boundary\n * differs: there it is the worktree, here it is the session scope, which is\n * already a directory.\n *\n * AVOID:\n * - Treating an unreadable registry as empty. Unknown is not the same as none,\n * and acting on the difference is how a live run gets reaped\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { SESSIONS_ROOT_DIR, type SessionScope, scopeOwnerPath, sessionScopeDir } from '../shared/paths.ts';\nimport { parseVersioned } from '../shared/versioned.ts';\nimport { writeAtomicJson, writeAtomicJsonAsync } from './atomicJson.ts';\nimport { readScopeOwner, readScopeOwnerAsync } from './scopeOwner.ts';\n\nconst RUN_REGISTRY_VERSION = 1;\nconst REGISTRY_FILE_NAME = 'registry.json';\n\nexport interface RunRecord {\n runId: string;\n /** Process-group leader. Signals are sent to `-pid`. */\n pid: number;\n agent: string;\n runtime: string;\n startedAt: number;\n /** The process that spawned it, for orphan detection after a crash. */\n hostPid: number;\n}\n\ninterface RunRegistryFile {\n version: typeof RUN_REGISTRY_VERSION;\n runs: RunRecord[];\n}\n\n/** Liveness as a port, so a test never probes a real pid. */\nexport type PidLivenessProbe = (pid: number) => boolean;\n\nexport function defaultPidLiveness(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n // EPERM means it exists and belongs to someone else, which is still alive.\n return (error as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\nfunction registryPath(scope: SessionScope): string {\n return path.join(sessionScopeDir(scope), REGISTRY_FILE_NAME);\n}\n\nfunction parseRegistry(raw: string): RunRegistryFile | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return undefined;\n }\n const result = parseVersioned<RunRegistryFile>(parsed, [RUN_REGISTRY_VERSION]);\n if (!result.ok || !Array.isArray(result.value.runs)) return undefined;\n return result.value;\n}\n\nfunction readRegistry(scope: SessionScope): RunRegistryFile | undefined {\n try {\n return parseRegistry(fs.readFileSync(registryPath(scope), 'utf-8'));\n } catch {\n return undefined;\n }\n}\n\nasync function readRegistryAsync(scope: SessionScope): Promise<RunRegistryFile | undefined> {\n try {\n return parseRegistry(await fs.promises.readFile(registryPath(scope), 'utf-8'));\n } catch {\n return undefined;\n }\n}\n\nfunction writeRegistry(scope: SessionScope, runs: RunRecord[]): void {\n writeAtomicJson(registryPath(scope), { version: RUN_REGISTRY_VERSION, runs });\n}\n\nasync function writeRegistryAsync(scope: SessionScope, runs: RunRecord[]): Promise<void> {\n await writeAtomicJsonAsync(registryPath(scope), { version: RUN_REGISTRY_VERSION, runs });\n}\n\n/** Record a started run against its scope. Replaces any earlier record for the same id. */\nexport function registerRun(scope: SessionScope, record: Omit<RunRecord, 'hostPid'>): void {\n const existing = readRegistry(scope)?.runs ?? [];\n const runs = existing.filter((run) => run.runId !== record.runId);\n runs.push({ ...record, hostPid: process.pid });\n writeRegistry(scope, runs);\n}\n\n/** Drop one run. Does not signal it; the caller decides that. */\nexport function releaseRun(scope: SessionScope, runId: string): void {\n const existing = readRegistry(scope)?.runs;\n if (!existing) return;\n writeRegistry(\n scope,\n existing.filter((run) => run.runId !== runId),\n );\n}\n\nexport function listRuns(scope: SessionScope): RunRecord[] {\n return readRegistry(scope)?.runs ?? [];\n}\n\nexport async function listRunsAsync(scope: SessionScope): Promise<RunRecord[]> {\n return (await readRegistryAsync(scope))?.runs ?? [];\n}\n\n/** Forget records whose process is gone. Returns the run ids dropped. */\nexport function pruneDeadRuns(scope: SessionScope, isAlive: PidLivenessProbe = defaultPidLiveness): string[] {\n const existing = readRegistry(scope)?.runs;\n if (!existing) return [];\n const live = existing.filter((run) => isAlive(run.pid));\n if (live.length === existing.length) return [];\n writeRegistry(scope, live);\n return existing.filter((run) => !isAlive(run.pid)).map((run) => run.runId);\n}\n\nexport async function pruneDeadRunsAsync(\n scope: SessionScope,\n isAlive: PidLivenessProbe = defaultPidLiveness,\n): Promise<string[]> {\n const existing = (await readRegistryAsync(scope))?.runs;\n if (!existing) return [];\n const dead = existing.filter((run) => !isAlive(run.pid));\n if (dead.length === 0) return [];\n const deadIds = new Set(dead.map((run) => run.runId));\n await writeRegistryAsync(\n scope,\n existing.filter((run) => !deadIds.has(run.runId)),\n );\n return dead.map((run) => run.runId);\n}\n\n/**\n * Scopes other than `keep` that hold nothing alive.\n *\n * A scope with no readable owner is skipped rather than reaped: unreadable\n * means unknown, and deleting on unknown is how a live session loses its tree.\n */\nexport function findReapableScopes(keep: SessionScope, isAlive: PidLivenessProbe = defaultPidLiveness): SessionScope[] {\n let entries: string[];\n try {\n entries = fs.readdirSync(SESSIONS_ROOT_DIR);\n } catch {\n return [];\n }\n\n const reapable: SessionScope[] = [];\n for (const scopeKey of entries) {\n if (scopeKey === keep.scopeKey) continue;\n const candidate: SessionScope = { rootSessionId: '', scopeKey };\n const owner = readScopeOwner(candidate);\n if (!owner) continue;\n const scope: SessionScope = { rootSessionId: owner.rootSessionId, scopeKey };\n if (listRuns(scope).some((run) => isAlive(run.pid))) continue;\n reapable.push(scope);\n }\n return reapable;\n}\n\nexport async function findReapableScopesAsync(\n keep: SessionScope,\n isAlive: PidLivenessProbe = defaultPidLiveness,\n): Promise<SessionScope[]> {\n let entries: string[];\n try {\n entries = await fs.promises.readdir(SESSIONS_ROOT_DIR);\n } catch {\n return [];\n }\n\n const reapable: SessionScope[] = [];\n for (const scopeKey of entries) {\n if (scopeKey === keep.scopeKey) continue;\n const candidate: SessionScope = { rootSessionId: '', scopeKey };\n const owner = await readScopeOwnerAsync(candidate);\n if (!owner) continue;\n const scope: SessionScope = { rootSessionId: owner.rootSessionId, scopeKey };\n if ((await listRunsAsync(scope)).some((run) => isAlive(run.pid))) continue;\n reapable.push(scope);\n }\n return reapable;\n}\n\n/** Remove a scope's whole tree. Only ever called on a scope `findReapableScopes` returned. */\nexport function reapScope(scope: SessionScope): boolean {\n try {\n fs.rmSync(sessionScopeDir(scope), { recursive: true, force: true });\n return true;\n } catch {\n // Best effort: a tree that resists removal is reconsidered on the next\n // sweep, and its owner file still reads as reapable, which is the safe\n // direction for this to fail in.\n void scopeOwnerPath(scope);\n return false;\n }\n}\n\nexport async function reapScopeAsync(scope: SessionScope): Promise<boolean> {\n try {\n await fs.promises.rm(sessionScopeDir(scope), { recursive: true, force: true });\n return true;\n } catch {\n void scopeOwnerPath(scope);\n return false;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{__esmMin as e}from"../_virtual/_rolldown/runtime.mjs";import{init_atomicJson as t,writeAtomicJson as n,writeAtomicJsonAsync as r}from"./atomicJson.mjs";import{SESSIONS_ROOT_DIR as i,init_paths as a,scopeOwnerPath as o,sessionScopeDir as s}from"../shared/paths.mjs";import{init_versioned as c,parseVersioned as l}from"../shared/versioned.mjs";import{init_scopeOwner as u,readScopeOwnerAsync as d}from"./scopeOwner.mjs";import*as f from"node:fs";import*as p from"node:path";function m(e){try{return process.kill(e,0),!0}catch(e){return e.code===`EPERM`}}function h(e){return p.join(s(e),k)}function g(e){let t;try{t=JSON.parse(e)}catch{return}let n=l(t,[O]);if(!(!n.ok||!Array.isArray(n.value.runs)))return n.value}function _(e){try{return g(f.readFileSync(h(e),`utf-8`))}catch{return}}async function v(e){try{return g(await f.promises.readFile(h(e),`utf-8`))}catch{return}}function y(e,t){n(h(e),{version:O,runs:t})}async function b(e,t){await r(h(e),{version:O,runs:t})}function x(e,t){let n=(_(e)?.runs??[]).filter(e=>e.runId!==t.runId);n.push({...t,hostPid:process.pid}),y(e,n)}function S(e,t){let n=_(e)?.runs;n&&y(e,n.filter(e=>e.runId!==t))}function C(e){return _(e)?.runs??[]}async function w(e){return(await v(e))?.runs??[]}async function T(e,t=m){let n=(await v(e))?.runs;if(!n)return[];let r=n.filter(e=>!t(e.pid));if(r.length===0)return[];let i=new Set(r.map(e=>e.runId));return await b(e,n.filter(e=>!i.has(e.runId))),r.map(e=>e.runId)}async function E(e,t=m){let n;try{n=await f.promises.readdir(i)}catch{return[]}let r=[];for(let i of n){if(i===e.scopeKey)continue;let n=await d({rootSessionId:``,scopeKey:i});if(!n)continue;let a={rootSessionId:n.rootSessionId,scopeKey:i};(await w(a)).some(e=>t(e.pid))||r.push(a)}return r}async function D(e){try{return await f.promises.rm(s(e),{recursive:!0,force:!0}),!0}catch{return o(e),!1}}var O,k,A=e((()=>{a(),c(),t(),u(),O=1,k=`registry.json`}));A();export{m as defaultPidLiveness,E as findReapableScopesAsync,A as init_runRegistry,C as listRuns,w as listRunsAsync,T as pruneDeadRunsAsync,D as reapScopeAsync,x as registerRun,S as releaseRun};
2
+ //# sourceMappingURL=runRegistry.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runRegistry.mjs","names":[],"sources":["../../src/adapters/runRegistry.ts"],"sourcesContent":["/**\n * Which runs this session started, and which process owns them.\n *\n * WHY A FILE AND NOT THE IN-MEMORY TRACKER:\n * `AsyncJobTracker` already knows this session's runs, but only in memory, and\n * its own header refuses to scan the runs directory to work out ownership. That\n * is the right call for delivery, and useless for the one question that matters\n * at shutdown and after a crash: which pids does this scope still own? A\n * process that died cannot answer, so the answer has to be on disk.\n *\n * REAPING KEYS ON RUN LIVENESS, NOT ON `hostPid` ALONE:\n * The obvious rule - \"reap a scope whose owning process is dead\" - is wrong.\n * After `/new` the abandoned scope's `hostPid` is the still-live current\n * process, so a hostPid-only test would never reap it. A scope is reapable when\n * none of its recorded pids is alive; a dead `hostPid` only widens that to\n * scopes whose owner is gone entirely.\n *\n * MODELLED ON `doom-runner`'s `RunnerRegistry`, whose `hostPid`-for-orphan-\n * detection and `listBySession` shape this reuses. The containment boundary\n * differs: there it is the worktree, here it is the session scope, which is\n * already a directory.\n *\n * AVOID:\n * - Treating an unreadable registry as empty. Unknown is not the same as none,\n * and acting on the difference is how a live run gets reaped\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { SESSIONS_ROOT_DIR, type SessionScope, scopeOwnerPath, sessionScopeDir } from '../shared/paths.ts';\nimport { parseVersioned } from '../shared/versioned.ts';\nimport { writeAtomicJson, writeAtomicJsonAsync } from './atomicJson.ts';\nimport { readScopeOwner, readScopeOwnerAsync } from './scopeOwner.ts';\n\nconst RUN_REGISTRY_VERSION = 1;\nconst REGISTRY_FILE_NAME = 'registry.json';\n\nexport interface RunRecord {\n runId: string;\n /** Process-group leader. Signals are sent to `-pid`. */\n pid: number;\n agent: string;\n runtime: string;\n startedAt: number;\n /** The process that spawned it, for orphan detection after a crash. */\n hostPid: number;\n}\n\ninterface RunRegistryFile {\n version: typeof RUN_REGISTRY_VERSION;\n runs: RunRecord[];\n}\n\n/** Liveness as a port, so a test never probes a real pid. */\nexport type PidLivenessProbe = (pid: number) => boolean;\n\nexport function defaultPidLiveness(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n // EPERM means it exists and belongs to someone else, which is still alive.\n return (error as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\nfunction registryPath(scope: SessionScope): string {\n return path.join(sessionScopeDir(scope), REGISTRY_FILE_NAME);\n}\n\nfunction parseRegistry(raw: string): RunRegistryFile | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return undefined;\n }\n const result = parseVersioned<RunRegistryFile>(parsed, [RUN_REGISTRY_VERSION]);\n if (!result.ok || !Array.isArray(result.value.runs)) return undefined;\n return result.value;\n}\n\nfunction readRegistry(scope: SessionScope): RunRegistryFile | undefined {\n try {\n return parseRegistry(fs.readFileSync(registryPath(scope), 'utf-8'));\n } catch {\n return undefined;\n }\n}\n\nasync function readRegistryAsync(scope: SessionScope): Promise<RunRegistryFile | undefined> {\n try {\n return parseRegistry(await fs.promises.readFile(registryPath(scope), 'utf-8'));\n } catch {\n return undefined;\n }\n}\n\nfunction writeRegistry(scope: SessionScope, runs: RunRecord[]): void {\n writeAtomicJson(registryPath(scope), { version: RUN_REGISTRY_VERSION, runs });\n}\n\nasync function writeRegistryAsync(scope: SessionScope, runs: RunRecord[]): Promise<void> {\n await writeAtomicJsonAsync(registryPath(scope), { version: RUN_REGISTRY_VERSION, runs });\n}\n\n/** Record a started run against its scope. Replaces any earlier record for the same id. */\nexport function registerRun(scope: SessionScope, record: Omit<RunRecord, 'hostPid'>): void {\n const existing = readRegistry(scope)?.runs ?? [];\n const runs = existing.filter((run) => run.runId !== record.runId);\n runs.push({ ...record, hostPid: process.pid });\n writeRegistry(scope, runs);\n}\n\n/** Drop one run. Does not signal it; the caller decides that. */\nexport function releaseRun(scope: SessionScope, runId: string): void {\n const existing = readRegistry(scope)?.runs;\n if (!existing) return;\n writeRegistry(\n scope,\n existing.filter((run) => run.runId !== runId),\n );\n}\n\nexport function listRuns(scope: SessionScope): RunRecord[] {\n return readRegistry(scope)?.runs ?? [];\n}\n\nexport async function listRunsAsync(scope: SessionScope): Promise<RunRecord[]> {\n return (await readRegistryAsync(scope))?.runs ?? [];\n}\n\n/** Forget records whose process is gone. Returns the run ids dropped. */\nexport function pruneDeadRuns(scope: SessionScope, isAlive: PidLivenessProbe = defaultPidLiveness): string[] {\n const existing = readRegistry(scope)?.runs;\n if (!existing) return [];\n const live = existing.filter((run) => isAlive(run.pid));\n if (live.length === existing.length) return [];\n writeRegistry(scope, live);\n return existing.filter((run) => !isAlive(run.pid)).map((run) => run.runId);\n}\n\nexport async function pruneDeadRunsAsync(\n scope: SessionScope,\n isAlive: PidLivenessProbe = defaultPidLiveness,\n): Promise<string[]> {\n const existing = (await readRegistryAsync(scope))?.runs;\n if (!existing) return [];\n const dead = existing.filter((run) => !isAlive(run.pid));\n if (dead.length === 0) return [];\n const deadIds = new Set(dead.map((run) => run.runId));\n await writeRegistryAsync(\n scope,\n existing.filter((run) => !deadIds.has(run.runId)),\n );\n return dead.map((run) => run.runId);\n}\n\n/**\n * Scopes other than `keep` that hold nothing alive.\n *\n * A scope with no readable owner is skipped rather than reaped: unreadable\n * means unknown, and deleting on unknown is how a live session loses its tree.\n */\nexport function findReapableScopes(keep: SessionScope, isAlive: PidLivenessProbe = defaultPidLiveness): SessionScope[] {\n let entries: string[];\n try {\n entries = fs.readdirSync(SESSIONS_ROOT_DIR);\n } catch {\n return [];\n }\n\n const reapable: SessionScope[] = [];\n for (const scopeKey of entries) {\n if (scopeKey === keep.scopeKey) continue;\n const candidate: SessionScope = { rootSessionId: '', scopeKey };\n const owner = readScopeOwner(candidate);\n if (!owner) continue;\n const scope: SessionScope = { rootSessionId: owner.rootSessionId, scopeKey };\n if (listRuns(scope).some((run) => isAlive(run.pid))) continue;\n reapable.push(scope);\n }\n return reapable;\n}\n\nexport async function findReapableScopesAsync(\n keep: SessionScope,\n isAlive: PidLivenessProbe = defaultPidLiveness,\n): Promise<SessionScope[]> {\n let entries: string[];\n try {\n entries = await fs.promises.readdir(SESSIONS_ROOT_DIR);\n } catch {\n return [];\n }\n\n const reapable: SessionScope[] = [];\n for (const scopeKey of entries) {\n if (scopeKey === keep.scopeKey) continue;\n const candidate: SessionScope = { rootSessionId: '', scopeKey };\n const owner = await readScopeOwnerAsync(candidate);\n if (!owner) continue;\n const scope: SessionScope = { rootSessionId: owner.rootSessionId, scopeKey };\n if ((await listRunsAsync(scope)).some((run) => isAlive(run.pid))) continue;\n reapable.push(scope);\n }\n return reapable;\n}\n\n/** Remove a scope's whole tree. Only ever called on a scope `findReapableScopes` returned. */\nexport function reapScope(scope: SessionScope): boolean {\n try {\n fs.rmSync(sessionScopeDir(scope), { recursive: true, force: true });\n return true;\n } catch {\n // Best effort: a tree that resists removal is reconsidered on the next\n // sweep, and its owner file still reads as reapable, which is the safe\n // direction for this to fail in.\n void scopeOwnerPath(scope);\n return false;\n }\n}\n\nexport async function reapScopeAsync(scope: SessionScope): Promise<boolean> {\n try {\n await fs.promises.rm(sessionScopeDir(scope), { recursive: true, force: true });\n return true;\n } catch {\n void scopeOwnerPath(scope);\n return false;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ const e=require(`../_virtual/_rolldown/runtime.cjs`),t=require(`./atomicJson.cjs`),n=require(`../shared/paths.cjs`),r=require(`../shared/versioned.cjs`);let i=require(`node:fs`);i=e.__toESM(i,1);function a(e,t){return{version:1,rootSessionId:e.rootSessionId,hostPid:process.pid,startedAt:t}}async function o(e,r=Date.now()){let i=a(e,r);return await t.writeAtomicJsonAsync(n.scopeOwnerPath(e),i),i}function s(e){let t;try{t=JSON.parse(e)}catch{return}let n=r.parseVersioned(t,[1]);if(!n.ok)return;let i=n.value;if(!(typeof i.rootSessionId!=`string`||!i.rootSessionId)&&Number.isInteger(i.hostPid))return i}async function c(e){try{return s(await i.promises.readFile(n.scopeOwnerPath(e),`utf-8`))}catch{return}}exports.readScopeOwnerAsync=c,exports.writeScopeOwnerAsync=o;
2
+ //# sourceMappingURL=scopeOwner.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scopeOwner.cjs","names":["writeAtomicJsonAsync","scopeOwnerPath","parseVersioned","fs"],"sources":["../../src/adapters/scopeOwner.ts"],"sourcesContent":["/**\n * Who owns a session scope, recorded on disk so a later sweep can tell an\n * abandoned tree from a live one.\n *\n * WHY A FILE AND NOT AN IN-MEMORY REGISTRY:\n * The process that owns a scope is exactly the process that cannot report on\n * it once it is gone. A crashed session leaves its whole tree behind, and the\n * only thing able to reclaim that is a different process reading something the\n * dead one left written down.\n *\n * WHY LIVENESS IS NOT DECIDED HERE:\n * This module records `hostPid` and reads it back; it does not decide whether a\n * scope is reapable. That rule needs run liveness too - after `/new` the\n * abandoned scope's `hostPid` is the still-live current process, so a\n * hostPid-only test would never reap it - and it belongs with the run registry\n * that knows about runs.\n *\n * AVOID:\n * - Treating a missing or unreadable owner file as \"safe to delete\". An\n * unreadable owner is unknown, not dead\n */\n\nimport * as fs from 'node:fs';\n\nimport { type SessionScope, scopeOwnerPath } from '../shared/paths.ts';\nimport { parseVersioned } from '../shared/versioned.ts';\nimport { writeAtomicJson, writeAtomicJsonAsync } from './atomicJson.ts';\n\nconst SCOPE_OWNER_VERSION = 1;\n\nexport interface ScopeOwnerRecord {\n version: typeof SCOPE_OWNER_VERSION;\n rootSessionId: string;\n /** The process that adopted this scope. Dead pid plus no live runs means reapable. */\n hostPid: number;\n startedAt: number;\n}\n\n/**\n * Claim a scope for this process.\n *\n * Rewritten rather than written once: `/resume` reopens the same session id,\n * so the same scope is legitimately re-adopted by a later process, and the\n * record has to name the process that owns it now rather than the one that\n * first created the directory.\n */\nfunction scopeOwnerRecord(scope: SessionScope, now: number): ScopeOwnerRecord {\n return {\n version: SCOPE_OWNER_VERSION,\n rootSessionId: scope.rootSessionId,\n hostPid: process.pid,\n startedAt: now,\n };\n}\n\nexport function writeScopeOwner(scope: SessionScope, now: number = Date.now()): ScopeOwnerRecord {\n const record = scopeOwnerRecord(scope, now);\n writeAtomicJson(scopeOwnerPath(scope), record);\n return record;\n}\n\nexport async function writeScopeOwnerAsync(scope: SessionScope, now: number = Date.now()): Promise<ScopeOwnerRecord> {\n const record = scopeOwnerRecord(scope, now);\n await writeAtomicJsonAsync(scopeOwnerPath(scope), record);\n return record;\n}\n\n/** `undefined` when the scope has no readable owner: unknown, which is not the same as dead. */\nfunction parseScopeOwner(raw: string): ScopeOwnerRecord | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return undefined;\n }\n\n const result = parseVersioned<ScopeOwnerRecord>(parsed, [SCOPE_OWNER_VERSION]);\n if (!result.ok) return undefined;\n const record = result.value;\n if (typeof record.rootSessionId !== 'string' || !record.rootSessionId) return undefined;\n if (!Number.isInteger(record.hostPid)) return undefined;\n return record;\n}\n\nexport function readScopeOwner(scope: SessionScope): ScopeOwnerRecord | undefined {\n let raw: string;\n try {\n raw = fs.readFileSync(scopeOwnerPath(scope), 'utf-8');\n } catch {\n // Absent or unreadable. Both mean \"cannot say who owns this\", and the\n // caller must treat that as unknown rather than unowned.\n return undefined;\n }\n\n return parseScopeOwner(raw);\n}\n\nexport async function readScopeOwnerAsync(scope: SessionScope): Promise<ScopeOwnerRecord | undefined> {\n try {\n return parseScopeOwner(await fs.promises.readFile(scopeOwnerPath(scope), 'utf-8'));\n } catch {\n return undefined;\n }\n}\n"],"mappings":"mMA8CA,SAAS,EAAiB,EAAqB,EAA+B,CAC5E,MAAO,CACL,QAAS,EACT,cAAe,EAAM,cACrB,QAAS,QAAQ,IACjB,UAAW,EACZ,CASH,eAAsB,EAAqB,EAAqB,EAAc,KAAK,KAAK,CAA6B,CACnH,IAAM,EAAS,EAAiB,EAAO,EAAI,CAE3C,OADA,MAAMA,EAAAA,qBAAqBC,EAAAA,eAAe,EAAM,CAAE,EAAO,CAClD,EAIT,SAAS,EAAgB,EAA2C,CAClE,IAAI,EACJ,GAAI,CACF,EAAS,KAAK,MAAM,EAAI,MAClB,CACN,OAGF,IAAM,EAASC,EAAAA,eAAiC,EAAQ,CAAC,EAAoB,CAAC,CAC9E,GAAI,CAAC,EAAO,GAAI,OAChB,IAAM,EAAS,EAAO,MAClB,YAAO,EAAO,eAAkB,UAAY,CAAC,EAAO,gBACnD,OAAO,UAAU,EAAO,QAAQ,CACrC,OAAO,EAgBT,eAAsB,EAAoB,EAA4D,CACpG,GAAI,CACF,OAAO,EAAgB,MAAMC,EAAG,SAAS,SAASF,EAAAA,eAAe,EAAM,CAAE,QAAQ,CAAC,MAC5E,CACN"}
@@ -0,0 +1,2 @@
1
+ import{__esmMin as e}from"../_virtual/_rolldown/runtime.mjs";import{init_atomicJson as t,writeAtomicJsonAsync as n}from"./atomicJson.mjs";import{init_paths as r,scopeOwnerPath as i}from"../shared/paths.mjs";import{init_versioned as a,parseVersioned as o}from"../shared/versioned.mjs";import*as s from"node:fs";function c(e,t){return{version:f,rootSessionId:e.rootSessionId,hostPid:process.pid,startedAt:t}}async function l(e,t=Date.now()){let r=c(e,t);return await n(i(e),r),r}function u(e){let t;try{t=JSON.parse(e)}catch{return}let n=o(t,[f]);if(!n.ok)return;let r=n.value;if(!(typeof r.rootSessionId!=`string`||!r.rootSessionId)&&Number.isInteger(r.hostPid))return r}async function d(e){try{return u(await s.promises.readFile(i(e),`utf-8`))}catch{return}}var f,p=e((()=>{r(),a(),t(),f=1}));p();export{p as init_scopeOwner,d as readScopeOwnerAsync,l as writeScopeOwnerAsync};
2
+ //# sourceMappingURL=scopeOwner.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scopeOwner.mjs","names":[],"sources":["../../src/adapters/scopeOwner.ts"],"sourcesContent":["/**\n * Who owns a session scope, recorded on disk so a later sweep can tell an\n * abandoned tree from a live one.\n *\n * WHY A FILE AND NOT AN IN-MEMORY REGISTRY:\n * The process that owns a scope is exactly the process that cannot report on\n * it once it is gone. A crashed session leaves its whole tree behind, and the\n * only thing able to reclaim that is a different process reading something the\n * dead one left written down.\n *\n * WHY LIVENESS IS NOT DECIDED HERE:\n * This module records `hostPid` and reads it back; it does not decide whether a\n * scope is reapable. That rule needs run liveness too - after `/new` the\n * abandoned scope's `hostPid` is the still-live current process, so a\n * hostPid-only test would never reap it - and it belongs with the run registry\n * that knows about runs.\n *\n * AVOID:\n * - Treating a missing or unreadable owner file as \"safe to delete\". An\n * unreadable owner is unknown, not dead\n */\n\nimport * as fs from 'node:fs';\n\nimport { type SessionScope, scopeOwnerPath } from '../shared/paths.ts';\nimport { parseVersioned } from '../shared/versioned.ts';\nimport { writeAtomicJson, writeAtomicJsonAsync } from './atomicJson.ts';\n\nconst SCOPE_OWNER_VERSION = 1;\n\nexport interface ScopeOwnerRecord {\n version: typeof SCOPE_OWNER_VERSION;\n rootSessionId: string;\n /** The process that adopted this scope. Dead pid plus no live runs means reapable. */\n hostPid: number;\n startedAt: number;\n}\n\n/**\n * Claim a scope for this process.\n *\n * Rewritten rather than written once: `/resume` reopens the same session id,\n * so the same scope is legitimately re-adopted by a later process, and the\n * record has to name the process that owns it now rather than the one that\n * first created the directory.\n */\nfunction scopeOwnerRecord(scope: SessionScope, now: number): ScopeOwnerRecord {\n return {\n version: SCOPE_OWNER_VERSION,\n rootSessionId: scope.rootSessionId,\n hostPid: process.pid,\n startedAt: now,\n };\n}\n\nexport function writeScopeOwner(scope: SessionScope, now: number = Date.now()): ScopeOwnerRecord {\n const record = scopeOwnerRecord(scope, now);\n writeAtomicJson(scopeOwnerPath(scope), record);\n return record;\n}\n\nexport async function writeScopeOwnerAsync(scope: SessionScope, now: number = Date.now()): Promise<ScopeOwnerRecord> {\n const record = scopeOwnerRecord(scope, now);\n await writeAtomicJsonAsync(scopeOwnerPath(scope), record);\n return record;\n}\n\n/** `undefined` when the scope has no readable owner: unknown, which is not the same as dead. */\nfunction parseScopeOwner(raw: string): ScopeOwnerRecord | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return undefined;\n }\n\n const result = parseVersioned<ScopeOwnerRecord>(parsed, [SCOPE_OWNER_VERSION]);\n if (!result.ok) return undefined;\n const record = result.value;\n if (typeof record.rootSessionId !== 'string' || !record.rootSessionId) return undefined;\n if (!Number.isInteger(record.hostPid)) return undefined;\n return record;\n}\n\nexport function readScopeOwner(scope: SessionScope): ScopeOwnerRecord | undefined {\n let raw: string;\n try {\n raw = fs.readFileSync(scopeOwnerPath(scope), 'utf-8');\n } catch {\n // Absent or unreadable. Both mean \"cannot say who owns this\", and the\n // caller must treat that as unknown rather than unowned.\n return undefined;\n }\n\n return parseScopeOwner(raw);\n}\n\nexport async function readScopeOwnerAsync(scope: SessionScope): Promise<ScopeOwnerRecord | undefined> {\n try {\n return parseScopeOwner(await fs.promises.readFile(scopeOwnerPath(scope), 'utf-8'));\n } catch {\n return undefined;\n }\n}\n"],"mappings":"sTA8CA,SAAS,EAAiB,EAAqB,EAA+B,CAC5E,MAAO,CACL,QAAS,EACT,cAAe,EAAM,cACrB,QAAS,QAAQ,IACjB,UAAW,EACZ,CASH,eAAsB,EAAqB,EAAqB,EAAc,KAAK,KAAK,CAA6B,CACnH,IAAM,EAAS,EAAiB,EAAO,EAAI,CAE3C,OADA,MAAM,EAAqB,EAAe,EAAM,CAAE,EAAO,CAClD,EAIT,SAAS,EAAgB,EAA2C,CAClE,IAAI,EACJ,GAAI,CACF,EAAS,KAAK,MAAM,EAAI,MAClB,CACN,OAGF,IAAM,EAAS,EAAiC,EAAQ,CAAC,EAAoB,CAAC,CAC9E,GAAI,CAAC,EAAO,GAAI,OAChB,IAAM,EAAS,EAAO,MAClB,YAAO,EAAO,eAAkB,UAAY,CAAC,EAAO,gBACnD,OAAO,UAAU,EAAO,QAAQ,CACrC,OAAO,EAgBT,eAAsB,EAAoB,EAA4D,CACpG,GAAI,CACF,OAAO,EAAgB,MAAM,EAAG,SAAS,SAAS,EAAe,EAAM,CAAE,QAAQ,CAAC,MAC5E,CACN,2BA7EmE,IACf,IACgB,CAElE,EAAsB"}
@@ -0,0 +1,2 @@
1
+ const e=require(`../_virtual/_rolldown/runtime.cjs`),t=require(`../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorate.cjs`),n=require(`../container/types.cjs`),r=require(`../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateMetadata.cjs`),i=require(`../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateParam.cjs`),a=require(`../shared/paths.cjs`),o=require(`../services/runs/background/statusWriter.cjs`),s=require(`./asyncJobTracker.cjs`),c=require(`../shared/artifacts.cjs`);let l=require(`node:fs`);l=e.__toESM(l,1);let u=require(`node:path`);u=e.__toESM(u,1);let d=require(`inversify`);const f=`events.jsonl`,p=`.claim-`;function m(e){return u.join(a.currentRunsDir(),e,o.STATUS_FILE_NAME)}function h(e){return typeof e==`object`&&e&&`code`in e?e.code:void 0}function g(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}let _=class{constructor(e,t){this.processTerminal=e,this.runIdResolver=t}now(){return Date.now()}createStatusWriter(){return new o.CoalescedStatusWriter}readFile(e){try{return l.readFileSync(e,`utf-8`)}catch(e){if(h(e)===`ENOENT`)return;throw e}}readStatus(e){let t=this.readFile(m(e));if(t===void 0)return;let n;try{n=JSON.parse(t)}catch{return}return g(n)?n:void 0}async readStatusAsync(e){let t;try{t=await l.promises.readFile(m(e),`utf-8`)}catch(e){if(h(e)===`ENOENT`)return;throw e}let n;try{n=JSON.parse(t)}catch{return}return g(n)?n:void 0}appendEvent(e,t){try{c.appendJsonl(u.join(a.currentRunsDir(),e,f),JSON.stringify(t))}catch{}}async appendEventAsync(e,t){try{await l.promises.appendFile(u.join(a.currentRunsDir(),e,f),`${JSON.stringify(t)}\n`)}catch{}}walkForClaims(e,t=0){let n;try{n=l.readdirSync(e,{withFileTypes:!0})}catch(e){if(h(e)===`ENOENT`)return[];throw e}let r=[];for(let i of n)i.name.includes(p)?r.push({dir:e,name:i.name}):i.isDirectory()&&t<8&&r.push(...this.walkForClaims(u.join(e,i.name),t+1));return r}async walkForClaimsAsync(e,t=0){let n;try{n=await l.promises.readdir(e,{withFileTypes:!0})}catch(e){if(h(e)===`ENOENT`)return[];throw e}let r=[];for(let i of n)i.name.includes(p)?r.push({dir:e,name:i.name}):i.isDirectory()&&t<8&&r.push(...await this.walkForClaimsAsync(u.join(e,i.name),t+1));return r}recoverClaim(e,t){try{return l.renameSync(e,t),!0}catch{try{l.unlinkSync(e)}catch{}return!1}}async recoverClaimAsync(e,t){try{return await l.promises.rename(e,t),!0}catch{try{await l.promises.unlink(e)}catch{}return!1}}inspectOnce(e){let t;return()=>t??=this.processTerminal.inspect(e)}reconcile(e){return this.reconcileWith(e,this.inspectOnce(e))}sweepOrphanedClaims(e){return this.sweepWith(e,this.inspectOnce(e))}reconcileAndSweep(e){let t=this.inspectOnce(e);return{reconcile:this.reconcileWith(e,t),sweep:this.sweepWith(e,t)}}async reconcileAndSweepAsync(e){let t,n=async()=>t||(t=this.processTerminal.inspectAsync?await this.processTerminal.inspectAsync(e):this.processTerminal.inspect(e),t);return{reconcile:await this.reconcileWithAsync(e,n),sweep:await this.sweepWithAsync(e,n)}}reconcileWith(e,t){let n=this.readStatus(e);if(!n)return{repaired:!1,reason:`No status file for this run.`};let r=n.state;if(r!==void 0&&s.TERMINAL_ASYNC_JOB_STATES.has(r))return{repaired:!1,reason:`Run already terminal (${r}).`};if(this.runIdResolver.resolve(e)?.resultPath)return{repaired:!1,reason:`A result already exists; delivery is ResultWatcher's job, not a repair.`};let i=t();return i.state===`crashed`?this.repair(e,n,i.marker.pid):{repaired:!1,reason:`Owning process liveness is '${i.state}', which does not prove death; leaving the run as is.`}}async reconcileWithAsync(e,t){let n=await this.readStatusAsync(e);if(!n)return{repaired:!1,reason:`No status file for this run.`};let r=n.state;if(r!==void 0&&s.TERMINAL_ASYNC_JOB_STATES.has(r))return{repaired:!1,reason:`Run already terminal (${r}).`};if((this.runIdResolver.resolveAsync?await this.runIdResolver.resolveAsync(e):this.runIdResolver.resolve(e))?.resultPath)return{repaired:!1,reason:`A result already exists; delivery is ResultWatcher's job, not a repair.`};let i=await t();return i.state===`crashed`?this.repairAsync(e,n,i.marker.pid):{repaired:!1,reason:`Owning process liveness is '${i.state}', which does not prove death; leaving the run as is.`}}sweepWith(e,t){let n=t();if(n.state!==`crashed`)return{recovered:[]};let r=u.join(a.currentRunsDir(),e),i=[];for(let{dir:t,name:a}of this.walkForClaims(r)){let o=a.lastIndexOf(p);if(o===-1)continue;let s=a.slice(0,o);if(this.recoverClaim(u.join(t,a),u.join(t,s))){let a=u.relative(r,t).split(u.sep).join(`/`);i.push({queue:a,fileName:s}),this.appendEvent(e,{type:`subagent.run.claim_recovered`,ts:this.now(),runId:e,pid:n.marker.pid,queue:a,recovered:s})}}return{recovered:i}}async sweepWithAsync(e,t){let n=await t();if(n.state!==`crashed`)return{recovered:[]};let r=u.join(a.currentRunsDir(),e),i=[];for(let{dir:t,name:a}of await this.walkForClaimsAsync(r)){let o=a.lastIndexOf(p);if(o===-1)continue;let s=a.slice(0,o);if(await this.recoverClaimAsync(u.join(t,a),u.join(t,s))){let a=u.relative(r,t).split(u.sep).join(`/`);i.push({queue:a,fileName:s}),await this.appendEventAsync(e,{type:`subagent.run.claim_recovered`,ts:this.now(),runId:e,pid:n.marker.pid,queue:a,recovered:s})}}return{recovered:i}}repair(e,t,n){let r=this.now(),i=`Runner process ${n} exited without finalizing this run. Marked failed by stale-run reconciliation.`,a=this.createStatusWriter();return a.open(e,t),a.updateSync(e=>{e.state=`failed`,e.error=e.error??i,e.lastUpdate=r,e.endedAt=e.endedAt??r}),this.appendEvent(e,{type:`subagent.run.repaired_stale`,ts:r,runId:e,pid:n,message:i}),{repaired:!0,reason:i}}async repairAsync(e,t,n){let r=this.now(),i=`Runner process ${n} exited without finalizing this run. Marked failed by stale-run reconciliation.`,a=this.createStatusWriter();return a.open(e,t),a.updateSync(e=>{e.state=`failed`,e.error=e.error??i,e.lastUpdate=r,e.endedAt=e.endedAt??r}),await this.appendEventAsync(e,{type:`subagent.run.repaired_stale`,ts:r,runId:e,pid:n,message:i}),{repaired:!0,reason:i}}};_=t.__decorate([(0,d.injectable)(),i.__decorateParam(0,(0,d.inject)(n.DOOM_TEAM_TYPES.ProcessTerminalInspector)),i.__decorateParam(1,(0,d.inject)(n.DOOM_TEAM_TYPES.RunIdResolver)),r.__decorateMetadata(`design:paramtypes`,[Object,Object])],_),Object.defineProperty(exports,`StaleRunReconciler`,{enumerable:!0,get:function(){return _}});
2
+ //# sourceMappingURL=staleRunReconciler.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"staleRunReconciler.cjs","names":["path","currentRunsDir","STATUS_FILE_NAME","CoalescedStatusWriter","fs","TERMINAL_ASYNC_JOB_STATES","DOOM_TEAM_TYPES"],"sources":["../../src/adapters/staleRunReconciler.ts"],"sourcesContent":["/**\n * Repairs a run's `status.json` from `running` to `failed` when, and only\n * when, its owning process is provably dead.\n *\n * WHY THIS EXISTS:\n * A runner process can die without ever reaching\n * `TerminalPersistenceService.finalize()` - `SIGKILL`, a host crash, an `OOM`\n * kill. `status.json` is then stuck reporting `running` forever: nothing else\n * in this package ever revisits it, because nothing else has a reason to.\n * This is that reason. It is read-only reconnaissance until it has actual\n * proof, and exactly one narrow write when it does.\n *\n * WHY THIS DOES NOT TOUCH THE RESULT FILE (the TOCTOU the predecessor had):\n * `doom-pi-subagents/src/runs/background/staleRunReconciler.ts:436-450`\n * checks `fs.existsSync(resultPath)`, then separately reads and parses that\n * same path to decide how to repair `status.json` from the result's content.\n * Between those two steps, `ResultWatcher.claimResult()` on the other side of\n * this same package can rename that exact path away as part of its own\n * atomic claim (`resultWatcher.ts`'s fix 4) - so the existence check and the\n * read do not necessarily observe the same file. This module never reads or\n * interprets a result file at all. It asks `RunIdResolver`, which already\n * knows about both the pending and the claimed location, only ONE question:\n * \"does a result exist anywhere for this run yet, in either form?\" If the\n * answer is yes, this module does nothing - a result existing at all means\n * the run finished, which is `ResultWatcher` and whatever consumes its\n * delivery's job to turn into a status update, not a staleness this module\n * needs to repair. This module only ever acts when NO result exists anywhere\n * and the owning process is provably gone, which is a state `ResultWatcher`\n * has no reason to ever contend for.\n *\n * WHY THIS NEVER CALLS `writeAtomicJson(statusPath, ...)`:\n * `CoalescedStatusWriter` is the only thing in this package permitted to write\n * `status.json` (see that module's header). This class is not exempt: a\n * repair opens its OWN, dedicated `CoalescedStatusWriter` instance - never the\n * process-wide singleton, which may simultaneously be the live write path for\n * a run this process is actually running - seeds it from the on-disk status\n * that was just read, and applies the repair through `updateSync()`, the same\n * as any other terminal transition in this package. The write is still, in\n * every observable way, `CoalescedStatusWriter` writing `status.json`; only\n * the instance's *lifetime* is scoped to this one repair.\n *\n * WHAT \"PROVABLE\" MEANS HERE:\n * Only `ProcessTerminalInspector.inspect()` returning `'crashed'` - an\n * OS-confirmed `ESRCH` on the crash marker's pid - authorizes a repair. The\n * predecessor also repaired a run whose pid was still alive (or unverifiable)\n * purely because `status.json` had not updated in 24 hours\n * (`staleAlivePidMs`). That branch is deliberately NOT ported: an\n * unresponsive-but-alive pid is still alive, and treating a long silence as\n * death is a guess, not proof. A run that is merely slow survives this module\n * untouched, by design.\n *\n * ORPHANED CLAIMS (steer-requests, append-requests, and any future queue):\n * `control-channel.ts` claims a queued steer request by renaming it to\n * `<name>.claim-<claimant>` before dispatch, then `commit()`s (deletes) it on\n * success or `release()`s (renames it back) on a throw. `chain-append.ts`\n * does the identical thing for append-requests. A hard kill between claim and\n * release leaves the `.claim-<claimant>` file behind with neither outcome\n * ever happening - the request just sits there, invisible, forever.\n * `sweepOrphanedClaims()` is the same kind of repair as `reconcile()`, for a\n * different piece of state: it puts every orphaned claim back under its\n * original queued name so the next scan retries it, on the same \"provable\n * death, never a guess\" standard.\n *\n * Per `port-steering`, confirmed against the actual code for both queues\n * separately rather than assumed to carry over from one to the other: the\n * two queues do NOT construct their queued filenames the same way (steer\n * zero-pads a timestamp and base64url-encodes an id; append uses a raw\n * `Date.now()` integer and a raw `randomUUID()`), but both still guarantee no\n * literal `.` before the trailing `.json`, so a last-occurrence split on\n * `.claim-` is unambiguous for either. For claimant identity: steer's only\n * production claimant is `` `${process.pid}-${randomUUID()}` ``, built by the\n * run's own runner process. Append-requests has no production claimant yet -\n * nothing in this package claims from it in production, only tests - so its\n * eventual format is undecided. What DOES hold for append-requests today,\n * independent of whatever claimant format is eventually chosen, is that\n * `append-requests/` lives under `currentRunsDir()/<runId>/`, scoped to exactly one\n * run, and nothing in this package's architecture has a reason for any\n * process other than that run's own runner to ever claim from its own queue.\n * So for every queue this module knows about, \"is this claim orphaned\" and\n * \"is this run's owning process provably dead\" are the same question,\n * answered by the same evidence - for steer-requests because of what its one\n * real claimant is, for append-requests because of what the directory can\n * structurally ever contain - which is why this method takes no claimant and\n * gates purely on `ProcessTerminalInspector.inspect(runId)` the same as\n * `reconcile()` does - once that says `'crashed'`, every claim under every\n * one of the run's claim-queue directories is orphaned, full stop; there is\n * nothing left to correlate a specific claim against.\n *\n * STRUCTURAL DISCOVERY, NOT A REGISTRY (revised from an earlier version of\n * this module that kept a `CLAIM_QUEUES` list of known queue directories):\n * A registry means a third queue added later is swept only if whoever adds it\n * remembers to add an entry here too - the exact \"someone has to remember\"\n * failure mode this whole feature exists to close for orphaned claims\n * themselves. So `sweepOrphanedClaims()` does not enumerate queues at all: it\n * walks every directory under the run's own folder (`currentRunsDir()/<runId>/`,\n * bounded by `MAX_CLAIM_SCAN_DEPTH` as insurance, not because any real queue\n * nests that deep) and recovers whatever it finds containing `.claim-`. A\n * queue added anywhere under a run's directory is covered from the day it\n * exists, with no change to this file.\n *\n * WHAT THIS GIVES UP, DELIBERATELY: there is no longer a single list a reader\n * can check to see \"which queues does this module know about\" - the queues\n * ARE whatever is on disk, discovered structurally. It also makes the\n * `.claim-<claimant>` suffix a load-bearing, package-wide contract rather\n * than an internal detail of `control-channel.ts` and `chain-append.ts`: any\n * future file under a run's directory whose name happens to contain that\n * substring is treated as an orphaned claim and unconditionally recovered by\n * stripping the suffix, on the assumption that anything shaped like this\n * package's one established claim-by-rename convention behaves like it. That\n * assumption held for both queues that exist today (see the `port-steering`\n * paragraph above) and is judged worth the coverage guarantee in exchange.\n *\n * `sweepOrphanedClaims()` is a separate public method rather than a step\n * folded into `reconcile()`, because it has an independent gating condition\n * (it is not related to `status.json`'s state, or to whether a result\n * exists) and an independent, testable outcome.\n *\n * AVOID:\n * - Synthesizing a status for a run whose `status.json` does not exist yet.\n * `CoalescedStatusWriter.open()` writes it synchronously before returning\n * (see that module's header), so a genuinely started run already has one by\n * the time anything could observe it as missing; a missing file is either a\n * run that has not started yet or an id that does not exist, and this\n * module cannot tell those apart, so it reports nothing for either\n * - Treating `'unknown'` liveness the same as `'crashed'`; see\n * `processTerminal.ts` for why they are kept distinct\n * - Parsing a pid out of a claimant string. It is `ControlChannelWatcher`'s\n * implementation detail, not a documented wire format (`port-steering`'s\n * own words); the crash marker is already the load-bearing evidence\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { inject, injectable } from 'inversify';\nimport { DOOM_TEAM_TYPES } from '../container/types.ts';\nimport {\n CoalescedStatusWriter,\n type ICoalescedStatusWriter,\n STATUS_FILE_NAME,\n type StatusWithRecentEntries,\n} from '../services/runs/background/statusWriter.ts';\nimport { appendJsonl } from '../shared/artifacts.ts';\nimport { currentRunsDir } from '../shared/paths.ts';\nimport { TERMINAL_ASYNC_JOB_STATES } from './asyncJobTracker.ts';\nimport type { IProcessTerminalInspector, ProcessTerminalVerdict } from './processTerminal.ts';\nimport type { IRunIdResolver } from './runIdResolver.ts';\n\nconst EVENTS_FILE_NAME = 'events.jsonl';\n\n/**\n * Matches `control-channel.ts`'s and `chain-append.ts`'s private\n * `CLAIM_SUFFIX_PREFIX` exactly (the two are identical to each other); not\n * exported from either module, and now the one thing that makes a directory\n * entry \"a claim\" for the purposes of the structural scan below.\n */\nconst CLAIM_SUFFIX_PREFIX = '.claim-';\n\n/**\n * Insurance, not a modeled limit: no real claim queue nests anywhere near\n * this deep under a run's directory. Bounds `walkForClaims()` so a pathological\n * or symlink-cycle directory tree cannot make the scan unbounded.\n */\nconst MAX_CLAIM_SCAN_DEPTH = 8;\n\n/**\n * The minimum shape this module reads and repairs. A run's actual status\n * carries far more than this - this package has not settled a shared status\n * type yet - so everything else on disk is preserved verbatim by round\n * tripping the whole parsed object through `open()`, never reconstructed here.\n */\nexport interface ReconcilableStatus extends StatusWithRecentEntries {\n runId?: string;\n state?: string;\n error?: string;\n startedAt?: number;\n lastUpdate?: number;\n endedAt?: number;\n [key: string]: unknown;\n}\n\nexport interface ReconcileOutcome {\n repaired: boolean;\n /** Why nothing happened, or what was repaired - for logging, never parsed. */\n reason: string;\n}\n\n/** One orphaned claim recovered back to its original queued name. */\nexport interface RecoveredClaim {\n /**\n * The queue's directory, relative to the run's own directory and\n * forward-slash-normalized (e.g. `'append-requests'`, `'control/steer-requests'`).\n * Discovered structurally by `walkForClaims()`, not looked up in a registry.\n */\n queue: string;\n /** The original (recovered) filename, in that queue. */\n fileName: string;\n}\n\nexport interface SweepOutcome {\n recovered: RecoveredClaim[];\n}\n\n/** Both repairs from one pass over a run. See `IStaleRunReconciler.reconcileAndSweep`. */\nexport interface ReconcileSweepOutcome {\n reconcile: ReconcileOutcome;\n sweep: SweepOutcome;\n}\n\nexport interface IStaleRunReconciler {\n /** Repair `runId`'s status if, and only if, its process is provably dead. */\n reconcile(runId: string): ReconcileOutcome;\n /** Recover every one of `runId`'s orphaned claims, discovered structurally under its run directory, if and only if its process is provably dead. */\n sweepOrphanedClaims(runId: string): SweepOutcome;\n /**\n * Both repairs, sharing ONE `ProcessTerminalInspector.inspect()` call.\n *\n * Calling `reconcile()` and `sweepOrphanedClaims()` separately - which is\n * what the parent's poll subscriber used to do - inspects the same run\n * twice per tick, and each inspect is a crash-marker read, a JSON parse and\n * a `kill(pid, 0)`. Both gate on the identical question (\"is this run's\n * owning process provably dead?\"), so the second answer is always the first\n * answer; only the cost is new. For a healthy child, which is the\n * overwhelmingly common case, every bit of that work is discarded.\n *\n * The two methods stay public and independently callable: they have\n * genuinely independent gating conditions (see `sweepOrphanedClaims`'s own\n * doc) and independent tests. This is a shared-work wrapper over them, not\n * a replacement.\n *\n * The inspect is lazy and `reconcile`'s cheap file gates run before it, so\n * a terminal or already-delivered run never pays for liveness on the\n * reconcile side. It still pays once on the sweep side: orphaned claims are\n * gated on process death alone, deliberately independent of `status.json`\n * (see `sweepOrphanedClaims`), and folding them into the status gate would\n * quietly stop recovering claims for a run that finished. The guarantee here\n * is exactly one probe per pass, not zero.\n *\n * In practice the parent's subscriber skips terminal runs before calling\n * this at all, so that case costs nothing in the hot path either.\n */\n reconcileAndSweep(runId: string): ReconcileSweepOutcome;\n /** Promise-based reconciliation for the recurring parent poll subscriber. */\n reconcileAndSweepAsync?(runId: string): Promise<ReconcileSweepOutcome>;\n}\n\nfunction statusPathFor(runId: string): string {\n return path.join(currentRunsDir(), runId, STATUS_FILE_NAME);\n}\n\nfunction errorCode(error: unknown): string | undefined {\n return typeof error === 'object' && error !== null && 'code' in error\n ? (error as NodeJS.ErrnoException).code\n : undefined;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n@injectable()\nexport class StaleRunReconciler implements IStaleRunReconciler {\n constructor(\n @inject(DOOM_TEAM_TYPES.ProcessTerminalInspector) private readonly processTerminal: IProcessTerminalInspector,\n @inject(DOOM_TEAM_TYPES.RunIdResolver) private readonly runIdResolver: IRunIdResolver,\n ) {}\n\n protected now(): number {\n return Date.now();\n }\n\n /**\n * A fresh writer per repair, deliberately never the container's singleton -\n * see the class doc for why sharing it would be unsafe. Protected so a test\n * can substitute a recording fake instead of touching disk.\n */\n protected createStatusWriter(): ICoalescedStatusWriter<ReconcilableStatus> {\n return new CoalescedStatusWriter<ReconcilableStatus>();\n }\n\n protected readFile(filePath: string): string | undefined {\n try {\n return fs.readFileSync(filePath, 'utf-8');\n } catch (error) {\n if (errorCode(error) === 'ENOENT') return undefined;\n throw error;\n }\n }\n\n protected readStatus(runId: string): ReconcilableStatus | undefined {\n const raw = this.readFile(statusPathFor(runId));\n if (raw === undefined) return undefined;\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n // A status file mid-write (CoalescedStatusWriter's rename not yet\n // visible, or a torn read) is not evidence of anything; retried later.\n return undefined;\n }\n return isRecord(parsed) ? (parsed as ReconcilableStatus) : undefined;\n }\n\n protected async readStatusAsync(runId: string): Promise<ReconcilableStatus | undefined> {\n let raw: string;\n try {\n raw = await fs.promises.readFile(statusPathFor(runId), 'utf-8');\n } catch (error) {\n if (errorCode(error) === 'ENOENT') return undefined;\n throw error;\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return undefined;\n }\n return isRecord(parsed) ? (parsed as ReconcilableStatus) : undefined;\n }\n\n /** Best-effort diagnostic trail; a failing event log must never fail a repair. */\n protected appendEvent(runId: string, payload: object): void {\n try {\n appendJsonl(path.join(currentRunsDir(), runId, EVENTS_FILE_NAME), JSON.stringify(payload));\n } catch {\n // See method doc.\n }\n }\n\n /**\n * Walks `dir` and every subdirectory beneath it (bounded by\n * `MAX_CLAIM_SCAN_DEPTH`), returning every entry whose name contains\n * `.claim-`, wherever it is found. Replaces a flat, single-directory\n * listing keyed off a registry: see the class doc's \"STRUCTURAL DISCOVERY\"\n * section for why. Recursing only on `entry.isDirectory()` means a symlink\n * (`isSymbolicLink()`, not `isDirectory()`) is never followed, so this\n * needs no separate cycle-detection.\n */\n protected async appendEventAsync(runId: string, payload: object): Promise<void> {\n try {\n await fs.promises.appendFile(\n path.join(currentRunsDir(), runId, EVENTS_FILE_NAME),\n `${JSON.stringify(payload)}\\n`,\n );\n } catch {\n // See method doc.\n }\n }\n\n protected walkForClaims(dir: string, depth = 0): Array<{ dir: string; name: string }> {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch (error) {\n if (errorCode(error) === 'ENOENT') return [];\n throw error;\n }\n\n const found: Array<{ dir: string; name: string }> = [];\n for (const entry of entries) {\n if (entry.name.includes(CLAIM_SUFFIX_PREFIX)) {\n found.push({ dir, name: entry.name });\n } else if (entry.isDirectory() && depth < MAX_CLAIM_SCAN_DEPTH) {\n found.push(...this.walkForClaims(path.join(dir, entry.name), depth + 1));\n }\n }\n return found;\n }\n\n protected async walkForClaimsAsync(dir: string, depth = 0): Promise<Array<{ dir: string; name: string }>> {\n let entries: fs.Dirent[];\n try {\n entries = await fs.promises.readdir(dir, { withFileTypes: true });\n } catch (error) {\n if (errorCode(error) === 'ENOENT') return [];\n throw error;\n }\n\n const found: Array<{ dir: string; name: string }> = [];\n for (const entry of entries) {\n if (entry.name.includes(CLAIM_SUFFIX_PREFIX)) {\n found.push({ dir, name: entry.name });\n } else if (entry.isDirectory() && depth < MAX_CLAIM_SCAN_DEPTH) {\n found.push(...(await this.walkForClaimsAsync(path.join(dir, entry.name), depth + 1)));\n }\n }\n return found;\n }\n\n /**\n * Put a claimed file back under its original queued name. Returns `true`\n * only when the rename actually happened. Mirrors `control-channel.ts`'s\n * own `release()`: on any failure (original name occupied - astronomically\n * unlikely, the directory gone), there is nothing left to preserve by\n * retrying, so the orphaned claim is dropped rather than left unreachable.\n */\n protected recoverClaim(claimedPath: string, originalPath: string): boolean {\n try {\n fs.renameSync(claimedPath, originalPath);\n return true;\n } catch {\n try {\n fs.unlinkSync(claimedPath);\n } catch {\n // Best effort; see method doc.\n }\n return false;\n }\n }\n\n protected async recoverClaimAsync(claimedPath: string, originalPath: string): Promise<boolean> {\n try {\n await fs.promises.rename(claimedPath, originalPath);\n return true;\n } catch {\n try {\n await fs.promises.unlink(claimedPath);\n } catch {\n // Best effort; see method doc.\n }\n return false;\n }\n }\n\n /**\n * A memoizing wrapper over `inspect()`, so one tick's pair of repairs asks\n * the OS about a pid once instead of twice. Each public entry point makes\n * its own, so a standalone `reconcile()` still inspects exactly when it\n * always did.\n */\n private inspectOnce(runId: string): () => ReturnType<IProcessTerminalInspector['inspect']> {\n let verdict: ReturnType<IProcessTerminalInspector['inspect']> | undefined;\n return () => (verdict ??= this.processTerminal.inspect(runId));\n }\n\n reconcile(runId: string): ReconcileOutcome {\n return this.reconcileWith(runId, this.inspectOnce(runId));\n }\n\n sweepOrphanedClaims(runId: string): SweepOutcome {\n return this.sweepWith(runId, this.inspectOnce(runId));\n }\n\n reconcileAndSweep(runId: string): ReconcileSweepOutcome {\n const inspect = this.inspectOnce(runId);\n // Order matters: `reconcileWith` runs its cheap file gates before ever\n // calling `inspect`, so a terminal or already-delivered run pays nothing\n // extra here. Whichever of the two asks first funds the single call.\n return { reconcile: this.reconcileWith(runId, inspect), sweep: this.sweepWith(runId, inspect) };\n }\n\n async reconcileAndSweepAsync(runId: string): Promise<ReconcileSweepOutcome> {\n let verdict: ProcessTerminalVerdict | undefined;\n const inspect = async (): Promise<ProcessTerminalVerdict> => {\n if (verdict) return verdict;\n verdict = this.processTerminal.inspectAsync\n ? await this.processTerminal.inspectAsync(runId)\n : this.processTerminal.inspect(runId);\n return verdict;\n };\n const reconcile = await this.reconcileWithAsync(runId, inspect);\n const sweep = await this.sweepWithAsync(runId, inspect);\n return { reconcile, sweep };\n }\n\n private reconcileWith(\n runId: string,\n inspect: () => ReturnType<IProcessTerminalInspector['inspect']>,\n ): ReconcileOutcome {\n const status = this.readStatus(runId);\n if (!status) return { repaired: false, reason: 'No status file for this run.' };\n\n const state = status.state;\n if (state !== undefined && TERMINAL_ASYNC_JOB_STATES.has(state)) {\n return { repaired: false, reason: `Run already terminal (${state}).` };\n }\n\n const location = this.runIdResolver.resolve(runId);\n if (location?.resultPath) {\n return { repaired: false, reason: \"A result already exists; delivery is ResultWatcher's job, not a repair.\" };\n }\n\n const verdict = inspect();\n if (verdict.state !== 'crashed') {\n return {\n repaired: false,\n reason: `Owning process liveness is '${verdict.state}', which does not prove death; leaving the run as is.`,\n };\n }\n\n return this.repair(runId, status, verdict.marker.pid);\n }\n\n private async reconcileWithAsync(\n runId: string,\n inspect: () => Promise<ProcessTerminalVerdict>,\n ): Promise<ReconcileOutcome> {\n const status = await this.readStatusAsync(runId);\n if (!status) return { repaired: false, reason: 'No status file for this run.' };\n\n const state = status.state;\n if (state !== undefined && TERMINAL_ASYNC_JOB_STATES.has(state)) {\n return { repaired: false, reason: `Run already terminal (${state}).` };\n }\n\n const location = this.runIdResolver.resolveAsync\n ? await this.runIdResolver.resolveAsync(runId)\n : this.runIdResolver.resolve(runId);\n if (location?.resultPath) {\n return { repaired: false, reason: \"A result already exists; delivery is ResultWatcher's job, not a repair.\" };\n }\n\n const verdict = await inspect();\n if (verdict.state !== 'crashed') {\n return {\n repaired: false,\n reason: `Owning process liveness is '${verdict.state}', which does not prove death; leaving the run as is.`,\n };\n }\n\n return this.repairAsync(runId, status, verdict.marker.pid);\n }\n\n private sweepWith(runId: string, inspect: () => ReturnType<IProcessTerminalInspector['inspect']>): SweepOutcome {\n const verdict = inspect();\n if (verdict.state !== 'crashed') return { recovered: [] };\n\n const runDir = path.join(currentRunsDir(), runId);\n const recovered: RecoveredClaim[] = [];\n for (const { dir, name: entry } of this.walkForClaims(runDir)) {\n const claimIndex = entry.lastIndexOf(CLAIM_SUFFIX_PREFIX);\n // Filtered by walkForClaims() already; defensive, not reachable.\n if (claimIndex === -1) continue;\n const fileName = entry.slice(0, claimIndex);\n const wasRecovered = this.recoverClaim(path.join(dir, entry), path.join(dir, fileName));\n if (wasRecovered) {\n const queue = path.relative(runDir, dir).split(path.sep).join('/');\n recovered.push({ queue, fileName });\n this.appendEvent(runId, {\n type: 'subagent.run.claim_recovered',\n ts: this.now(),\n runId,\n pid: verdict.marker.pid,\n queue,\n recovered: fileName,\n });\n }\n }\n return { recovered };\n }\n\n private async sweepWithAsync(runId: string, inspect: () => Promise<ProcessTerminalVerdict>): Promise<SweepOutcome> {\n const verdict = await inspect();\n if (verdict.state !== 'crashed') return { recovered: [] };\n\n const runDir = path.join(currentRunsDir(), runId);\n const recovered: RecoveredClaim[] = [];\n\n for (const { dir, name: entry } of await this.walkForClaimsAsync(runDir)) {\n const claimIndex = entry.lastIndexOf(CLAIM_SUFFIX_PREFIX);\n if (claimIndex === -1) continue;\n const fileName = entry.slice(0, claimIndex);\n const wasRecovered = await this.recoverClaimAsync(path.join(dir, entry), path.join(dir, fileName));\n if (wasRecovered) {\n const queue = path.relative(runDir, dir).split(path.sep).join('/');\n recovered.push({ queue, fileName });\n await this.appendEventAsync(runId, {\n type: 'subagent.run.claim_recovered',\n ts: this.now(),\n runId,\n pid: verdict.marker.pid,\n queue,\n recovered: fileName,\n });\n }\n }\n return { recovered };\n }\n\n private repair(runId: string, status: ReconcilableStatus, deadPid: number): ReconcileOutcome {\n const now = this.now();\n const message = `Runner process ${deadPid} exited without finalizing this run. Marked failed by stale-run reconciliation.`;\n\n const writer = this.createStatusWriter();\n writer.open(runId, status);\n writer.updateSync((current) => {\n current.state = 'failed';\n current.error = current.error ?? message;\n current.lastUpdate = now;\n current.endedAt = current.endedAt ?? now;\n });\n\n this.appendEvent(runId, { type: 'subagent.run.repaired_stale', ts: now, runId, pid: deadPid, message });\n\n return { repaired: true, reason: message };\n }\n\n private async repairAsync(runId: string, status: ReconcilableStatus, deadPid: number): Promise<ReconcileOutcome> {\n const now = this.now();\n const message = `Runner process ${deadPid} exited without finalizing this run. Marked failed by stale-run reconciliation.`;\n\n const writer = this.createStatusWriter();\n writer.open(runId, status);\n writer.updateSync((current) => {\n current.state = 'failed';\n current.error = current.error ?? message;\n current.lastUpdate = now;\n current.endedAt = current.endedAt ?? now;\n });\n\n await this.appendEventAsync(runId, {\n type: 'subagent.run.repaired_stale',\n ts: now,\n runId,\n pid: deadPid,\n message,\n });\n\n return { repaired: true, reason: message };\n }\n}\n"],"mappings":"8lBAmJA,MAAM,EAAmB,eAQnB,EAAsB,UA0F5B,SAAS,EAAc,EAAuB,CAC5C,OAAOA,EAAK,KAAKC,EAAAA,gBAAgB,CAAE,EAAOC,EAAAA,iBAAiB,CAG7D,SAAS,EAAU,EAAoC,CACrD,OAAO,OAAO,GAAU,UAAY,GAAkB,SAAU,EAC3D,EAAgC,KACjC,IAAA,GAGN,SAAS,EAAS,EAAkD,CAClE,OAAO,OAAO,GAAU,YAAY,GAAkB,CAAC,MAAM,QAAQ,EAAM,CAItE,IAAA,EAAA,KAAwD,CAC7D,YACE,EACA,EACA,CAFmE,KAAA,gBAAA,EACX,KAAA,cAAA,EAG1D,KAAwB,CACtB,OAAO,KAAK,KAAK,CAQnB,oBAA2E,CACzE,OAAO,IAAIC,EAAAA,sBAGb,SAAmB,EAAsC,CACvD,GAAI,CACF,OAAOC,EAAG,aAAa,EAAU,QAAQ,OAClC,EAAO,CACd,GAAI,EAAU,EAAM,GAAK,SAAU,OACnC,MAAM,GAIV,WAAqB,EAA+C,CAClE,IAAM,EAAM,KAAK,SAAS,EAAc,EAAM,CAAC,CAC/C,GAAI,IAAQ,IAAA,GAAW,OACvB,IAAI,EACJ,GAAI,CACF,EAAS,KAAK,MAAM,EAAI,MAClB,CAGN,OAEF,OAAO,EAAS,EAAO,CAAI,EAAgC,IAAA,GAG7D,MAAgB,gBAAgB,EAAwD,CACtF,IAAI,EACJ,GAAI,CACF,EAAM,MAAMA,EAAG,SAAS,SAAS,EAAc,EAAM,CAAE,QAAQ,OACxD,EAAO,CACd,GAAI,EAAU,EAAM,GAAK,SAAU,OACnC,MAAM,EAER,IAAI,EACJ,GAAI,CACF,EAAS,KAAK,MAAM,EAAI,MAClB,CACN,OAEF,OAAO,EAAS,EAAO,CAAI,EAAgC,IAAA,GAI7D,YAAsB,EAAe,EAAuB,CAC1D,GAAI,CACF,EAAA,YAAYJ,EAAK,KAAKC,EAAAA,gBAAgB,CAAE,EAAO,EAAiB,CAAE,KAAK,UAAU,EAAQ,CAAC,MACpF,GAcV,MAAgB,iBAAiB,EAAe,EAAgC,CAC9E,GAAI,CACF,MAAMG,EAAG,SAAS,WAChBJ,EAAK,KAAKC,EAAAA,gBAAgB,CAAE,EAAO,EAAiB,CACpD,GAAG,KAAK,UAAU,EAAQ,CAAC,IAC5B,MACK,GAKV,cAAwB,EAAa,EAAQ,EAAyC,CACpF,IAAI,EACJ,GAAI,CACF,EAAUG,EAAG,YAAY,EAAK,CAAE,cAAe,GAAM,CAAC,OAC/C,EAAO,CACd,GAAI,EAAU,EAAM,GAAK,SAAU,MAAO,EAAE,CAC5C,MAAM,EAGR,IAAM,EAA8C,EAAE,CACtD,IAAK,IAAM,KAAS,EACd,EAAM,KAAK,SAAS,EAAoB,CAC1C,EAAM,KAAK,CAAE,MAAK,KAAM,EAAM,KAAM,CAAC,CAC5B,EAAM,aAAa,EAAI,EAAQ,GACxC,EAAM,KAAK,GAAG,KAAK,cAAcJ,EAAK,KAAK,EAAK,EAAM,KAAK,CAAE,EAAQ,EAAE,CAAC,CAG5E,OAAO,EAGT,MAAgB,mBAAmB,EAAa,EAAQ,EAAkD,CACxG,IAAI,EACJ,GAAI,CACF,EAAU,MAAMI,EAAG,SAAS,QAAQ,EAAK,CAAE,cAAe,GAAM,CAAC,OAC1D,EAAO,CACd,GAAI,EAAU,EAAM,GAAK,SAAU,MAAO,EAAE,CAC5C,MAAM,EAGR,IAAM,EAA8C,EAAE,CACtD,IAAK,IAAM,KAAS,EACd,EAAM,KAAK,SAAS,EAAoB,CAC1C,EAAM,KAAK,CAAE,MAAK,KAAM,EAAM,KAAM,CAAC,CAC5B,EAAM,aAAa,EAAI,EAAQ,GACxC,EAAM,KAAK,GAAI,MAAM,KAAK,mBAAmBJ,EAAK,KAAK,EAAK,EAAM,KAAK,CAAE,EAAQ,EAAE,CAAE,CAGzF,OAAO,EAUT,aAAuB,EAAqB,EAA+B,CACzE,GAAI,CAEF,OADA,EAAG,WAAW,EAAa,EAAa,CACjC,QACD,CACN,GAAI,CACF,EAAG,WAAW,EAAY,MACpB,EAGR,MAAO,IAIX,MAAgB,kBAAkB,EAAqB,EAAwC,CAC7F,GAAI,CAEF,OADA,MAAMI,EAAG,SAAS,OAAO,EAAa,EAAa,CAC5C,QACD,CACN,GAAI,CACF,MAAMA,EAAG,SAAS,OAAO,EAAY,MAC/B,EAGR,MAAO,IAUX,YAAoB,EAAuE,CACzF,IAAI,EACJ,UAAc,IAAY,KAAK,gBAAgB,QAAQ,EAAM,CAG/D,UAAU,EAAiC,CACzC,OAAO,KAAK,cAAc,EAAO,KAAK,YAAY,EAAM,CAAC,CAG3D,oBAAoB,EAA6B,CAC/C,OAAO,KAAK,UAAU,EAAO,KAAK,YAAY,EAAM,CAAC,CAGvD,kBAAkB,EAAsC,CACtD,IAAM,EAAU,KAAK,YAAY,EAAM,CAIvC,MAAO,CAAE,UAAW,KAAK,cAAc,EAAO,EAAQ,CAAE,MAAO,KAAK,UAAU,EAAO,EAAQ,CAAE,CAGjG,MAAM,uBAAuB,EAA+C,CAC1E,IAAI,EACE,EAAU,SACV,IACJ,EAAU,KAAK,gBAAgB,aAC3B,MAAM,KAAK,gBAAgB,aAAa,EAAM,CAC9C,KAAK,gBAAgB,QAAQ,EAAM,CAChC,GAIT,MAAO,CAAE,UAAA,MAFe,KAAK,mBAAmB,EAAO,EAAQ,CAE3C,MAAA,MADA,KAAK,eAAe,EAAO,EAAQ,CAC5B,CAG7B,cACE,EACA,EACkB,CAClB,IAAM,EAAS,KAAK,WAAW,EAAM,CACrC,GAAI,CAAC,EAAQ,MAAO,CAAE,SAAU,GAAO,OAAQ,+BAAgC,CAE/E,IAAM,EAAQ,EAAO,MACrB,GAAI,IAAU,IAAA,IAAaC,EAAAA,0BAA0B,IAAI,EAAM,CAC7D,MAAO,CAAE,SAAU,GAAO,OAAQ,yBAAyB,EAAM,IAAK,CAIxE,GADiB,KAAK,cAAc,QAAQ,EAChC,EAAE,WACZ,MAAO,CAAE,SAAU,GAAO,OAAQ,0EAA2E,CAG/G,IAAM,EAAU,GAAS,CAQzB,OAPI,EAAQ,QAAU,UAOf,KAAK,OAAO,EAAO,EAAQ,EAAQ,OAAO,IAAI,CAN5C,CACL,SAAU,GACV,OAAQ,+BAA+B,EAAQ,MAAM,uDACtD,CAML,MAAc,mBACZ,EACA,EAC2B,CAC3B,IAAM,EAAS,MAAM,KAAK,gBAAgB,EAAM,CAChD,GAAI,CAAC,EAAQ,MAAO,CAAE,SAAU,GAAO,OAAQ,+BAAgC,CAE/E,IAAM,EAAQ,EAAO,MACrB,GAAI,IAAU,IAAA,IAAaA,EAAAA,0BAA0B,IAAI,EAAM,CAC7D,MAAO,CAAE,SAAU,GAAO,OAAQ,yBAAyB,EAAM,IAAK,CAMxE,IAHiB,KAAK,cAAc,aAChC,MAAM,KAAK,cAAc,aAAa,EAAM,CAC5C,KAAK,cAAc,QAAQ,EAAM,GACvB,WACZ,MAAO,CAAE,SAAU,GAAO,OAAQ,0EAA2E,CAG/G,IAAM,EAAU,MAAM,GAAS,CAQ/B,OAPI,EAAQ,QAAU,UAOf,KAAK,YAAY,EAAO,EAAQ,EAAQ,OAAO,IAAI,CANjD,CACL,SAAU,GACV,OAAQ,+BAA+B,EAAQ,MAAM,uDACtD,CAML,UAAkB,EAAe,EAA+E,CAC9G,IAAM,EAAU,GAAS,CACzB,GAAI,EAAQ,QAAU,UAAW,MAAO,CAAE,UAAW,EAAE,CAAE,CAEzD,IAAM,EAASL,EAAK,KAAKC,EAAAA,gBAAgB,CAAE,EAAM,CAC3C,EAA8B,EAAE,CACtC,IAAK,GAAM,CAAE,MAAK,KAAM,KAAW,KAAK,cAAc,EAAO,CAAE,CAC7D,IAAM,EAAa,EAAM,YAAY,EAAoB,CAEzD,GAAI,IAAe,GAAI,SACvB,IAAM,EAAW,EAAM,MAAM,EAAG,EAAW,CAE3C,GADqB,KAAK,aAAaD,EAAK,KAAK,EAAK,EAAM,CAAEA,EAAK,KAAK,EAAK,EAAS,CACtE,CAAE,CAChB,IAAM,EAAQA,EAAK,SAAS,EAAQ,EAAI,CAAC,MAAMA,EAAK,IAAI,CAAC,KAAK,IAAI,CAClE,EAAU,KAAK,CAAE,QAAO,WAAU,CAAC,CACnC,KAAK,YAAY,EAAO,CACtB,KAAM,+BACN,GAAI,KAAK,KAAK,CACd,QACA,IAAK,EAAQ,OAAO,IACpB,QACA,UAAW,EACZ,CAAC,EAGN,MAAO,CAAE,YAAW,CAGtB,MAAc,eAAe,EAAe,EAAuE,CACjH,IAAM,EAAU,MAAM,GAAS,CAC/B,GAAI,EAAQ,QAAU,UAAW,MAAO,CAAE,UAAW,EAAE,CAAE,CAEzD,IAAM,EAASA,EAAK,KAAKC,EAAAA,gBAAgB,CAAE,EAAM,CAC3C,EAA8B,EAAE,CAEtC,IAAK,GAAM,CAAE,MAAK,KAAM,KAAW,MAAM,KAAK,mBAAmB,EAAO,CAAE,CACxE,IAAM,EAAa,EAAM,YAAY,EAAoB,CACzD,GAAI,IAAe,GAAI,SACvB,IAAM,EAAW,EAAM,MAAM,EAAG,EAAW,CAE3C,GAAI,MADuB,KAAK,kBAAkBD,EAAK,KAAK,EAAK,EAAM,CAAEA,EAAK,KAAK,EAAK,EAAS,CAAC,CAChF,CAChB,IAAM,EAAQA,EAAK,SAAS,EAAQ,EAAI,CAAC,MAAMA,EAAK,IAAI,CAAC,KAAK,IAAI,CAClE,EAAU,KAAK,CAAE,QAAO,WAAU,CAAC,CACnC,MAAM,KAAK,iBAAiB,EAAO,CACjC,KAAM,+BACN,GAAI,KAAK,KAAK,CACd,QACA,IAAK,EAAQ,OAAO,IACpB,QACA,UAAW,EACZ,CAAC,EAGN,MAAO,CAAE,YAAW,CAGtB,OAAe,EAAe,EAA4B,EAAmC,CAC3F,IAAM,EAAM,KAAK,KAAK,CAChB,EAAU,kBAAkB,EAAQ,iFAEpC,EAAS,KAAK,oBAAoB,CAWxC,OAVA,EAAO,KAAK,EAAO,EAAO,CAC1B,EAAO,WAAY,GAAY,CAC7B,EAAQ,MAAQ,SAChB,EAAQ,MAAQ,EAAQ,OAAS,EACjC,EAAQ,WAAa,EACrB,EAAQ,QAAU,EAAQ,SAAW,GACrC,CAEF,KAAK,YAAY,EAAO,CAAE,KAAM,8BAA+B,GAAI,EAAK,QAAO,IAAK,EAAS,UAAS,CAAC,CAEhG,CAAE,SAAU,GAAM,OAAQ,EAAS,CAG5C,MAAc,YAAY,EAAe,EAA4B,EAA4C,CAC/G,IAAM,EAAM,KAAK,KAAK,CAChB,EAAU,kBAAkB,EAAQ,iFAEpC,EAAS,KAAK,oBAAoB,CAiBxC,OAhBA,EAAO,KAAK,EAAO,EAAO,CAC1B,EAAO,WAAY,GAAY,CAC7B,EAAQ,MAAQ,SAChB,EAAQ,MAAQ,EAAQ,OAAS,EACjC,EAAQ,WAAa,EACrB,EAAQ,QAAU,EAAQ,SAAW,GACrC,CAEF,MAAM,KAAK,iBAAiB,EAAO,CACjC,KAAM,8BACN,GAAI,EACJ,QACA,IAAK,EACL,UACD,CAAC,CAEK,CAAE,SAAU,GAAM,OAAQ,EAAS,qCAtWjC,kCAGDM,EAAAA,gBAAgB,yBAAyB,CAAA,kCACzCA,EAAAA,gBAAgB,cAAc,CAAA"}
@@ -0,0 +1,2 @@
1
+ import{__esmMin as e}from"../_virtual/_rolldown/runtime.mjs";import{__decorate as t,init_decorate as n}from"../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorate.mjs";import{DOOM_TEAM_TYPES as r,init_types as i}from"../container/types.mjs";import{__decorateMetadata as a,init_decorateMetadata as o}from"../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateMetadata.mjs";import{__decorateParam as s,init_decorateParam as c}from"../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateParam.mjs";import{currentRunsDir as l,init_paths as u}from"../shared/paths.mjs";import{CoalescedStatusWriter as d,STATUS_FILE_NAME as f,init_statusWriter as p}from"../services/runs/background/statusWriter.mjs";import{TERMINAL_ASYNC_JOB_STATES as m,init_asyncJobTracker as h}from"./asyncJobTracker.mjs";import{appendJsonl as g,init_artifacts as _}from"../shared/artifacts.mjs";import*as v from"node:fs";import*as y from"node:path";import{inject as b,injectable as x}from"inversify";function S(e){return y.join(l(),e,f)}function C(e){return typeof e==`object`&&e&&`code`in e?e.code:void 0}function w(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}var T,E,D,O,k=e((()=>{i(),p(),_(),u(),h(),o(),c(),n(),T=`events.jsonl`,E=`.claim-`,D=8,O=class{constructor(e,t){this.processTerminal=e,this.runIdResolver=t}now(){return Date.now()}createStatusWriter(){return new d}readFile(e){try{return v.readFileSync(e,`utf-8`)}catch(e){if(C(e)===`ENOENT`)return;throw e}}readStatus(e){let t=this.readFile(S(e));if(t===void 0)return;let n;try{n=JSON.parse(t)}catch{return}return w(n)?n:void 0}async readStatusAsync(e){let t;try{t=await v.promises.readFile(S(e),`utf-8`)}catch(e){if(C(e)===`ENOENT`)return;throw e}let n;try{n=JSON.parse(t)}catch{return}return w(n)?n:void 0}appendEvent(e,t){try{g(y.join(l(),e,T),JSON.stringify(t))}catch{}}async appendEventAsync(e,t){try{await v.promises.appendFile(y.join(l(),e,T),`${JSON.stringify(t)}\n`)}catch{}}walkForClaims(e,t=0){let n;try{n=v.readdirSync(e,{withFileTypes:!0})}catch(e){if(C(e)===`ENOENT`)return[];throw e}let r=[];for(let i of n)i.name.includes(E)?r.push({dir:e,name:i.name}):i.isDirectory()&&t<D&&r.push(...this.walkForClaims(y.join(e,i.name),t+1));return r}async walkForClaimsAsync(e,t=0){let n;try{n=await v.promises.readdir(e,{withFileTypes:!0})}catch(e){if(C(e)===`ENOENT`)return[];throw e}let r=[];for(let i of n)i.name.includes(E)?r.push({dir:e,name:i.name}):i.isDirectory()&&t<D&&r.push(...await this.walkForClaimsAsync(y.join(e,i.name),t+1));return r}recoverClaim(e,t){try{return v.renameSync(e,t),!0}catch{try{v.unlinkSync(e)}catch{}return!1}}async recoverClaimAsync(e,t){try{return await v.promises.rename(e,t),!0}catch{try{await v.promises.unlink(e)}catch{}return!1}}inspectOnce(e){let t;return()=>t??=this.processTerminal.inspect(e)}reconcile(e){return this.reconcileWith(e,this.inspectOnce(e))}sweepOrphanedClaims(e){return this.sweepWith(e,this.inspectOnce(e))}reconcileAndSweep(e){let t=this.inspectOnce(e);return{reconcile:this.reconcileWith(e,t),sweep:this.sweepWith(e,t)}}async reconcileAndSweepAsync(e){let t,n=async()=>t||(t=this.processTerminal.inspectAsync?await this.processTerminal.inspectAsync(e):this.processTerminal.inspect(e),t);return{reconcile:await this.reconcileWithAsync(e,n),sweep:await this.sweepWithAsync(e,n)}}reconcileWith(e,t){let n=this.readStatus(e);if(!n)return{repaired:!1,reason:`No status file for this run.`};let r=n.state;if(r!==void 0&&m.has(r))return{repaired:!1,reason:`Run already terminal (${r}).`};if(this.runIdResolver.resolve(e)?.resultPath)return{repaired:!1,reason:`A result already exists; delivery is ResultWatcher's job, not a repair.`};let i=t();return i.state===`crashed`?this.repair(e,n,i.marker.pid):{repaired:!1,reason:`Owning process liveness is '${i.state}', which does not prove death; leaving the run as is.`}}async reconcileWithAsync(e,t){let n=await this.readStatusAsync(e);if(!n)return{repaired:!1,reason:`No status file for this run.`};let r=n.state;if(r!==void 0&&m.has(r))return{repaired:!1,reason:`Run already terminal (${r}).`};if((this.runIdResolver.resolveAsync?await this.runIdResolver.resolveAsync(e):this.runIdResolver.resolve(e))?.resultPath)return{repaired:!1,reason:`A result already exists; delivery is ResultWatcher's job, not a repair.`};let i=await t();return i.state===`crashed`?this.repairAsync(e,n,i.marker.pid):{repaired:!1,reason:`Owning process liveness is '${i.state}', which does not prove death; leaving the run as is.`}}sweepWith(e,t){let n=t();if(n.state!==`crashed`)return{recovered:[]};let r=y.join(l(),e),i=[];for(let{dir:t,name:a}of this.walkForClaims(r)){let o=a.lastIndexOf(E);if(o===-1)continue;let s=a.slice(0,o);if(this.recoverClaim(y.join(t,a),y.join(t,s))){let a=y.relative(r,t).split(y.sep).join(`/`);i.push({queue:a,fileName:s}),this.appendEvent(e,{type:`subagent.run.claim_recovered`,ts:this.now(),runId:e,pid:n.marker.pid,queue:a,recovered:s})}}return{recovered:i}}async sweepWithAsync(e,t){let n=await t();if(n.state!==`crashed`)return{recovered:[]};let r=y.join(l(),e),i=[];for(let{dir:t,name:a}of await this.walkForClaimsAsync(r)){let o=a.lastIndexOf(E);if(o===-1)continue;let s=a.slice(0,o);if(await this.recoverClaimAsync(y.join(t,a),y.join(t,s))){let a=y.relative(r,t).split(y.sep).join(`/`);i.push({queue:a,fileName:s}),await this.appendEventAsync(e,{type:`subagent.run.claim_recovered`,ts:this.now(),runId:e,pid:n.marker.pid,queue:a,recovered:s})}}return{recovered:i}}repair(e,t,n){let r=this.now(),i=`Runner process ${n} exited without finalizing this run. Marked failed by stale-run reconciliation.`,a=this.createStatusWriter();return a.open(e,t),a.updateSync(e=>{e.state=`failed`,e.error=e.error??i,e.lastUpdate=r,e.endedAt=e.endedAt??r}),this.appendEvent(e,{type:`subagent.run.repaired_stale`,ts:r,runId:e,pid:n,message:i}),{repaired:!0,reason:i}}async repairAsync(e,t,n){let r=this.now(),i=`Runner process ${n} exited without finalizing this run. Marked failed by stale-run reconciliation.`,a=this.createStatusWriter();return a.open(e,t),a.updateSync(e=>{e.state=`failed`,e.error=e.error??i,e.lastUpdate=r,e.endedAt=e.endedAt??r}),await this.appendEventAsync(e,{type:`subagent.run.repaired_stale`,ts:r,runId:e,pid:n,message:i}),{repaired:!0,reason:i}}},O=t([x(),s(0,b(r.ProcessTerminalInspector)),s(1,b(r.RunIdResolver)),a(`design:paramtypes`,[Object,Object])],O)}));k();export{O as StaleRunReconciler,k as init_staleRunReconciler};
2
+ //# sourceMappingURL=staleRunReconciler.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"staleRunReconciler.mjs","names":[],"sources":["../../src/adapters/staleRunReconciler.ts"],"sourcesContent":["/**\n * Repairs a run's `status.json` from `running` to `failed` when, and only\n * when, its owning process is provably dead.\n *\n * WHY THIS EXISTS:\n * A runner process can die without ever reaching\n * `TerminalPersistenceService.finalize()` - `SIGKILL`, a host crash, an `OOM`\n * kill. `status.json` is then stuck reporting `running` forever: nothing else\n * in this package ever revisits it, because nothing else has a reason to.\n * This is that reason. It is read-only reconnaissance until it has actual\n * proof, and exactly one narrow write when it does.\n *\n * WHY THIS DOES NOT TOUCH THE RESULT FILE (the TOCTOU the predecessor had):\n * `doom-pi-subagents/src/runs/background/staleRunReconciler.ts:436-450`\n * checks `fs.existsSync(resultPath)`, then separately reads and parses that\n * same path to decide how to repair `status.json` from the result's content.\n * Between those two steps, `ResultWatcher.claimResult()` on the other side of\n * this same package can rename that exact path away as part of its own\n * atomic claim (`resultWatcher.ts`'s fix 4) - so the existence check and the\n * read do not necessarily observe the same file. This module never reads or\n * interprets a result file at all. It asks `RunIdResolver`, which already\n * knows about both the pending and the claimed location, only ONE question:\n * \"does a result exist anywhere for this run yet, in either form?\" If the\n * answer is yes, this module does nothing - a result existing at all means\n * the run finished, which is `ResultWatcher` and whatever consumes its\n * delivery's job to turn into a status update, not a staleness this module\n * needs to repair. This module only ever acts when NO result exists anywhere\n * and the owning process is provably gone, which is a state `ResultWatcher`\n * has no reason to ever contend for.\n *\n * WHY THIS NEVER CALLS `writeAtomicJson(statusPath, ...)`:\n * `CoalescedStatusWriter` is the only thing in this package permitted to write\n * `status.json` (see that module's header). This class is not exempt: a\n * repair opens its OWN, dedicated `CoalescedStatusWriter` instance - never the\n * process-wide singleton, which may simultaneously be the live write path for\n * a run this process is actually running - seeds it from the on-disk status\n * that was just read, and applies the repair through `updateSync()`, the same\n * as any other terminal transition in this package. The write is still, in\n * every observable way, `CoalescedStatusWriter` writing `status.json`; only\n * the instance's *lifetime* is scoped to this one repair.\n *\n * WHAT \"PROVABLE\" MEANS HERE:\n * Only `ProcessTerminalInspector.inspect()` returning `'crashed'` - an\n * OS-confirmed `ESRCH` on the crash marker's pid - authorizes a repair. The\n * predecessor also repaired a run whose pid was still alive (or unverifiable)\n * purely because `status.json` had not updated in 24 hours\n * (`staleAlivePidMs`). That branch is deliberately NOT ported: an\n * unresponsive-but-alive pid is still alive, and treating a long silence as\n * death is a guess, not proof. A run that is merely slow survives this module\n * untouched, by design.\n *\n * ORPHANED CLAIMS (steer-requests, append-requests, and any future queue):\n * `control-channel.ts` claims a queued steer request by renaming it to\n * `<name>.claim-<claimant>` before dispatch, then `commit()`s (deletes) it on\n * success or `release()`s (renames it back) on a throw. `chain-append.ts`\n * does the identical thing for append-requests. A hard kill between claim and\n * release leaves the `.claim-<claimant>` file behind with neither outcome\n * ever happening - the request just sits there, invisible, forever.\n * `sweepOrphanedClaims()` is the same kind of repair as `reconcile()`, for a\n * different piece of state: it puts every orphaned claim back under its\n * original queued name so the next scan retries it, on the same \"provable\n * death, never a guess\" standard.\n *\n * Per `port-steering`, confirmed against the actual code for both queues\n * separately rather than assumed to carry over from one to the other: the\n * two queues do NOT construct their queued filenames the same way (steer\n * zero-pads a timestamp and base64url-encodes an id; append uses a raw\n * `Date.now()` integer and a raw `randomUUID()`), but both still guarantee no\n * literal `.` before the trailing `.json`, so a last-occurrence split on\n * `.claim-` is unambiguous for either. For claimant identity: steer's only\n * production claimant is `` `${process.pid}-${randomUUID()}` ``, built by the\n * run's own runner process. Append-requests has no production claimant yet -\n * nothing in this package claims from it in production, only tests - so its\n * eventual format is undecided. What DOES hold for append-requests today,\n * independent of whatever claimant format is eventually chosen, is that\n * `append-requests/` lives under `currentRunsDir()/<runId>/`, scoped to exactly one\n * run, and nothing in this package's architecture has a reason for any\n * process other than that run's own runner to ever claim from its own queue.\n * So for every queue this module knows about, \"is this claim orphaned\" and\n * \"is this run's owning process provably dead\" are the same question,\n * answered by the same evidence - for steer-requests because of what its one\n * real claimant is, for append-requests because of what the directory can\n * structurally ever contain - which is why this method takes no claimant and\n * gates purely on `ProcessTerminalInspector.inspect(runId)` the same as\n * `reconcile()` does - once that says `'crashed'`, every claim under every\n * one of the run's claim-queue directories is orphaned, full stop; there is\n * nothing left to correlate a specific claim against.\n *\n * STRUCTURAL DISCOVERY, NOT A REGISTRY (revised from an earlier version of\n * this module that kept a `CLAIM_QUEUES` list of known queue directories):\n * A registry means a third queue added later is swept only if whoever adds it\n * remembers to add an entry here too - the exact \"someone has to remember\"\n * failure mode this whole feature exists to close for orphaned claims\n * themselves. So `sweepOrphanedClaims()` does not enumerate queues at all: it\n * walks every directory under the run's own folder (`currentRunsDir()/<runId>/`,\n * bounded by `MAX_CLAIM_SCAN_DEPTH` as insurance, not because any real queue\n * nests that deep) and recovers whatever it finds containing `.claim-`. A\n * queue added anywhere under a run's directory is covered from the day it\n * exists, with no change to this file.\n *\n * WHAT THIS GIVES UP, DELIBERATELY: there is no longer a single list a reader\n * can check to see \"which queues does this module know about\" - the queues\n * ARE whatever is on disk, discovered structurally. It also makes the\n * `.claim-<claimant>` suffix a load-bearing, package-wide contract rather\n * than an internal detail of `control-channel.ts` and `chain-append.ts`: any\n * future file under a run's directory whose name happens to contain that\n * substring is treated as an orphaned claim and unconditionally recovered by\n * stripping the suffix, on the assumption that anything shaped like this\n * package's one established claim-by-rename convention behaves like it. That\n * assumption held for both queues that exist today (see the `port-steering`\n * paragraph above) and is judged worth the coverage guarantee in exchange.\n *\n * `sweepOrphanedClaims()` is a separate public method rather than a step\n * folded into `reconcile()`, because it has an independent gating condition\n * (it is not related to `status.json`'s state, or to whether a result\n * exists) and an independent, testable outcome.\n *\n * AVOID:\n * - Synthesizing a status for a run whose `status.json` does not exist yet.\n * `CoalescedStatusWriter.open()` writes it synchronously before returning\n * (see that module's header), so a genuinely started run already has one by\n * the time anything could observe it as missing; a missing file is either a\n * run that has not started yet or an id that does not exist, and this\n * module cannot tell those apart, so it reports nothing for either\n * - Treating `'unknown'` liveness the same as `'crashed'`; see\n * `processTerminal.ts` for why they are kept distinct\n * - Parsing a pid out of a claimant string. It is `ControlChannelWatcher`'s\n * implementation detail, not a documented wire format (`port-steering`'s\n * own words); the crash marker is already the load-bearing evidence\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { inject, injectable } from 'inversify';\nimport { DOOM_TEAM_TYPES } from '../container/types.ts';\nimport {\n CoalescedStatusWriter,\n type ICoalescedStatusWriter,\n STATUS_FILE_NAME,\n type StatusWithRecentEntries,\n} from '../services/runs/background/statusWriter.ts';\nimport { appendJsonl } from '../shared/artifacts.ts';\nimport { currentRunsDir } from '../shared/paths.ts';\nimport { TERMINAL_ASYNC_JOB_STATES } from './asyncJobTracker.ts';\nimport type { IProcessTerminalInspector, ProcessTerminalVerdict } from './processTerminal.ts';\nimport type { IRunIdResolver } from './runIdResolver.ts';\n\nconst EVENTS_FILE_NAME = 'events.jsonl';\n\n/**\n * Matches `control-channel.ts`'s and `chain-append.ts`'s private\n * `CLAIM_SUFFIX_PREFIX` exactly (the two are identical to each other); not\n * exported from either module, and now the one thing that makes a directory\n * entry \"a claim\" for the purposes of the structural scan below.\n */\nconst CLAIM_SUFFIX_PREFIX = '.claim-';\n\n/**\n * Insurance, not a modeled limit: no real claim queue nests anywhere near\n * this deep under a run's directory. Bounds `walkForClaims()` so a pathological\n * or symlink-cycle directory tree cannot make the scan unbounded.\n */\nconst MAX_CLAIM_SCAN_DEPTH = 8;\n\n/**\n * The minimum shape this module reads and repairs. A run's actual status\n * carries far more than this - this package has not settled a shared status\n * type yet - so everything else on disk is preserved verbatim by round\n * tripping the whole parsed object through `open()`, never reconstructed here.\n */\nexport interface ReconcilableStatus extends StatusWithRecentEntries {\n runId?: string;\n state?: string;\n error?: string;\n startedAt?: number;\n lastUpdate?: number;\n endedAt?: number;\n [key: string]: unknown;\n}\n\nexport interface ReconcileOutcome {\n repaired: boolean;\n /** Why nothing happened, or what was repaired - for logging, never parsed. */\n reason: string;\n}\n\n/** One orphaned claim recovered back to its original queued name. */\nexport interface RecoveredClaim {\n /**\n * The queue's directory, relative to the run's own directory and\n * forward-slash-normalized (e.g. `'append-requests'`, `'control/steer-requests'`).\n * Discovered structurally by `walkForClaims()`, not looked up in a registry.\n */\n queue: string;\n /** The original (recovered) filename, in that queue. */\n fileName: string;\n}\n\nexport interface SweepOutcome {\n recovered: RecoveredClaim[];\n}\n\n/** Both repairs from one pass over a run. See `IStaleRunReconciler.reconcileAndSweep`. */\nexport interface ReconcileSweepOutcome {\n reconcile: ReconcileOutcome;\n sweep: SweepOutcome;\n}\n\nexport interface IStaleRunReconciler {\n /** Repair `runId`'s status if, and only if, its process is provably dead. */\n reconcile(runId: string): ReconcileOutcome;\n /** Recover every one of `runId`'s orphaned claims, discovered structurally under its run directory, if and only if its process is provably dead. */\n sweepOrphanedClaims(runId: string): SweepOutcome;\n /**\n * Both repairs, sharing ONE `ProcessTerminalInspector.inspect()` call.\n *\n * Calling `reconcile()` and `sweepOrphanedClaims()` separately - which is\n * what the parent's poll subscriber used to do - inspects the same run\n * twice per tick, and each inspect is a crash-marker read, a JSON parse and\n * a `kill(pid, 0)`. Both gate on the identical question (\"is this run's\n * owning process provably dead?\"), so the second answer is always the first\n * answer; only the cost is new. For a healthy child, which is the\n * overwhelmingly common case, every bit of that work is discarded.\n *\n * The two methods stay public and independently callable: they have\n * genuinely independent gating conditions (see `sweepOrphanedClaims`'s own\n * doc) and independent tests. This is a shared-work wrapper over them, not\n * a replacement.\n *\n * The inspect is lazy and `reconcile`'s cheap file gates run before it, so\n * a terminal or already-delivered run never pays for liveness on the\n * reconcile side. It still pays once on the sweep side: orphaned claims are\n * gated on process death alone, deliberately independent of `status.json`\n * (see `sweepOrphanedClaims`), and folding them into the status gate would\n * quietly stop recovering claims for a run that finished. The guarantee here\n * is exactly one probe per pass, not zero.\n *\n * In practice the parent's subscriber skips terminal runs before calling\n * this at all, so that case costs nothing in the hot path either.\n */\n reconcileAndSweep(runId: string): ReconcileSweepOutcome;\n /** Promise-based reconciliation for the recurring parent poll subscriber. */\n reconcileAndSweepAsync?(runId: string): Promise<ReconcileSweepOutcome>;\n}\n\nfunction statusPathFor(runId: string): string {\n return path.join(currentRunsDir(), runId, STATUS_FILE_NAME);\n}\n\nfunction errorCode(error: unknown): string | undefined {\n return typeof error === 'object' && error !== null && 'code' in error\n ? (error as NodeJS.ErrnoException).code\n : undefined;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n@injectable()\nexport class StaleRunReconciler implements IStaleRunReconciler {\n constructor(\n @inject(DOOM_TEAM_TYPES.ProcessTerminalInspector) private readonly processTerminal: IProcessTerminalInspector,\n @inject(DOOM_TEAM_TYPES.RunIdResolver) private readonly runIdResolver: IRunIdResolver,\n ) {}\n\n protected now(): number {\n return Date.now();\n }\n\n /**\n * A fresh writer per repair, deliberately never the container's singleton -\n * see the class doc for why sharing it would be unsafe. Protected so a test\n * can substitute a recording fake instead of touching disk.\n */\n protected createStatusWriter(): ICoalescedStatusWriter<ReconcilableStatus> {\n return new CoalescedStatusWriter<ReconcilableStatus>();\n }\n\n protected readFile(filePath: string): string | undefined {\n try {\n return fs.readFileSync(filePath, 'utf-8');\n } catch (error) {\n if (errorCode(error) === 'ENOENT') return undefined;\n throw error;\n }\n }\n\n protected readStatus(runId: string): ReconcilableStatus | undefined {\n const raw = this.readFile(statusPathFor(runId));\n if (raw === undefined) return undefined;\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n // A status file mid-write (CoalescedStatusWriter's rename not yet\n // visible, or a torn read) is not evidence of anything; retried later.\n return undefined;\n }\n return isRecord(parsed) ? (parsed as ReconcilableStatus) : undefined;\n }\n\n protected async readStatusAsync(runId: string): Promise<ReconcilableStatus | undefined> {\n let raw: string;\n try {\n raw = await fs.promises.readFile(statusPathFor(runId), 'utf-8');\n } catch (error) {\n if (errorCode(error) === 'ENOENT') return undefined;\n throw error;\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return undefined;\n }\n return isRecord(parsed) ? (parsed as ReconcilableStatus) : undefined;\n }\n\n /** Best-effort diagnostic trail; a failing event log must never fail a repair. */\n protected appendEvent(runId: string, payload: object): void {\n try {\n appendJsonl(path.join(currentRunsDir(), runId, EVENTS_FILE_NAME), JSON.stringify(payload));\n } catch {\n // See method doc.\n }\n }\n\n /**\n * Walks `dir` and every subdirectory beneath it (bounded by\n * `MAX_CLAIM_SCAN_DEPTH`), returning every entry whose name contains\n * `.claim-`, wherever it is found. Replaces a flat, single-directory\n * listing keyed off a registry: see the class doc's \"STRUCTURAL DISCOVERY\"\n * section for why. Recursing only on `entry.isDirectory()` means a symlink\n * (`isSymbolicLink()`, not `isDirectory()`) is never followed, so this\n * needs no separate cycle-detection.\n */\n protected async appendEventAsync(runId: string, payload: object): Promise<void> {\n try {\n await fs.promises.appendFile(\n path.join(currentRunsDir(), runId, EVENTS_FILE_NAME),\n `${JSON.stringify(payload)}\\n`,\n );\n } catch {\n // See method doc.\n }\n }\n\n protected walkForClaims(dir: string, depth = 0): Array<{ dir: string; name: string }> {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch (error) {\n if (errorCode(error) === 'ENOENT') return [];\n throw error;\n }\n\n const found: Array<{ dir: string; name: string }> = [];\n for (const entry of entries) {\n if (entry.name.includes(CLAIM_SUFFIX_PREFIX)) {\n found.push({ dir, name: entry.name });\n } else if (entry.isDirectory() && depth < MAX_CLAIM_SCAN_DEPTH) {\n found.push(...this.walkForClaims(path.join(dir, entry.name), depth + 1));\n }\n }\n return found;\n }\n\n protected async walkForClaimsAsync(dir: string, depth = 0): Promise<Array<{ dir: string; name: string }>> {\n let entries: fs.Dirent[];\n try {\n entries = await fs.promises.readdir(dir, { withFileTypes: true });\n } catch (error) {\n if (errorCode(error) === 'ENOENT') return [];\n throw error;\n }\n\n const found: Array<{ dir: string; name: string }> = [];\n for (const entry of entries) {\n if (entry.name.includes(CLAIM_SUFFIX_PREFIX)) {\n found.push({ dir, name: entry.name });\n } else if (entry.isDirectory() && depth < MAX_CLAIM_SCAN_DEPTH) {\n found.push(...(await this.walkForClaimsAsync(path.join(dir, entry.name), depth + 1)));\n }\n }\n return found;\n }\n\n /**\n * Put a claimed file back under its original queued name. Returns `true`\n * only when the rename actually happened. Mirrors `control-channel.ts`'s\n * own `release()`: on any failure (original name occupied - astronomically\n * unlikely, the directory gone), there is nothing left to preserve by\n * retrying, so the orphaned claim is dropped rather than left unreachable.\n */\n protected recoverClaim(claimedPath: string, originalPath: string): boolean {\n try {\n fs.renameSync(claimedPath, originalPath);\n return true;\n } catch {\n try {\n fs.unlinkSync(claimedPath);\n } catch {\n // Best effort; see method doc.\n }\n return false;\n }\n }\n\n protected async recoverClaimAsync(claimedPath: string, originalPath: string): Promise<boolean> {\n try {\n await fs.promises.rename(claimedPath, originalPath);\n return true;\n } catch {\n try {\n await fs.promises.unlink(claimedPath);\n } catch {\n // Best effort; see method doc.\n }\n return false;\n }\n }\n\n /**\n * A memoizing wrapper over `inspect()`, so one tick's pair of repairs asks\n * the OS about a pid once instead of twice. Each public entry point makes\n * its own, so a standalone `reconcile()` still inspects exactly when it\n * always did.\n */\n private inspectOnce(runId: string): () => ReturnType<IProcessTerminalInspector['inspect']> {\n let verdict: ReturnType<IProcessTerminalInspector['inspect']> | undefined;\n return () => (verdict ??= this.processTerminal.inspect(runId));\n }\n\n reconcile(runId: string): ReconcileOutcome {\n return this.reconcileWith(runId, this.inspectOnce(runId));\n }\n\n sweepOrphanedClaims(runId: string): SweepOutcome {\n return this.sweepWith(runId, this.inspectOnce(runId));\n }\n\n reconcileAndSweep(runId: string): ReconcileSweepOutcome {\n const inspect = this.inspectOnce(runId);\n // Order matters: `reconcileWith` runs its cheap file gates before ever\n // calling `inspect`, so a terminal or already-delivered run pays nothing\n // extra here. Whichever of the two asks first funds the single call.\n return { reconcile: this.reconcileWith(runId, inspect), sweep: this.sweepWith(runId, inspect) };\n }\n\n async reconcileAndSweepAsync(runId: string): Promise<ReconcileSweepOutcome> {\n let verdict: ProcessTerminalVerdict | undefined;\n const inspect = async (): Promise<ProcessTerminalVerdict> => {\n if (verdict) return verdict;\n verdict = this.processTerminal.inspectAsync\n ? await this.processTerminal.inspectAsync(runId)\n : this.processTerminal.inspect(runId);\n return verdict;\n };\n const reconcile = await this.reconcileWithAsync(runId, inspect);\n const sweep = await this.sweepWithAsync(runId, inspect);\n return { reconcile, sweep };\n }\n\n private reconcileWith(\n runId: string,\n inspect: () => ReturnType<IProcessTerminalInspector['inspect']>,\n ): ReconcileOutcome {\n const status = this.readStatus(runId);\n if (!status) return { repaired: false, reason: 'No status file for this run.' };\n\n const state = status.state;\n if (state !== undefined && TERMINAL_ASYNC_JOB_STATES.has(state)) {\n return { repaired: false, reason: `Run already terminal (${state}).` };\n }\n\n const location = this.runIdResolver.resolve(runId);\n if (location?.resultPath) {\n return { repaired: false, reason: \"A result already exists; delivery is ResultWatcher's job, not a repair.\" };\n }\n\n const verdict = inspect();\n if (verdict.state !== 'crashed') {\n return {\n repaired: false,\n reason: `Owning process liveness is '${verdict.state}', which does not prove death; leaving the run as is.`,\n };\n }\n\n return this.repair(runId, status, verdict.marker.pid);\n }\n\n private async reconcileWithAsync(\n runId: string,\n inspect: () => Promise<ProcessTerminalVerdict>,\n ): Promise<ReconcileOutcome> {\n const status = await this.readStatusAsync(runId);\n if (!status) return { repaired: false, reason: 'No status file for this run.' };\n\n const state = status.state;\n if (state !== undefined && TERMINAL_ASYNC_JOB_STATES.has(state)) {\n return { repaired: false, reason: `Run already terminal (${state}).` };\n }\n\n const location = this.runIdResolver.resolveAsync\n ? await this.runIdResolver.resolveAsync(runId)\n : this.runIdResolver.resolve(runId);\n if (location?.resultPath) {\n return { repaired: false, reason: \"A result already exists; delivery is ResultWatcher's job, not a repair.\" };\n }\n\n const verdict = await inspect();\n if (verdict.state !== 'crashed') {\n return {\n repaired: false,\n reason: `Owning process liveness is '${verdict.state}', which does not prove death; leaving the run as is.`,\n };\n }\n\n return this.repairAsync(runId, status, verdict.marker.pid);\n }\n\n private sweepWith(runId: string, inspect: () => ReturnType<IProcessTerminalInspector['inspect']>): SweepOutcome {\n const verdict = inspect();\n if (verdict.state !== 'crashed') return { recovered: [] };\n\n const runDir = path.join(currentRunsDir(), runId);\n const recovered: RecoveredClaim[] = [];\n for (const { dir, name: entry } of this.walkForClaims(runDir)) {\n const claimIndex = entry.lastIndexOf(CLAIM_SUFFIX_PREFIX);\n // Filtered by walkForClaims() already; defensive, not reachable.\n if (claimIndex === -1) continue;\n const fileName = entry.slice(0, claimIndex);\n const wasRecovered = this.recoverClaim(path.join(dir, entry), path.join(dir, fileName));\n if (wasRecovered) {\n const queue = path.relative(runDir, dir).split(path.sep).join('/');\n recovered.push({ queue, fileName });\n this.appendEvent(runId, {\n type: 'subagent.run.claim_recovered',\n ts: this.now(),\n runId,\n pid: verdict.marker.pid,\n queue,\n recovered: fileName,\n });\n }\n }\n return { recovered };\n }\n\n private async sweepWithAsync(runId: string, inspect: () => Promise<ProcessTerminalVerdict>): Promise<SweepOutcome> {\n const verdict = await inspect();\n if (verdict.state !== 'crashed') return { recovered: [] };\n\n const runDir = path.join(currentRunsDir(), runId);\n const recovered: RecoveredClaim[] = [];\n\n for (const { dir, name: entry } of await this.walkForClaimsAsync(runDir)) {\n const claimIndex = entry.lastIndexOf(CLAIM_SUFFIX_PREFIX);\n if (claimIndex === -1) continue;\n const fileName = entry.slice(0, claimIndex);\n const wasRecovered = await this.recoverClaimAsync(path.join(dir, entry), path.join(dir, fileName));\n if (wasRecovered) {\n const queue = path.relative(runDir, dir).split(path.sep).join('/');\n recovered.push({ queue, fileName });\n await this.appendEventAsync(runId, {\n type: 'subagent.run.claim_recovered',\n ts: this.now(),\n runId,\n pid: verdict.marker.pid,\n queue,\n recovered: fileName,\n });\n }\n }\n return { recovered };\n }\n\n private repair(runId: string, status: ReconcilableStatus, deadPid: number): ReconcileOutcome {\n const now = this.now();\n const message = `Runner process ${deadPid} exited without finalizing this run. Marked failed by stale-run reconciliation.`;\n\n const writer = this.createStatusWriter();\n writer.open(runId, status);\n writer.updateSync((current) => {\n current.state = 'failed';\n current.error = current.error ?? message;\n current.lastUpdate = now;\n current.endedAt = current.endedAt ?? now;\n });\n\n this.appendEvent(runId, { type: 'subagent.run.repaired_stale', ts: now, runId, pid: deadPid, message });\n\n return { repaired: true, reason: message };\n }\n\n private async repairAsync(runId: string, status: ReconcilableStatus, deadPid: number): Promise<ReconcileOutcome> {\n const now = this.now();\n const message = `Runner process ${deadPid} exited without finalizing this run. Marked failed by stale-run reconciliation.`;\n\n const writer = this.createStatusWriter();\n writer.open(runId, status);\n writer.updateSync((current) => {\n current.state = 'failed';\n current.error = current.error ?? message;\n current.lastUpdate = now;\n current.endedAt = current.endedAt ?? now;\n });\n\n await this.appendEventAsync(runId, {\n type: 'subagent.run.repaired_stale',\n ts: now,\n runId,\n pid: deadPid,\n message,\n });\n\n return { repaired: true, reason: message };\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ const e=require(`../_virtual/_rolldown/runtime.cjs`),t=require(`../shared/paths.cjs`),n=require(`../services/runs/background/statusWriter.cjs`);let r=require(`node:fs`);r=e.__toESM(r,1);let i=require(`node:path`);i=e.__toESM(i,1);function a(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function o(e,t=`<memory>`){let n;try{n=JSON.parse(e)}catch(e){return{kind:`malformed`,path:t,message:e instanceof Error?e.message:String(e)}}return!a(n)||typeof n.runId!=`string`||typeof n.agent!=`string`||typeof n.state!=`string`?{kind:`malformed`,path:t,message:`Expected runId, agent and state fields.`}:n.version!==void 0&&n.version!==1?{kind:`incompatible`,path:t,version:n.version}:(n.state===`complete`&&(n.state=`completed`),typeof n.reason==`string`&&n.attentionReason===void 0&&(n.attentionReason=n.reason),{kind:`ok`,path:t,status:n})}function s(e){let t;try{t=r.readFileSync(e,`utf8`)}catch(t){return t instanceof Error&&`code`in t&&t.code===`ENOENT`?{kind:`missing`,path:e}:{kind:`io_error`,path:e,message:t instanceof Error?t.message:String(t)}}return o(t,e)}async function c(e){let t;try{t=await r.promises.readFile(e,`utf8`)}catch(t){return t instanceof Error&&`code`in t&&t.code===`ENOENT`?{kind:`missing`,path:e}:{kind:`io_error`,path:e,message:t instanceof Error?t.message:String(t)}}return o(t,e)}function l(e){return s(i.join(t.currentRunsDir(),e,n.STATUS_FILE_NAME))}function u(e){let t=l(e);return t.kind===`ok`?t.status:void 0}exports.parseAsyncRunStatus=o,exports.readAsyncRunStatus=u,exports.readAsyncRunStatusResult=l,exports.readAsyncRunStatusResultAt=s,exports.readAsyncRunStatusResultAtAsync=c;
2
+ //# sourceMappingURL=statusReader.cjs.map