@namzu/cli 26.3.0 → 28.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (376) hide show
  1. package/CHANGELOG.md +271 -0
  2. package/README.md +67 -46
  3. package/dist/bin.js +7 -0
  4. package/dist/bin.js.map +1 -1
  5. package/dist/checkpoints/store.d.ts +17 -4
  6. package/dist/checkpoints/store.d.ts.map +1 -1
  7. package/dist/checkpoints/store.js +20 -5
  8. package/dist/checkpoints/store.js.map +1 -1
  9. package/dist/cli.d.ts +2 -0
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +124 -14
  12. package/dist/cli.js.map +1 -1
  13. package/dist/commands/acp.d.ts +23 -1
  14. package/dist/commands/acp.d.ts.map +1 -1
  15. package/dist/commands/acp.js +41 -2
  16. package/dist/commands/acp.js.map +1 -1
  17. package/dist/commands/drain.d.ts +69 -2
  18. package/dist/commands/drain.d.ts.map +1 -1
  19. package/dist/commands/drain.js +216 -136
  20. package/dist/commands/drain.js.map +1 -1
  21. package/dist/commands/eval.js +1 -1
  22. package/dist/commands/eval.js.map +1 -1
  23. package/dist/commands/{run-flags.d.ts → exec-flags.d.ts} +36 -28
  24. package/dist/commands/exec-flags.d.ts.map +1 -0
  25. package/dist/commands/{run-flags.js → exec-flags.js} +35 -27
  26. package/dist/commands/exec-flags.js.map +1 -0
  27. package/dist/commands/exec-json.d.ts +75 -0
  28. package/dist/commands/exec-json.d.ts.map +1 -0
  29. package/dist/commands/exec-json.js +473 -0
  30. package/dist/commands/exec-json.js.map +1 -0
  31. package/dist/commands/exec.d.ts +42 -0
  32. package/dist/commands/exec.d.ts.map +1 -0
  33. package/dist/commands/exec.js +683 -0
  34. package/dist/commands/exec.js.map +1 -0
  35. package/dist/commands/host-queries.d.ts +12 -0
  36. package/dist/commands/host-queries.d.ts.map +1 -0
  37. package/dist/commands/host-queries.js +149 -0
  38. package/dist/commands/host-queries.js.map +1 -0
  39. package/dist/commands/prior-messages.js +1 -1
  40. package/dist/commands/prior-messages.js.map +1 -1
  41. package/dist/commands/provider-wait.d.ts +5 -5
  42. package/dist/commands/provider-wait.d.ts.map +1 -1
  43. package/dist/commands/provider-wait.js +2 -2
  44. package/dist/commands/registry.d.ts.map +1 -1
  45. package/dist/commands/registry.js +2 -0
  46. package/dist/commands/registry.js.map +1 -1
  47. package/dist/commands/resident-flags.d.ts +2 -2
  48. package/dist/commands/resident-flags.d.ts.map +1 -1
  49. package/dist/commands/resident-flags.js +4 -2
  50. package/dist/commands/resident-flags.js.map +1 -1
  51. package/dist/commands/resident-learning.js +1 -1
  52. package/dist/commands/resident-learning.js.map +1 -1
  53. package/dist/commands/resident.d.ts.map +1 -1
  54. package/dist/commands/resident.js +3 -2
  55. package/dist/commands/resident.js.map +1 -1
  56. package/dist/commands/serve.js +2 -2
  57. package/dist/commands/serve.js.map +1 -1
  58. package/dist/commands/skills.js +1 -1
  59. package/dist/commands/skills.js.map +1 -1
  60. package/dist/commands/state.js +8 -8
  61. package/dist/commands/state.js.map +1 -1
  62. package/dist/commands/stdin.d.ts +5 -5
  63. package/dist/commands/stdin.js +5 -5
  64. package/dist/commands/types.d.ts +2 -0
  65. package/dist/commands/types.d.ts.map +1 -1
  66. package/dist/config/load.d.ts +1 -1
  67. package/dist/config/load.d.ts.map +1 -1
  68. package/dist/config/load.js +36 -5
  69. package/dist/config/load.js.map +1 -1
  70. package/dist/config/schema.d.ts +59 -20
  71. package/dist/config/schema.d.ts.map +1 -1
  72. package/dist/config/schema.js.map +1 -1
  73. package/dist/config/tool-result-screens.js +1 -1
  74. package/dist/config/turn-guards.d.ts +23 -0
  75. package/dist/config/turn-guards.d.ts.map +1 -0
  76. package/dist/config/turn-guards.js +46 -0
  77. package/dist/config/turn-guards.js.map +1 -0
  78. package/dist/context/directories.d.ts +37 -0
  79. package/dist/context/directories.d.ts.map +1 -0
  80. package/dist/context/directories.js +69 -0
  81. package/dist/context/directories.js.map +1 -0
  82. package/dist/context/environment.d.ts +55 -0
  83. package/dist/context/environment.d.ts.map +1 -1
  84. package/dist/context/environment.js +96 -0
  85. package/dist/context/environment.js.map +1 -1
  86. package/dist/context/project-tracker.d.ts +3 -3
  87. package/dist/context/project-tracker.d.ts.map +1 -1
  88. package/dist/context/project-tracker.js +3 -3
  89. package/dist/context/project-tracker.js.map +1 -1
  90. package/dist/context/project.d.ts +2 -2
  91. package/dist/context/project.js +2 -2
  92. package/dist/context/sandbox.d.ts +22 -3
  93. package/dist/context/sandbox.d.ts.map +1 -1
  94. package/dist/context/sandbox.js +42 -4
  95. package/dist/context/sandbox.js.map +1 -1
  96. package/dist/doctor/checks/chain.d.ts +1 -1
  97. package/dist/doctor/checks/chain.js +7 -7
  98. package/dist/doctor/checks/chain.js.map +1 -1
  99. package/dist/doctor/checks/invariants.d.ts +2 -2
  100. package/dist/doctor/checks/invariants.js +5 -5
  101. package/dist/doctor/checks/invariants.js.map +1 -1
  102. package/dist/doctor/checks/logging.js +1 -1
  103. package/dist/doctor/checks/logging.js.map +1 -1
  104. package/dist/doctor/checks/sandbox.js +1 -1
  105. package/dist/doctor/checks/sandbox.js.map +1 -1
  106. package/dist/doctor/checks/session-export.d.ts +1 -1
  107. package/dist/doctor/checks/session-export.js +2 -2
  108. package/dist/doctor/checks/session-export.js.map +1 -1
  109. package/dist/doctor/checks/telemetry.d.ts +1 -1
  110. package/dist/doctor/checks/telemetry.js +1 -1
  111. package/dist/doctor/registry.js +2 -2
  112. package/dist/doctor/registry.js.map +1 -1
  113. package/dist/exit-codes.d.ts +2 -2
  114. package/dist/exit-codes.js +2 -2
  115. package/dist/integrations/mcp/servers.d.ts +1 -1
  116. package/dist/integrations/providers/access.d.ts +4 -1
  117. package/dist/integrations/providers/access.d.ts.map +1 -1
  118. package/dist/integrations/providers/access.js +6 -3
  119. package/dist/integrations/providers/access.js.map +1 -1
  120. package/dist/integrations/providers/chain-capabilities.d.ts +6 -6
  121. package/dist/integrations/providers/chain-capabilities.js +8 -8
  122. package/dist/integrations/providers/chain-capabilities.js.map +1 -1
  123. package/dist/integrations/providers/register.d.ts +1 -1
  124. package/dist/integrations/providers/register.js +1 -1
  125. package/dist/integrations/providers/registry.d.ts +1 -1
  126. package/dist/integrations/providers/zen-catalogue.d.ts +100 -0
  127. package/dist/integrations/providers/zen-catalogue.d.ts.map +1 -0
  128. package/dist/integrations/providers/zen-catalogue.js +196 -0
  129. package/dist/integrations/providers/zen-catalogue.js.map +1 -0
  130. package/dist/integrations/resident/inspection.d.ts +4 -1
  131. package/dist/integrations/resident/inspection.d.ts.map +1 -1
  132. package/dist/integrations/resident/inspection.js +53 -34
  133. package/dist/integrations/resident/inspection.js.map +1 -1
  134. package/dist/integrations/resident/learning-storage.d.ts +7 -2
  135. package/dist/integrations/resident/learning-storage.d.ts.map +1 -1
  136. package/dist/integrations/resident/learning-storage.js +12 -8
  137. package/dist/integrations/resident/learning-storage.js.map +1 -1
  138. package/dist/integrations/resident/runner-launch.d.ts +3 -3
  139. package/dist/integrations/resident/runner-launch.d.ts.map +1 -1
  140. package/dist/integrations/resident/runner-store.d.ts +1 -1
  141. package/dist/integrations/resident/runner-store.d.ts.map +1 -1
  142. package/dist/integrations/resident/runner-store.js +5 -2
  143. package/dist/integrations/resident/runner-store.js.map +1 -1
  144. package/dist/integrations/resident/runner-worker.js +23 -0
  145. package/dist/integrations/resident/runner-worker.js.map +1 -1
  146. package/dist/integrations/resident/session-log-reads.d.ts +37 -0
  147. package/dist/integrations/resident/session-log-reads.d.ts.map +1 -0
  148. package/dist/integrations/resident/session-log-reads.js +125 -0
  149. package/dist/integrations/resident/session-log-reads.js.map +1 -0
  150. package/dist/integrations/resident/session-step.d.ts +4 -2
  151. package/dist/integrations/resident/session-step.d.ts.map +1 -1
  152. package/dist/integrations/resident/session-step.js +16 -14
  153. package/dist/integrations/resident/session-step.js.map +1 -1
  154. package/dist/integrations/resident/storage.d.ts +23 -1
  155. package/dist/integrations/resident/storage.d.ts.map +1 -1
  156. package/dist/integrations/resident/storage.js +60 -17
  157. package/dist/integrations/resident/storage.js.map +1 -1
  158. package/dist/integrations/resident/tool-evidence.d.ts +4 -2
  159. package/dist/integrations/resident/tool-evidence.d.ts.map +1 -1
  160. package/dist/integrations/resident/tool-evidence.js +36 -30
  161. package/dist/integrations/resident/tool-evidence.js.map +1 -1
  162. package/dist/integrations/resident/verification.d.ts +2 -2
  163. package/dist/integrations/resident/verification.d.ts.map +1 -1
  164. package/dist/integrations/resident/verification.js +4 -4
  165. package/dist/integrations/resident/verification.js.map +1 -1
  166. package/dist/integrations/sessions/checkpoint-view.d.ts +13 -0
  167. package/dist/integrations/sessions/checkpoint-view.d.ts.map +1 -0
  168. package/dist/integrations/sessions/checkpoint-view.js +48 -0
  169. package/dist/integrations/sessions/checkpoint-view.js.map +1 -0
  170. package/dist/integrations/sessions/context-inventory.js +1 -1
  171. package/dist/integrations/sessions/context-inventory.js.map +1 -1
  172. package/dist/integrations/sessions/conversation-search.d.ts +16 -20
  173. package/dist/integrations/sessions/conversation-search.d.ts.map +1 -1
  174. package/dist/integrations/sessions/conversation-search.js +305 -731
  175. package/dist/integrations/sessions/conversation-search.js.map +1 -1
  176. package/dist/integrations/sessions/evidence-page-validation.d.ts +4 -4
  177. package/dist/integrations/sessions/evidence-page-validation.d.ts.map +1 -1
  178. package/dist/integrations/sessions/evidence-page-validation.js +1 -1
  179. package/dist/integrations/sessions/evidence-page-validation.js.map +1 -1
  180. package/dist/integrations/sessions/evidence-recall.d.ts +40 -2
  181. package/dist/integrations/sessions/evidence-recall.d.ts.map +1 -1
  182. package/dist/integrations/sessions/evidence-recall.js +41 -97
  183. package/dist/integrations/sessions/evidence-recall.js.map +1 -1
  184. package/dist/integrations/sessions/store.d.ts +153 -85
  185. package/dist/integrations/sessions/store.d.ts.map +1 -1
  186. package/dist/integrations/sessions/store.js +414 -484
  187. package/dist/integrations/sessions/store.js.map +1 -1
  188. package/dist/integrations/sessions/task-context.d.ts +15 -2
  189. package/dist/integrations/sessions/task-context.d.ts.map +1 -1
  190. package/dist/integrations/sessions/task-context.js +34 -11
  191. package/dist/integrations/sessions/task-context.js.map +1 -1
  192. package/dist/integrations/sessions/transcript-export.d.ts +13 -6
  193. package/dist/integrations/sessions/transcript-export.d.ts.map +1 -1
  194. package/dist/integrations/sessions/transcript-export.js +148 -263
  195. package/dist/integrations/sessions/transcript-export.js.map +1 -1
  196. package/dist/integrations/state/home.d.ts +4 -16
  197. package/dist/integrations/state/home.d.ts.map +1 -1
  198. package/dist/integrations/state/home.js +6 -42
  199. package/dist/integrations/state/home.js.map +1 -1
  200. package/dist/integrations/state/project.d.ts +6 -7
  201. package/dist/integrations/state/project.d.ts.map +1 -1
  202. package/dist/integrations/state/project.js +7 -9
  203. package/dist/integrations/state/project.js.map +1 -1
  204. package/dist/integrations/state/report.d.ts +27 -41
  205. package/dist/integrations/state/report.d.ts.map +1 -1
  206. package/dist/integrations/state/report.js +172 -464
  207. package/dist/integrations/state/report.js.map +1 -1
  208. package/dist/integrations/state/retention.d.ts +18 -0
  209. package/dist/integrations/state/retention.d.ts.map +1 -0
  210. package/dist/integrations/state/retention.js +18 -0
  211. package/dist/integrations/state/retention.js.map +1 -0
  212. package/dist/integrations/subagents/activity.d.ts +36 -31
  213. package/dist/integrations/subagents/activity.d.ts.map +1 -1
  214. package/dist/integrations/subagents/activity.js +27 -23
  215. package/dist/integrations/subagents/activity.js.map +1 -1
  216. package/dist/integrations/subagents/batches.d.ts +91 -0
  217. package/dist/integrations/subagents/batches.d.ts.map +1 -0
  218. package/dist/integrations/subagents/batches.js +172 -0
  219. package/dist/integrations/subagents/batches.js.map +1 -0
  220. package/dist/integrations/subagents/parent.d.ts +26 -11
  221. package/dist/integrations/subagents/parent.d.ts.map +1 -1
  222. package/dist/integrations/subagents/parent.js +13 -28
  223. package/dist/integrations/subagents/parent.js.map +1 -1
  224. package/dist/integrations/subagents/policy.d.ts +2 -2
  225. package/dist/integrations/subagents/policy.d.ts.map +1 -1
  226. package/dist/integrations/subagents/policy.js +2 -2
  227. package/dist/integrations/subagents/policy.js.map +1 -1
  228. package/dist/integrations/subagents/replay.d.ts +91 -26
  229. package/dist/integrations/subagents/replay.d.ts.map +1 -1
  230. package/dist/integrations/subagents/replay.js +216 -114
  231. package/dist/integrations/subagents/replay.js.map +1 -1
  232. package/dist/integrations/subagents/runtime.d.ts +50 -26
  233. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  234. package/dist/integrations/subagents/runtime.js +132 -126
  235. package/dist/integrations/subagents/runtime.js.map +1 -1
  236. package/dist/integrations/subagents/saved-agents.d.ts +57 -0
  237. package/dist/integrations/subagents/saved-agents.d.ts.map +1 -0
  238. package/dist/integrations/subagents/saved-agents.js +102 -0
  239. package/dist/integrations/subagents/saved-agents.js.map +1 -0
  240. package/dist/integrations/subagents/slash.d.ts +55 -0
  241. package/dist/integrations/subagents/slash.d.ts.map +1 -0
  242. package/dist/integrations/subagents/slash.js +45 -0
  243. package/dist/integrations/subagents/slash.js.map +1 -0
  244. package/dist/integrations/telemetry/session-export.d.ts +6 -6
  245. package/dist/integrations/telemetry/session-export.d.ts.map +1 -1
  246. package/dist/integrations/telemetry/session-export.js +2 -2
  247. package/dist/integrations/web/search.d.ts +1 -1
  248. package/dist/logging.d.ts +5 -5
  249. package/dist/logging.js +6 -6
  250. package/dist/memory/presentation.d.ts +11 -0
  251. package/dist/memory/presentation.d.ts.map +1 -1
  252. package/dist/memory/presentation.js +25 -0
  253. package/dist/memory/presentation.js.map +1 -1
  254. package/dist/memory/store.d.ts +13 -7
  255. package/dist/memory/store.d.ts.map +1 -1
  256. package/dist/memory/store.js +37 -11
  257. package/dist/memory/store.js.map +1 -1
  258. package/dist/memory/typed.d.ts +116 -0
  259. package/dist/memory/typed.d.ts.map +1 -0
  260. package/dist/memory/typed.js +336 -0
  261. package/dist/memory/typed.js.map +1 -0
  262. package/dist/permissions/checks.d.ts +1 -1
  263. package/dist/permissions/checks.js +1 -1
  264. package/dist/permissions/headless-trust.d.ts +4 -4
  265. package/dist/permissions/headless-trust.d.ts.map +1 -1
  266. package/dist/permissions/headless-trust.js +6 -6
  267. package/dist/permissions/headless-trust.js.map +1 -1
  268. package/dist/permissions/mode.d.ts +4 -4
  269. package/dist/permissions/mode.js +1 -1
  270. package/dist/permissions/rules.d.ts +1 -1
  271. package/dist/permissions/rules.js +1 -1
  272. package/dist/termination.d.ts +75 -0
  273. package/dist/termination.d.ts.map +1 -0
  274. package/dist/termination.js +143 -0
  275. package/dist/termination.js.map +1 -0
  276. package/dist/tui/AgentExplorer.d.ts +2 -2
  277. package/dist/tui/AgentExplorer.js +4 -4
  278. package/dist/tui/AgentExplorer.js.map +1 -1
  279. package/dist/tui/App.d.ts +9 -1
  280. package/dist/tui/App.d.ts.map +1 -1
  281. package/dist/tui/App.js +456 -307
  282. package/dist/tui/App.js.map +1 -1
  283. package/dist/tui/LiveActivity.d.ts +1 -1
  284. package/dist/tui/LiveActivity.d.ts.map +1 -1
  285. package/dist/tui/PermissionOverlay.d.ts +8 -0
  286. package/dist/tui/PermissionOverlay.d.ts.map +1 -1
  287. package/dist/tui/PermissionOverlay.js +25 -2
  288. package/dist/tui/PermissionOverlay.js.map +1 -1
  289. package/dist/tui/Picker.js +1 -1
  290. package/dist/tui/agent.d.ts +211 -135
  291. package/dist/tui/agent.d.ts.map +1 -1
  292. package/dist/tui/agent.js +535 -316
  293. package/dist/tui/agent.js.map +1 -1
  294. package/dist/tui/conversation-evidence-view.js +3 -3
  295. package/dist/tui/conversation-evidence-view.js.map +1 -1
  296. package/dist/tui/conversation-history.d.ts +4 -21
  297. package/dist/tui/conversation-history.d.ts.map +1 -1
  298. package/dist/tui/conversation-history.js +4 -28
  299. package/dist/tui/conversation-history.js.map +1 -1
  300. package/dist/tui/index.d.ts.map +1 -1
  301. package/dist/tui/index.js +27 -1
  302. package/dist/tui/index.js.map +1 -1
  303. package/dist/tui/log-pane.js +1 -1
  304. package/dist/tui/markdownParser.d.ts +1 -1
  305. package/dist/tui/markdownParser.js +1 -1
  306. package/dist/tui/permission-review.d.ts +16 -0
  307. package/dist/tui/permission-review.d.ts.map +1 -1
  308. package/dist/tui/permission-review.js +25 -2
  309. package/dist/tui/permission-review.js.map +1 -1
  310. package/dist/tui/slashCommands.d.ts +30 -41
  311. package/dist/tui/slashCommands.d.ts.map +1 -1
  312. package/dist/tui/slashCommands.js +75 -66
  313. package/dist/tui/slashCommands.js.map +1 -1
  314. package/dist/tui/status-panel-layout.js +1 -1
  315. package/dist/tui/status-panel-layout.js.map +1 -1
  316. package/dist/tui/{run-interruption.d.ts → turn-interruption.d.ts} +4 -4
  317. package/dist/tui/turn-interruption.d.ts.map +1 -0
  318. package/dist/tui/{run-interruption.js → turn-interruption.js} +20 -20
  319. package/dist/tui/turn-interruption.js.map +1 -0
  320. package/dist/tui/turn-limits-settings.d.ts +39 -0
  321. package/dist/tui/turn-limits-settings.d.ts.map +1 -0
  322. package/dist/tui/{run-limits-settings.js → turn-limits-settings.js} +18 -18
  323. package/dist/tui/turn-limits-settings.js.map +1 -0
  324. package/dist/tui/types.d.ts +3 -3
  325. package/dist/tui/types.d.ts.map +1 -1
  326. package/dist/user-commands/store.d.ts +4 -4
  327. package/dist/user-commands/store.js +5 -5
  328. package/dist/user-commands/store.js.map +1 -1
  329. package/package.json +6 -6
  330. package/dist/commands/run-flags.d.ts.map +0 -1
  331. package/dist/commands/run-flags.js.map +0 -1
  332. package/dist/commands/run-stream.d.ts +0 -65
  333. package/dist/commands/run-stream.d.ts.map +0 -1
  334. package/dist/commands/run-stream.js +0 -603
  335. package/dist/commands/run-stream.js.map +0 -1
  336. package/dist/commands/run.d.ts +0 -39
  337. package/dist/commands/run.d.ts.map +0 -1
  338. package/dist/commands/run.js +0 -533
  339. package/dist/commands/run.js.map +0 -1
  340. package/dist/config/run-limits.d.ts +0 -11
  341. package/dist/config/run-limits.d.ts.map +0 -1
  342. package/dist/config/run-limits.js +0 -64
  343. package/dist/config/run-limits.js.map +0 -1
  344. package/dist/integrations/sessions/compaction-evidence.d.ts +0 -5
  345. package/dist/integrations/sessions/compaction-evidence.d.ts.map +0 -1
  346. package/dist/integrations/sessions/compaction-evidence.js +0 -59
  347. package/dist/integrations/sessions/compaction-evidence.js.map +0 -1
  348. package/dist/integrations/sessions/database.d.ts +0 -5
  349. package/dist/integrations/sessions/database.d.ts.map +0 -1
  350. package/dist/integrations/sessions/database.js +0 -34
  351. package/dist/integrations/sessions/database.js.map +0 -1
  352. package/dist/integrations/sessions/paths.d.ts +0 -7
  353. package/dist/integrations/sessions/paths.d.ts.map +0 -1
  354. package/dist/integrations/sessions/paths.js +0 -14
  355. package/dist/integrations/sessions/paths.js.map +0 -1
  356. package/dist/integrations/sessions/run-discovery.d.ts +0 -24
  357. package/dist/integrations/sessions/run-discovery.d.ts.map +0 -1
  358. package/dist/integrations/sessions/run-discovery.js +0 -142
  359. package/dist/integrations/sessions/run-discovery.js.map +0 -1
  360. package/dist/integrations/sessions/turn-evidence.d.ts +0 -129
  361. package/dist/integrations/sessions/turn-evidence.d.ts.map +0 -1
  362. package/dist/integrations/sessions/turn-evidence.js +0 -517
  363. package/dist/integrations/sessions/turn-evidence.js.map +0 -1
  364. package/dist/integrations/subagents/history.d.ts +0 -26
  365. package/dist/integrations/subagents/history.d.ts.map +0 -1
  366. package/dist/integrations/subagents/history.js +0 -76
  367. package/dist/integrations/subagents/history.js.map +0 -1
  368. package/dist/integrations/subagents/runs.d.ts +0 -72
  369. package/dist/integrations/subagents/runs.d.ts.map +0 -1
  370. package/dist/integrations/subagents/runs.js +0 -155
  371. package/dist/integrations/subagents/runs.js.map +0 -1
  372. package/dist/tui/run-interruption.d.ts.map +0 -1
  373. package/dist/tui/run-interruption.js.map +0 -1
  374. package/dist/tui/run-limits-settings.d.ts +0 -39
  375. package/dist/tui/run-limits-settings.d.ts.map +0 -1
  376. package/dist/tui/run-limits-settings.js.map +0 -1
@@ -1,4 +1,23 @@
1
- import type { ProjectId, SessionId, TenantId } from '@namzu/sdk';
1
+ /**
2
+ * `namzu drain` — one pass over a session's parked turn.
3
+ *
4
+ * namzu can park a turn in one process and continue it in another: the turn
5
+ * stops with `turn_paused`, a decision it waits on is a `decision_requested`
6
+ * record, and whoever works on a session holds its `lease.json`. This is the
7
+ * caller that picks such a turn up. It finds the session's active turn, takes
8
+ * the session's lease, continues the turn under that lease, and gives the
9
+ * lease back.
10
+ *
11
+ * **It is not a daemon, and `namzu serve` still says namzu has no daemon.**
12
+ * That refusal is not weakened by this command, it is the reason for its
13
+ * shape: one bounded pass, exit code says what happened, and whatever the
14
+ * operator already uses to run things periodically runs it again. A turn is
15
+ * work a process does; a drain is a process that picks up work somebody
16
+ * else's machine dropped.
17
+ *
18
+ * A session has at most one active turn, so one pass continues at most one.
19
+ */
20
+ import { type ProjectId, type ResumeOutcome, type SessionId, type SessionLease, type SessionLog, type TenantId, type TurnId } from '@namzu/sdk';
2
21
  import type { CommandDef } from './types.js';
3
22
  export interface DrainFlags {
4
23
  store: string | null;
@@ -22,7 +41,7 @@ export interface DrainFlags {
22
41
  * would act on a scope the operator did not name.
23
42
  */
24
43
  export declare function parseDrainFlags(rawArgs: readonly string[]): DrainFlags;
25
- /** The scope refusal, or the scope. Contiguous prefix — see the store contract. */
44
+ /** The scope refusal, or the scope: the full tenant → project → session prefix. */
26
45
  export declare function resolveDrainScope(flags: Pick<DrainFlags, 'tenant' | 'project' | 'session'>): {
27
46
  readonly error: string;
28
47
  } | {
@@ -32,5 +51,53 @@ export declare function resolveDrainScope(flags: Pick<DrainFlags, 'tenant' | 'pr
32
51
  };
33
52
  /** A per-process holder when the operator names none. */
34
53
  export declare function defaultHolder(): string;
54
+ /** What one pass over a session did with its active turn. */
55
+ export interface DrainPassResult {
56
+ /** Turns the pass found to work on: the session's active turn, if any. */
57
+ readonly listed: number;
58
+ readonly resumed: readonly TurnId[];
59
+ /** Parked on a human decision: reported, never resumed past. */
60
+ readonly awaitingDecision: readonly TurnId[];
61
+ /** Nothing to continue from. */
62
+ readonly noCheckpoint: readonly TurnId[];
63
+ /** Another process holds the session's lease. */
64
+ readonly heldByOthers: readonly TurnId[];
65
+ /** Settled or replaced between the listing and the claim. */
66
+ readonly alreadyHandled: readonly TurnId[];
67
+ readonly failed: readonly {
68
+ readonly turnId: TurnId;
69
+ readonly error: string;
70
+ }[];
71
+ /** The work landed but the lease could not be given back. */
72
+ readonly unreleased: readonly {
73
+ readonly turnId: TurnId;
74
+ readonly error: string;
75
+ }[];
76
+ }
77
+ /** What {@link drainSessionPass} composes. Each is one SDK call in the command. */
78
+ export interface DrainPassDeps {
79
+ /** The session's log: where its active turn is read from. */
80
+ readonly log: Pick<SessionLog, 'activeTurn'>;
81
+ /** Turns with an unresolved `decision_requested` (`SessionIndex.listPendingDecisions`). */
82
+ readonly pendingDecisionTurns: ReadonlySet<TurnId>;
83
+ /** `claimSession`: the lease, or `null` when another process holds it. */
84
+ claim(): Promise<SessionLease | null>;
85
+ /** `releaseSession`, in a `finally`. */
86
+ release(lease: SessionLease): Promise<void>;
87
+ /** Continue the turn under the lease (`resumeSession` with this host's half attached). */
88
+ resume(turnId: TurnId, lease: SessionLease): Promise<ResumeOutcome>;
89
+ readonly info: (message: string) => void;
90
+ }
91
+ /**
92
+ * One pass: find the session's active turn, report it if a human owes it an
93
+ * answer, otherwise take the session's lease, check the turn is still the
94
+ * active one under that lease, continue it, and give the lease back.
95
+ *
96
+ * A turn waiting on a decision is reported before any claim. The answer
97
+ * belongs to a person; a drainer that continued without it would discard
98
+ * the question the turn stopped to ask, and claiming the session to find
99
+ * that out would only block the person's own resolver.
100
+ */
101
+ export declare function drainSessionPass(deps: DrainPassDeps): Promise<DrainPassResult>;
35
102
  export declare const drainCommand: CommandDef;
36
103
  //# sourceMappingURL=drain.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"drain.d.ts","sourceRoot":"","sources":["../../src/commands/drain.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAmB,SAAS,EAAW,SAAS,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAU1F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAK5C,MAAM,WAAW,UAAU;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,KAAK,EAAE,OAAO,CAAA;IACd,OAAO,EAAE,MAAM,EAAE,CAAA;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,CAgEtE;AAED,mFAAmF;AACnF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,QAAQ,GAAG,SAAS,GAAG,SAAS,CAAC,GACxF;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC1B;IAGA,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;IAC3B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAA;IAC7B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAA;CAC5B,CAoCH;AAED,yDAAyD;AACzD,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAkBD,eAAO,MAAM,YAAY,EAAE,UAyQ1B,CAAA"}
1
+ {"version":3,"file":"drain.d.ts","sourceRoot":"","sources":["../../src/commands/drain.ts"],"names":[],"mappings":"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"}
@@ -1,29 +1,36 @@
1
- import { sessionStore } from '../integrations/sessions/database.js';
2
1
  /**
3
- * `namzu drain` — one pass over a queue of durable runs.
2
+ * `namzu drain` — one pass over a session's parked turn.
4
3
  *
5
- * namzu could park a run in one process and continue it in another, and no
6
- * shipped surface ever did: `claimRun`, `listDurableRuns` and `resumeRun`
7
- * all existed, and the only caller of any of them was a test. This is the
8
- * caller. It lists the runs nobody holds under a scope, takes each one,
9
- * continues it under that claim, and gives it back.
4
+ * namzu can park a turn in one process and continue it in another: the turn
5
+ * stops with `turn_paused`, a decision it waits on is a `decision_requested`
6
+ * record, and whoever works on a session holds its `lease.json`. This is the
7
+ * caller that picks such a turn up. It finds the session's active turn, takes
8
+ * the session's lease, continues the turn under that lease, and gives the
9
+ * lease back.
10
10
  *
11
11
  * **It is not a daemon, and `namzu serve` still says namzu has no daemon.**
12
12
  * That refusal is not weakened by this command, it is the reason for its
13
13
  * shape: one bounded pass, exit code says what happened, and whatever the
14
- * operator already uses to run things periodically runs it again. A run is
15
- * a process; a drain is a process that picks up processes somebody else's
16
- * machine dropped.
14
+ * operator already uses to run things periodically runs it again. A turn is
15
+ * work a process does; a drain is a process that picks up work somebody
16
+ * else's machine dropped.
17
17
  *
18
+ * A session has at most one active turn, so one pass continues at most one.
18
19
  */
19
- import { DiskCheckpointStore, InvalidIdError, asProjectId, asSessionId, asTenantId, drainRuns, } from '@namzu/sdk';
20
+ import { existsSync, lstatSync } from 'node:fs';
21
+ import { join, resolve } from 'node:path';
22
+ import { DiskSessionLog, InvalidIdError, SessionPaths, asProjectId, asSessionId, asTenantId, claimSession, openSessionIndex, releaseSession, } from '@namzu/sdk';
20
23
  import { resolveTrustedProjectContext } from '../config/trusted-project-context.js';
21
- import { EXIT_UNTRUSTED, EXIT_USAGE } from '../exit-codes.js';
22
- import { resolveNamzuHome } from '../integrations/state/home.js';
24
+ import { EXIT_FAIL, EXIT_UNTRUSTED, EXIT_USAGE } from '../exit-codes.js';
25
+ import { readSessionStart } from '../integrations/resident/session-log-reads.js';
23
26
  import { contextLogging, createStderrSink, installCliLogging } from '../logging.js';
24
27
  import { decideHeadlessTrust } from '../permissions/headless-trust.js';
25
28
  import { compilePermissions } from '../permissions/rules.js';
26
- import { applyProviderFlags, resolveWorkingDirectory } from './run-flags.js';
29
+ import { applyProviderFlags, resolveWorkingDirectory } from './exec-flags.js';
30
+ /** The drain flags name a scope the store does not hold: an argument error, exit 64. */
31
+ class DrainScopeError extends Error {
32
+ name = 'DrainScopeError';
33
+ }
27
34
  /** Lease length when the operator names none. Long enough for a real turn. */
28
35
  const DEFAULT_TTL_MS = 600_000;
29
36
  /**
@@ -94,25 +101,25 @@ export function parseDrainFlags(rawArgs) {
94
101
  }
95
102
  // Anything unrecognised is refused rather than ignored, positional
96
103
  // arguments included — this command takes none. It acts on other
97
- // people's runs; a typo'd `--tenant` that fell through would drain a
104
+ // people's turns; a typo'd `--tenant` that fell through would drain a
98
105
  // scope nobody asked for.
99
106
  out.unknown.push(a.split('=')[0]);
100
107
  }
101
108
  return out;
102
109
  }
103
- /** The scope refusal, or the scope. Contiguous prefix — see the store contract. */
110
+ /** The scope refusal, or the scope: the full tenant → project → session prefix. */
104
111
  export function resolveDrainScope(flags) {
105
112
  if (!flags.tenant) {
106
113
  return {
107
- error: '--tenant is required. A run listing with no tenant is a cross-tenant read with a friendly name, so there is no default to fall back to.',
114
+ error: '--tenant is required. A drain with no tenant is a cross-tenant read with a friendly name, so there is no default to fall back to.',
108
115
  };
109
116
  }
110
- // The disk store keys checkpoints by directory and needs the project and
111
- // session to attribute what it finds, so this command asks for the full
112
- // prefix rather than the contract's minimum.
117
+ // A parked turn belongs to one session, and the session to one project;
118
+ // the pass checks both against the session's own log before it claims
119
+ // anything, so it asks for the full prefix.
113
120
  if (!flags.project || !flags.session) {
114
121
  return {
115
- error: '--project and --session are both required for a disk store: its layout carries no attribution, so it cannot say which project and session the runs it finds belong to.',
122
+ error: '--project and --session are both required: a turn is drained within one session, and the pass checks that the session belongs to that project before it claims anything.',
116
123
  };
117
124
  }
118
125
  // Prefix-checked, and refused in THIS function's shape rather than by
@@ -120,7 +127,7 @@ export function resolveDrainScope(flags) {
120
127
  // sentence; an `InvalidIdError` escaping to the top level would be the one
121
128
  // that arrives as a stack trace. A typo'd `--tenant prj_x` previously
122
129
  // reached the store as a TenantId and listed nothing, which reads as "no
123
- // runs" rather than "wrong flag".
130
+ // turns" rather than "wrong flag".
124
131
  try {
125
132
  return {
126
133
  tenantId: asTenantId(flags.tenant),
@@ -141,9 +148,86 @@ export function resolveDrainScope(flags) {
141
148
  export function defaultHolder() {
142
149
  return `namzu-drain-${process.pid}-${Date.now().toString(36)}`;
143
150
  }
144
- function describe(entry) {
145
- const park = entry.park ? `${entry.park.state} ${entry.park.requestType}` : 'not parked';
146
- return `${entry.runId} · ${entry.checkpointCount} checkpoints · ${park}`;
151
+ function errorText(error) {
152
+ return error instanceof Error ? error.message : String(error);
153
+ }
154
+ /**
155
+ * One pass: find the session's active turn, report it if a human owes it an
156
+ * answer, otherwise take the session's lease, check the turn is still the
157
+ * active one under that lease, continue it, and give the lease back.
158
+ *
159
+ * A turn waiting on a decision is reported before any claim. The answer
160
+ * belongs to a person; a drainer that continued without it would discard
161
+ * the question the turn stopped to ask, and claiming the session to find
162
+ * that out would only block the person's own resolver.
163
+ */
164
+ export async function drainSessionPass(deps) {
165
+ const result = {
166
+ listed: 0,
167
+ resumed: [],
168
+ awaitingDecision: [],
169
+ noCheckpoint: [],
170
+ heldByOthers: [],
171
+ alreadyHandled: [],
172
+ failed: [],
173
+ unreleased: [],
174
+ };
175
+ const active = await deps.log.activeTurn();
176
+ if (!active)
177
+ return result;
178
+ result.listed = 1;
179
+ const { turnId } = active;
180
+ if (deps.pendingDecisionTurns.has(turnId)) {
181
+ result.awaitingDecision.push(turnId);
182
+ deps.info(`⏸ ${turnId} · waiting on a human decision`);
183
+ return result;
184
+ }
185
+ const lease = await deps.claim();
186
+ if (!lease) {
187
+ result.heldByOthers.push(turnId);
188
+ deps.info(`⋯ ${turnId} · another process holds the session`);
189
+ return result;
190
+ }
191
+ try {
192
+ deps.info(`⏵ ${turnId} · ${active.state} · fence ${lease.fence}`);
193
+ // Re-read under the lease: between the listing and the claim, the
194
+ // previous holder may have settled the turn, or a new one begun.
195
+ const current = await deps.log.activeTurn({ lease });
196
+ if (current?.turnId !== turnId) {
197
+ result.alreadyHandled.push(turnId);
198
+ deps.info(`↷ ${turnId} · settled before this pass took it`);
199
+ return result;
200
+ }
201
+ const outcome = await deps.resume(turnId, lease);
202
+ if (outcome.resumed) {
203
+ if (outcome.turn.status === 'failed' || outcome.turn.status === 'cancelled') {
204
+ throw new Error(`Resumed turn ended with status "${outcome.turn.status}".`);
205
+ }
206
+ result.resumed.push(turnId);
207
+ deps.info(`✔ ${turnId} · ${outcome.turn.status}`);
208
+ }
209
+ else if (outcome.reason === 'awaiting-decision') {
210
+ // The index had not yet seen the decision; the log had.
211
+ result.awaitingDecision.push(turnId);
212
+ deps.info(`⏸ ${turnId} · waiting on a human decision`);
213
+ }
214
+ else {
215
+ result.noCheckpoint.push(turnId);
216
+ deps.info(`∅ ${turnId} · no checkpoint to continue from`);
217
+ }
218
+ }
219
+ catch (error) {
220
+ result.failed.push({ turnId, error: errorText(error) });
221
+ }
222
+ finally {
223
+ try {
224
+ await deps.release(lease);
225
+ }
226
+ catch (error) {
227
+ result.unreleased.push({ turnId, error: errorText(error) });
228
+ }
229
+ }
230
+ return result;
147
231
  }
148
232
  function defaultPrefs(detected) {
149
233
  const first = detected[0];
@@ -155,35 +239,45 @@ function defaultPrefs(detected) {
155
239
  }
156
240
  : null;
157
241
  }
242
+ /** `--store` must already be a namzu home: a drain never creates state where it looks. */
243
+ function isNamzuHome(home) {
244
+ const projects = join(home, 'projects');
245
+ if (!existsSync(projects))
246
+ return false;
247
+ const entry = lstatSync(projects);
248
+ return entry.isDirectory() && !entry.isSymbolicLink();
249
+ }
158
250
  export const drainCommand = {
159
251
  name: 'drain',
160
- description: 'Continue runs another process left behind (one pass, then exit)',
252
+ description: 'Continue a turn another process left behind (one pass, then exit)',
161
253
  passThrough: true,
162
254
  help: [
163
255
  'Usage: namzu drain --store <dir> --tenant <id> --project <id> --session <id>',
164
256
  '',
165
- 'Take every run under that scope that no worker currently holds, continue',
166
- 'it from its last checkpoint, and release it. One pass, then exit — namzu',
167
- 'has no daemon, and this is a command your scheduler runs, not a service.',
257
+ "Take the session's parked or interrupted turn if no worker holds the",
258
+ 'session, continue it from its last checkpoint under the session lease, and',
259
+ 'release the lease. One pass, then exit — namzu has no daemon, and this is a',
260
+ 'command your scheduler runs, not a service.',
168
261
  '',
169
262
  'Options:',
170
- ' --store <dir> The runs/ directory a checkpoint store writes to',
263
+ ' --store <dir> The namzu home whose sessions to drain (NAMZU_HOME)',
171
264
  ' --tenant <id> Isolation boundary. Required; there is no default',
172
- ' --project <id> Required — the disk layout carries no attribution',
173
- ' --session <id> Required, same reason',
174
- ' --holder <id> Who is taking the runs. Must be unique PER PROCESS',
265
+ ' --project <id> Required — checked against the session log',
266
+ ' --session <id> Required: the session whose turn to continue',
267
+ ' --holder <id> Who is taking the session. Must be unique PER PROCESS',
175
268
  ' --ttl <ms> Lease length (default 600000)',
176
- ' --max-concurrent <n> Runs in flight at once (default 1)',
177
- ' --cwd <path> Directory the resumed runs work in',
269
+ ' --max-concurrent <n> Turns in flight at once (default 1). A session has',
270
+ ' one active turn, so a pass continues at most one',
271
+ ' --cwd <path> Directory the resumed turn works in',
178
272
  ' --provider <id> Provider to continue with',
179
273
  ' --model <id> Model to continue with',
180
274
  ' --trust Accept this folder for this pass',
181
275
  '',
182
- 'A run that is parked on a human decision is REPORTED, never resumed past.',
276
+ 'A turn that is parked on a human decision is REPORTED, never resumed past.',
183
277
  'The answer belongs to a person; a drainer that continued without it would',
184
- 'discard the question the run stopped to ask.',
278
+ 'discard the question the turn stopped to ask.',
185
279
  '',
186
- 'Exit codes: 0 when every run it took was continued, 1 when any run failed,',
280
+ 'Exit codes: 0 when every turn it took was continued, 1 when any turn failed,',
187
281
  '64 when an argument is wrong, 77 when the folder has not been trusted.',
188
282
  ].join('\n'),
189
283
  handler: async ({ ctx: bootstrapCtx, rawArgs }) => {
@@ -197,7 +291,7 @@ export const drainCommand = {
197
291
  }
198
292
  if (!flags.store) {
199
293
  ctx.formatter.error({
200
- message: '--store is required: name the runs/ directory the checkpoint store writes to. There is no default, because an empty queue and a directory nobody writes to would report the same thing.',
294
+ message: '--store is required: name the namzu home whose sessions to drain. There is no default, because an empty queue and a directory nobody writes to would report the same thing.',
201
295
  });
202
296
  return EXIT_USAGE;
203
297
  }
@@ -213,6 +307,13 @@ export const drainCommand = {
213
307
  });
214
308
  return EXIT_USAGE;
215
309
  }
310
+ if (flags.maxConcurrent !== null &&
311
+ (!Number.isSafeInteger(flags.maxConcurrent) || flags.maxConcurrent < 1)) {
312
+ ctx.formatter.error({
313
+ message: `--max-concurrent must be a whole number above zero, got ${flags.maxConcurrent}`,
314
+ });
315
+ return EXIT_USAGE;
316
+ }
216
317
  const resolved = resolveWorkingDirectory(flags.cwd);
217
318
  if ('error' in resolved) {
218
319
  ctx.formatter.error({ message: resolved.error });
@@ -231,7 +332,7 @@ export const drainCommand = {
231
332
  // The CLI owns stderr, not the kernel it drives (LOG-05) — a live sink
232
333
  // at the level --verbose/--quiet/NAMZU_LOG_LEVEL named, instead of
233
334
  // forcing the level to `silent` via `configureLogger`. `{ replace:
234
- // true }`: see the identical comment in `run.ts` — a real invocation
335
+ // true }`: see the identical comment in `exec.ts` — a real invocation
235
336
  // calls this once, this package's own tests call a handler's more
236
337
  // than once per process.
237
338
  const logging = contextLogging(ctx);
@@ -253,54 +354,64 @@ export const drainCommand = {
253
354
  : `permissions.${diagnostic.tool}`;
254
355
  ctx.formatter.error({ message: `${where}: ${diagnostic.message}` });
255
356
  }
256
- let stateRoot;
257
- try {
258
- stateRoot = resolveNamzuHome();
259
- }
260
- catch (error) {
261
- ctx.formatter.error({
262
- message: `application state is unavailable: ${error instanceof Error ? error.message : String(error)}`,
263
- });
264
- return 1;
265
- }
266
- let conversationStore;
267
- let owningSession;
357
+ const home = resolve(flags.store);
358
+ let log;
359
+ let topicId;
360
+ let pendingDecisionTurns;
268
361
  try {
269
- conversationStore = sessionStore(stateRoot, true);
270
- owningSession = await conversationStore.getSession(scope.sessionId, scope.tenantId);
271
- if (!owningSession ||
272
- owningSession.id !== scope.sessionId ||
273
- owningSession.tenantId !== scope.tenantId ||
274
- owningSession.projectId !== scope.projectId) {
275
- throw new Error(`Session ${scope.sessionId} is not persisted under Project ${scope.projectId}`);
362
+ if (!isNamzuHome(home))
363
+ throw new DrainScopeError(`${home} holds no projects/ directory`);
364
+ // The index is derived from the logs; opening it brings it up to date.
365
+ const index = await openSessionIndex({ home });
366
+ try {
367
+ const indexed = await index.getSession(scope.sessionId);
368
+ if (!indexed || indexed.projectId !== scope.projectId) {
369
+ throw new DrainScopeError(`Session ${scope.sessionId} is not persisted under Project ${scope.projectId}`);
370
+ }
371
+ if (indexed.depth > 0) {
372
+ throw new DrainScopeError(`Session ${scope.sessionId} is a child session; drain its root session ${indexed.rootId}`);
373
+ }
374
+ const paths = new SessionPaths({ home, slug: indexed.slug });
375
+ const opened = await readSessionStart(home, paths.sessionLog({ sessionId: scope.sessionId }));
376
+ if (!opened ||
377
+ opened.sessionId !== scope.sessionId ||
378
+ opened.projectId !== scope.projectId ||
379
+ opened.tenantId !== scope.tenantId) {
380
+ throw new DrainScopeError(`Session ${scope.sessionId} was not opened under Tenant ${scope.tenantId} and Project ${scope.projectId}`);
381
+ }
382
+ if (!opened.topicId) {
383
+ throw new DrainScopeError(`Session ${scope.sessionId} records no topic to continue under`);
384
+ }
385
+ topicId = opened.topicId;
386
+ log = DiskSessionLog.at(paths, { sessionId: scope.sessionId });
387
+ pendingDecisionTurns = new Set((await index.listPendingDecisions({ sessionId: scope.sessionId })).map((decision) => decision.turnId));
388
+ }
389
+ finally {
390
+ index.close();
276
391
  }
277
392
  }
278
393
  catch (error) {
279
394
  ctx.formatter.error({
280
- message: `cannot resolve the drain session's topic: ${error instanceof Error ? error.message : String(error)}`,
395
+ message: `cannot resolve the drain session: ${errorText(error)}`,
281
396
  });
282
- return EXIT_USAGE;
397
+ // The flags named something that is not there (no namzu home, an
398
+ // unknown or child session, a scope mismatch): the caller's to fix.
399
+ // Anything else is state that could not be read, which is exit 1.
400
+ return error instanceof DrainScopeError ? EXIT_USAGE : EXIT_FAIL;
283
401
  }
284
402
  const session = await createAgentSession(prefs, probe.detected, {
285
403
  cwd,
286
404
  scope: {
287
- // The checkpoint queue is already exactly scoped by the required
288
- // flags below. Constructing the provider under a random CLI Project
289
- // and Session would create generated state for a scope the operator
290
- // never named, then resume the run under a different one.
405
+ // The session is exactly the one the required flags named.
406
+ // Constructing the provider under a random CLI Project and
407
+ // Session would create generated state for a scope the operator
408
+ // never named, then resume the turn under a different one.
291
409
  sessionId: scope.sessionId,
292
- topicId: owningSession.topicId,
293
- projectId: scope.projectId,
294
- tenantId: scope.tenantId,
295
- },
296
- stateRoot,
297
- conversationSessions: {
298
- store: conversationStore,
299
- root: stateRoot,
410
+ topicId,
300
411
  projectId: scope.projectId,
301
412
  tenantId: scope.tenantId,
302
- topicId: owningSession.topicId,
303
413
  },
414
+ stateRoot: home,
304
415
  rules: permissions.rules,
305
416
  permissionMode: 'auto',
306
417
  ...(ctx.config.limits ? { limits: ctx.config.limits } : {}),
@@ -323,87 +434,56 @@ export const drainCommand = {
323
434
  });
324
435
  return 1;
325
436
  }
326
- // `resolveDrainScope` refuses a partial prefix above, so all three are
327
- // present here; the store's attribution has no optional fields because
328
- // a listing that guessed one would file runs under a project nobody
329
- // named.
330
- const store = new DiskCheckpointStore({ baseDir: flags.store }, {
331
- tenantId: scope.tenantId,
332
- projectId: scope.projectId,
333
- sessionId: scope.sessionId,
334
- });
335
437
  const holder = flags.holder ?? defaultHolder();
336
- const awaiting = [];
337
- const empty = [];
338
438
  try {
339
- const result = await drainRuns({
340
- store,
341
- scope,
342
- holder,
343
- ttlMs,
344
- ...(flags.maxConcurrent !== null ? { maxConcurrent: flags.maxConcurrent } : {}),
345
- onRun: async (entry, claim) => {
346
- ctx.formatter.info(`⏵ ${describe(entry)} · fence ${claim.fence}`);
347
- const outcome = await session.resumeDurable({
348
- entry,
349
- checkpointStore: store,
350
- // The reason the claim is handed over at all: every durable
351
- // write this run makes carries it, so a drainer that stalled
352
- // past its lease cannot overwrite whoever took over.
353
- claimFence: claim.fence,
354
- });
355
- if (outcome.resumed) {
356
- if (outcome.run.status === 'failed' || outcome.run.status === 'cancelled') {
357
- throw new Error(`Resumed run ended with status "${outcome.run.status}".`);
358
- }
359
- ctx.formatter.info(`✔ ${entry.runId} · ${outcome.run.status}`);
360
- return;
361
- }
362
- // Reported, not resumed past. The two non-resumed outcomes mean
363
- // opposite things and an operator has to be able to tell them
364
- // apart: one is a question waiting on a person, the other is a
365
- // run with nothing to continue.
366
- if (outcome.reason === 'awaiting-decision') {
367
- awaiting.push(entry.runId);
368
- ctx.formatter.info(`⏸ ${entry.runId} · waiting on a human decision`);
369
- }
370
- else {
371
- empty.push(entry.runId);
372
- ctx.formatter.info(`∅ ${entry.runId} · no checkpoint to continue from`);
373
- }
374
- },
439
+ const result = await drainSessionPass({
440
+ log,
441
+ pendingDecisionTurns,
442
+ claim: () => claimSession(scope.sessionId, { holder, ttlMs, log }),
443
+ release: (lease) => releaseSession(scope.sessionId, lease, { log }),
444
+ resume: (turnId, lease) => session.resumeDurable({
445
+ entry: {
446
+ tenantId: scope.tenantId,
447
+ projectId: scope.projectId,
448
+ sessionId: scope.sessionId,
449
+ turnId,
450
+ },
451
+ sessionLog: log,
452
+ // The reason the lease is handed over at all: every record
453
+ // the resumed turn appends carries its fence, so a drainer
454
+ // that stalled past its lease cannot write over whoever
455
+ // took the session over.
456
+ lease,
457
+ }),
458
+ info: (message) => ctx.formatter.info(message),
375
459
  });
376
460
  ctx.formatter.print({
377
461
  listed: result.listed,
378
- resumed: result.drained.filter((id) => !awaiting.includes(id) && !empty.includes(id))
379
- .length,
380
- awaitingDecision: awaiting,
381
- noCheckpoint: empty,
382
- heldByOthers: result.skipped,
383
- alreadyHandled: result.stale,
462
+ resumed: result.resumed.length,
463
+ awaitingDecision: result.awaitingDecision,
464
+ noCheckpoint: result.noCheckpoint,
465
+ heldByOthers: result.heldByOthers,
466
+ alreadyHandled: result.alreadyHandled,
384
467
  failed: result.failed,
385
468
  unreleased: result.unreleased,
386
469
  });
387
470
  for (const f of result.failed) {
388
- ctx.formatter.error({ message: `${f.runId}: ${f.error}` });
471
+ ctx.formatter.error({ message: `${f.turnId}: ${f.error}` });
389
472
  }
390
473
  // A drain that could not give a lease back is reported too. The work
391
- // landed, but the run is unavailable to the next reader until the
474
+ // landed, but the session is unavailable to the next reader until the
392
475
  // lease lapses, and an operator watching throughput needs to know.
393
476
  for (const u of result.unreleased) {
394
477
  ctx.formatter.error({
395
- message: `${u.runId}: lease not released — ${u.error}`,
478
+ message: `${u.turnId}: lease not released — ${u.error}`,
396
479
  });
397
480
  }
398
481
  return result.failed.length > 0 ? 1 : 0;
399
482
  }
400
483
  catch (err) {
401
- // The refusals from `drainRuns` land here: a store that cannot claim,
402
- // a lease that cannot mean what it says. Reported rather than
403
- // swallowed into an empty pass that reads as "nothing was parked".
404
- ctx.formatter.error({
405
- message: err instanceof Error ? err.message : String(err),
406
- });
484
+ // A refusal from the log or the lease lands here. Reported rather
485
+ // than swallowed into an empty pass that reads as "nothing was parked".
486
+ ctx.formatter.error({ message: errorText(err) });
407
487
  return 1;
408
488
  }
409
489
  finally {