@namzu/cli 26.3.0 → 27.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 (321) hide show
  1. package/CHANGELOG.md +214 -0
  2. package/README.md +49 -34
  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.js +2 -2
  10. package/dist/cli.js.map +1 -1
  11. package/dist/commands/acp.d.ts +23 -1
  12. package/dist/commands/acp.d.ts.map +1 -1
  13. package/dist/commands/acp.js +41 -2
  14. package/dist/commands/acp.js.map +1 -1
  15. package/dist/commands/drain.d.ts +69 -2
  16. package/dist/commands/drain.d.ts.map +1 -1
  17. package/dist/commands/drain.js +214 -134
  18. package/dist/commands/drain.js.map +1 -1
  19. package/dist/commands/eval.js +1 -1
  20. package/dist/commands/eval.js.map +1 -1
  21. package/dist/commands/provider-wait.d.ts +5 -5
  22. package/dist/commands/provider-wait.d.ts.map +1 -1
  23. package/dist/commands/provider-wait.js +2 -2
  24. package/dist/commands/resident.d.ts.map +1 -1
  25. package/dist/commands/resident.js +2 -1
  26. package/dist/commands/resident.js.map +1 -1
  27. package/dist/commands/run-flags.d.ts +7 -7
  28. package/dist/commands/run-flags.js +4 -4
  29. package/dist/commands/run-stream.d.ts +16 -12
  30. package/dist/commands/run-stream.d.ts.map +1 -1
  31. package/dist/commands/run-stream.js +142 -98
  32. package/dist/commands/run-stream.js.map +1 -1
  33. package/dist/commands/run.d.ts +6 -4
  34. package/dist/commands/run.d.ts.map +1 -1
  35. package/dist/commands/run.js +129 -45
  36. package/dist/commands/run.js.map +1 -1
  37. package/dist/commands/serve.js +2 -2
  38. package/dist/commands/serve.js.map +1 -1
  39. package/dist/commands/state.js +8 -8
  40. package/dist/commands/state.js.map +1 -1
  41. package/dist/config/load.d.ts +1 -1
  42. package/dist/config/load.d.ts.map +1 -1
  43. package/dist/config/load.js +17 -5
  44. package/dist/config/load.js.map +1 -1
  45. package/dist/config/run-limits.d.ts +20 -8
  46. package/dist/config/run-limits.d.ts.map +1 -1
  47. package/dist/config/run-limits.js +28 -46
  48. package/dist/config/run-limits.js.map +1 -1
  49. package/dist/config/schema.d.ts +44 -19
  50. package/dist/config/schema.d.ts.map +1 -1
  51. package/dist/config/schema.js.map +1 -1
  52. package/dist/config/tool-result-screens.js +1 -1
  53. package/dist/context/directories.d.ts +37 -0
  54. package/dist/context/directories.d.ts.map +1 -0
  55. package/dist/context/directories.js +69 -0
  56. package/dist/context/directories.js.map +1 -0
  57. package/dist/context/environment.d.ts +55 -0
  58. package/dist/context/environment.d.ts.map +1 -1
  59. package/dist/context/environment.js +96 -0
  60. package/dist/context/environment.js.map +1 -1
  61. package/dist/context/project-tracker.d.ts +3 -3
  62. package/dist/context/project-tracker.d.ts.map +1 -1
  63. package/dist/context/project-tracker.js +3 -3
  64. package/dist/context/project-tracker.js.map +1 -1
  65. package/dist/context/project.d.ts +1 -1
  66. package/dist/context/project.js +1 -1
  67. package/dist/context/sandbox.d.ts +22 -3
  68. package/dist/context/sandbox.d.ts.map +1 -1
  69. package/dist/context/sandbox.js +42 -4
  70. package/dist/context/sandbox.js.map +1 -1
  71. package/dist/doctor/checks/chain.d.ts +1 -1
  72. package/dist/doctor/checks/chain.js +5 -5
  73. package/dist/doctor/checks/chain.js.map +1 -1
  74. package/dist/doctor/checks/invariants.d.ts +2 -2
  75. package/dist/doctor/checks/invariants.js +5 -5
  76. package/dist/doctor/checks/invariants.js.map +1 -1
  77. package/dist/doctor/checks/logging.js +1 -1
  78. package/dist/doctor/checks/logging.js.map +1 -1
  79. package/dist/doctor/checks/sandbox.js +1 -1
  80. package/dist/doctor/checks/sandbox.js.map +1 -1
  81. package/dist/doctor/checks/session-export.d.ts +1 -1
  82. package/dist/doctor/checks/session-export.js +2 -2
  83. package/dist/doctor/checks/session-export.js.map +1 -1
  84. package/dist/doctor/checks/telemetry.d.ts +1 -1
  85. package/dist/doctor/checks/telemetry.js +1 -1
  86. package/dist/doctor/registry.js +2 -2
  87. package/dist/doctor/registry.js.map +1 -1
  88. package/dist/exit-codes.d.ts +2 -2
  89. package/dist/exit-codes.js +2 -2
  90. package/dist/integrations/mcp/servers.d.ts +1 -1
  91. package/dist/integrations/providers/chain-capabilities.d.ts +6 -6
  92. package/dist/integrations/providers/chain-capabilities.js +8 -8
  93. package/dist/integrations/providers/chain-capabilities.js.map +1 -1
  94. package/dist/integrations/providers/register.d.ts +1 -1
  95. package/dist/integrations/providers/register.js +1 -1
  96. package/dist/integrations/providers/registry.d.ts +1 -1
  97. package/dist/integrations/resident/inspection.d.ts +4 -1
  98. package/dist/integrations/resident/inspection.d.ts.map +1 -1
  99. package/dist/integrations/resident/inspection.js +53 -34
  100. package/dist/integrations/resident/inspection.js.map +1 -1
  101. package/dist/integrations/resident/learning-storage.d.ts +7 -2
  102. package/dist/integrations/resident/learning-storage.d.ts.map +1 -1
  103. package/dist/integrations/resident/learning-storage.js +12 -8
  104. package/dist/integrations/resident/learning-storage.js.map +1 -1
  105. package/dist/integrations/resident/runner-store.d.ts +1 -1
  106. package/dist/integrations/resident/runner-store.d.ts.map +1 -1
  107. package/dist/integrations/resident/runner-store.js +5 -2
  108. package/dist/integrations/resident/runner-store.js.map +1 -1
  109. package/dist/integrations/resident/runner-worker.js +1 -0
  110. package/dist/integrations/resident/runner-worker.js.map +1 -1
  111. package/dist/integrations/resident/session-log-reads.d.ts +37 -0
  112. package/dist/integrations/resident/session-log-reads.d.ts.map +1 -0
  113. package/dist/integrations/resident/session-log-reads.js +125 -0
  114. package/dist/integrations/resident/session-log-reads.js.map +1 -0
  115. package/dist/integrations/resident/session-step.d.ts +2 -0
  116. package/dist/integrations/resident/session-step.d.ts.map +1 -1
  117. package/dist/integrations/resident/session-step.js +15 -13
  118. package/dist/integrations/resident/session-step.js.map +1 -1
  119. package/dist/integrations/resident/storage.d.ts +23 -1
  120. package/dist/integrations/resident/storage.d.ts.map +1 -1
  121. package/dist/integrations/resident/storage.js +60 -17
  122. package/dist/integrations/resident/storage.js.map +1 -1
  123. package/dist/integrations/resident/tool-evidence.d.ts +4 -2
  124. package/dist/integrations/resident/tool-evidence.d.ts.map +1 -1
  125. package/dist/integrations/resident/tool-evidence.js +36 -30
  126. package/dist/integrations/resident/tool-evidence.js.map +1 -1
  127. package/dist/integrations/resident/verification.d.ts +2 -2
  128. package/dist/integrations/resident/verification.d.ts.map +1 -1
  129. package/dist/integrations/resident/verification.js +4 -4
  130. package/dist/integrations/resident/verification.js.map +1 -1
  131. package/dist/integrations/sessions/checkpoint-view.d.ts +13 -0
  132. package/dist/integrations/sessions/checkpoint-view.d.ts.map +1 -0
  133. package/dist/integrations/sessions/checkpoint-view.js +48 -0
  134. package/dist/integrations/sessions/checkpoint-view.js.map +1 -0
  135. package/dist/integrations/sessions/context-inventory.js +1 -1
  136. package/dist/integrations/sessions/context-inventory.js.map +1 -1
  137. package/dist/integrations/sessions/conversation-search.d.ts +16 -20
  138. package/dist/integrations/sessions/conversation-search.d.ts.map +1 -1
  139. package/dist/integrations/sessions/conversation-search.js +305 -731
  140. package/dist/integrations/sessions/conversation-search.js.map +1 -1
  141. package/dist/integrations/sessions/evidence-page-validation.d.ts +4 -4
  142. package/dist/integrations/sessions/evidence-page-validation.d.ts.map +1 -1
  143. package/dist/integrations/sessions/evidence-page-validation.js +1 -1
  144. package/dist/integrations/sessions/evidence-page-validation.js.map +1 -1
  145. package/dist/integrations/sessions/evidence-recall.d.ts +40 -2
  146. package/dist/integrations/sessions/evidence-recall.d.ts.map +1 -1
  147. package/dist/integrations/sessions/evidence-recall.js +41 -97
  148. package/dist/integrations/sessions/evidence-recall.js.map +1 -1
  149. package/dist/integrations/sessions/store.d.ts +153 -85
  150. package/dist/integrations/sessions/store.d.ts.map +1 -1
  151. package/dist/integrations/sessions/store.js +414 -484
  152. package/dist/integrations/sessions/store.js.map +1 -1
  153. package/dist/integrations/sessions/task-context.d.ts +15 -2
  154. package/dist/integrations/sessions/task-context.d.ts.map +1 -1
  155. package/dist/integrations/sessions/task-context.js +34 -11
  156. package/dist/integrations/sessions/task-context.js.map +1 -1
  157. package/dist/integrations/sessions/transcript-export.d.ts +13 -6
  158. package/dist/integrations/sessions/transcript-export.d.ts.map +1 -1
  159. package/dist/integrations/sessions/transcript-export.js +148 -263
  160. package/dist/integrations/sessions/transcript-export.js.map +1 -1
  161. package/dist/integrations/state/home.d.ts +4 -16
  162. package/dist/integrations/state/home.d.ts.map +1 -1
  163. package/dist/integrations/state/home.js +6 -42
  164. package/dist/integrations/state/home.js.map +1 -1
  165. package/dist/integrations/state/project.d.ts +6 -7
  166. package/dist/integrations/state/project.d.ts.map +1 -1
  167. package/dist/integrations/state/project.js +7 -9
  168. package/dist/integrations/state/project.js.map +1 -1
  169. package/dist/integrations/state/report.d.ts +27 -41
  170. package/dist/integrations/state/report.d.ts.map +1 -1
  171. package/dist/integrations/state/report.js +172 -464
  172. package/dist/integrations/state/report.js.map +1 -1
  173. package/dist/integrations/state/retention.d.ts +18 -0
  174. package/dist/integrations/state/retention.d.ts.map +1 -0
  175. package/dist/integrations/state/retention.js +18 -0
  176. package/dist/integrations/state/retention.js.map +1 -0
  177. package/dist/integrations/subagents/activity.d.ts +36 -31
  178. package/dist/integrations/subagents/activity.d.ts.map +1 -1
  179. package/dist/integrations/subagents/activity.js +27 -23
  180. package/dist/integrations/subagents/activity.js.map +1 -1
  181. package/dist/integrations/subagents/batches.d.ts +91 -0
  182. package/dist/integrations/subagents/batches.d.ts.map +1 -0
  183. package/dist/integrations/subagents/batches.js +172 -0
  184. package/dist/integrations/subagents/batches.js.map +1 -0
  185. package/dist/integrations/subagents/parent.d.ts +26 -11
  186. package/dist/integrations/subagents/parent.d.ts.map +1 -1
  187. package/dist/integrations/subagents/parent.js +13 -28
  188. package/dist/integrations/subagents/parent.js.map +1 -1
  189. package/dist/integrations/subagents/policy.d.ts +2 -2
  190. package/dist/integrations/subagents/policy.d.ts.map +1 -1
  191. package/dist/integrations/subagents/policy.js +2 -2
  192. package/dist/integrations/subagents/policy.js.map +1 -1
  193. package/dist/integrations/subagents/replay.d.ts +91 -26
  194. package/dist/integrations/subagents/replay.d.ts.map +1 -1
  195. package/dist/integrations/subagents/replay.js +216 -114
  196. package/dist/integrations/subagents/replay.js.map +1 -1
  197. package/dist/integrations/subagents/runtime.d.ts +50 -26
  198. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  199. package/dist/integrations/subagents/runtime.js +132 -126
  200. package/dist/integrations/subagents/runtime.js.map +1 -1
  201. package/dist/integrations/subagents/saved-agents.d.ts +57 -0
  202. package/dist/integrations/subagents/saved-agents.d.ts.map +1 -0
  203. package/dist/integrations/subagents/saved-agents.js +102 -0
  204. package/dist/integrations/subagents/saved-agents.js.map +1 -0
  205. package/dist/integrations/subagents/slash.d.ts +55 -0
  206. package/dist/integrations/subagents/slash.d.ts.map +1 -0
  207. package/dist/integrations/subagents/slash.js +45 -0
  208. package/dist/integrations/subagents/slash.js.map +1 -0
  209. package/dist/integrations/telemetry/session-export.d.ts +6 -6
  210. package/dist/integrations/telemetry/session-export.d.ts.map +1 -1
  211. package/dist/integrations/telemetry/session-export.js +2 -2
  212. package/dist/memory/presentation.d.ts +11 -0
  213. package/dist/memory/presentation.d.ts.map +1 -1
  214. package/dist/memory/presentation.js +25 -0
  215. package/dist/memory/presentation.js.map +1 -1
  216. package/dist/memory/store.d.ts +13 -7
  217. package/dist/memory/store.d.ts.map +1 -1
  218. package/dist/memory/store.js +37 -11
  219. package/dist/memory/store.js.map +1 -1
  220. package/dist/memory/typed.d.ts +116 -0
  221. package/dist/memory/typed.d.ts.map +1 -0
  222. package/dist/memory/typed.js +336 -0
  223. package/dist/memory/typed.js.map +1 -0
  224. package/dist/permissions/checks.d.ts +1 -1
  225. package/dist/permissions/checks.js +1 -1
  226. package/dist/permissions/headless-trust.d.ts +2 -2
  227. package/dist/permissions/headless-trust.d.ts.map +1 -1
  228. package/dist/permissions/headless-trust.js +3 -3
  229. package/dist/permissions/headless-trust.js.map +1 -1
  230. package/dist/permissions/mode.d.ts +4 -4
  231. package/dist/permissions/mode.js +1 -1
  232. package/dist/permissions/rules.d.ts +1 -1
  233. package/dist/permissions/rules.js +1 -1
  234. package/dist/termination.d.ts +75 -0
  235. package/dist/termination.d.ts.map +1 -0
  236. package/dist/termination.js +143 -0
  237. package/dist/termination.js.map +1 -0
  238. package/dist/tui/AgentExplorer.d.ts +2 -2
  239. package/dist/tui/AgentExplorer.js +4 -4
  240. package/dist/tui/AgentExplorer.js.map +1 -1
  241. package/dist/tui/App.d.ts +9 -1
  242. package/dist/tui/App.d.ts.map +1 -1
  243. package/dist/tui/App.js +452 -303
  244. package/dist/tui/App.js.map +1 -1
  245. package/dist/tui/LiveActivity.d.ts +1 -1
  246. package/dist/tui/LiveActivity.d.ts.map +1 -1
  247. package/dist/tui/PermissionOverlay.d.ts +8 -0
  248. package/dist/tui/PermissionOverlay.d.ts.map +1 -1
  249. package/dist/tui/PermissionOverlay.js +25 -2
  250. package/dist/tui/PermissionOverlay.js.map +1 -1
  251. package/dist/tui/Picker.js +1 -1
  252. package/dist/tui/agent.d.ts +208 -132
  253. package/dist/tui/agent.d.ts.map +1 -1
  254. package/dist/tui/agent.js +523 -313
  255. package/dist/tui/agent.js.map +1 -1
  256. package/dist/tui/conversation-evidence-view.js +3 -3
  257. package/dist/tui/conversation-evidence-view.js.map +1 -1
  258. package/dist/tui/conversation-history.d.ts +4 -21
  259. package/dist/tui/conversation-history.d.ts.map +1 -1
  260. package/dist/tui/conversation-history.js +4 -28
  261. package/dist/tui/conversation-history.js.map +1 -1
  262. package/dist/tui/index.d.ts.map +1 -1
  263. package/dist/tui/index.js +27 -1
  264. package/dist/tui/index.js.map +1 -1
  265. package/dist/tui/log-pane.js +1 -1
  266. package/dist/tui/markdownParser.d.ts +1 -1
  267. package/dist/tui/markdownParser.js +1 -1
  268. package/dist/tui/permission-review.d.ts +16 -0
  269. package/dist/tui/permission-review.d.ts.map +1 -1
  270. package/dist/tui/permission-review.js +25 -2
  271. package/dist/tui/permission-review.js.map +1 -1
  272. package/dist/tui/slashCommands.d.ts +29 -40
  273. package/dist/tui/slashCommands.d.ts.map +1 -1
  274. package/dist/tui/slashCommands.js +70 -61
  275. package/dist/tui/slashCommands.js.map +1 -1
  276. package/dist/tui/{run-interruption.d.ts → turn-interruption.d.ts} +4 -4
  277. package/dist/tui/turn-interruption.d.ts.map +1 -0
  278. package/dist/tui/{run-interruption.js → turn-interruption.js} +20 -20
  279. package/dist/tui/turn-interruption.js.map +1 -0
  280. package/dist/tui/turn-limits-settings.d.ts +39 -0
  281. package/dist/tui/turn-limits-settings.d.ts.map +1 -0
  282. package/dist/tui/{run-limits-settings.js → turn-limits-settings.js} +18 -18
  283. package/dist/tui/turn-limits-settings.js.map +1 -0
  284. package/dist/tui/types.d.ts +3 -3
  285. package/dist/tui/types.d.ts.map +1 -1
  286. package/dist/user-commands/store.d.ts +1 -1
  287. package/dist/user-commands/store.js +1 -1
  288. package/package.json +7 -7
  289. package/dist/integrations/sessions/compaction-evidence.d.ts +0 -5
  290. package/dist/integrations/sessions/compaction-evidence.d.ts.map +0 -1
  291. package/dist/integrations/sessions/compaction-evidence.js +0 -59
  292. package/dist/integrations/sessions/compaction-evidence.js.map +0 -1
  293. package/dist/integrations/sessions/database.d.ts +0 -5
  294. package/dist/integrations/sessions/database.d.ts.map +0 -1
  295. package/dist/integrations/sessions/database.js +0 -34
  296. package/dist/integrations/sessions/database.js.map +0 -1
  297. package/dist/integrations/sessions/paths.d.ts +0 -7
  298. package/dist/integrations/sessions/paths.d.ts.map +0 -1
  299. package/dist/integrations/sessions/paths.js +0 -14
  300. package/dist/integrations/sessions/paths.js.map +0 -1
  301. package/dist/integrations/sessions/run-discovery.d.ts +0 -24
  302. package/dist/integrations/sessions/run-discovery.d.ts.map +0 -1
  303. package/dist/integrations/sessions/run-discovery.js +0 -142
  304. package/dist/integrations/sessions/run-discovery.js.map +0 -1
  305. package/dist/integrations/sessions/turn-evidence.d.ts +0 -129
  306. package/dist/integrations/sessions/turn-evidence.d.ts.map +0 -1
  307. package/dist/integrations/sessions/turn-evidence.js +0 -517
  308. package/dist/integrations/sessions/turn-evidence.js.map +0 -1
  309. package/dist/integrations/subagents/history.d.ts +0 -26
  310. package/dist/integrations/subagents/history.d.ts.map +0 -1
  311. package/dist/integrations/subagents/history.js +0 -76
  312. package/dist/integrations/subagents/history.js.map +0 -1
  313. package/dist/integrations/subagents/runs.d.ts +0 -72
  314. package/dist/integrations/subagents/runs.d.ts.map +0 -1
  315. package/dist/integrations/subagents/runs.js +0 -155
  316. package/dist/integrations/subagents/runs.js.map +0 -1
  317. package/dist/tui/run-interruption.d.ts.map +0 -1
  318. package/dist/tui/run-interruption.js.map +0 -1
  319. package/dist/tui/run-limits-settings.d.ts +0 -39
  320. package/dist/tui/run-limits-settings.d.ts.map +0 -1
  321. package/dist/tui/run-limits-settings.js.map +0 -1
package/dist/tui/App.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { resolveRunGuards } from '../config/run-limits.js';
3
- import { RUN_LIMIT_FIELDS, runLimitCommands } from './run-limits-settings.js';
4
- import { CliPathBuilder } from '../integrations/sessions/paths.js';
2
+ import { resolveTurnGuards } from '../config/run-limits.js';
3
+ import { TURN_LIMIT_FIELDS, turnLimitCommands } from './turn-limits-settings.js';
5
4
  /**
6
5
  * TUI root. Composes the banner, transcript, composer, status bar, and
7
6
  * the first-run provider picker overlay.
@@ -17,8 +16,8 @@ import { CliPathBuilder } from '../integrations/sessions/paths.js';
17
16
  import { discoverProviders } from '../integrations/providers/discover.js';
18
17
  import { ProviderSetup } from './ProviderSetup.js';
19
18
  import { ToolOutputViewer } from './ToolOutputViewer.js';
20
- import { join, relative } from 'node:path';
21
- import { DEFAULT_MAX_GOAL_ROUNDS, DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry, RunCancelled, SessionGoalActivation, StaleGoalError, asSessionId, createAssistantMessage, createUserMessage, generateRunId, generateSessionId, isCompactionMessage, kernelHostCommands, } from '@namzu/sdk';
19
+ import { relative } from 'node:path';
20
+ import { DEFAULT_MAX_GOAL_ROUNDS, DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry, SessionGoalActivation, StaleGoalError, TurnCancelled, asSessionId, createAssistantMessage, createUserMessage, generateSessionId, generateTurnId, isCompactionMessage, kernelHostCommands, } from '@namzu/sdk';
22
21
  import { Box, Text, useApp, useInput, useStdout, useWindowSize } from 'ink';
23
22
  import { useCallback, useEffect, useRef, useState } from 'react';
24
23
  import { pinTrustedProjectPath, resolveTrustedProjectContext, } from '../config/trusted-project-context.js';
@@ -27,13 +26,13 @@ import { runtimeContextLabel } from '../context/runtime-message.js';
27
26
  import { writeClipboardText } from '../integrations/clipboard/text.js';
28
27
  import { terminalNotificationEnabled, writeTerminalNotification, } from '../integrations/notifications/terminal.js';
29
28
  import { beginCodexDeviceLogin, beginSubscriptionLogin, clearAllStoredCredentials, clearStoredCodexCredential, clearStoredSubscriptionCredential, credentialsPath, parsePastedInput, primaryProvider, readStoredCodexCredential, readStoredSubscriptionCredential, signedInSubscriptionProviders, writePreferences, } from '../integrations/providers/index.js';
30
- import { appendMessages, archiveConversation, forkConversation, forkConversationBeforeUser, listRecent, loadConversation, loadResumableConversation, openSessions, replaceConversation, requireWritableConversation, setTitle, startConversation, titleOf, } from '../integrations/sessions/store.js';
29
+ import { activeConversationTurn, archiveConversation, forkConversation, forkConversationBeforeUser, listRecent, loadConversation, loadResumableConversation, openSessions, requireWritableConversation, setTitle, startConversation, titleOf, } from '../integrations/sessions/store.js';
31
30
  import { conversationMarkdown, writeConversationExport, } from '../integrations/sessions/transcript-export.js';
32
- import { liveOrchestrationRuns } from '../integrations/subagents/runs.js';
33
31
  import { NARRATION_TOOL_NAME } from '../integrations/subagents/runtime.js';
32
+ import { agentsSlashCommand } from '../integrations/subagents/slash.js';
34
33
  import { isTrusted, trustDir } from '../integrations/trust/store.js';
35
34
  import { checkUpdates } from '../integrations/updates.js';
36
- import { renderMemoryReport, renderMemorySaveResult } from '../memory/presentation.js';
35
+ import { renderMemoryReport, renderMemorySaveResult, renderStoredMemorySection, renderTypedNoteResult, } from '../memory/presentation.js';
37
36
  import { appendMemoryWithStatus, readMemory } from '../memory/store.js';
38
37
  import { effectivePermissionMode, permissionModeDescription, permissionModeLabel, } from '../permissions/mode.js';
39
38
  import { composeSkillsPrompt, discoverSkills, loadSkillBody } from '../skills/store.js';
@@ -61,7 +60,7 @@ import { createAgentSession, describeProviderModels, probeAgentSession, } from '
61
60
  import { choicePickerWindowSize, filterChoiceOptions, moveChoiceSelection, } from './choice-selection.js';
62
61
  import { keepRecentRows } from './compact-transcript.js';
63
62
  import { approvalIsDeliberate } from './consent-timing.js';
64
- import { assistantTranscriptTexts, planTurnPublication } from './conversation-history.js';
63
+ import { assistantTranscriptTexts } from './conversation-history.js';
65
64
  import { copyTargetsForResponse } from './copy-targets.js';
66
65
  import { editablePrompts } from './edit-prompts.js';
67
66
  import { editDraftInExternalEditor } from './external-editor.js';
@@ -74,11 +73,11 @@ import { eraseLastChoiceGrapheme } from './terminal-choice-text.js';
74
73
  import { renderCheckpoints, renderRestore } from '../checkpoints/store.js';
75
74
  import { expandFileMentions, listMentionableFiles } from './mentions.js';
76
75
  import { openInBrowser } from './open-browser.js';
77
- import { buildPermissionReview, buildPermissionSummary, permissionReviewPageRows, permissionReviewRefusal, permissionReviewRows, } from './permission-review.js';
78
- import { describeRunInterruption, describeRunStop } from './run-interruption.js';
76
+ import { buildPermissionReview, buildPermissionSummary, permissionReviewPageRows, permissionReviewRefusal, permissionReviewRows, releasedByApproveAll, } from './permission-review.js';
77
+ import { describeTurnInterruption, describeTurnStop } from './turn-interruption.js';
79
78
  import { moveSelection } from './selection-window.js';
80
79
  import { describeShellEscape, describeShellEscapeForModel, runShellEscape, shellEscapeCommand, } from './shell-escape.js';
81
- import { baseBranchReviewPrompt, combineOrchestrationRuns, commitReviewPrompt, hostCommandNames, kernelCommandDescriptors, mergeHostCommands, renderAgentRuns, renderOutcome, reviewPrompt, runSlash, } from './slashCommands.js';
80
+ import { baseBranchReviewPrompt, commitReviewPrompt, hostCommandNames, kernelCommandDescriptors, mergeHostCommands, renderAgents, renderOutcome, reviewPrompt, runSlash, } from './slashCommands.js';
82
81
  import { splitCompleteBlocks, splitSafeCut } from './stream-blocks.js';
83
82
  import { terminalSupportsHyperlinks } from './terminal-hyperlinks.js';
84
83
  import { theme } from './theme.js';
@@ -246,7 +245,7 @@ function goalRoundPrompt(authority) {
246
245
  */
247
246
  const LIVE_FURNITURE_ROWS = 10;
248
247
  const defaultExternalEditor = ({ seed, cwd, signal }) => editDraftInExternalEditor(seed, { cwd, signal });
249
- export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultExternalEditor, }) {
248
+ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultExternalEditor, terminationExit, }) {
250
249
  // Bind approval and every operation it admits to one real directory. A
251
250
  // lexical cwd may be a writable symlink; resolving it again after the gate
252
251
  // would let it be repointed from the approved project to another.
@@ -264,7 +263,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
264
263
  const terminal = useWindowSize();
265
264
  const hyperlinks = terminalSupportsHyperlinks(process.env, stdout.isTTY === true);
266
265
  /**
267
- * The last assistant message id the run reported, for `/feedback`.
266
+ * The last assistant message id the turn reported, for `/feedback`.
268
267
  *
269
268
  * A ref rather than state: nothing renders it, and making it state would
270
269
  * re-render the transcript on every delta — the exact cost the `pending`
@@ -276,7 +275,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
276
275
  *
277
276
  * Separate from `lastAssistantMessage`: feedback needs ids as soon as a
278
277
  * streamed message exists, while copying must keep the previous finished
279
- * answer until the current run reaches a normal `done`. A budget stop,
278
+ * answer until the current turn reaches a normal `done`. A budget stop,
280
279
  * cancellation, guardrail or thrown error may leave partial text and must not
281
280
  * silently promote it to a completed answer. A resumed session is different:
282
281
  * the conversation store predates stop-reason persistence, so its last saved
@@ -384,7 +383,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
384
383
  const permissionModeSourceRef = useRef(ctx.skipPermissions === true ? 'launch-bypass' : 'default');
385
384
  const [reasoningEffort, setReasoningEffortState] = useState();
386
385
  const reasoningEffortRef = useRef(undefined);
387
- const runLimitsOverrideRef = useRef({});
386
+ const turnLimitsOverrideRef = useRef({});
388
387
  const setReasoningEffort = useCallback((next) => {
389
388
  reasoningEffortRef.current = next;
390
389
  setReasoningEffortState(next);
@@ -594,7 +593,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
594
593
  const [composerDraft, setComposerDraft] = useState(null);
595
594
  const [composerHasDraft, setComposerHasDraft] = useState(false);
596
595
  const composerDraftTokenRef = useRef(0);
597
- /** Bounded child-run projection published by the current AgentSession. */
596
+ /** Bounded child session projection published by the current AgentSession. */
598
597
  const [subagents, setSubagentsState] = useState([]);
599
598
  const subagentsRef = useRef([]);
600
599
  /**
@@ -623,8 +622,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
623
622
  agentSurfaceCommittedRef.current = agentSurface;
624
623
  }, [agentSurface]);
625
624
  /**
626
- * Children of this conversation's earlier runs, rebuilt from saved
627
- * evidence, kept apart from the live monitor's own snapshot.
625
+ * Children of this conversation's earlier turns, rebuilt from their own
626
+ * session logs, kept apart from the live monitor's own snapshot.
628
627
  *
629
628
  * A ref and not state because it is an input to `replaceSubagents`, which
630
629
  * runs from the monitor's subscription: holding it as state would make
@@ -638,11 +637,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
638
637
  if (saved.length === 0)
639
638
  return live;
640
639
  // A child the live monitor still holds wins over its own saved copy:
641
- // both describe one run, and only one of the two is attached to
642
- // anything. Saved rows land after the live ones, so a cohort that is
640
+ // both describe one child session, and only one of the two is attached
641
+ // to anything. Saved rows land after the live ones, so a cohort that is
643
642
  // still running keeps the top of the list.
644
- const liveRunIds = new Set(live.map((agent) => agent.runId).filter((id) => id !== undefined));
645
- return [...live, ...saved.filter((agent) => !agent.runId || !liveRunIds.has(agent.runId))];
643
+ const liveSessionIds = new Set(live.map((agent) => agent.sessionId).filter((id) => id !== undefined));
644
+ return [
645
+ ...live,
646
+ ...saved.filter((agent) => !agent.sessionId || !liveSessionIds.has(agent.sessionId)),
647
+ ];
646
648
  }, []);
647
649
  const replaceSubagents = useCallback((incoming) => {
648
650
  const next = withSavedSubagents(incoming);
@@ -753,6 +755,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
753
755
  const reviewChoiceInFlightRef = useRef(null);
754
756
  const exitArmedRef = useRef(false);
755
757
  const abortRef = useRef(null);
758
+ /**
759
+ * A prompt that holds the turn slot while it waits at the turn boundary,
760
+ * before it has an abort controller. An interrupt or a conversation switch
761
+ * takes the slot back by clearing this; the prompt then drops itself.
762
+ */
763
+ const admissionRef = useRef(null);
756
764
  /** Identity of the turn allowed to notify when it settles. */
757
765
  const activeTurnTokenRef = useRef(null);
758
766
  /** A tool can reserve a switch; only the owning turn's finalizer may publish it. */
@@ -1118,81 +1126,87 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1118
1126
  return id;
1119
1127
  }, [nextId]);
1120
1128
  /**
1121
- * `/agents runs`: opens a picker immediately with a disabled loading row,
1122
- * then replaces it once the live snapshot and the disk walk both resolve.
1123
- *
1124
- * The composer stays reachable the whole time — nothing here awaits before
1125
- * the first `setChoicePicker`. A stale read is dropped by comparing
1126
- * `choicePickerRef.current` against the exact loading-picker object this
1127
- * call put there (the idiom the `/review` pickers already use below),
1128
- * rather than a counter: Escape and every other picker-replacing path
1129
- * already route through `setChoicePicker`, so they retire this object as a
1130
- * side effect and need no separate token bump of their own. That covers
1131
- * both "the operator left this picker" (any other object is current now,
1132
- * `null` included) and "opened it again" (a second call's own object is
1133
- * current instead).
1129
+ * `/agents [running|available|batches]`, answered by the delegation UI's
1130
+ * `agentsSlashCommand` over this session's live monitor and the batches the
1131
+ * session index holds. A `batches` answer opens a picker immediately with a
1132
+ * disabled loading row, then replaces it once the listing resolves; the
1133
+ * composer stays reachable the whole time. A stale read is dropped by
1134
+ * comparing `choicePickerRef.current` against the exact loading-picker
1135
+ * object this call put there.
1134
1136
  */
1135
- const openAgentRunsPicker = useCallback(() => {
1136
- setSelectedChoice(0);
1137
- const pending = {
1138
- kind: 'agent-runs',
1139
- title: 'Orchestration runs',
1140
- values: ['loading'],
1141
- options: [
1142
- { label: 'Loading…', description: 'Reading saved evidence…', disabledReason: 'Loading…' },
1143
- ],
1144
- };
1145
- setChoicePicker(pending);
1146
- void (async () => {
1147
- const live = liveOrchestrationRuns(subagentsRef.current, Date.now());
1148
- let finished = [];
1149
- try {
1150
- finished = (await session?.listOrchestrationRuns?.()) ?? [];
1151
- }
1152
- catch {
1153
- // Disk evidence is optional here: a live-only listing is still an
1154
- // honest answer, and the empty-history message below still applies
1155
- // if that live half turns out empty too.
1137
+ const runAgentsCommand = useCallback((args, openCockpit, listAvailable) => {
1138
+ const wantsBatches = args.length === 1 && args[0] === 'batches';
1139
+ const pending = wantsBatches
1140
+ ? {
1141
+ kind: 'agent-batches',
1142
+ title: 'Batches',
1143
+ values: ['loading'],
1144
+ options: [
1145
+ {
1146
+ label: 'Loading…',
1147
+ description: 'Reading saved batches…',
1148
+ // A disabled row shows its reason in place of its description.
1149
+ disabledReason: 'Reading saved batches…',
1150
+ },
1151
+ ],
1156
1152
  }
1157
- if (appLifetime.signal.aborted || choicePickerRef.current !== pending)
1153
+ : undefined;
1154
+ if (pending) {
1155
+ setSelectedChoice(0);
1156
+ setChoicePicker(pending);
1157
+ }
1158
+ void (async () => {
1159
+ const result = await agentsSlashCommand(args, {
1160
+ liveAgents: () => subagentsRef.current,
1161
+ ...(session?.listSavedBatches ? { savedBatches: session.listSavedBatches } : {}),
1162
+ });
1163
+ if (appLifetime.signal.aborted)
1158
1164
  return;
1159
- const listing = combineOrchestrationRuns(live, finished);
1160
- const empty = renderAgentRuns(listing);
1161
- if (empty) {
1162
- setChoicePicker(null);
1163
- pushMessage('system', empty);
1165
+ if (pending && choicePickerRef.current !== pending)
1164
1166
  return;
1167
+ switch (result.kind) {
1168
+ case 'agent-cockpit':
1169
+ openCockpit();
1170
+ return;
1171
+ case 'available-agents':
1172
+ listAvailable();
1173
+ return;
1174
+ case 'message':
1175
+ if (pending)
1176
+ setChoicePicker(null);
1177
+ pushMessage('system', result.content);
1178
+ return;
1179
+ case 'batches':
1180
+ setChoicePicker({
1181
+ kind: 'agent-batches',
1182
+ title: 'Batches',
1183
+ notice: result.listing.omitted > 0
1184
+ ? `${result.listing.omitted} older batch${result.listing.omitted === 1 ? '' : 'es'} not shown.`
1185
+ : undefined,
1186
+ values: result.listing.batches,
1187
+ options: result.listing.batches.map(batchOption),
1188
+ });
1189
+ return;
1165
1190
  }
1166
- setChoicePicker({
1167
- kind: 'agent-runs',
1168
- title: 'Orchestration runs',
1169
- notice: listing.omitted > 0
1170
- ? `${listing.omitted} older run${listing.omitted === 1 ? '' : 's'} not shown.`
1171
- : undefined,
1172
- values: listing.runs,
1173
- options: listing.runs.map(agentRunOption),
1174
- });
1175
1191
  })();
1176
1192
  }, [appLifetime, pushMessage, session, setChoicePicker, setSelectedChoice]);
1177
1193
  /**
1178
- * Opens an `/agents runs` selection straight into its first agent's
1194
+ * Opens an `/agents batches` selection straight into its first agent's
1179
1195
  * transcript — where the replayed banner actually lives, for a finished
1180
- * run — rather than the phase/agent list Ctrl+T lands on.
1196
+ * batch — rather than the phase/agent list Ctrl+T lands on.
1181
1197
  *
1182
- * A finished run's evidence is hydrated first, since a run this operator
1183
- * has not opened the cockpit for yet may not be in `subagents` at all.
1184
- * A live run skips that: it is already there, and re-reading disk for a
1185
- * run this process is still writing would risk the very half-written
1186
- * evidence `listSavedOrchestrationRuns` exists to stay off of.
1198
+ * A finished batch's children are hydrated from their logs first, since a
1199
+ * batch this operator has not opened the cockpit for yet may not be in
1200
+ * `subagents` at all. A live batch skips that: it is already there.
1187
1201
  */
1188
- const openOrchestrationRun = useCallback(async (run) => {
1189
- if (!run.live)
1202
+ const openBatch = useCallback(async (batch) => {
1203
+ if (!batch.live)
1190
1204
  await hydrateSavedChildren();
1191
- const workflow = agentWorkflows(subagentsRef.current).find((candidate) => candidate.agents.some((agent) => agent.workflowId === run.id));
1205
+ const workflow = agentWorkflows(subagentsRef.current).find((candidate) => candidate.agents.some((agent) => agent.workflowId === batch.id));
1192
1206
  const firstPhase = workflow?.phases[0];
1193
1207
  const firstAgent = firstPhase?.agents[0];
1194
1208
  if (!firstPhase || !firstAgent) {
1195
- pushMessage('system', `Could not open "${run.name}" — its saved evidence is no longer available.`);
1209
+ pushMessage('system', `Could not open "${batch.name}" — its saved children are no longer available.`);
1196
1210
  return;
1197
1211
  }
1198
1212
  setAgentSurface({
@@ -1379,31 +1393,25 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1379
1393
  }
1380
1394
  applyReasoningEffort(next);
1381
1395
  }, [applyReasoningEffort, pushMessage, session]);
1382
- const recordFeedback = useCallback((sessionId, runIdValue, messageIdValue, rating, note) => {
1396
+ const recordFeedback = useCallback((sessionId, messageIdValue, rating, note) => {
1383
1397
  const sessions = sessionsRef.current;
1384
1398
  if (!sessions) {
1385
1399
  pushMessage('system', 'Could not record feedback: conversation persistence is unavailable.');
1386
1400
  return;
1387
1401
  }
1388
- // Run evidence is session-scoped in the canonical store. The old flat
1389
- // `<cwd>/.namzu/runs` lookup never existed after the hierarchy migration,
1390
- // so the feedback store correctly refused every TUI rating as unverifiable.
1391
- // Bind both trees to the exact conversation captured with the streamed
1392
- // message; `/resume` may change the active scope before a delayed click.
1393
- const sessionDir = new CliPathBuilder(sessions.root).sessionDir(sessions.projectId, sessionId);
1394
- const store = new DiskMessageFeedbackStore({
1395
- rootDir: join(sessionDir, 'feedback'),
1396
- runsDir: join(sessionDir, 'runs'),
1397
- });
1398
- const runId = runIdValue;
1402
+ // Feedback lives beside the conversation's log (`<session-id>/feedback/`)
1403
+ // and is checked against it: a rating names a message that log holds.
1404
+ // Bound to the exact conversation captured with the streamed message;
1405
+ // `/resume` may change the active scope before a delayed click.
1406
+ const store = new DiskMessageFeedbackStore({ paths: sessions.paths });
1399
1407
  const messageId = messageIdValue;
1400
1408
  void (async () => {
1401
1409
  try {
1402
1410
  // A later rating replaces the first. Read-then-write preserves the
1403
1411
  // store's owner-version collision check instead of blind overwrite.
1404
- const current = (await store.listMessageFeedback({ runId })).find((record) => record.messageId === messageId);
1412
+ const current = (await store.listMessageFeedback({ sessionId })).find((record) => record.messageId === messageId);
1405
1413
  const record = await store.putMessageFeedback({
1406
- runId,
1414
+ sessionId,
1407
1415
  messageId,
1408
1416
  rating,
1409
1417
  ...(note ? { note } : {}),
@@ -1857,10 +1865,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1857
1865
  setChoicePicker({ ...child, back: picker });
1858
1866
  return;
1859
1867
  }
1860
- if (picker.kind === 'agent-runs') {
1868
+ if (picker.kind === 'agent-batches') {
1861
1869
  if (value === 'loading')
1862
1870
  return;
1863
- void openOrchestrationRun(value);
1871
+ void openBatch(value);
1864
1872
  return;
1865
1873
  }
1866
1874
  if (picker.kind === 'archive-conversation') {
@@ -1905,7 +1913,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1905
1913
  return;
1906
1914
  }
1907
1915
  if (picker.kind === 'feedback-rating') {
1908
- recordFeedback(picker.sessionId, picker.runId, picker.messageId, value);
1916
+ recordFeedback(picker.sessionId, picker.messageId, value);
1909
1917
  return;
1910
1918
  }
1911
1919
  if (picker.kind === 'skill') {
@@ -1931,7 +1939,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1931
1939
  archiveCurrentConversation,
1932
1940
  ctx.cwd,
1933
1941
  enqueueQueued,
1934
- openOrchestrationRun,
1942
+ openBatch,
1935
1943
  pushMessage,
1936
1944
  recordFeedback,
1937
1945
  removeStoredCredential,
@@ -2655,7 +2663,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2655
2663
  // same reason, as `neverPrompted` below.
2656
2664
  availableTools: () => session?.toolNames() ?? [],
2657
2665
  // From the session, not re-resolved: resolving builds a provider, and a
2658
- // second one would describe a different sandbox than the run is using.
2666
+ // second one would describe a different sandbox than the turn is using.
2659
2667
  sandbox: session?.sandbox ?? null,
2660
2668
  mcp: () => session
2661
2669
  ? (session.mcpStatus?.() ?? {
@@ -2700,12 +2708,104 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2700
2708
  sessionId: scopeRef.current?.sessionId,
2701
2709
  };
2702
2710
  // `/resume`: open the picker with this folder's recent conversations.
2711
+ /**
2712
+ * `/abandon`: close this conversation's paused or interrupted turn without
2713
+ * resuming it (`turn_failed{ abandoned }`), so the next prompt can begin a
2714
+ * turn. A turn running in this process is stopped with esc, not abandoned.
2715
+ */
2716
+ const abandonActiveTurn = useCallback(async (reason) => {
2717
+ const sessions = sessionsRef.current;
2718
+ const scope = scopeRef.current;
2719
+ if (!sessions || !scope || !conversationMaterializedRef.current) {
2720
+ pushMessage('system', 'Nothing to abandon: this conversation has no recorded turn yet.');
2721
+ return;
2722
+ }
2723
+ if (abortRef.current || state !== 'idle') {
2724
+ pushMessage('system', 'A turn is running here. Press esc to stop it; /abandon closes a paused or interrupted one.');
2725
+ return;
2726
+ }
2727
+ try {
2728
+ const active = await activeConversationTurn(sessions, scope.sessionId);
2729
+ if (!active) {
2730
+ pushMessage('system', 'This conversation has no paused or interrupted turn to abandon.');
2731
+ return;
2732
+ }
2733
+ if (!session?.abandonTurn) {
2734
+ pushMessage('system', 'This session cannot abandon turns.');
2735
+ return;
2736
+ }
2737
+ await session.abandonTurn(active.turnId, reason);
2738
+ pushMessage('system', `Abandoned turn ${active.turnId}. The next prompt starts a new turn in this conversation.`);
2739
+ }
2740
+ catch (err) {
2741
+ pushMessage('system', `Could not abandon the turn: ${err instanceof Error ? err.message : String(err)}`);
2742
+ }
2743
+ }, [pushMessage, session, state]);
2744
+ /**
2745
+ * `/resume` in a conversation whose turn is parked: continue that turn —
2746
+ * the same turn id, from its checkpoint — rendering it like any turn.
2747
+ * Returns false when there is no parked turn to continue.
2748
+ */
2749
+ // `applyEvent` is declared further down; the resume path reads it at call time.
2750
+ const applyEventRef = useRef(null);
2751
+ const resumeActiveTurn = useCallback(async () => {
2752
+ const sessions = sessionsRef.current;
2753
+ const scope = scopeRef.current;
2754
+ if (!sessions || !scope || !conversationMaterializedRef.current || !session?.hasProvider)
2755
+ return false;
2756
+ const active = await activeConversationTurn(sessions, scope.sessionId).catch(() => undefined);
2757
+ if (!active)
2758
+ return false;
2759
+ if (abortRef.current || state !== 'idle') {
2760
+ pushMessage('system', 'Wait for the running turn to finish before resuming the parked one.');
2761
+ return true;
2762
+ }
2763
+ const ac = new AbortController();
2764
+ abortRef.current = ac;
2765
+ setState('thinking');
2766
+ const st = {
2767
+ assistantId: null,
2768
+ text: '',
2769
+ conversationMessages: undefined,
2770
+ pending: '',
2771
+ completed: false,
2772
+ outcome: null,
2773
+ sessionId: scope.sessionId,
2774
+ notification: null,
2775
+ };
2776
+ pushMessage('system', `Resuming turn ${active.turnId} from its checkpoint.`, false, '▶');
2777
+ try {
2778
+ for await (const event of session.resumePaused({
2779
+ turnId: active.turnId,
2780
+ signal: ac.signal,
2781
+ })) {
2782
+ applyEventRef.current?.(event, st);
2783
+ }
2784
+ flushStream(st);
2785
+ if (st.assistantId)
2786
+ finalizeMessage(st.assistantId);
2787
+ // The resumed segment was appended to the log; the fold is the context
2788
+ // the next prompt continues from.
2789
+ modelHistoryRef.current = await loadConversation(sessions, scope.sessionId);
2790
+ }
2791
+ catch (err) {
2792
+ pushMessage('system', `Could not resume the turn: ${err instanceof Error ? err.message : String(err)}`);
2793
+ }
2794
+ finally {
2795
+ if (abortRef.current === ac)
2796
+ abortRef.current = null;
2797
+ setState('idle');
2798
+ }
2799
+ return true;
2800
+ }, [finalizeMessage, flushStream, pushMessage, session, state]);
2703
2801
  const doResume = useCallback(async () => {
2704
2802
  const sessions = sessionsRef.current ?? (await ensureSessions(), sessionsRef.current);
2705
2803
  if (!sessions) {
2706
2804
  pushMessage('system', 'Conversation history is unavailable in this folder.');
2707
2805
  return;
2708
2806
  }
2807
+ if (await resumeActiveTurn())
2808
+ return;
2709
2809
  try {
2710
2810
  const recent = await listRecent(sessions);
2711
2811
  // Don't offer the active (empty/just-started) conversation.
@@ -2721,7 +2821,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2721
2821
  catch (err) {
2722
2822
  pushMessage('system', `Could not list conversations: ${err instanceof Error ? err.message : String(err)}`);
2723
2823
  }
2724
- }, [ensureSessions, pushMessage]);
2824
+ }, [ensureSessions, pushMessage, resumeActiveTurn]);
2725
2825
  // Resolve a pending permission prompt with the user's decision and tear
2726
2826
  // down the overlay. No-op if nothing is pending.
2727
2827
  const resolvePermission = useCallback((decision) => {
@@ -2743,8 +2843,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2743
2843
  resolve(decision);
2744
2844
  }
2745
2845
  if (decision.kind === 'approve-all') {
2746
- for (const pending of permissionQueueRef.current.splice(0))
2747
- pending.resolve(decision);
2846
+ // A queued sandbox escape or outside path stays queued: "allow
2847
+ // all" was answered on another prompt, and a crossing is approved
2848
+ // only on its own.
2849
+ const kept = [];
2850
+ for (const pending of permissionQueueRef.current.splice(0)) {
2851
+ if (releasedByApproveAll(pending.permission.toolCalls))
2852
+ pending.resolve(decision);
2853
+ else
2854
+ kept.push(pending);
2855
+ }
2856
+ permissionQueueRef.current.push(...kept);
2748
2857
  }
2749
2858
  const next = permissionQueueRef.current.shift();
2750
2859
  setQueuedPermissionCount(permissionQueueRef.current.length);
@@ -2784,11 +2893,24 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2784
2893
  resolvePermission({ kind: 'reject', feedback: 'User interrupted.' });
2785
2894
  const ac = abortRef.current;
2786
2895
  if (!ac) {
2896
+ // A prompt still at the turn boundary: nothing has begun, so taking
2897
+ // the slot back is the whole interrupt. It drops itself when its
2898
+ // await returns.
2899
+ if (admissionRef.current) {
2900
+ admissionRef.current = null;
2901
+ const activeSessionId = scopeRef.current?.sessionId;
2902
+ if (activeSessionId)
2903
+ goalActivation.disarm(activeSessionId);
2904
+ wakeGoalDriver();
2905
+ discardQueued();
2906
+ setState('idle');
2907
+ return true;
2908
+ }
2787
2909
  if (cancelledSwitch)
2788
2910
  discardQueued();
2789
2911
  return cancelledSwitch;
2790
2912
  }
2791
- ac.abort(new RunCancelled('user'));
2913
+ ac.abort(new TurnCancelled('user'));
2792
2914
  const activeSessionId = scopeRef.current?.sessionId;
2793
2915
  if (activeSessionId)
2794
2916
  goalActivation.disarm(activeSessionId);
@@ -2811,6 +2933,20 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2811
2933
  resolvePermission,
2812
2934
  wakeGoalDriver,
2813
2935
  ]);
2936
+ // A termination signal leaves like `/exit`, after stopping the turn: the
2937
+ // session still owns tool servers, jobs and the `session_end` hook, and
2938
+ // Ink's unmount is what gives the terminal back.
2939
+ useEffect(() => {
2940
+ if (!terminationExit)
2941
+ return;
2942
+ terminationExit.current = () => {
2943
+ interruptTurn();
2944
+ exitWithSummary();
2945
+ };
2946
+ return () => {
2947
+ terminationExit.current = null;
2948
+ };
2949
+ }, [terminationExit, interruptTurn, exitWithSummary]);
2814
2950
  /**
2815
2951
  * Load the chosen conversation into the transcript and continue in it.
2816
2952
  *
@@ -2821,12 +2957,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2821
2957
  * would have run against a conversation nobody asked them of. And, the one
2822
2958
  * that outlived the process, its `appendMessages` wrote into the RESUMED
2823
2959
  * conversation's durable record, because `sessionId` was mutated on a
2824
- * `RunScope` the running loop held the very same object of.
2960
+ * `SessionScope` the running loop held the very same object of.
2825
2961
  *
2826
2962
  * The mutation below stays, and it is the sharing that makes it necessary:
2827
2963
  * `createAgentSession` closed over this exact object and spreads it into
2828
2964
  * every `query()`, so replacing it here would leave the agent attributing
2829
- * every future turn to the conversation the operator has left. `RunScope`
2965
+ * every future turn to the conversation the operator has left. `SessionScope`
2830
2966
  * says as much — `sessionId` is its one non-`readonly` field. What changed is
2831
2967
  * on the other side: a turn now fixes its own destination when it starts
2832
2968
  * (see `runTurn`), so a shared cursor moving under it can no longer decide
@@ -2940,7 +3076,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2940
3076
  * because its empty successor could not be published. Once it exists, moving
2941
3077
  * the shared scope and advancing the generation are one synchronous boundary:
2942
3078
  * old events can still unwind and persist to their captured destination, but
2943
- * they cannot render into or start an SDK run under the new conversation.
3079
+ * they cannot render into or start an SDK turn under the new conversation.
2944
3080
  *
2945
3081
  * A process whose initial persistence setup failed has no shared scope to
2946
3082
  * move. It still gets an honest in-memory context reset; the notice names that
@@ -2978,7 +3114,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2978
3114
  if (sourceScope && targetSessionId) {
2979
3115
  sourceScope.sessionId = targetSessionId;
2980
3116
  conversationMaterializedRef.current = true;
2981
- // A fresh conversation owns no runs yet, so this normally reads
3117
+ // A fresh conversation owns no turns yet, so this normally reads
2982
3118
  // nothing. It runs anyway because the rule is "the scope changed,
2983
3119
  // re-read", and an exception here is how the resume path lost
2984
3120
  // its own re-read.
@@ -3038,7 +3174,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3038
3174
  }
3039
3175
  try {
3040
3176
  if (!clear && title === '') {
3041
- const current = titleOf(sessions, scope.sessionId);
3177
+ const current = await titleOf(sessions, scope.sessionId);
3042
3178
  textPromptTokenRef.current += 1;
3043
3179
  setTextPrompt({
3044
3180
  token: textPromptTokenRef.current,
@@ -3051,7 +3187,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3051
3187
  });
3052
3188
  return;
3053
3189
  }
3054
- setTitle(sessions, scope.sessionId, clear ? '' : title);
3190
+ await setTitle(sessions, scope.sessionId, clear ? '' : title);
3055
3191
  pushMessage('system', clear
3056
3192
  ? 'Name removed. /resume will list this conversation by its opening message again.'
3057
3193
  : `Named "${title}".`);
@@ -3083,8 +3219,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3083
3219
  pushMessage('system', 'The conversation changed while this editor was open. Reopen the command in the current conversation.');
3084
3220
  return;
3085
3221
  }
3086
- if (prompt.kind === 'run-limit') {
3087
- const field = RUN_LIMIT_FIELDS.find((field) => field.key === prompt.limitKey);
3222
+ if (prompt.kind === 'turn-limit') {
3223
+ const field = TURN_LIMIT_FIELDS.find((field) => field.key === prompt.limitKey);
3088
3224
  if (field)
3089
3225
  commandPickerSubmitRef.current(`/config limits ${field.name} ${value.trim()}`);
3090
3226
  return;
@@ -3103,13 +3239,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3103
3239
  pushMessage('system', 'Conversation history is unavailable in this folder.');
3104
3240
  return;
3105
3241
  }
3106
- try {
3107
- setTitle(sessions, prompt.sessionId, value);
3108
- pushMessage('system', `Named "${value.trim()}".`);
3109
- }
3110
- catch (error) {
3111
- pushMessage('system', `Could not save the name: ${error instanceof Error ? error.message : String(error)}`);
3112
- }
3242
+ void setTitle(sessions, prompt.sessionId, value).then(() => pushMessage('system', `Named "${value.trim()}".`), (error) => pushMessage('system', `Could not save the name: ${error instanceof Error ? error.message : String(error)}`));
3113
3243
  }, [pushMessage, runConversationExport, setTextPrompt]);
3114
3244
  const cancelTextPrompt = useCallback(() => {
3115
3245
  const prompt = textPromptRef.current;
@@ -3431,9 +3561,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3431
3561
  st.sourceTextPartId = event.textPart?.id;
3432
3562
  }
3433
3563
  setState('thinking');
3434
- if (event.messageId && event.runId && st.sessionId) {
3564
+ if (event.messageId && event.turnId && st.sessionId) {
3435
3565
  lastAssistantMessage.current = {
3436
- runId: event.runId,
3437
3566
  messageId: event.messageId,
3438
3567
  sessionId: st.sessionId,
3439
3568
  };
@@ -3476,12 +3605,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3476
3605
  setState('tool');
3477
3606
  const waitingAgent = event.toolName === 'wait_for_task' && event.taskId
3478
3607
  ? subagentsRef.current.find((agent) => agent.taskId === event.taskId &&
3479
- (event.runId === undefined || agent.workflowId === event.runId))
3608
+ (event.turnId === undefined || agent.workflowId === event.turnId))
3480
3609
  : undefined;
3481
3610
  const tool = {
3482
3611
  ...(waitingAgent ? { taskId: waitingAgent.taskId } : {}),
3483
3612
  id: event.toolUseId,
3484
- ...(event.runId ? { runId: event.runId } : {}),
3613
+ ...(event.turnId ? { turnId: event.turnId } : {}),
3485
3614
  toolName: event.toolName,
3486
3615
  activity: event.activity,
3487
3616
  label: waitingAgent
@@ -3497,7 +3626,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3497
3626
  case 'tool-progress': {
3498
3627
  const running = activeToolsRef.current;
3499
3628
  const index = running.findIndex((tool) => tool.id === event.toolUseId &&
3500
- (event.runId === undefined || tool.runId === event.runId));
3629
+ (event.turnId === undefined || tool.turnId === event.turnId));
3501
3630
  // A terminal event may already have removed this call. Never recreate
3502
3631
  // a live row from late diagnostic state.
3503
3632
  if (index < 0)
@@ -3524,7 +3653,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3524
3653
  // result to the wrong call. If no id matches, render the
3525
3654
  // completion on its own (label from the end event itself).
3526
3655
  const i = running.findIndex((tool) => tool.id === event.toolUseId &&
3527
- (event.runId === undefined || tool.runId === event.runId));
3656
+ (event.turnId === undefined || tool.turnId === event.turnId));
3528
3657
  const done = i >= 0 ? running[i] : undefined;
3529
3658
  if (i >= 0) {
3530
3659
  activeToolsRef.current = [...running.slice(0, i), ...running.slice(i + 1)];
@@ -3533,7 +3662,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3533
3662
  // Lifecycle updates own completion rows, even when the model never waits.
3534
3663
  // Keep unknown waits and errors visible; never hide an unrepresented task.
3535
3664
  const inspectableWait = done?.taskId && subagentsRef.current.some((agent) => agent.taskId === done.taskId &&
3536
- (event.runId === undefined || agent.workflowId === event.runId) &&
3665
+ (event.turnId === undefined || agent.workflowId === event.turnId) &&
3537
3666
  agent.transcript.some((row) => row.kind === 'assistant' && row.text.trim().length > 0));
3538
3667
  if (!event.isError && inspectableWait && event.toolName === 'wait_for_task') {
3539
3668
  setState(activeToolsRef.current.length > 0 ? 'tool' : 'thinking');
@@ -3542,7 +3671,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3542
3671
  const representedSuccessfulAgent = !event.isError &&
3543
3672
  event.toolName.toLowerCase() === 'agent' &&
3544
3673
  subagentsRef.current.some((agent) => agent.toolUseId === event.toolUseId &&
3545
- (event.runId === undefined || agent.workflowId === event.runId));
3674
+ (event.turnId === undefined || agent.workflowId === event.turnId));
3546
3675
  // The band above the rail already carries a narrated line, in the
3547
3676
  // parent's own words, from the moment it is written. Printing the
3548
3677
  // call that wrote it would show the same sentence twice, the
@@ -3697,7 +3826,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3697
3826
  // commentary deltas. The transcript keeps each visible part above.
3698
3827
  if (event.text !== undefined)
3699
3828
  st.text = event.text;
3700
- // `run_completed` is not synonymous with success: budgets,
3829
+ // `turn_completed` is not synonymous with success: budgets,
3701
3830
  // cancellation and output guardrails arrive through this event too.
3702
3831
  // Missing remains a normal end for older producers, matching the
3703
3832
  // headless command's compatibility rule.
@@ -3709,7 +3838,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3709
3838
  outcome: st.completed ? 'completed' : 'stopped',
3710
3839
  };
3711
3840
  closeAssistant();
3712
- const stopNotice = describeRunStop(event.stopReason, event.budget);
3841
+ const stopNotice = describeTurnStop(event.stopReason, event.budget);
3713
3842
  if (stopNotice)
3714
3843
  pushMessage('system', stopNotice, false, '■');
3715
3844
  break;
@@ -3723,20 +3852,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3723
3852
  st.outcome = 'stopped';
3724
3853
  st.queuePauseOutcome = 'paused';
3725
3854
  st.notification = { kind: 'turn-settled', outcome: 'stopped' };
3726
- pushMessage('system', describeRunInterruption(event), false, '⏸');
3855
+ pushMessage('system', describeTurnInterruption(event), false, '⏸');
3727
3856
  break;
3728
3857
  case 'error':
3729
3858
  closeAssistant();
3730
3859
  st.outcome = event.message === 'aborted' ? 'cancelled' : 'failed';
3731
3860
  if (event.message !== 'aborted') {
3732
3861
  st.notification = { kind: 'turn-settled', outcome: 'failed' };
3733
- pushMessage('system', describeRunInterruption(event), false, '!');
3862
+ pushMessage('system', describeTurnInterruption(event), false, '!');
3734
3863
  }
3735
3864
  else
3736
3865
  st.notification = null;
3737
3866
  break;
3738
3867
  }
3739
3868
  }, [appendToMessage, finalizeMessage, flushStream, pushMessage]);
3869
+ applyEventRef.current = applyEvent;
3740
3870
  const runTurn = useCallback(async (prompt) => {
3741
3871
  // A passive queue effect can still hold the previous render's session
3742
3872
  // while a replacement is being committed. Keep FIFO ownership until
@@ -3764,6 +3894,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3764
3894
  // state idle while the admission read was pending let a second submit
3765
3895
  // start beside it and broke the queue's FIFO ownership.
3766
3896
  setState('thinking');
3897
+ // Both awaits below can span an interrupt, a `/clear` or a `/resume`,
3898
+ // and neither has an abort controller yet for them to cancel. The
3899
+ // token is what they take back instead.
3900
+ const admission = {};
3901
+ admissionRef.current = admission;
3767
3902
  let durableScope;
3768
3903
  try {
3769
3904
  durableScope = await materializeConversation();
@@ -3786,6 +3921,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3786
3921
  await requireWritableConversation(turnSessions, destination, 'start conversation turn');
3787
3922
  }
3788
3923
  catch (err) {
3924
+ // Taken back meanwhile: the slot is no longer this prompt's.
3925
+ if (admissionRef.current !== admission)
3926
+ return;
3927
+ admissionRef.current = null;
3789
3928
  if (prompt.kind === 'goal' && prompt.goalRound) {
3790
3929
  goalActivation.disarm(prompt.goalRound.sessionId, prompt.goalRound);
3791
3930
  wakeGoalDriver();
@@ -3795,6 +3934,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3795
3934
  return;
3796
3935
  }
3797
3936
  }
3937
+ // Interrupted, or the conversation changed, while this prompt waited at
3938
+ // the boundary. Whoever took the slot back already freed it and said
3939
+ // so, and a turn of the new conversation may hold it now; this prompt
3940
+ // is dropped rather than begun under that conversation.
3941
+ if (admissionRef.current !== admission)
3942
+ return;
3943
+ admissionRef.current = null;
3798
3944
  // `@path` mentions: the visible human message keeps the readable token,
3799
3945
  // but the model receives the file contents inlined. An automatic goal
3800
3946
  // prompt is already host-authored context and is never reinterpreted as
@@ -3815,7 +3961,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3815
3961
  }
3816
3962
  : undefined);
3817
3963
  const priorForSdk = [...historyBeforeTurn, userMessage];
3818
- const runId = generateRunId();
3964
+ // Reserved before the turn begins, so everything that refers to this
3965
+ // turn — its goal-round authority above all — names it from the start.
3966
+ const turnId = generateTurnId();
3819
3967
  if (goalRound) {
3820
3968
  pushMessage('system', `Goal round ${goalRound.round} / ${goalRound.maxGoalRounds}`, false, '◎', [`Objective: ${goalRound.objective}`]);
3821
3969
  }
@@ -4010,7 +4158,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4010
4158
  const turnPermissionMode = permissionModeRef.current;
4011
4159
  const turnReasoningEffort = reasoningEffortRef.current;
4012
4160
  const turnOrchestrateMode = orchestrateModeRef.current;
4013
- const turnRunLimits = resolveRunGuards(ctxRef.current.limits, runLimitsOverrideRef.current);
4161
+ const turnLimits = resolveTurnGuards(ctxRef.current.limits, turnLimitsOverrideRef.current);
4014
4162
  // Always carry the guarded callback. `auto` and `strict` decide before
4015
4163
  // calling it in makeResumeHandler; retaining it is what lets a session
4016
4164
  // launched with --yolo later return to prompt mode truthfully.
@@ -4023,41 +4171,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4023
4171
  }
4024
4172
  return onPermission(req);
4025
4173
  };
4026
- let evidenceTurn;
4027
- let evidenceReady = goalRound === undefined;
4028
- if (turnSessions?.turnEvidence && destination) {
4029
- try {
4030
- evidenceTurn = await turnSessions.turnEvidence.recordTurnStarted({
4031
- sessionId: destination,
4032
- runId,
4033
- displayText: text,
4034
- user: userMessage,
4035
- });
4036
- evidenceReady = true;
4037
- }
4038
- catch (err) {
4039
- if (goalRound) {
4040
- goalActivation.disarm(goalRound.sessionId, goalRound);
4041
- wakeGoalDriver();
4042
- pushMessage('system', `Goal round ${goalRound.round} was not started because its durable evidence could not be recorded: ${err instanceof Error ? err.message : String(err)}. Automatic continuation is disarmed; /goal resume retries explicitly.`);
4043
- }
4044
- else {
4045
- pushMessage('system', `Could not record durable evidence for this turn before run ${runId}: ${err instanceof Error ? err.message : String(err)}. The turn will continue, but a complete export of conversation ${destination} will refuse rather than omit it.`);
4046
- }
4047
- }
4048
- }
4049
- else if (goalRound) {
4174
+ // A goal round is a turn of a durable conversation: its round is recorded
4175
+ // on the turn (`origin.kind: 'goal-round'`), and a conversation that is
4176
+ // not in a log has nowhere to record it.
4177
+ const admitted = goalRound === undefined || Boolean(turnSessions && destination);
4178
+ if (!admitted && goalRound) {
4050
4179
  goalActivation.disarm(goalRound.sessionId, goalRound);
4051
4180
  wakeGoalDriver();
4052
- pushMessage('system', `Goal round ${goalRound.round} was not started because durable turn evidence is unavailable. Automatic continuation is disarmed; /goal resume retries explicitly.`);
4181
+ pushMessage('system', `Goal round ${goalRound.round} was not started because this conversation is not durable. Automatic continuation is disarmed; /goal resume retries explicitly.`);
4053
4182
  }
4054
4183
  try {
4055
- // `recordTurnStarted` is an awaited durability boundary. A conversation
4056
- // switch can happen while it is pending and move the mutable RunScope
4057
- // captured by `createAgentSession`. Re-admit the turn here, immediately
4058
- // before the generator exists, or an abandoned prompt can initialize its
4059
- // reserved SDK run under the new conversation.
4060
- if (evidenceReady &&
4184
+ // Setup above awaited. A conversation switch can happen meanwhile and
4185
+ // move the mutable SessionScope captured by `createAgentSession`. Re-admit
4186
+ // the turn here, immediately before the generator exists, or an
4187
+ // abandoned prompt can begin its reserved turn under the new
4188
+ // conversation.
4189
+ if (admitted &&
4061
4190
  !ac.signal.aborted &&
4062
4191
  stillHere() &&
4063
4192
  activeTurnTokenRef.current === turnToken &&
@@ -4066,9 +4195,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4066
4195
  goalActivation.isArmed(goalRound.sessionId, goalRound)))) {
4067
4196
  for await (const event of session.send(priorForSdk, {
4068
4197
  signal: ac.signal,
4069
- runId,
4198
+ turnId,
4199
+ origin: { protocol: 'cli', kind: goalRound ? 'goal-round' : 'prompt' },
4200
+ // The TUI never resumes a turn whose process is gone; the next
4201
+ // prompt closes it. A paused turn is never closed this way.
4202
+ abandonInterrupted: true,
4070
4203
  permissionMode: turnPermissionMode,
4071
- limits: turnRunLimits,
4204
+ limits: turnLimits,
4072
4205
  ...(turnReasoningEffort !== undefined ? { effort: turnReasoningEffort } : {}),
4073
4206
  ...(turnOrchestrateMode ? { orchestrate: true } : {}),
4074
4207
  ...(goalRound ? { goalRound } : {}),
@@ -4143,16 +4276,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4143
4276
  // Prefer the kernel's settled conversation projection: it retains opaque
4144
4277
  // reasoning, citations and complete tool turns that the visible delta
4145
4278
  // stream cannot reconstruct. A fake/legacy session that does not publish
4146
- // one keeps the old text-only shape. The visible transcript still keeps
4147
- // the operator's readable `@file` token; both persistence paths keep what
4148
- // was actually sent, including expanded contents and attachments.
4149
- const fallbackTurn = goalRound && !evidenceReady ? [] : [userMessage];
4279
+ // one keeps the old text-only shape. The durable record is the session
4280
+ // log, which the kernel appended to as the turn ran; this is only the
4281
+ // in-memory context the next send starts from.
4282
+ const fallbackTurn = admitted ? [userMessage] : [];
4150
4283
  if (st.text.trim().length > 0)
4151
4284
  fallbackTurn.push(createAssistantMessage(st.text));
4152
- const conversationMessages = goalRound && !evidenceReady ? undefined : st.conversationMessages;
4153
- const publication = conversationMessages
4154
- ? planTurnPublication(historyBeforeTurn, userMessage, conversationMessages)
4155
- : { kind: 'append', messages: fallbackTurn };
4285
+ const conversationMessages = admitted ? st.conversationMessages : undefined;
4156
4286
  const nextModelHistory = conversationMessages ?? [...historyBeforeTurn, ...fallbackTurn];
4157
4287
  if (goalRound && (ac.signal.aborted || st.outcome !== 'completed')) {
4158
4288
  goalActivation.disarm(goalRound.sessionId, goalRound);
@@ -4207,61 +4337,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4207
4337
  sendTerminalNotification(st.notification);
4208
4338
  }
4209
4339
  }
4210
- // Persisted either way, and into the conversation this turn was
4211
- // started in — captured above, never re-read.
4212
- //
4213
- // Best-effort is about not FAILING, not about staying quiet. The
4214
- // rejection used to be swallowed whole, and it is the one failure here
4215
- // that makes a LATER surface wrong: `/resume` comes back missing a
4216
- // turn that was on screen, and the next turn in that conversation
4217
- // silently lacks it as context, with nothing connecting either to a
4218
- // write that failed minutes ago. `run-stream` already says this, in
4219
- // these words and for this reason.
4220
- //
4221
- // Said wherever the operator is now, even when that is a different
4222
- // conversation, because there is no other channel and it is news they
4223
- // need. Naming the conversation is what keeps it from reading as a
4224
- // fault of the one in front of them.
4225
- if (turnSessions &&
4226
- destination &&
4227
- (publication.kind === 'replace' || publication.messages.length > 0 || evidenceTurn)) {
4228
- persistenceTailRef.current = persistenceTailRef.current.then(async () => {
4229
- if (evidenceTurn && turnSessions.turnEvidence) {
4230
- try {
4231
- await turnSessions.turnEvidence.recordTurnSettled({
4232
- sessionId: destination,
4233
- turnId: evidenceTurn.turnId,
4234
- runId,
4235
- outcome: ac.signal.aborted
4236
- ? 'cancelled'
4237
- : (st.outcome ?? (st.completed ? 'completed' : 'stopped')),
4238
- assistantText: st.text,
4239
- });
4240
- }
4241
- catch (err) {
4242
- pushMessage('system', `Could not finish the durable evidence for turn ${evidenceTurn.turnId}: ${err instanceof Error ? err.message : String(err)}. A complete export of conversation ${destination} will refuse if the SDK run record cannot prove the missing text.`);
4243
- }
4244
- }
4245
- try {
4246
- if (publication.kind === 'replace') {
4247
- await replaceConversation(turnSessions, destination, publication.messages);
4248
- }
4249
- else if (publication.messages.length > 0) {
4250
- await appendMessages(turnSessions, destination, publication.messages);
4251
- }
4252
- }
4253
- catch (err) {
4254
- if (goalRound) {
4255
- goalActivation.disarm(goalRound.sessionId, goalRound);
4256
- wakeGoalDriver();
4257
- }
4258
- pushMessage('system', `A turn was not saved to conversation ${destination}: ${err instanceof Error ? err.message : String(err)}. Its durable history will not include it. It remains only in this process's in-memory context if that conversation is still open; resuming or restarting will lose it.`);
4259
- }
4260
- });
4261
- }
4262
- // Removed only AFTER the write has been attached to the tail. A history
4263
- // operation that sees no current-generation entries can now await that
4264
- // tail without a turn appearing behind its read later.
4340
+ // The kernel recorded the turn in the session log as it ran; nothing is
4341
+ // left to publish once the loop has unwound.
4265
4342
  unsettledTurnGenerationsRef.current.delete(turnToken);
4266
4343
  const pending = pendingModelSwitchRef.current;
4267
4344
  if (pending?.turnToken === turnToken && pending.selection) {
@@ -4445,6 +4522,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4445
4522
  hydrateSession,
4446
4523
  wakeGoalDriver,
4447
4524
  ]);
4525
+ // `#note` and `/memory add`: a typed memory file in the session's stored
4526
+ // memory, default type `project`. A session without a store — no provider
4527
+ // yet — appends to the curated project file the way every note used to.
4528
+ const rememberProjectNote = useCallback(async (note, type) => {
4529
+ try {
4530
+ if (session?.rememberNote) {
4531
+ pushMessage('system', renderTypedNoteResult(await session.rememberNote(note, type), note));
4532
+ return;
4533
+ }
4534
+ const saved = appendMemoryWithStatus(note, { scope: 'project', cwd: ctx.cwd });
4535
+ pushMessage('system', renderMemorySaveResult(saved, note));
4536
+ }
4537
+ catch (err) {
4538
+ pushMessage('system', `Could not save memory: ${err instanceof Error ? err.message : String(err)}`);
4539
+ }
4540
+ }, [ctx.cwd, pushMessage, session]);
4448
4541
  const handleSubmit = useCallback((value, attachments, mode = 'submit') => {
4449
4542
  if (goalCommandInFlightRef.current) {
4450
4543
  pushMessage('system', 'A goal command is still reaching durable session state. Wait for its result before sending another command or prompt.');
@@ -4478,13 +4571,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4478
4571
  // and both leave a row the model reads on its next turn.
4479
4572
  if (value.startsWith('#') && value.slice(1).trim().length > 0) {
4480
4573
  const note = value.slice(1).trim();
4481
- try {
4482
- const saved = appendMemoryWithStatus(note, { scope: 'project', cwd: ctx.cwd });
4483
- pushMessage('system', renderMemorySaveResult(saved, note));
4484
- }
4485
- catch (err) {
4486
- pushMessage('system', `Could not save memory: ${err instanceof Error ? err.message : String(err)}`);
4487
- }
4574
+ void rememberProjectNote(note);
4488
4575
  return;
4489
4576
  }
4490
4577
  const escaped = shellEscapeCommand(value);
@@ -4550,21 +4637,23 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4550
4637
  });
4551
4638
  return;
4552
4639
  }
4553
- case 'agent-cockpit': {
4554
- // Reported after the read, not before it: the answer to "are
4555
- // there any" may be on disk, and a conversation whose evidence
4556
- // is sitting in its own directory must never be told it
4557
- // delegated nothing.
4640
+ case 'agents': {
4558
4641
  const agentIds = session?.agentIds ?? [];
4559
- void openAgentCockpit().then((opened) => {
4560
- if (opened)
4561
- return;
4562
- pushMessage('system', `No delegated agents in this conversation.${agentIds.length ? `\nAvailable: ${agentIds.join(', ')}` : '\nNo agents are configured.'}`);
4563
- });
4642
+ runAgentsCommand(slash.args, () => {
4643
+ // Reported after the read, not before it: the answer to "are
4644
+ // there any" may be in the session's logs, and a conversation
4645
+ // whose children are recorded there must never be told it
4646
+ // delegated nothing.
4647
+ void openAgentCockpit().then((opened) => {
4648
+ if (opened)
4649
+ return;
4650
+ pushMessage('system', `No delegated agents in this conversation.${agentIds.length ? `\nAvailable: ${agentIds.join(', ')}` : '\nNo agents are configured.'}`);
4651
+ });
4652
+ }, () => pushMessage('system', renderAgents(agentIds)));
4564
4653
  return;
4565
4654
  }
4566
- case 'agent-runs': {
4567
- openAgentRunsPicker();
4655
+ case 'abandon': {
4656
+ void abandonActiveTurn(slash.reason);
4568
4657
  return;
4569
4658
  }
4570
4659
  case 'provider-setup':
@@ -4574,21 +4663,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4574
4663
  }
4575
4664
  setProviderSetup(true);
4576
4665
  return;
4577
- case 'run-limits-set':
4578
- case 'run-limit-editor':
4579
- case 'run-limits-picker': {
4580
- const currentLimits = resolveRunGuards(ctxRef.current.limits, runLimitsOverrideRef.current);
4581
- if (slash.kind === 'run-limit-editor') {
4666
+ case 'turn-limits-set':
4667
+ case 'turn-limit-editor':
4668
+ case 'turn-limits-picker': {
4669
+ const currentLimits = resolveTurnGuards(ctxRef.current.limits, turnLimitsOverrideRef.current);
4670
+ if (slash.kind === 'turn-limit-editor') {
4582
4671
  const scope = scopeRef.current;
4583
4672
  if (!scope) {
4584
- pushMessage('system', 'Run limits are available after the session starts.');
4673
+ pushMessage('system', 'Turn limits are available after the session starts.');
4585
4674
  return;
4586
4675
  }
4587
- const field = RUN_LIMIT_FIELDS.find((field) => field.key === slash.key);
4676
+ const field = TURN_LIMIT_FIELDS.find((field) => field.key === slash.key);
4588
4677
  textPromptTokenRef.current += 1;
4589
4678
  setTextPrompt({
4590
4679
  token: textPromptTokenRef.current,
4591
- kind: 'run-limit',
4680
+ kind: 'turn-limit',
4592
4681
  limitKey: slash.key,
4593
4682
  title: `${field.label} (${slash.key === 'timeoutMs' ? 'ms; ' : ''}0 = unlimited)`,
4594
4683
  placeholder: field.hint,
@@ -4598,15 +4687,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4598
4687
  });
4599
4688
  return;
4600
4689
  }
4601
- if (slash.kind === 'run-limits-set') {
4602
- runLimitsOverrideRef.current = { ...runLimitsOverrideRef.current, ...slash.limits };
4690
+ if (slash.kind === 'turn-limits-set') {
4691
+ turnLimitsOverrideRef.current = { ...turnLimitsOverrideRef.current, ...slash.limits };
4603
4692
  }
4604
- const commands = runLimitCommands(resolveRunGuards(currentLimits, runLimitsOverrideRef.current));
4693
+ const commands = turnLimitCommands(resolveTurnGuards(currentLimits, turnLimitsOverrideRef.current));
4605
4694
  setSelectedChoice(0);
4606
4695
  setChoicePicker({
4607
4696
  kind: 'command',
4608
- title: 'Run limits',
4609
- notice: `${slash.kind === 'run-limits-set' ? 'Updated. ' : ''}This session · new turns and their agents. Existing runs keep their limits.`,
4697
+ title: 'Turn limits',
4698
+ notice: `${slash.kind === 'turn-limits-set' ? 'Updated. ' : ''}This session · new turns and their agents. Existing turns keep their limits.`,
4610
4699
  values: commands,
4611
4700
  options: commands.map((command) => ({
4612
4701
  label: command.label,
@@ -4638,8 +4727,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4638
4727
  },
4639
4728
  {
4640
4729
  name: 'config limits',
4641
- label: 'Run limits',
4642
- description: runLimitCommands(resolveRunGuards(ctxRef.current.limits, runLimitsOverrideRef.current))
4730
+ label: 'Turn limits',
4731
+ description: turnLimitCommands(resolveTurnGuards(ctxRef.current.limits, turnLimitsOverrideRef.current))
4643
4732
  .slice(0, 3)
4644
4733
  .map((field) => `${field.label}: ${field.description}`)
4645
4734
  .join(' · '),
@@ -4940,6 +5029,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4940
5029
  return;
4941
5030
  }
4942
5031
  case 'remember':
5032
+ if (slash.scope === 'project') {
5033
+ void rememberProjectNote(slash.text, slash.memoryType);
5034
+ return;
5035
+ }
4943
5036
  try {
4944
5037
  const saved = appendMemoryWithStatus(slash.text, { scope: slash.scope, cwd: ctx.cwd });
4945
5038
  pushMessage('system', renderMemorySaveResult(saved, slash.text));
@@ -4948,8 +5041,29 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4948
5041
  pushMessage('system', `Could not save memory: ${err instanceof Error ? err.message : String(err)}`);
4949
5042
  }
4950
5043
  return;
5044
+ case 'import-notes': {
5045
+ if (!session?.importCuratedNotes) {
5046
+ pushMessage('system', 'Stored memory is not open yet; /memory import-notes works once a session has started.');
5047
+ return;
5048
+ }
5049
+ void session.importCuratedNotes().then((report) => pushMessage('system', report), (err) => pushMessage('system', `Notes were not copied: ${err instanceof Error ? err.message : String(err)}`));
5050
+ return;
5051
+ }
4951
5052
  case 'show-memory': {
4952
- pushMessage('system', renderMemoryReport(readMemory(undefined, ctx.cwd), { cwd: ctx.cwd }));
5053
+ const curated = renderMemoryReport(readMemory(undefined, ctx.cwd), { cwd: ctx.cwd });
5054
+ const stored = session?.storedMemoryIndex;
5055
+ if (!stored) {
5056
+ pushMessage('system', curated);
5057
+ return;
5058
+ }
5059
+ void stored().then(({ directory, index, derived }) => {
5060
+ const section = renderStoredMemorySection(directory, index, derived);
5061
+ pushMessage('system', section
5062
+ ? curated.startsWith('No saved memory.')
5063
+ ? section
5064
+ : `${section}\n\n${curated}`
5065
+ : curated);
5066
+ }, (err) => pushMessage('system', `${curated}\n\nStored memories not loaded: ${err instanceof Error ? err.message : String(err)}`));
4953
5067
  return;
4954
5068
  }
4955
5069
  case 'list-skills': {
@@ -5042,9 +5156,35 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5042
5156
  pushMessage('system', 'No session yet; /add-dir works once one has started.');
5043
5157
  return;
5044
5158
  }
5045
- void dirs.add(slash.path).then((result) => {
5159
+ void dirs
5160
+ .add(slash.path, {
5161
+ // Outside the working directory, adding it lets every file
5162
+ // tool reach it unasked, so it is asked about once here.
5163
+ approve: (absolute) => {
5164
+ if (questionRef.current) {
5165
+ pushMessage('system', 'Answer the open question first; /add-dir asks one of its own.');
5166
+ return Promise.resolve(false);
5167
+ }
5168
+ return askQuestion({
5169
+ questionId: `add-dir:${absolute}`,
5170
+ question: `Let the file tools read and write ${absolute} for this session?`,
5171
+ header: 'Outside the working directory',
5172
+ options: [
5173
+ {
5174
+ id: 'yes',
5175
+ label: 'Yes, add it',
5176
+ description: 'File tools reach it without asking again; a sandboxed turn binds it read-write.',
5177
+ },
5178
+ { id: 'no', label: 'No', description: 'Leave it out' },
5179
+ ],
5180
+ multiSelect: false,
5181
+ allowFreeText: false,
5182
+ }).then((answer) => answer.kind === 'answer' && answer.selectedOptionIds.includes('yes'));
5183
+ },
5184
+ })
5185
+ .then((result) => {
5046
5186
  pushMessage('system', result.added
5047
- ? `Added ${result.path}. The file tools reach it by absolute path from the next turn; a sandboxed run binds it read-write.`
5187
+ ? `Added ${result.path}. The file tools reach it by absolute path from the next turn; a sandboxed turn binds it read-write.`
5048
5188
  : `Not added: ${result.path} — ${result.reason ?? 'refused'}`);
5049
5189
  });
5050
5190
  return;
@@ -5079,7 +5219,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5079
5219
  if (goalCommand)
5080
5220
  await materializeConversation();
5081
5221
  const durableSessions = sessionsRef.current;
5082
- const runScope = scopeRef.current;
5222
+ const sessionScope = scopeRef.current;
5083
5223
  const taskStore = slash.name === 'tasks' ? session?.currentTaskStore?.() : undefined;
5084
5224
  if (slash.name === 'tasks' && !taskStore) {
5085
5225
  pushMessage('system', session?.currentTaskStore
@@ -5091,11 +5231,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5091
5231
  registry.register(kernelHostCommands({
5092
5232
  allowedAgentIds: session?.agentIds ?? [],
5093
5233
  ...(taskStore ? { taskStore } : {}),
5094
- ...(durableSessions && runScope && conversationMaterializedRef.current
5234
+ ...(durableSessions && sessionScope && conversationMaterializedRef.current
5095
5235
  ? {
5096
5236
  goal: {
5097
5237
  store: durableSessions.goals,
5098
- sessionId: runScope.sessionId,
5238
+ sessionId: sessionScope.sessionId,
5099
5239
  tenantId: durableSessions.tenantId,
5100
5240
  activation: goalActivation,
5101
5241
  },
@@ -5140,7 +5280,6 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5140
5280
  setChoicePicker({
5141
5281
  kind: 'feedback-rating',
5142
5282
  title: 'Rate the latest answer',
5143
- runId: target.runId,
5144
5283
  messageId: target.messageId,
5145
5284
  sessionId: target.sessionId,
5146
5285
  values,
@@ -5160,7 +5299,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5160
5299
  pushMessage('system', 'Nothing to rate yet.');
5161
5300
  return;
5162
5301
  }
5163
- recordFeedback(target.sessionId, target.runId, slash.messageId, slash.rating, slash.note);
5302
+ recordFeedback(target.sessionId, slash.messageId, slash.rating, slash.note);
5164
5303
  return;
5165
5304
  }
5166
5305
  case 'review': {
@@ -5249,16 +5388,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5249
5388
  pushMessage('system', 'Nothing to compact yet — this conversation is short enough that adding a summary would save no messages.');
5250
5389
  return;
5251
5390
  }
5252
- // Durable first, screen second. Reporting success and only then
5253
- // discovering `/resume` still has the old history is the exact
5254
- // false-success state this command used to create.
5255
- const sessions = sessionsRef.current;
5256
- const destination = scopeRef.current?.sessionId;
5257
- if (sessions && destination) {
5258
- await persistenceTailRef.current;
5259
- await replaceConversation(sessions, destination, result.messages);
5260
- }
5261
- modelHistoryRef.current = result.messages;
5391
+ // Durable first, screen second: for a conversation in a log,
5392
+ // `session.compact` has already appended the compaction record
5393
+ // the next `/resume` folds, before it returned.
5394
+ const durable = Boolean(sessionsRef.current && scopeRef.current && conversationMaterializedRef.current);
5395
+ modelHistoryRef.current = [...result.messages];
5262
5396
  // How many user/assistant turns survived the pass.
5263
5397
  // `keepRecentRows` explains why the transcript is trimmed
5264
5398
  // to match rather than rebuilt from `result.messages`.
@@ -5274,7 +5408,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5274
5408
  role: 'system',
5275
5409
  content: `Compacted ${result.shed} earlier message(s) into a summary.${result.usage.totalTokens > 0
5276
5410
  ? ` Verifier used ${result.usage.totalTokens.toLocaleString('en-US')} tokens.`
5277
- : ''}${sessions && destination
5411
+ : ''}${durable
5278
5412
  ? ''
5279
5413
  : ' Conversation persistence is unavailable, so this compacted history lasts only for this process.'}`,
5280
5414
  detail: summaryDetail(result.summary),
@@ -5406,9 +5540,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5406
5540
  hostCommands,
5407
5541
  nextId,
5408
5542
  openAgentCockpit,
5409
- openAgentRunsPicker,
5543
+ runAgentsCommand,
5544
+ abandonActiveTurn,
5410
5545
  pushMessage,
5411
5546
  rawOutput,
5547
+ rememberProjectNote,
5412
5548
  removeStoredCredential,
5413
5549
  resetTranscript,
5414
5550
  runConversationExport,
@@ -5502,7 +5638,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5502
5638
  }
5503
5639
  const durableSessions = sessionsRef.current;
5504
5640
  const scope = scopeRef.current;
5505
- if (!durableSessions?.turnEvidence || !scope)
5641
+ if (!durableSessions || !scope || !conversationMaterializedRef.current)
5506
5642
  return;
5507
5643
  const generation = conversationGenRef.current;
5508
5644
  const sessionId = scope.sessionId;
@@ -5858,7 +5994,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5858
5994
  ? permission.review
5859
5995
  : (permission.summary.compactText ?? permission.summary.text);
5860
5996
  const count = permissionReviewRows(source, terminal.columns).length;
5861
- const pageRows = Math.max(1, permissionReviewPageRows(terminal.rows) - (permission.runId ? 1 : 0));
5997
+ const pageRows = Math.max(1, permissionReviewPageRows(terminal.rows) - (reviewSource(permission, scopeRef.current?.sessionId) ? 1 : 0));
5862
5998
  const maxOffset = Math.max(0, count - pageRows);
5863
5999
  const current = Math.min(permissionReviewOffsetRef.current, maxOffset);
5864
6000
  const next = key.home
@@ -6263,8 +6399,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6263
6399
  // while the provider event that removes the generic tool row arrives on the
6264
6400
  // next tick; filtering only live children made that raw row flash back.
6265
6401
  const visibleActiveTools = activeTools.filter((tool) => tool.toolName.toLowerCase() !== 'agent' ||
6266
- !(tool.runId
6267
- ? representedSubagentTools.has(JSON.stringify([tool.runId, tool.id]))
6402
+ !(tool.turnId
6403
+ ? representedSubagentTools.has(JSON.stringify([tool.turnId, tool.id]))
6268
6404
  : representedUnscopedSubagentToolUseIds.has(tool.id)));
6269
6405
  const outputBlocks = outputViewer
6270
6406
  ? messages.filter((m) => m.detailRef !== undefined && (m.detail?.length ?? 0) > 0)
@@ -6321,11 +6457,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6321
6457
  : goalStatus && phase === 'ready' && state === 'idle'
6322
6458
  ? undefined
6323
6459
  : hintForPhase(phase, state, session?.hasProvider === true, composerHasDraft);
6324
- const permissionOwner = permission?.runId
6325
- ? subagents.find((agent) => agent.runId === permission.runId)
6460
+ const permissionSource = reviewSource(permission, scopeRef.current?.sessionId);
6461
+ const permissionOwner = permissionSource
6462
+ ? subagents.find((agent) => agent.sessionId === permissionSource)
6326
6463
  : undefined;
6327
- const permissionSourceLabel = permission?.runId
6328
- ? permissionOwner ? `Agent: ${permissionOwner.description}` : `Run: ${permission.runId}`
6464
+ const permissionSourceLabel = permissionSource
6465
+ ? permissionOwner
6466
+ ? `Agent: ${permissionOwner.description}`
6467
+ : `Session: ${permissionSource}`
6329
6468
  : undefined;
6330
6469
  const displayedPermissionMode = effectivePermissionMode(permissionMode, session?.approvalLatched() ?? false);
6331
6470
  // True exactly when the phase ternary below renders its final branch — the
@@ -6483,27 +6622,37 @@ function goalStatusLabel(goal, armed) {
6483
6622
  return 'Goal achieved';
6484
6623
  }
6485
6624
  }
6625
+ /**
6626
+ * The child session a tool review came from, when it is not this
6627
+ * conversation's own turn: what names the agent asking in the prompt. Read by
6628
+ * field, because the review request names its session only when the kernel
6629
+ * attributes it.
6630
+ */
6631
+ function reviewSource(request, currentSessionId) {
6632
+ const sessionId = request?.sessionId;
6633
+ return typeof sessionId === 'string' && sessionId !== currentSessionId ? sessionId : undefined;
6634
+ }
6486
6635
  /** The label the operator saw for the option id they chose. */
6487
6636
  function labelOfOption(question, optionId) {
6488
6637
  return question.options.find((option) => option.id === optionId)?.label ?? optionId;
6489
6638
  }
6490
- /** `/agents runs`' one-line-per-run text, mirroring `/jobs`' own column shape. */
6491
- function agentRunOption(run) {
6492
- const agents = `${run.agentsDone}/${run.agentsTotal} agent${run.agentsTotal === 1 ? '' : 's'}`;
6493
- const tokens = `${run.tokensTotal.toLocaleString('en-US')} tokens`;
6639
+ /** `/agents batches`' one-line-per-batch text, mirroring `/jobs`' own column shape. */
6640
+ function batchOption(batch) {
6641
+ const agents = `${batch.agentsDone}/${batch.agentsTotal} agent${batch.agentsTotal === 1 ? '' : 's'}`;
6642
+ const tokens = `${batch.tokensTotal.toLocaleString('en-US')} tokens`;
6494
6643
  const columns = [
6495
- relativeRunStart(run.startedAt),
6496
- run.phases.join(', '),
6644
+ relativeStart(batch.startedAt),
6645
+ batch.phases.join(', '),
6497
6646
  agents,
6498
6647
  tokens,
6499
- formatElapsed(run.elapsedMs),
6648
+ formatElapsed(batch.elapsedMs),
6500
6649
  ];
6501
- if (run.live)
6650
+ if (batch.live)
6502
6651
  columns.push('live');
6503
- return { label: run.name, description: columns.join(' · ') };
6652
+ return { label: batch.name, description: columns.join(' · ') };
6504
6653
  }
6505
6654
  /** `just now` / `12m ago` / `3h ago` / `2d ago` — the same buckets a conversation's own resume picker shows. */
6506
- function relativeRunStart(startedAt) {
6655
+ function relativeStart(startedAt) {
6507
6656
  const minutes = Math.round((Date.now() - startedAt) / 60_000);
6508
6657
  if (minutes < 1)
6509
6658
  return 'just now';