@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/agent.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { createCurrentCredentialReader } from '../integrations/providers/current-credential.js';
2
- import { CliPathBuilder } from '../integrations/sessions/paths.js';
3
2
  import { createWebSearchTool, resolveWebSearch, webSearchLabel, } from '../integrations/web/search.js';
4
3
  /**
5
4
  * TUI agent session — provider-direct, tool-enabled.
@@ -14,26 +13,28 @@ import { createWebSearchTool, resolveWebSearch, webSearchLabel, } from '../integ
14
13
  * Unlike the earlier `chatStream()`-only adapter, this drives the full
15
14
  * tool-execution loop: the model can call tools, their results are fed
16
15
  * back, and the loop iterates until the turn settles. We translate the
17
- * SDK's `RunEvent` stream into the TUI's smaller `AgentEvent` vocabulary
16
+ * SDK's `SessionEvent` stream into the TUI's smaller `AgentEvent` vocabulary
18
17
  * (text deltas + tool start/end + done/error).
19
18
  *
20
- * The TUI owns conversation history and passes the full `Message[]` on
21
- * every turn (stateless session). Empty / partial states (no
19
+ * The TUI holds the conversation it renders and passes the full `Message[]`
20
+ * on every turn; the session's log, which the kernel appends to while the
21
+ * turn runs, is what a later resume folds back. Empty / partial states (no
22
22
  * credentials, no preferences, no matching detected provider) return an
23
23
  * `emptySession()` whose `send()` yields a single error event so the UI
24
24
  * renders an actionable hint rather than crashing.
25
25
  */
26
- import { BOOT_EVENT_NAMES, BackgroundJobRegistry, DiskCheckpointStore, DiskMemoryStore, DiskTaskStore, EVENT_NAME_ATTRIBUTE, GuardedFetchProvider, PromptContributionRegistry, ProviderRegistry, SESSION_GOAL_TOOL_NAMES, ToolRegistry, WebFetchTool, asRunId, batchNeedsReview, buildAskUserQuestionTool, buildMemoryTools, buildResidentHistoryTools, buildResidentToolEvidenceTools, buildSessionGoalTools, compactNow, createComputerUseTool, createFileReadTracker, createMemoryPromoter, createMemoryRecallStep, createResidentStepContext, createResidentStepContributions, createReviewHandler, createToolPresenter, generateProjectId, generateRunId, generateSessionId, generateTenantId, generateTopicId, getBuiltinTools, isReviewExempt, query, resumeRun, seedObservationLedger, webGuidanceContribution, withProviderFallback, } from '@namzu/sdk';
26
+ import { BOOT_EVENT_NAMES, BackgroundJobRegistry, DiskSessionCheckpointStore, DiskSessionLog, DiskTaskStore, EVENT_NAME_ATTRIBUTE, GuardedFetchProvider, InMemorySessionLog, MarkdownMemoryStore, PromptContributionRegistry, ProviderRegistry, SESSION_GOAL_TOOL_NAMES, SessionPaths, ToolRegistry, WebFetchTool, abandonTurn, batchNeedsReview, buildAskUserQuestionTool, buildMemoryTools, buildResidentHistoryTools, buildResidentToolEvidenceTools, buildSessionGoalTools, compactNow, compactSession, createComputerUseTool, createFileReadTracker, createMemoryPromoter, createMemoryRecallStep, createResidentStepContext, createResidentStepContributions, createReviewHandler, createToolPresenter, ensureProject, generateSessionId, generateTenantId, generateTopicId, generateTurnId, getBuiltinTools, isReviewExempt, isTurnInProgressError, query, resumeSession, seedObservationLedger, webGuidanceContribution, withProviderFallback, } from '@namzu/sdk';
27
27
  import { SubprocessComputerUseHost } from '@namzu/computer-use';
28
28
  import { realpath, stat } from 'node:fs/promises';
29
- import { join, parse, resolve } from 'node:path';
29
+ import { parse, resolve } from 'node:path';
30
30
  import { FileCheckpointStore } from '../checkpoints/store.js';
31
31
  import { CHECKPOINTED_TOOLS, withCheckpoints } from '../checkpoints/wrap.js';
32
- import { readStoredRunGuards, resolveRunGuards } from '../config/run-limits.js';
32
+ import { readStoredTurnGuards, resolveTurnGuards } from '../config/run-limits.js';
33
33
  import { configuredPassthroughTools, resolveToolResultScreens, unmatchedPassthroughTools, } from '../config/tool-result-screens.js';
34
34
  import { probeCapabilities } from '../context/capabilities.js';
35
+ import { createSessionDirectories } from '../context/directories.js';
35
36
  import { NAMZU_DELEGATION_DOCTRINE, NAMZU_ORCHESTRATE_DOCTRINE, NAMZU_PLAN_MODE_DOCTRINE, NAMZU_WORKING_DOCTRINE, } from '../context/doctrine.js';
36
- import { composeEnvironmentPrompt, readEnvironmentFacts } from '../context/environment.js';
37
+ import { composeEnvironmentPrompt, detectWsl, readEnvironmentFacts, } from '../context/environment.js';
37
38
  import { ProjectInstructionTracker } from '../context/project-tracker.js';
38
39
  import { resolveSandbox, sandboxResolvedSeverity, } from '../context/sandbox.js';
39
40
  import { composeTurnSnapshot, readTurnSnapshot } from '../context/turn-snapshot.js';
@@ -44,23 +45,28 @@ import { canSelectModel } from '../integrations/providers/access.js';
44
45
  import { createGeminiAccessTokenResolver } from '../integrations/providers/gemini-credentials.js';
45
46
  import { CredentialRefreshRejectedError, CredentialWithdrawnError, PROVIDER_REGISTRY, chainCapabilityDisagreements, chainPositionName, describeAcceptedMismatch, describeCapabilityRefusal, discoverProviders, ensureFreshAnthropicToken, ensureFreshStoredCodexCredential, ensureRegistered, findDetected, isAnthropicOAuthToken, isRegistered, missingCredentialMessage, primaryProvider, readCodexCredentialFile, readPreferences, readSubscriptionCredential, resolveChainCapabilities, sameOAuthCredential, unresolvedMembers, unsupportedProviderMessage, } from '../integrations/providers/index.js';
46
47
  import { modelReasoningView } from '../integrations/providers/model-reasoning.js';
47
- import { retainManualCompaction } from '../integrations/sessions/compaction-evidence.js';
48
+ import { sessionLogCheckpointView } from '../integrations/sessions/checkpoint-view.js';
48
49
  import { createContextInventoryStep } from '../integrations/sessions/context-inventory.js';
49
50
  import { CONVERSATION_EVIDENCE_GUIDANCE, buildConversationReadTool, buildConversationSearchTool, releaseConversationEvidence, } from '../integrations/sessions/conversation-search.js';
50
51
  import { createConversationEvidenceRecall } from '../integrations/sessions/evidence-recall.js';
52
+ import { ensureSessionStarted } from '../integrations/sessions/store.js';
51
53
  import { createTaskContextStep } from '../integrations/sessions/task-context.js';
54
+ import { resolveNamzuHome } from '../integrations/state/home.js';
52
55
  import { ensurePrivateStateDirectory } from '../integrations/state/private-directory.js';
56
+ import { cliProjectRoot } from '../integrations/state/project.js';
57
+ import { CLI_CHECKPOINT_RETENTION } from '../integrations/state/retention.js';
58
+ import { listSavedBatches } from '../integrations/subagents/batches.js';
53
59
  import { discoverAgentDefinitions } from '../integrations/subagents/definitions.js';
54
- import { createDelegationHistoryStep } from '../integrations/subagents/history.js';
55
60
  import { prepareDelegatedEffort } from '../integrations/subagents/model-effort.js';
56
- import { SubagentPathBuilder, resolveSubagentParent } from '../integrations/subagents/parent.js';
61
+ import { resolveSubagentParent } from '../integrations/subagents/parent.js';
57
62
  import { replaySavedChildrenFor } from '../integrations/subagents/replay.js';
58
- import { listSavedOrchestrationRuns, } from '../integrations/subagents/runs.js';
59
63
  import { createSubagentRuntime } from '../integrations/subagents/runtime.js';
64
+ import { createSavedAgentHistory, createSavedAgentsStep, } from '../integrations/subagents/saved-agents.js';
60
65
  import { cliLogger } from '../logging.js';
61
66
  import { formatMemoryDiagnostics } from '../memory/presentation.js';
62
67
  import { composeMemoryPrompt, readMemory } from '../memory/store.js';
63
- import { projectRunConversation } from './conversation-history.js';
68
+ import { composeStoredMemoryPrompt, describeCuratedNotesImport, describeMemoryMigration, importCuratedNotes, migrateMemoryOnce, saveTypedNote, } from '../memory/typed.js';
69
+ import { projectTurnConversation } from './conversation-history.js';
64
70
  import { buildSwitchModelTool } from './model-switch-tool.js';
65
71
  import { resolveModelSwitch } from './model-switch.js';
66
72
  /**
@@ -438,10 +444,10 @@ function builtinTools(backgroundJobs) {
438
444
  return [tool.name === 'bash' ? foregroundOnlyBash(tool) : tool];
439
445
  });
440
446
  }
441
- function buildToolRegistry(cwd, projectStateRoot = join(cwd, '.namzu'), backgroundJobs = true, checkpoints, projectId, screens) {
442
- // Configured here rather than on the run, so every registry this CLI
443
- // builds for a run carries the operator's choice — including the sub-agent
444
- // registries below, which a run-level option would reach only if each
447
+ function buildToolRegistry(paths, backgroundJobs, checkpoints, screens) {
448
+ // Configured here rather than on the turn, so every registry this CLI
449
+ // builds for a turn carries the operator's choice — including the sub-agent
450
+ // registries below, which a turn-level option would reach only if each
445
451
  // child's config were threaded as well. An absent key stays absent, so the
446
452
  // kernel's default applies exactly as it does for any other host.
447
453
  const screensConfig = resolveToolResultScreens(screens);
@@ -459,21 +465,20 @@ function buildToolRegistry(cwd, projectStateRoot = join(cwd, '.namzu'), backgrou
459
465
  registry.register(withCheckpoints(tool, checkpoints));
460
466
  }
461
467
  }
462
- // SDK memory: the agent gets search_memory / read_memory / save_memory over
463
- // a structured store in this Project's generated-state directory. CLI
464
- // surfaces inject the central application-home hierarchy; embedded callers
465
- // that omit it retain the historical `<cwd>/.namzu` layout.
466
- // Separate from the user-curated MEMORY.md that is injected into the prompt.
467
- const memoryRoot = ensurePrivateStateDirectory(projectStateRoot, 'memory');
468
- const directory = projectId ? ensurePrivateStateDirectory(memoryRoot, projectId) : memoryRoot;
469
- const memoryStore = new DiskMemoryStore({ baseDir: projectStateRoot, directory });
468
+ // Stored memory: the agent gets search_memory / read_memory / save_memory
469
+ // over typed Markdown files, one per memory, in this project's `memory/`
470
+ // under the application home (`projects/<slug>/memory`), so every
471
+ // workspace keeps its own. Separate from the operator-curated files, which
472
+ // are prompt text.
473
+ const directory = ensurePrivateStateDirectory(paths.projectDir(), 'memory');
474
+ const memoryStore = new MarkdownMemoryStore({ directory });
470
475
  // Search through the store's async boundary. Its concrete index is lazy:
471
476
  // handing `getIndex()` to the synchronous overload before the first store
472
477
  // read makes a new process report every persisted memory as absent.
473
478
  registry.register(buildMemoryTools(memoryStore));
474
479
  // query() mounts search_tools only if a deferred roster actually exists,
475
480
  // after runtime tools are registered. Ordinary CLI task tools are active.
476
- return { registry, memoryStore };
481
+ return { registry, memoryStore, memoryDirectory: directory };
477
482
  }
478
483
  /** Refresh plugin skill metadata before each provider operation. */
479
484
  async function currentPluginSkills(registry) {
@@ -483,7 +488,6 @@ async function currentPluginSkills(registry) {
483
488
  return registry.list();
484
489
  }
485
490
  export async function createAgentSession(prefs, detected, options = {}) {
486
- const scope = options.scope ?? mintScope();
487
491
  const fileObservations = new Map();
488
492
  /**
489
493
  * This process's observation ledger for one conversation, seeded from that
@@ -552,21 +556,34 @@ export async function createAgentSession(prefs, detected, options = {}) {
552
556
  cwd === parse(cwd).root) {
553
557
  return emptySession(`Working-directory sandboxing refuses filesystem root ${cwd}; choose a project directory so confinement has a boundary, or explicitly select an ephemeral workspace.`, 'invocation');
554
558
  }
555
- const hierarchyRoot = resolve(options.stateRoot ?? join(cwd, '.namzu'));
556
- const pathBuilder = new CliPathBuilder(hierarchyRoot);
557
- const projectStateRoot = hierarchyRoot;
559
+ // Generated state never defaults into the working directory. It used to:
560
+ // `<cwd>/.namzu` for any caller without a state root, which put runtime
561
+ // trees inside checkouts (and, run from the home directory, made the
562
+ // project root and the application home the same directory). It lives in
563
+ // the checkout's project under the application home: `projects/<slug>/`.
564
+ let paths;
565
+ let projectId;
558
566
  try {
559
- ensurePrivateStateDirectory(hierarchyRoot, 'sessions');
567
+ if (options.conversationSessions) {
568
+ paths = options.conversationSessions.paths;
569
+ projectId = options.conversationSessions.projectId;
570
+ }
571
+ else {
572
+ const home = resolve(options.stateRoot ?? resolveNamzuHome());
573
+ ensurePrivateStateDirectory(home, 'projects');
574
+ const project = await ensureProject({ home, cwd: cliProjectRoot(cwd) });
575
+ paths = new SessionPaths({ home, slug: project.slug });
576
+ projectId = project.projectId;
577
+ }
560
578
  // Refuse an aliased or otherwise unsafe generated-state root before any
561
- // provider, sandbox or plugin runtime is constructed. Project-authored
562
- // `.namzu` content may coexist here, but generated memory must never be
563
- // redirected outside the trusted working directory through an ancestor
564
- // symlink.
565
- ensurePrivateStateDirectory(projectStateRoot, 'memory');
579
+ // provider, sandbox or plugin runtime is constructed.
580
+ ensurePrivateStateDirectory(resolve(paths.projectDir(), '..'), paths.slug);
581
+ ensurePrivateStateDirectory(paths.projectDir(), 'memory');
566
582
  }
567
583
  catch (error) {
568
584
  return emptySession(`Project state is unavailable: ${describeError(error)}`, 'environment');
569
585
  }
586
+ const scope = options.scope ?? mintScope(projectId, options.sessionId);
570
587
  // The head serves; the tail is fallen over to, in order, when it cannot.
571
588
  const primary = primaryProvider(prefs);
572
589
  const entry = PROVIDER_REGISTRY[primary.id];
@@ -585,7 +602,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
585
602
  // can actually be entered. What still arrives here is a headless caller —
586
603
  // `run`, `run-stream`, `drain` — which has no picker and for which both
587
604
  // pieces of advice below are real: an environment variable, or
588
- // `--provider`. Keeping the refusal is what makes those runs fail rather
605
+ // `--provider`. Keeping the refusal is what makes those turns fail rather
589
606
  // than quietly start on something else.
590
607
  return emptySession(`No credential found for ${entry.label}${entry.id === 'zen' ? ' with the selected model. Choose muse-spark-1.3-contributor-free for public access' : ''}. Set one of: ${entry.envVars.join(', ')} — or pass --provider with one that is configured.`);
591
608
  }
@@ -826,7 +843,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
826
843
  const providerForSession = (sessionId) => primary.id === 'zen' || primary.id === 'zen-go'
827
844
  ? constructProvider(primary.id, det, model, { sessionId })
828
845
  : provider;
829
- // Session-owned discovery with one drain cursor per run. A child shares the
846
+ // Session-owned discovery with one drain cursor per turn. A child shares the
830
847
  // discovered scopes without being able to consume the parent's update, and
831
848
  // an edit takes effect in this session rather than only after reconnecting.
832
849
  const projectInstructions = new ProjectInstructionTracker(cwd);
@@ -860,7 +877,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
860
877
  });
861
878
  throw err;
862
879
  }
863
- // AFTER resolveSandbox returns — the honest report of what THIS run got,
880
+ // AFTER resolveSandbox returns — the honest report of what THIS turn got,
864
881
  // never what was attempted. `unconfined` decides the severity: per the
865
882
  // design, this is "the single highest-value line in the whole design,
866
883
  // today computed and thrown away" — an operator reading default `info`
@@ -870,6 +887,33 @@ export async function createAgentSession(prefs, detected, options = {}) {
870
887
  [EVENT_NAME_ATTRIBUTE]: BOOT_EVENT_NAMES.SANDBOX_RESOLVED,
871
888
  'namzu.sandbox.unconfined': sandbox.unconfined,
872
889
  });
890
+ // The two boundaries a turn can be asked to cross, as questions rather
891
+ // than refusals. A path outside the working directory is reviewed on a
892
+ // host turn (the kernel looks only when there is no sandbox, since a
893
+ // sandboxed path is not mounted to be reached). A sandboxed command's
894
+ // escape is reviewed unless the operator turned escapes off; it is
895
+ // confirmed only by a person, or by `allowUnattendedEscape`.
896
+ const sandboxEscape = options.sandbox?.allowEscape === false ? 'refuse' : 'review';
897
+ const escalation = { outsideRootAccess: 'review', sandboxEscape };
898
+ const unattendedSandboxEscape = options.sandbox?.allowUnattendedEscape === true ? 'allow' : 'refuse';
899
+ // Read once: whether this is WSL does not change inside a session.
900
+ const wsl = detectWsl();
901
+ // Whether the last send had somebody to answer a prompt. A child's
902
+ // environment is composed outside any send, so it reads this.
903
+ let lastSendInteractive = false;
904
+ const boundaryFor = (interactive) => ({
905
+ ...(sandbox.provider && sandbox.environment
906
+ ? { sandbox: { environment: sandbox.environment, enforced: sandbox.enforced } }
907
+ : {}),
908
+ escape: sandboxEscape === 'refuse'
909
+ ? 'refused'
910
+ : interactive
911
+ ? 'ask'
912
+ : unattendedSandboxEscape === 'allow'
913
+ ? 'unattended'
914
+ : 'refused',
915
+ interactive,
916
+ });
873
917
  // Always built: the executor hands it to the tools only where it is
874
918
  // safe — on the host, or inside a sandbox that can start a detached
875
919
  // process — so a session under a sandbox that cannot simply has none.
@@ -889,27 +933,41 @@ export async function createAgentSession(prefs, detected, options = {}) {
889
933
  if (absolute !== resolve(cwd) && !directories.includes(absolute))
890
934
  directories.push(absolute);
891
935
  }
892
- const sessionDirectories = {
893
- list: () => [...directories],
894
- add: async (path) => {
895
- const absolute = resolve(cwd, path);
896
- if (absolute === resolve(cwd))
897
- return {
898
- added: false,
899
- path: absolute,
900
- reason: 'That is the working directory.',
901
- };
902
- if (directories.includes(absolute))
903
- return { added: false, path: absolute, reason: 'Already added.' };
904
- const entry = await stat(absolute).catch(() => null);
905
- if (!entry?.isDirectory())
906
- return { added: false, path: absolute, reason: 'Not a directory.' };
907
- directories.push(absolute);
908
- return { added: true, path: absolute };
909
- },
936
+ const sessionDirectories = createSessionDirectories(cwd, directories);
937
+ // `/restore` snapshots live with the conversation they belong to
938
+ // (`<session-id>/file-history/`), not in a tree of their own. Read per
939
+ // turn: `scope.sessionId` moves when the operator switches conversation.
940
+ const checkpoints = new FileCheckpointStore(() => paths.fileHistory({ sessionId: scope.sessionId }), cwd);
941
+ const { registry, memoryStore, memoryDirectory } = buildToolRegistry(paths, backgroundJobs, checkpoints, options.toolResultScreens);
942
+ // Once per store, idempotently: a launch that finds nothing to move moves
943
+ // nothing, and one interrupted halfway is finished by the next. A failure
944
+ // is a notice, never a refusal to start — the curated files and the store
945
+ // both still work without it.
946
+ const memoryMigrationNotices = await migrateMemoryOnce({
947
+ store: memoryStore,
948
+ directory: memoryDirectory,
949
+ cwd,
950
+ })
951
+ .then((report) => describeMemoryMigration(report, memoryDirectory))
952
+ .catch((error) => [
953
+ `Stored memory migration did not run: ${error instanceof Error ? error.message : String(error)}`,
954
+ ]);
955
+ /**
956
+ * The stored-memory index for this turn's prompt, or null. A store that
957
+ * refuses to read (a malformed memory file) costs the turn its index and
958
+ * the operator a notice, not the turn.
959
+ */
960
+ const storedMemoryPrompt = async () => {
961
+ try {
962
+ return { prompt: composeStoredMemoryPrompt(await memoryStore.readIndex()) };
963
+ }
964
+ catch (error) {
965
+ return {
966
+ prompt: null,
967
+ notice: `Stored memory index not loaded: ${error instanceof Error ? error.message : String(error)}`,
968
+ };
969
+ }
910
970
  };
911
- const checkpoints = new FileCheckpointStore(join(ensurePrivateStateDirectory(projectStateRoot, 'checkpoints'), scope.sessionId), cwd);
912
- const { registry, memoryStore } = buildToolRegistry(cwd, projectStateRoot, backgroundJobs, checkpoints, options.stateRoot ? scope.projectId : undefined, options.toolResultScreens);
913
971
  // Package presence is not tool reachability. The CLI used to probe and
914
972
  // report @namzu/computer-use without ever constructing its host or mounting
915
973
  // SDK's computer_use definition, so even an installed, healthy package was
@@ -958,12 +1016,12 @@ export async function createAgentSession(prefs, detected, options = {}) {
958
1016
  // Per-send denial further keeps the schemas out of ordinary human turns.
959
1017
  const goalAuthorities = new Map();
960
1018
  // A child is created after the parent query has started, from inside its
961
- // Agent tool. Keying the review channel by the executing run keeps two
1019
+ // Agent tool. Keying the review channel by the executing turn keeps two
962
1020
  // concurrent sends from borrowing each other's prompt or approval latch.
963
1021
  const delegatedResumeHandlers = new Map();
964
1022
  const goalToolNames = new Set(SESSION_GOAL_TOOL_NAMES);
965
1023
  if (options.sessionGoals) {
966
- registry.register(buildSessionGoalTools(options.sessionGoals, (runId) => goalAuthorities.get(runId)));
1024
+ registry.register(buildSessionGoalTools(options.sessionGoals, (turnId) => goalAuthorities.get(turnId)));
967
1025
  }
968
1026
  // External tool servers, before the roster is counted, so `toolNames` and
969
1027
  // the `/tools` list a user reads include what they configured. Connecting
@@ -1036,13 +1094,13 @@ export async function createAgentSession(prefs, detected, options = {}) {
1036
1094
  const history = options.residentHistory;
1037
1095
  const historyOwner = { ...scope };
1038
1096
  registry.register(buildResidentHistoryTools((context) => {
1039
- const owner = delegationScopes.get(context.runId);
1097
+ const owner = delegationScopes.get(context.turnId);
1040
1098
  if (!owner ||
1041
1099
  owner.sessionId !== historyOwner.sessionId ||
1042
1100
  owner.projectId !== historyOwner.projectId ||
1043
1101
  owner.tenantId !== historyOwner.tenantId ||
1044
1102
  owner.tenantId !== history.scope.tenantId)
1045
- throw new Error('The requesting run does not own this resident history.');
1103
+ throw new Error('The requesting turn does not own this resident history.');
1046
1104
  return history;
1047
1105
  }));
1048
1106
  }
@@ -1050,14 +1108,14 @@ export async function createAgentSession(prefs, detected, options = {}) {
1050
1108
  const evidence = options.residentToolEvidence;
1051
1109
  const evidenceOwner = { ...scope };
1052
1110
  registry.register(buildResidentToolEvidenceTools((context) => {
1053
- const owner = delegationScopes.get(context.runId);
1111
+ const owner = delegationScopes.get(context.turnId);
1054
1112
  if (!owner ||
1055
1113
  owner.sessionId !== evidenceOwner.sessionId ||
1056
1114
  owner.projectId !== evidenceOwner.projectId ||
1057
1115
  owner.tenantId !== evidenceOwner.tenantId ||
1058
1116
  owner.projectId !== evidence.scope.projectId ||
1059
1117
  owner.tenantId !== evidence.scope.tenantId)
1060
- throw new Error('The requesting run does not own this resident tool evidence.');
1118
+ throw new Error('The requesting turn does not own this resident tool evidence.');
1061
1119
  return evidence;
1062
1120
  }));
1063
1121
  }
@@ -1065,11 +1123,11 @@ export async function createAgentSession(prefs, detected, options = {}) {
1065
1123
  const sessions = options.conversationSessions;
1066
1124
  for (const build of [buildConversationSearchTool, buildConversationReadTool])
1067
1125
  registry.register(build((context) => {
1068
- const owner = delegationScopes.get(context.runId);
1126
+ const owner = delegationScopes.get(context.turnId);
1069
1127
  if (!owner ||
1070
1128
  owner.projectId !== sessions.projectId ||
1071
1129
  owner.tenantId !== sessions.tenantId)
1072
- throw new Error('The requesting run does not own this conversation.');
1130
+ throw new Error('The requesting turn does not own this conversation.');
1073
1131
  return { sessions, sessionId: owner.sessionId };
1074
1132
  }));
1075
1133
  }
@@ -1080,18 +1138,31 @@ export async function createAgentSession(prefs, detected, options = {}) {
1080
1138
  return [];
1081
1139
  let step = evidenceRecallSteps.get(sessionId);
1082
1140
  if (!step) {
1083
- step = createConversationEvidenceRecall(sessions, sessionId, (runId) => {
1084
- const owner = delegationScopes.get(asRunId(runId));
1141
+ step = createConversationEvidenceRecall(sessions, sessionId, (turnId) => {
1142
+ const owner = turnId === undefined ? undefined : delegationScopes.get(turnId);
1085
1143
  if (!owner ||
1086
1144
  owner.sessionId !== sessionId ||
1087
1145
  owner.tenantId !== sessions.tenantId ||
1088
1146
  owner.projectId !== sessions.projectId)
1089
- throw new Error('The requesting run no longer owns this conversation.');
1147
+ throw new Error('The requesting turn no longer owns this conversation.');
1090
1148
  }, options.compaction?.resolveEvidenceQueries !== false);
1091
1149
  evidenceRecallSteps.set(sessionId, step);
1092
1150
  }
1093
1151
  return [step];
1094
1152
  };
1153
+ // The saved children of a conversation, read back from the session index and
1154
+ // the child logs: the delegation history a turn is told about.
1155
+ const conversationIndex = options.conversationSessions?.index;
1156
+ const savedAgentsSteps = (sessionId) => conversationIndex
1157
+ ? [
1158
+ createSavedAgentsStep(createSavedAgentHistory({
1159
+ index: conversationIndex,
1160
+ paths,
1161
+ session: { sessionId },
1162
+ log: cliLogger(),
1163
+ })),
1164
+ ]
1165
+ : [];
1095
1166
  let subagentRuntime;
1096
1167
  // Stays empty when the runtime below throws, which is the honest answer: the
1097
1168
  // catch is non-fatal and the session then genuinely has no delegate to
@@ -1110,38 +1181,56 @@ export async function createAgentSession(prefs, detected, options = {}) {
1110
1181
  });
1111
1182
  }
1112
1183
  const sub = await createSubagentRuntime({
1113
- historyRoot: projectStateRoot,
1114
1184
  cwd,
1115
1185
  model,
1116
1186
  tokenBudget: options.limits?.tokenBudget,
1117
1187
  maxIterations: options.limits?.maxIterations,
1118
- resolveLimits: (runId) => delegationLimits.get(runId),
1188
+ resolveLimits: (turnId) => delegationLimits.get(turnId),
1119
1189
  timeoutMs: options.limits?.timeoutMs,
1120
1190
  definitions: discovered.definitions,
1121
- pathBuilder: new SubagentPathBuilder(projectStateRoot, scope.projectId),
1122
- resolveParent: async (runId) => {
1123
- const parent = delegationScopes.get(runId);
1191
+ // Children log under the parent's `<session-id>/subagents/`; an
1192
+ // ephemeral session keeps them in memory with its own log.
1193
+ ...(options.ephemeral ? {} : { paths }),
1194
+ ...(conversationIndex
1195
+ ? {
1196
+ savedAgents: (sessionId) => createSavedAgentHistory({
1197
+ index: conversationIndex,
1198
+ paths,
1199
+ session: { sessionId },
1200
+ log: cliLogger(),
1201
+ }),
1202
+ }
1203
+ : {}),
1204
+ resolveParent: async (turnId) => {
1205
+ const parent = delegationScopes.get(turnId);
1124
1206
  if (!parent)
1125
- throw new Error(`Run ${runId} no longer owns delegation authority`);
1126
- return resolveSubagentParent(parent, cwd, options.stateRoot ? hierarchyRoot : undefined);
1207
+ throw new Error(`Turn ${turnId} no longer owns delegation authority`);
1208
+ return resolveSubagentParent(parent, cwd);
1127
1209
  },
1128
1210
  sandboxWorkspace,
1129
- resolveResumeHandler: (runId) => delegatedResumeHandlers.get(runId),
1130
- resolveWaitForInbound: (runId) => delegatedInputWaiters.get(runId),
1211
+ resolveResumeHandler: (turnId) => delegatedResumeHandlers.get(turnId),
1212
+ resolveWaitForInbound: (turnId) => delegatedInputWaiters.get(turnId),
1131
1213
  ...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
1132
1214
  ...(options.sandbox?.teardownTimeoutMs !== undefined
1133
1215
  ? { sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs }
1134
1216
  : {}),
1217
+ // A child is reviewed through its parent's channel, so the same two
1218
+ // questions reach the same person — or the same refusal.
1219
+ ...escalation,
1135
1220
  // A sub-agent works in the same repository and writes the same code,
1136
1221
  // so it is bound by the same instructions. Without this the parent
1137
1222
  // honours the project's rules and every task it delegates quietly
1138
1223
  // does not — the worse half of the feature, because the delegating
1139
1224
  // turn reports success either way.
1140
- projectInstructionContext: () => projectInstructions.createRunContext(),
1225
+ projectInstructionContext: () => projectInstructions.createTurnContext(),
1141
1226
  // Same argument as the instructions, one step further: a sub-agent that
1142
1227
  // does not know what day it is dates a changelog entry from a training
1143
1228
  // cut-off, and the parent reports the delegation as successful.
1144
- readEnvironment: async () => composeEnvironmentPrompt(await readEnvironmentFacts(cwd)),
1229
+ readEnvironment: async () => composeEnvironmentPrompt({
1230
+ ...(await readEnvironmentFacts(cwd)),
1231
+ boundary: boundaryFor(lastSendInteractive),
1232
+ ...(wsl ? { wsl } : {}),
1233
+ }),
1145
1234
  // Each child has its own provider instance, never the parent's fallback cursor.
1146
1235
  resolveModel: async (request, signal) => {
1147
1236
  const resolution = await resolveModelSwitch(request, {
@@ -1234,11 +1323,11 @@ export async function createAgentSession(prefs, detected, options = {}) {
1234
1323
  // deferred and there is nothing for a search to load.
1235
1324
  //
1236
1325
  // The store this also builds is dropped, deliberately: a sub-agent
1237
- // promoting its own run memory would write a record per
1238
- // delegation, and a parent that delegated six times would leave
1239
- // seven accounts of one piece of work for the next run to read.
1240
- // The parent's settle is the one that speaks for the whole task.
1241
- const childTools = buildToolRegistry(cwd, projectStateRoot, backgroundJobs, undefined, options.stateRoot ? scope.projectId : undefined, options.toolResultScreens).registry;
1326
+ // promoting its own memory would write a record per delegation,
1327
+ // and a parent that delegated six times would leave seven accounts
1328
+ // of one piece of work for the next turn to read. The parent's
1329
+ // settle is the one that speaks for the whole task.
1330
+ const childTools = buildToolRegistry(paths, backgroundJobs, undefined, options.toolResultScreens).registry;
1242
1331
  // Search owns its provider connection per call, so it is safe to share
1243
1332
  // with a child. Preserve the parent's configured backend/off choice.
1244
1333
  const search = registry.get('web_search');
@@ -1264,7 +1353,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
1264
1353
  // somebody is there to read it, the same condition `ask_user_question`
1265
1354
  // mounts under further down. A child's roster is the registry
1266
1355
  // `buildTools` builds above, which carries none of these: that is what
1267
- // keeps narration the run's own voice rather than a child's. And a
1356
+ // keeps narration the turn's own voice rather than a child's. And a
1268
1357
  // headless host — `run`, `run-stream`, `drain`, the resident step —
1269
1358
  // has no rail for a line to appear above, so a tool whose entire
1270
1359
  // result is "the operator saw this" would be answering with something
@@ -1306,7 +1395,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
1306
1395
  const modelSwitchHandlers = new Map();
1307
1396
  if (options.allowModelSwitch) {
1308
1397
  registry.register(buildSwitchModelTool(async (request, context) => {
1309
- const handler = modelSwitchHandlers.get(context.runId);
1398
+ const handler = modelSwitchHandlers.get(context.turnId);
1310
1399
  if (!handler || context.abortSignal?.aborted) {
1311
1400
  return {
1312
1401
  kind: 'rejected',
@@ -1345,14 +1434,15 @@ export async function createAgentSession(prefs, detected, options = {}) {
1345
1434
  return { action: 'continue' };
1346
1435
  }
1347
1436
  };
1348
- // The park request carries the run id of the call that asked; the
1349
- // handler above routes by the question, not by the run, and no durable
1437
+ // The park request carries the turn of the call that asked; the
1438
+ // handler above routes by the question, not by the turn, and no durable
1350
1439
  // park recorder is supplied.
1351
1440
  registry.register(buildAskUserQuestionTool({ resumeHandler: parkQuestion }));
1352
1441
  }
1353
1442
  // Task store → query registers task_create / task_update / task_list and
1354
- // emits task_created/task_updated, so the agent can track a plan for the
1355
- // current request. Tasks are run-scoped. The kernel's default availability
1443
+ // emits task_created/task_updated, so the agent can track a plan. Tasks
1444
+ // belong to the session (`<session-id>/tasks/`) and record the turn that
1445
+ // created them, so a plan outlives the turn. The kernel's default availability
1356
1446
  // for them is `deferred`; this session overrides that to `active` at the
1357
1447
  // query call, because the doctrine tells the model to plan with them and a
1358
1448
  // tool it must search for first is a tool it skips.
@@ -1366,11 +1456,10 @@ export async function createAgentSession(prefs, detected, options = {}) {
1366
1456
  // It is also why `toolNames` below reads the registry rather than a list
1367
1457
  // captured on this line. The count at connect time is unchanged; what
1368
1458
  // changes is that asking again later gets a later answer.
1369
- ensurePrivateStateDirectory(projectStateRoot, 'tenants');
1370
- const taskStoreForRun = (runId, tenantId) => new DiskTaskStore({
1371
- baseDir: projectStateRoot,
1372
- defaultRunId: runId,
1373
- tenantId,
1459
+ const taskStoreFor = (sessionScope) => new DiskTaskStore({
1460
+ paths,
1461
+ session: { sessionId: sessionScope.sessionId },
1462
+ tenantId: sessionScope.tenantId,
1374
1463
  });
1375
1464
  let selectedTaskStore;
1376
1465
  let taskSelectionGeneration = 0;
@@ -1392,10 +1481,10 @@ export async function createAgentSession(prefs, detected, options = {}) {
1392
1481
  // and other asynchronous setup are still being prepared.
1393
1482
  resetTaskStore();
1394
1483
  const generation = taskSelectionGeneration;
1395
- return (runId, runScope) => {
1396
- const store = taskStoreForRun(runId, runScope.tenantId);
1397
- if (generation === taskSelectionGeneration && matchesCurrentScope(runScope)) {
1398
- selectedTaskStore = { scope: { ...runScope }, store };
1484
+ return (sessionScope) => {
1485
+ const store = taskStoreFor(sessionScope);
1486
+ if (generation === taskSelectionGeneration && matchesCurrentScope(sessionScope)) {
1487
+ selectedTaskStore = { scope: { ...sessionScope }, store };
1399
1488
  }
1400
1489
  return store;
1401
1490
  };
@@ -1403,7 +1492,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
1403
1492
  // Persists across turns: once the user picks "approve all", later tool
1404
1493
  // batches in this session run without prompting.
1405
1494
  const approval = { all: false };
1406
- // Share the project store with tools and recall. Each run selects either
1495
+ // Share the project store with tools and recall. Each turn selects either
1407
1496
  // this extracted-claim promoter or explicit consolidation, never both.
1408
1497
  // Candidates without useful claims write nothing.
1409
1498
  const promoteMemory = createMemoryPromoter({ store: memoryStore });
@@ -1421,21 +1510,19 @@ export async function createAgentSession(prefs, detected, options = {}) {
1421
1510
  return emptySession(describeError(error));
1422
1511
  }
1423
1512
  // The session's own lifecycle, for hooks that set up or tear down
1424
- // something per session rather than per run. The run id is minted for
1425
- // these two calls: they belong to no turn. `session_start` waits for
1426
- // the first turn rather than firing here, because the conversation id
1427
- // the scope holds at construction is provisional — it is replaced when
1428
- // the conversation is first made durable — and a hook given the
1429
- // provisional id could never match it to a run.
1513
+ // something per session rather than per turn. These two calls belong to no
1514
+ // turn, so they carry no turn id — nothing is minted to fill the field.
1515
+ // `session_start` waits for the first turn rather than firing here, because
1516
+ // the conversation id the scope holds at construction is provisional — it
1517
+ // is replaced when the conversation is first made durable — and a hook
1518
+ // given the provisional id could never match it to a turn.
1430
1519
  const sessionPlugins = pluginRuntime;
1431
- const sessionHookRunId = generateRunId();
1432
1520
  let sessionStarted = false;
1433
1521
  const announceSessionStart = async () => {
1434
1522
  if (!sessionPlugins || sessionStarted)
1435
1523
  return;
1436
1524
  sessionStarted = true;
1437
1525
  await sessionPlugins.manager.executeHooks('session_start', {
1438
- runId: sessionHookRunId,
1439
1526
  sessionId: scope.sessionId,
1440
1527
  });
1441
1528
  };
@@ -1484,7 +1571,6 @@ export async function createAgentSession(prefs, detected, options = {}) {
1484
1571
  sessionPlugins
1485
1572
  ? sessionPlugins.manager
1486
1573
  .executeHooks('session_end', {
1487
- runId: sessionHookRunId,
1488
1574
  sessionId: scope.sessionId,
1489
1575
  })
1490
1576
  .catch(() => [])
@@ -1549,14 +1635,15 @@ export async function createAgentSession(prefs, detected, options = {}) {
1549
1635
  effortNotice = `Reasoning effort levels could not be established for this session: ${describeError(error)}`;
1550
1636
  }
1551
1637
  /**
1552
- * The kernel's resume with this session's half of the run attached: the
1638
+ * The kernel's resume with this session's half of the turn attached: the
1553
1639
  * provider, the tools, the working directory, the doctrine — the part a
1554
1640
  * checkpoint cannot carry. `resumeDurable` and `resumePaused` differ only
1555
- * in where the run and its store come from.
1641
+ * in where the log and its lease come from.
1556
1642
  */
1557
- const kernelResume = ({ entry, checkpointStore, claimFence, signal, checkpointId, listener, }) => operations.promise(signal, async (ownedSignal) => {
1643
+ const kernelResume = ({ entry, sessionLog, checkpointStore, lease, signal, checkpointId, listener, }) => operations.promise(signal, async (ownedSignal) => {
1558
1644
  const selectTaskStore = beginTaskStoreReadout();
1559
- const resumedLimits = (await readStoredRunGuards(join(pathBuilder.runDir(entry.projectId, entry.sessionId, entry.runId), 'run.json'), entry)) ?? resolveRunGuards(options.limits);
1645
+ // The turn's own limits, as its `turn_started` recorded them.
1646
+ const resumedLimits = (await readStoredTurnGuards(sessionLog, entry.turnId)) ?? resolveTurnGuards(options.limits);
1560
1647
  // The same prelude a turn runs, and for the same reasons: a lapsed
1561
1648
  // OAuth token has to be renewed before the provider is used, and the
1562
1649
  // fallback chain has to be built AFTER that so its members do not
@@ -1568,10 +1655,17 @@ export async function createAgentSession(prefs, detected, options = {}) {
1568
1655
  const curatedMemory = readMemory(undefined, cwd);
1569
1656
  for (const notice of formatMemoryDiagnostics(curatedMemory))
1570
1657
  cliLogger().warn(notice);
1571
- const memoryPrompt = composeMemoryPrompt(curatedMemory);
1658
+ const storedMemory = await storedMemoryPrompt();
1659
+ if (storedMemory.notice)
1660
+ cliLogger().warn(storedMemory.notice);
1661
+ const memoryPrompt = [composeMemoryPrompt(curatedMemory), storedMemory.prompt]
1662
+ .filter((part) => Boolean(part))
1663
+ .join('\n\n') || null;
1572
1664
  const environmentPrompt = composeEnvironmentPrompt({
1573
1665
  ...(await readEnvironmentFacts(cwd)),
1574
1666
  additionalDirectories: [...directories],
1667
+ boundary: boundaryFor(false),
1668
+ ...(wsl ? { wsl } : {}),
1575
1669
  });
1576
1670
  const systemPrompt = [
1577
1671
  NAMZU_IDENTITY,
@@ -1583,29 +1677,27 @@ export async function createAgentSession(prefs, detected, options = {}) {
1583
1677
  ]
1584
1678
  .filter((s) => Boolean(s))
1585
1679
  .join('\n\n') || undefined;
1586
- const resumeHandler = makeResumeHandler(approval, undefined, options.permissionMode, (name, input) => isPromptExempt(registry, name, input));
1587
- if (delegatedResumeHandlers.has(entry.runId)) {
1588
- throw new Error(`Run ${entry.runId} already owns a delegated review channel.`);
1680
+ const resumeHandler = makeResumeHandler(approval, undefined, options.permissionMode, (name, input) => isPromptExempt(registry, name, input), { unattendedSandboxEscape });
1681
+ if (delegatedResumeHandlers.has(entry.turnId)) {
1682
+ throw new Error(`Turn ${entry.turnId} already owns a delegated review channel.`);
1589
1683
  }
1590
- delegatedResumeHandlers.set(entry.runId, resumeHandler);
1591
- delegationScopes.set(entry.runId, { ...entry, topicId: scope.topicId });
1592
- delegationLimits.set(entry.runId, resumedLimits);
1593
- const runTaskStore = selectTaskStore(entry.runId, {
1594
- ...entry,
1595
- topicId: scope.topicId,
1596
- });
1684
+ const turnScope = { ...entry, topicId: scope.topicId };
1685
+ delegatedResumeHandlers.set(entry.turnId, resumeHandler);
1686
+ delegationScopes.set(entry.turnId, turnScope);
1687
+ delegationLimits.set(entry.turnId, resumedLimits);
1688
+ const turnTaskStore = selectTaskStore(turnScope);
1597
1689
  try {
1598
- return await resumeRun({
1690
+ return await resumeSession({
1599
1691
  provider: providerForSession(entry.sessionId),
1600
1692
  fallbackProviders: fallbackPlan.build(currentToken, entry.sessionId),
1601
1693
  tools: registry,
1602
1694
  pluginManager: pluginRuntime?.manager,
1603
1695
  skillRegistry: pluginRuntime?.skills,
1604
1696
  skills: pluginSkills,
1605
- taskStore: runTaskStore,
1697
+ taskStore: turnTaskStore,
1606
1698
  ...(webCapability ? { web: webCapability } : {}),
1607
- // The same availability the original run registered under.
1608
- // A resumed run re-registers the task tools; leaving them at
1699
+ // The same availability the original turn registered under.
1700
+ // A resumed turn re-registers the task tools; leaving them at
1609
1701
  // the kernel's `deferred` default would hand the model a plan
1610
1702
  // it started with active tools and can no longer update.
1611
1703
  runtimeToolOverrides: {
@@ -1614,9 +1706,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
1614
1706
  task_list: 'active',
1615
1707
  },
1616
1708
  ...(subagentRuntime
1617
- ? {
1618
- taskScheduler: await subagentRuntime.gatewayForRun(entry.runId),
1619
- }
1709
+ ? { taskScheduler: await subagentRuntime.gatewayForTurn(entry.turnId) }
1620
1710
  : {}),
1621
1711
  authorizationGate: gateFor(options.rules),
1622
1712
  compactionConfig: compactionConfigFor(options.compaction),
@@ -1624,8 +1714,8 @@ export async function createAgentSession(prefs, detected, options = {}) {
1624
1714
  ? (options.compaction?.retainedToolPreviewChars ?? 4_000)
1625
1715
  : undefined,
1626
1716
  prepareStep: [
1627
- createTaskContextStep(runTaskStore, entry.tenantId),
1628
- createDelegationHistoryStep(projectStateRoot, entry.sessionId),
1717
+ createTaskContextStep(turnTaskStore, entry.tenantId),
1718
+ ...savedAgentsSteps(entry.sessionId),
1629
1719
  ...(options.memory?.recall === false
1630
1720
  ? []
1631
1721
  : [
@@ -1640,98 +1730,98 @@ export async function createAgentSession(prefs, detected, options = {}) {
1640
1730
  ...(options.compaction?.consolidate
1641
1731
  ? { consolidateInto: memoryStore }
1642
1732
  : { promoteMemory }),
1643
- projectInstructionContext: projectInstructions.createRunContext(),
1644
- pathBuilder,
1733
+ projectInstructionContext: projectInstructions.createTurnContext(),
1734
+ paths,
1645
1735
  ...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
1646
1736
  ...(options.sandbox?.teardownTimeoutMs !== undefined
1647
1737
  ? { sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs }
1648
1738
  : {}),
1649
- // NOT `emergencySave`, unlike a turn. The manager is a singleton
1650
- // whose `attach` detaches whoever held it before, so a caller
1651
- // resuming several runs in one process would leave only the last
1652
- // one covered — and would look covered. A turn owns its process
1653
- // end to end; a drainer does not.
1654
- runConfig: {
1739
+ turnConfig: {
1655
1740
  model,
1656
1741
  ...(nativeWebSearch ? { webSearch: nativeWebSearch } : {}),
1657
1742
  ...(sandbox.provider ? { sandbox: { workspace: sandboxWorkspace } } : {}),
1658
1743
  ...resumedLimits,
1659
1744
  maxResponseTokens: 8192,
1660
1745
  permissionMode: 'auto',
1746
+ pruneKeepLast: CLI_CHECKPOINT_RETENTION,
1661
1747
  },
1662
1748
  agentId: 'namzu',
1663
1749
  agentName: 'namzu',
1664
1750
  ...(systemPrompt ? { systemPrompt } : {}),
1665
1751
  workingDirectory: cwd,
1666
1752
  ...(directories.length > 0 ? { additionalDirectories: [...directories] } : {}),
1667
- ...(options.limits ? { limits: options.limits } : {}),
1753
+ ...escalation,
1668
1754
  // No `onPermission`: there is nobody at a drainer's terminal, so a
1669
- // prompt would block the pass forever on a run nobody is watching.
1755
+ // prompt would block the pass forever on a turn nobody is watching.
1670
1756
  // The gate's deny rules still apply.
1671
- // One presenter for the whole stream, built from the registry this
1672
- // scope already holds. It was the absence of the registry HERE that
1673
- // forced presentation to be name matching: `toAgentEvent` was pure
1674
- // over a `RunEvent` and could not ask a tool anything.
1675
1757
  resumeHandler,
1676
1758
  signal: ownedSignal,
1677
- // Attribution comes from the ENTRY, not from this session: the run
1759
+ // Attribution comes from the ENTRY, not from this session: the turn
1678
1760
  // belongs to whoever started it, and stamping the drainer's ids onto
1679
1761
  // it would file another tenant's work under this one.
1680
1762
  tenantId: entry.tenantId,
1681
1763
  projectId: entry.projectId,
1682
1764
  sessionId: entry.sessionId,
1683
- // …except the topic, which no checkpoint records — see
1684
- // `RunStateScope`. This one is the drainer's, and honestly so:
1685
- // supplied here rather than pretended to have been recovered.
1765
+ // …except the topic, which the drainer supplies honestly rather
1766
+ // than pretending to have recovered it.
1686
1767
  topicId: scope.topicId,
1687
- scope: { ...entry, topicId: scope.topicId },
1688
- checkpointStore,
1689
- ...(claimFence !== undefined ? { claimFence } : {}),
1768
+ scope: turnScope,
1769
+ sessionLog,
1770
+ checkpointStore: checkpointStore ??
1771
+ new DiskSessionCheckpointStore({
1772
+ paths,
1773
+ log: sessionLogCheckpointView(sessionLog),
1774
+ // A drained child's log sits under its parent's `subagents/`;
1775
+ // its checkpoints are in its own directory there, not at the top.
1776
+ ...(sessionLog instanceof DiskSessionLog && sessionLog.locator
1777
+ ? { session: sessionLog.locator }
1778
+ : {}),
1779
+ }),
1780
+ ...(lease ? { lease } : {}),
1690
1781
  ...(checkpointId !== undefined ? { checkpointId } : {}),
1691
- ...(listener ? { listener } : {}),
1782
+ ...(listener || options.onSessionEvent
1783
+ ? {
1784
+ listener: (event) => {
1785
+ options.onSessionEvent?.(event);
1786
+ listener?.(event);
1787
+ },
1788
+ }
1789
+ : {}),
1692
1790
  });
1693
1791
  }
1694
1792
  finally {
1695
- if (delegatedResumeHandlers.get(entry.runId) === resumeHandler) {
1696
- delegatedResumeHandlers.delete(entry.runId);
1697
- delegationScopes.delete(entry.runId);
1698
- delegationLimits.delete(entry.runId);
1699
- await subagentRuntime?.releaseRun(entry.runId);
1793
+ if (delegatedResumeHandlers.get(entry.turnId) === resumeHandler) {
1794
+ delegatedResumeHandlers.delete(entry.turnId);
1795
+ delegationScopes.delete(entry.turnId);
1796
+ delegationLimits.delete(entry.turnId);
1797
+ await subagentRuntime?.releaseTurn(entry.turnId);
1700
1798
  }
1701
1799
  }
1702
1800
  });
1703
1801
  /**
1704
- * `resumeRun` drains the loop and returns a settled run; the events go to a
1705
- * listener. A small queue turns that into the stream `send` gives, so a
1706
- * headless caller renders a resumed run exactly as it rendered the turn.
1802
+ * `resumeSession` drains the loop and returns a settled turn; the events go
1803
+ * to a listener. A small queue turns that into the stream `send` gives, so a
1804
+ * headless caller renders a resumed turn exactly as it rendered the first
1805
+ * segment.
1707
1806
  */
1708
- const resumePausedStream = ({ runId, checkpointId, signal, }) => {
1807
+ const resumePausedStream = ({ turnId, checkpointId, signal, }) => {
1709
1808
  const queue = [];
1710
1809
  let wake;
1711
1810
  let settled = false;
1712
1811
  let failure;
1713
1812
  const presenter = createToolPresenter(registry);
1714
- // The store the turn's run manager wrote to, built the same way it
1715
- // built it (see the kernel's `RunPersistence`): the session directory's
1716
- // `runs/`, attributed to this tenant and project.
1717
- const store = new DiskCheckpointStore({
1718
- baseDir: join(pathBuilder.sessionDir(scope.projectId, scope.sessionId), 'runs'),
1719
- }, {
1720
- tenantId: scope.tenantId,
1721
- projectId: scope.projectId,
1722
- sessionId: scope.sessionId,
1723
- });
1724
- const entry = {
1725
- tenantId: scope.tenantId,
1726
- projectId: scope.projectId,
1727
- sessionId: scope.sessionId,
1728
- runId: runId,
1729
- };
1813
+ // The log the turn appends to, and its checkpoints beside it.
1814
+ const sessionLog = DiskSessionLog.at(paths, { sessionId: scope.sessionId });
1730
1815
  const outcome = kernelResume({
1731
- entry,
1732
- checkpointStore: store,
1816
+ entry: {
1817
+ tenantId: scope.tenantId,
1818
+ projectId: scope.projectId,
1819
+ sessionId: scope.sessionId,
1820
+ turnId: turnId,
1821
+ },
1822
+ sessionLog,
1733
1823
  ...(signal ? { signal } : {}),
1734
- checkpointId: checkpointId,
1824
+ ...(checkpointId !== undefined ? { checkpointId: checkpointId } : {}),
1735
1825
  listener: (event) => {
1736
1826
  queue.push(event);
1737
1827
  wake?.();
@@ -1740,8 +1830,8 @@ export async function createAgentSession(prefs, detected, options = {}) {
1740
1830
  .then((result) => {
1741
1831
  if (!result.resumed) {
1742
1832
  failure = new Error(result.reason === 'no-checkpoint'
1743
- ? `no checkpoint ${checkpointId} is recorded for run ${runId}`
1744
- : `run ${runId} is parked on a decision only a person can answer`);
1833
+ ? `no checkpoint ${checkpointId ?? ''} is recorded for turn ${turnId}`.replace(' ', ' ')
1834
+ : `turn ${turnId} is parked on a decision only a person can answer`);
1745
1835
  }
1746
1836
  })
1747
1837
  .catch((err) => {
@@ -1790,23 +1880,30 @@ export async function createAgentSession(prefs, detected, options = {}) {
1790
1880
  const sessionId = scope.sessionId;
1791
1881
  const sessions = options.conversationSessions;
1792
1882
  await prepareProviderCredential(signal);
1793
- return compactNow({
1794
- messages,
1883
+ const common = {
1795
1884
  config: compactionConfigFor(options.compaction),
1796
1885
  provider: providerForSession(sessionId),
1797
1886
  model,
1798
1887
  signal,
1799
- ...(sessions
1800
- ? {
1801
- onShed: async (removed) => {
1802
- if (sessions.projectId !== scope.projectId ||
1803
- sessions.tenantId !== scope.tenantId)
1804
- throw new Error('Manual compaction is outside the current conversation scope.');
1805
- await retainManualCompaction(sessions, sessionId, removed, signal);
1806
- },
1807
- }
1808
- : {}),
1809
- });
1888
+ };
1889
+ // A durable conversation compacts its own log: the kernel folds the
1890
+ // context from it and appends a `compaction{ trigger: 'manual' }`
1891
+ // record outside any turn, so the originals stay in the log and a
1892
+ // resume folds the summary. A session with no log compacts the
1893
+ // history it was handed.
1894
+ if (sessions && !options.ephemeral) {
1895
+ if (sessions.projectId !== scope.projectId || sessions.tenantId !== scope.tenantId)
1896
+ throw new Error('Manual compaction is outside the current conversation scope.');
1897
+ return await compactSession({
1898
+ ...common,
1899
+ sessionId,
1900
+ locator: {
1901
+ log: DiskSessionLog.at(paths, { sessionId }),
1902
+ index: sessions.index,
1903
+ },
1904
+ });
1905
+ }
1906
+ return compactNow({ ...common, messages });
1810
1907
  }),
1811
1908
  // Reads the same registry object the deferred registration mutates, at
1812
1909
  // call time — the pair of `promptExemptTools` below, and for the same
@@ -1838,16 +1935,22 @@ export async function createAgentSession(prefs, detected, options = {}) {
1838
1935
  listener(job);
1839
1936
  }) ?? (() => { }),
1840
1937
  ...(subagentRuntime ? { subagents: subagentRuntime.activity } : {}),
1841
- savedChildren: () => replaySavedChildrenFor({
1842
- sessionsRoot: join(projectStateRoot, 'sessions'),
1843
- sessionId: scope.sessionId,
1844
- log: cliLogger(),
1845
- }),
1846
- listOrchestrationRuns: () => listSavedOrchestrationRuns({
1847
- sessionsRoot: join(projectStateRoot, 'sessions'),
1848
- sessionId: scope.sessionId,
1849
- log: cliLogger(),
1850
- }),
1938
+ ...(conversationIndex
1939
+ ? {
1940
+ savedChildren: () => replaySavedChildrenFor({
1941
+ index: conversationIndex,
1942
+ paths,
1943
+ session: { sessionId: scope.sessionId },
1944
+ log: cliLogger(),
1945
+ }),
1946
+ listSavedBatches: () => listSavedBatches({
1947
+ index: conversationIndex,
1948
+ paths,
1949
+ session: { sessionId: scope.sessionId },
1950
+ log: cliLogger(),
1951
+ }),
1952
+ }
1953
+ : {}),
1851
1954
  get instructionFiles() {
1852
1955
  return projectInstructions.instructionFiles;
1853
1956
  },
@@ -1870,7 +1973,18 @@ export async function createAgentSession(prefs, detected, options = {}) {
1870
1973
  : []),
1871
1974
  ...unresolvedNotice.map((line) => `Provider chain: capabilities could not be established for ${line}.`),
1872
1975
  ...fallbackPlan.notices,
1976
+ ...memoryMigrationNotices,
1873
1977
  ],
1978
+ rememberNote: (text, type) => saveTypedNote(memoryStore, text, type),
1979
+ importCuratedNotes: async () => describeCuratedNotesImport(await importCuratedNotes({ store: memoryStore, directory: memoryDirectory, cwd }), memoryDirectory),
1980
+ storedMemoryIndex: async () => ({
1981
+ directory: memoryDirectory,
1982
+ index: await memoryStore.readIndex({ maxLines: Number.POSITIVE_INFINITY }),
1983
+ derived: await memoryStore.readIndex({
1984
+ maxLines: Number.POSITIVE_INFINITY,
1985
+ derived: true,
1986
+ }),
1987
+ }),
1874
1988
  webSearchSummary: webSearchLabel(options.web, nativeSearchAvailable),
1875
1989
  close: () => operations.close(),
1876
1990
  errorHint: null,
@@ -1883,22 +1997,41 @@ export async function createAgentSession(prefs, detected, options = {}) {
1883
1997
  promptExemptTools: () => promptExemptToolNames(registry).filter((name) => !goalToolNames.has(name)),
1884
1998
  send: (messages, opts) => operations.stream(opts?.signal, (signal) => (async function* () {
1885
1999
  const selectTaskStore = beginTaskStoreReadout();
1886
- const runId = opts?.runId ?? generateRunId();
1887
- const turnLimits = resolveRunGuards(options.limits, opts?.limits);
1888
- const turnOpts = { ...opts, runId, signal };
2000
+ const turnLimits = resolveTurnGuards(options.limits, opts?.limits);
2001
+ const turnOpts = { ...opts, signal };
1889
2002
  let runTools = registry;
1890
- const resumeHandler = makeResumeHandler(approval, opts?.onPermission, opts?.permissionMode ?? options.permissionMode, (name, input) => isPromptExempt(runTools, name, input));
1891
- if (delegatedResumeHandlers.has(runId)) {
1892
- throw new Error(`Run ${runId} already owns a delegated review channel.`);
1893
- }
1894
- delegatedResumeHandlers.set(runId, resumeHandler);
1895
- if (opts?.onModelSwitch)
1896
- modelSwitchHandlers.set(runId, opts.onModelSwitch);
1897
- if (opts?.waitForInbound)
1898
- delegatedInputWaiters.set(runId, opts.waitForInbound);
2003
+ lastSendInteractive = opts?.onPermission !== undefined;
2004
+ const resumeHandler = makeResumeHandler(approval, opts?.onPermission, opts?.permissionMode ?? options.permissionMode, (name, input) => isPromptExempt(runTools, name, input), { unattendedSandboxEscape });
1899
2005
  const turnScope = { ...scope };
1900
- delegationScopes.set(runId, turnScope);
1901
- delegationLimits.set(runId, turnLimits);
2006
+ // The turn's id is reserved here, before the kernel begins it, because
2007
+ // everything that authorizes the turn is keyed by it: the review
2008
+ // channel its children borrow, the delegation gateway, the goal-round
2009
+ // authority. A caller may reserve it itself (a resident step names
2010
+ // its turn to its verifier first).
2011
+ const turnId = opts?.turnId ?? generateTurnId();
2012
+ const claimed = new Set();
2013
+ let capturedAuthority;
2014
+ const claimTurn = (turnId) => {
2015
+ if (claimed.has(turnId))
2016
+ return;
2017
+ if (delegatedResumeHandlers.has(turnId)) {
2018
+ throw new Error(`Turn ${turnId} already owns a delegated review channel.`);
2019
+ }
2020
+ claimed.add(turnId);
2021
+ delegatedResumeHandlers.set(turnId, resumeHandler);
2022
+ if (opts?.onModelSwitch)
2023
+ modelSwitchHandlers.set(turnId, opts.onModelSwitch);
2024
+ if (opts?.waitForInbound)
2025
+ delegatedInputWaiters.set(turnId, opts.waitForInbound);
2026
+ delegationScopes.set(turnId, turnScope);
2027
+ delegationLimits.set(turnId, turnLimits);
2028
+ if (capturedAuthority) {
2029
+ if (goalAuthorities.has(turnId)) {
2030
+ throw new Error(`Turn ${turnId} already owns goal-round authority.`);
2031
+ }
2032
+ goalAuthorities.set(turnId, capturedAuthority);
2033
+ }
2034
+ };
1902
2035
  try {
1903
2036
  // Renew a lapsed OAuth token before the turn runs (no-op for valid
1904
2037
  // tokens and non-subscription credentials).
@@ -1927,7 +2060,13 @@ export async function createAgentSession(prefs, detected, options = {}) {
1927
2060
  for (const notice of formatMemoryDiagnostics(curatedMemory)) {
1928
2061
  yield { kind: 'context', text: notice, shed: false };
1929
2062
  }
1930
- const memoryPrompt = composeMemoryPrompt(curatedMemory);
2063
+ const storedMemory = await storedMemoryPrompt();
2064
+ if (storedMemory.notice) {
2065
+ yield { kind: 'context', text: storedMemory.notice, shed: false };
2066
+ }
2067
+ const memoryPrompt = [composeMemoryPrompt(curatedMemory), storedMemory.prompt]
2068
+ .filter((part) => Boolean(part))
2069
+ .join('\n\n') || null;
1931
2070
  currentOnQuestion = opts?.onQuestion;
1932
2071
  const [environmentFacts, turnSnapshot] = await Promise.all([
1933
2072
  readEnvironmentFacts(cwd),
@@ -1936,10 +2075,16 @@ export async function createAgentSession(prefs, detected, options = {}) {
1936
2075
  const environmentPrompt = composeEnvironmentPrompt({
1937
2076
  ...environmentFacts,
1938
2077
  additionalDirectories: [...directories],
2078
+ boundary: boundaryFor(opts?.onPermission !== undefined),
2079
+ ...(wsl ? { wsl } : {}),
1939
2080
  });
1940
2081
  // The repository as it stood when THIS turn began, through the
1941
- // SDK's `turn` placement — the ephemeral trailing message that is
1942
- // never cached and never enters history. FIRST iteration only:
2082
+ // SDK's `context` placement — request-only context after the
2083
+ // history that never enters it. An observation, not an
2084
+ // instruction: under `turn` it rode a system message the
2085
+ // hoisting driver moves ahead of the conversation, so every
2086
+ // new send's changed snapshot re-read the whole history
2087
+ // uncached. FIRST iteration only:
1943
2088
  // later iterations work from state the model itself changed, and
1944
2089
  // `git status` is the honest source for that. A registry per
1945
2090
  // turn, closed over this turn's snapshot, rather than one
@@ -1949,7 +2094,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
1949
2094
  const promptContributions = new PromptContributionRegistry();
1950
2095
  promptContributions.register({
1951
2096
  id: 'namzu.turn-snapshot',
1952
- placement: 'turn',
2097
+ placement: 'context',
1953
2098
  render: ({ iteration }) => (iteration === 1 ? turnSnapshotPrompt : null),
1954
2099
  });
1955
2100
  // The citation rules that come with the web tools, only when the
@@ -1972,12 +2117,12 @@ export async function createAgentSession(prefs, detected, options = {}) {
1972
2117
  const bundle = opts?.residentLearningDisclosure === 'on-demand'
1973
2118
  ? createResidentStepContext({
1974
2119
  ...contextOptions,
1975
- authorizeLearningRead: (context) => context.runId === runId &&
1976
- delegationScopes.get(context.runId) === turnScope,
2120
+ authorizeLearningRead: (context) => claimed.has(context.turnId) &&
2121
+ delegationScopes.get(context.turnId) === turnScope,
1977
2122
  })
1978
2123
  : { contributions: createResidentStepContributions(contextOptions), tools: [] };
1979
2124
  if (bundle.tools.length) {
1980
- // Per-send membership: neither another send nor delegated runs inherit this tool.
2125
+ // Per-send membership: neither another send nor delegated sessions inherit this tool.
1981
2126
  runTools = runTools.fork();
1982
2127
  for (const tool of bundle.tools)
1983
2128
  runTools.register(tool);
@@ -2015,10 +2160,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
2015
2160
  .join('\n\n') || undefined;
2016
2161
  await announceSessionStart();
2017
2162
  checkpoints.beginTurn(lastUserText(messages));
2018
- let capturedAuthority;
2019
2163
  if (opts?.goalRound) {
2020
- if (!opts.runId)
2021
- throw new Error('A goal round requires a caller-reserved runId.');
2022
2164
  if (!options.sessionGoals)
2023
2165
  throw new Error('This session has no durable goal store.');
2024
2166
  if (opts.goalRound.sessionId !== scope.sessionId ||
@@ -2035,13 +2177,32 @@ export async function createAgentSession(prefs, detected, options = {}) {
2035
2177
  current.maxGoalRounds !== opts.goalRound.maxGoalRounds) {
2036
2178
  throw new Error('Goal-round authority is stale or does not match the durable goal.');
2037
2179
  }
2038
- if (goalAuthorities.has(opts.runId)) {
2039
- throw new Error(`Run ${opts.runId} already owns goal-round authority.`);
2040
- }
2041
2180
  capturedAuthority = Object.freeze({ ...opts.goalRound });
2042
- goalAuthorities.set(opts.runId, capturedAuthority);
2043
2181
  }
2044
- const runTaskStore = selectTaskStore(runId, turnScope);
2182
+ claimTurn(turnId);
2183
+ const turnTaskStore = selectTaskStore(turnScope);
2184
+ // An ephemeral session keeps its whole turn in memory: a fresh
2185
+ // log per send, opened with the `session_started` a turn follows.
2186
+ // A durable one appends to `<session-id>.jsonl`, created first
2187
+ // when this session is the one bringing it into existence.
2188
+ let ephemeralLog;
2189
+ if (options.ephemeral) {
2190
+ ephemeralLog = new InMemorySessionLog({ sessionId: turnScope.sessionId });
2191
+ await ensureSessionStarted(ephemeralLog, {
2192
+ ...turnScope,
2193
+ cwd,
2194
+ agent: { id: 'namzu', name: 'namzu' },
2195
+ ...(options.origin ? { origin: options.origin } : {}),
2196
+ });
2197
+ }
2198
+ else {
2199
+ await ensureSessionStarted(DiskSessionLog.at(paths, { sessionId: turnScope.sessionId }), {
2200
+ ...turnScope,
2201
+ cwd,
2202
+ agent: { id: 'namzu', name: 'namzu' },
2203
+ ...(options.origin ? { origin: options.origin } : {}),
2204
+ });
2205
+ }
2045
2206
  try {
2046
2207
  yield* runTurn({
2047
2208
  provider: providerForSession(turnScope.sessionId),
@@ -2071,9 +2232,13 @@ export async function createAgentSession(prefs, detected, options = {}) {
2071
2232
  skillRegistry: pluginRuntime?.skills,
2072
2233
  skills: pluginSkills,
2073
2234
  scope: turnScope,
2074
- pathBuilder,
2235
+ turnId,
2236
+ paths,
2237
+ ...(ephemeralLog ? { sessionLog: ephemeralLog } : {}),
2238
+ claimTurn,
2075
2239
  workingDirectory: cwd,
2076
2240
  ...(directories.length > 0 ? { additionalDirectories: [...directories] } : {}),
2241
+ escalation,
2077
2242
  limits: turnLimits,
2078
2243
  sandboxWorkspace,
2079
2244
  rules: options.rules,
@@ -2082,8 +2247,8 @@ export async function createAgentSession(prefs, detected, options = {}) {
2082
2247
  maxAnswerReviews: options.maxAnswerReviews,
2083
2248
  promoteMemory: options.compaction?.consolidate ? undefined : promoteMemory,
2084
2249
  prepareStep: [
2085
- createTaskContextStep(runTaskStore, turnScope.tenantId),
2086
- createDelegationHistoryStep(projectStateRoot, turnScope.sessionId),
2250
+ createTaskContextStep(turnTaskStore, turnScope.tenantId),
2251
+ ...savedAgentsSteps(turnScope.sessionId),
2087
2252
  ...(options.memory?.recall === false
2088
2253
  ? []
2089
2254
  : [
@@ -2097,26 +2262,25 @@ export async function createAgentSession(prefs, detected, options = {}) {
2097
2262
  ...evidenceRecallFor(turnScope.sessionId),
2098
2263
  ...(options.residentEvidenceRecall ? [options.residentEvidenceRecall] : []),
2099
2264
  ],
2100
- taskStore: runTaskStore,
2265
+ taskStore: turnTaskStore,
2101
2266
  systemPrompt,
2102
2267
  messages,
2103
- projectInstructionContext: projectInstructions.createRunContext(),
2268
+ projectInstructionContext: projectInstructions.createTurnContext(),
2104
2269
  opts: turnOpts,
2105
2270
  resumeHandler,
2106
- taskGateway: await subagentRuntime?.gatewayForRun(runId),
2107
- completionInbox: await subagentRuntime?.completionInboxForRun(runId),
2271
+ taskGateway: await subagentRuntime?.gatewayForTurn(turnId),
2272
+ completionInbox: await subagentRuntime?.completionInboxForTurn(turnId),
2108
2273
  promptContributions,
2109
2274
  ...(webCapability ? { web: webCapability } : {}),
2110
2275
  ...(nativeWebSearch ? { webSearch: nativeWebSearch } : {}),
2111
- // Tasks join this run's registry inside query, after the fork.
2276
+ // Tasks join this turn's registry inside query, after the fork.
2112
2277
  // Keep the existing eager path unless deferral was requested.
2113
2278
  runtimeToolOverrides: {
2114
2279
  task_create: options.toolLoading === 'deferred' ? 'deferred' : 'active',
2115
2280
  task_update: options.toolLoading === 'deferred' ? 'deferred' : 'active',
2116
2281
  task_list: options.toolLoading === 'deferred' ? 'deferred' : 'active',
2117
2282
  },
2118
- emergencySave: options.emergencySave ?? true,
2119
- onRunEvent: options.onRunEvent,
2283
+ onSessionEvent: options.onSessionEvent,
2120
2284
  ...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
2121
2285
  ...(options.sandbox?.teardownTimeoutMs !== undefined
2122
2286
  ? {
@@ -2126,31 +2290,36 @@ export async function createAgentSession(prefs, detected, options = {}) {
2126
2290
  });
2127
2291
  }
2128
2292
  finally {
2129
- if (opts?.runId &&
2130
- capturedAuthority &&
2131
- goalAuthorities.get(opts.runId) === capturedAuthority) {
2132
- goalAuthorities.delete(opts.runId);
2293
+ for (const turnId of claimed) {
2294
+ if (capturedAuthority && goalAuthorities.get(turnId) === capturedAuthority) {
2295
+ goalAuthorities.delete(turnId);
2296
+ }
2133
2297
  }
2134
2298
  }
2135
2299
  }
2136
2300
  finally {
2137
- if (delegatedResumeHandlers.get(runId) === resumeHandler) {
2138
- delegatedResumeHandlers.delete(runId);
2139
- modelSwitchHandlers.delete(runId);
2140
- delegatedInputWaiters.delete(runId);
2141
- delegationScopes.delete(runId);
2142
- delegationLimits.delete(runId);
2143
- await subagentRuntime?.releaseRun(runId);
2301
+ for (const turnId of claimed) {
2302
+ if (delegatedResumeHandlers.get(turnId) !== resumeHandler)
2303
+ continue;
2304
+ delegatedResumeHandlers.delete(turnId);
2305
+ modelSwitchHandlers.delete(turnId);
2306
+ delegatedInputWaiters.delete(turnId);
2307
+ delegationScopes.delete(turnId);
2308
+ delegationLimits.delete(turnId);
2309
+ await subagentRuntime?.releaseTurn(turnId);
2144
2310
  }
2145
2311
  }
2146
2312
  })()),
2147
- resumeDurable: ({ entry, checkpointStore, claimFence, signal }) => kernelResume({
2148
- entry,
2149
- checkpointStore,
2150
- ...(claimFence !== undefined ? { claimFence } : {}),
2151
- ...(signal ? { signal } : {}),
2152
- }),
2313
+ resumeDurable: (params) => kernelResume(params),
2153
2314
  resumePaused: (params) => resumePausedStream(params),
2315
+ abandonTurn: (turnId, reason) => operations.promise(undefined, async () => {
2316
+ if (options.ephemeral)
2317
+ throw new Error('An ephemeral session keeps no turn to abandon.');
2318
+ await abandonTurn(scope.sessionId, turnId, reason, {
2319
+ log: DiskSessionLog.at(paths, { sessionId: scope.sessionId }),
2320
+ ...(options.conversationSessions ? { index: options.conversationSessions.index } : {}),
2321
+ });
2322
+ }),
2154
2323
  };
2155
2324
  }
2156
2325
  function planFallbacks(members, detected) {
@@ -2379,7 +2548,7 @@ export async function describeProviderModels(id, det, signal) {
2379
2548
  signal?.throwIfAborted();
2380
2549
  // constructProvider calls ProviderRegistry.create, which throws
2381
2550
  // "Unsupported provider type" until the vendor package has registered
2382
- // itself. The run path registers lazily via ensureRegistered; the
2551
+ // itself. The turn path registers lazily via ensureRegistered; the
2383
2552
  // listing path must do the same or every provider returns nothing.
2384
2553
  await ensureRegistered(id);
2385
2554
  signal?.throwIfAborted();
@@ -2499,15 +2668,20 @@ function lastUserText(messages) {
2499
2668
  return '';
2500
2669
  }
2501
2670
  /**
2502
- * A scope for a session no host supplied one for: four minted ids. Minted
2503
- * rather than spelled, because a spelled id is a place a typo hides and
2504
- * these types accept either spelling until they are nominal.
2671
+ * A scope for a session no host supplied one for.
2672
+ *
2673
+ * The project is the one the working directory's checkout stands for
2674
+ * (`projects/<slug>/project.json`), not minted: a minted one gave every
2675
+ * session a project of its own, so generated memory and task state were
2676
+ * partitioned per launch and a second session in the same directory could
2677
+ * not see what the first had saved. The session, topic and tenant stay
2678
+ * minted — nothing here has a store to find existing ones in.
2505
2679
  */
2506
- function mintScope() {
2680
+ function mintScope(projectId, sessionId) {
2507
2681
  return {
2508
- sessionId: generateSessionId(),
2682
+ sessionId: sessionId ?? generateSessionId(),
2509
2683
  topicId: generateTopicId(),
2510
- projectId: generateProjectId(),
2684
+ projectId,
2511
2685
  tenantId: generateTenantId(),
2512
2686
  };
2513
2687
  }
@@ -2544,7 +2718,7 @@ function gateFor(rules) {
2544
2718
  // unbounded context growth on long ones.
2545
2719
  //
2546
2720
  // `contextWindowTokens` is deliberately omitted: the SDK resolves the window
2547
- // from `runConfig.model`, which is the value the user actually chose. Pinning
2721
+ // from `turnConfig.model`, which is the value the user actually chose. Pinning
2548
2722
  // a number here would fix one window across every model the CLI can talk to.
2549
2723
  const COMPACTION_CONFIG = {
2550
2724
  strategy: 'salience',
@@ -2588,11 +2762,11 @@ function compactionConfigFor(compaction) {
2588
2762
  : {}),
2589
2763
  };
2590
2764
  }
2591
- async function* runTurn({ emergencySave, retainedToolPreviewChars, fileReadTracker, provider, compactionConfig, consolidateInto, backgroundJobs, backgroundJobOwner, fallbackProviders, model, tools, pluginManager, skillRegistry, skills, scope, pathBuilder, workingDirectory, limits, additionalDirectories, sandboxWorkspace, rules, structuredOutput, reviewAnswer, maxAnswerReviews, promoteMemory, prepareStep, resumeHandler, taskStore, systemPrompt, messages, projectInstructionContext, opts, taskGateway, completionInbox, promptContributions, runtimeToolOverrides, webSearch, web, sandboxProvider, sandboxTeardownTimeoutMs, onRunEvent, }) {
2765
+ async function* runTurn({ retainedToolPreviewChars, fileReadTracker, provider, compactionConfig, consolidateInto, backgroundJobs, backgroundJobOwner, fallbackProviders, model, tools, pluginManager, skillRegistry, skills, scope, turnId, paths, sessionLog, claimTurn, workingDirectory, limits, additionalDirectories, escalation, sandboxWorkspace, rules, structuredOutput, reviewAnswer, maxAnswerReviews, promoteMemory, prepareStep, resumeHandler, taskStore, systemPrompt, messages, projectInstructionContext, opts, taskGateway, completionInbox, promptContributions, runtimeToolOverrides, webSearch, web, sandboxProvider, sandboxTeardownTimeoutMs, onSessionEvent, }) {
2592
2766
  const signal = opts?.signal;
2593
2767
  // One presenter for the whole stream, built from the registry this scope
2594
2768
  // already holds. Its absence HERE is what forced presentation to be name
2595
- // matching in the first place: `toAgentEvent` is pure over a `RunEvent`
2769
+ // matching in the first place: `toAgentEvent` is pure over a `SessionEvent`
2596
2770
  // and could not ask a tool anything, so the host guessed from the name.
2597
2771
  const presenter = createToolPresenter(tools);
2598
2772
  try {
@@ -2601,18 +2775,22 @@ async function* runTurn({ emergencySave, retainedToolPreviewChars, fileReadTrack
2601
2775
  ...(fileReadTracker ? { fileReadTracker } : {}),
2602
2776
  ...(structuredOutput ? { structuredOutput } : {}),
2603
2777
  provider,
2604
- pathBuilder,
2605
- ...(opts?.runId ? { runId: opts.runId } : {}),
2778
+ paths,
2779
+ ...(sessionLog ? { sessionLog } : {}),
2780
+ // Reserved by the session, so a new turn begins under this id.
2781
+ turnId,
2782
+ ...(opts?.origin ? { origin: opts.origin } : {}),
2783
+ ...(opts?.abandonInterrupted ? { abandonInterrupted: true } : {}),
2606
2784
  // Omitted rather than empty when there is no tail. `query` treats the
2607
- // two the same, but an absent option reads as "this run has no chain"
2608
- // where `[]` reads as "this run has a chain with nothing in it".
2785
+ // two the same, but an absent option reads as "this turn has no chain"
2786
+ // where `[]` reads as "this turn has a chain with nothing in it".
2609
2787
  ...(fallbackProviders.length > 0 ? { fallbackProviders } : {}),
2610
2788
  tools,
2611
2789
  ...(pluginManager ? { pluginManager } : {}),
2612
2790
  ...(skillRegistry ? { skillRegistry } : {}),
2613
2791
  ...(skills ? { skills } : {}),
2614
2792
  // Withheld at both provider and executor boundaries on every ordinary
2615
- // turn. An admitted send owns the exact run-scoped authority above.
2793
+ // turn. An admitted send owns the exact turn-scoped authority above.
2616
2794
  ...(!opts?.goalRound ? { deniedTools: SESSION_GOAL_TOOL_NAMES } : {}),
2617
2795
  taskStore,
2618
2796
  ...(taskGateway ? { taskScheduler: taskGateway } : {}),
@@ -2626,20 +2804,19 @@ async function* runTurn({ emergencySave, retainedToolPreviewChars, fileReadTrack
2626
2804
  compactionConfig,
2627
2805
  ...(consolidateInto ? { consolidateInto } : {}),
2628
2806
  ...(backgroundJobs ? { backgroundJobs, backgroundJobOwner } : {}),
2629
- // Interactive chat may own process exit. An enclosing resident host
2630
- // instead drains the run and writes its finish/runner receipts first.
2631
- emergencySave,
2632
- runConfig: {
2807
+ turnConfig: {
2633
2808
  model,
2634
2809
  ...(sandboxProvider ? { sandbox: { workspace: sandboxWorkspace } } : {}),
2635
2810
  ...(opts?.effort !== undefined ? { effort: opts.effort } : {}),
2636
2811
  ...(webSearch ? { webSearch } : {}),
2637
- ...resolveRunGuards(limits),
2812
+ ...resolveTurnGuards(limits),
2638
2813
  maxResponseTokens: 8192,
2639
2814
  permissionMode: 'auto',
2815
+ // The kernel keeps every checkpoint unless told otherwise.
2816
+ pruneKeepLast: CLI_CHECKPOINT_RETENTION,
2640
2817
  },
2641
2818
  // The operator's gate, if they set one. Omitted rather than passed
2642
- // as undefined so a run with no gate is byte-identical to the one
2819
+ // as undefined so a turn with no gate is byte-identical to the one
2643
2820
  // that shipped before gates existed.
2644
2821
  ...(reviewAnswer ? { reviewAnswer } : {}),
2645
2822
  ...(maxAnswerReviews !== undefined ? { maxAnswerReviews } : {}),
@@ -2656,6 +2833,7 @@ async function* runTurn({ emergencySave, retainedToolPreviewChars, fileReadTrack
2656
2833
  ...(completionInbox ? { completionInbox } : {}),
2657
2834
  workingDirectory,
2658
2835
  ...(additionalDirectories?.length ? { additionalDirectories } : {}),
2836
+ ...(escalation ?? {}),
2659
2837
  // The exemption reads `tools` at decision time, so it sees the task
2660
2838
  // tools `query()` registers deferred below and any tool server that
2661
2839
  // connected after this session was built.
@@ -2673,28 +2851,35 @@ async function* runTurn({ emergencySave, retainedToolPreviewChars, fileReadTrack
2673
2851
  const next = await events.next();
2674
2852
  if (next.done) {
2675
2853
  settled = true;
2676
- // A Run contains its fresh static/dynamic system floor as well as the
2677
- // conversation. Only the latter crosses this host seam. Compaction
2678
- // summaries survive because they ARE conversation state; arbitrary
2679
- // system prompts are rebuilt fresh on every send and stay private to it.
2680
- opts?.onConversationMessages?.(projectRunConversation(next.value.messages));
2854
+ // A Turn contains its fresh static/dynamic system floor as well as
2855
+ // the conversation. Only the latter crosses this host seam.
2856
+ // Compaction summaries survive because they ARE conversation state;
2857
+ // arbitrary system prompts are rebuilt fresh on every send and stay
2858
+ // private to it.
2859
+ opts?.onConversationMessages?.(projectTurnConversation(next.value.messages));
2681
2860
  return;
2682
2861
  }
2683
2862
  const event = next.value;
2863
+ // Every event inside a turn names it. Should the kernel have begun the
2864
+ // turn under an id other than the reserved one, that id gets the same
2865
+ // authority before any of its tools can run.
2866
+ if ('turnId' in event && typeof event.turnId === 'string' && event.turnId !== turnId) {
2867
+ claimTurn(event.turnId);
2868
+ }
2684
2869
  // Before the abort check and before `toAgentEvent`: a session
2685
2870
  // cancelled mid-turn still produced the events up to that point, and
2686
2871
  // they are the interesting ones. Every event, not just the ones the
2687
2872
  // TUI renders — an export that only saw what the screen showed would
2688
2873
  // be a recording of the interface rather than of the session.
2689
- onRunEvent?.(event);
2874
+ onSessionEvent?.(event);
2690
2875
  if (signal?.aborted) {
2691
2876
  if (!abortReported) {
2692
2877
  abortReported = true;
2693
2878
  yield { kind: 'error', message: 'aborted' };
2694
2879
  }
2695
2880
  // Let cancellation settle in the kernel. Calling return() here
2696
- // discarded its Run and forced App to save only visible prose,
2697
- // losing tool receipts and reasoning before the next user turn.
2881
+ // discarded its Turn before the recorder closed it, losing tool
2882
+ // receipts and reasoning before the next user turn.
2698
2883
  continue;
2699
2884
  }
2700
2885
  const mapped = toAgentEvent(event, presenter);
@@ -2704,7 +2889,7 @@ async function* runTurn({ emergencySave, retainedToolPreviewChars, fileReadTrack
2704
2889
  }
2705
2890
  }
2706
2891
  finally {
2707
- // Manual iteration is what exposes the generator's Run return value.
2892
+ // Manual iteration is what exposes the generator's Turn return value.
2708
2893
  // Preserve `for await`'s other guarantee too: a consumer that stops
2709
2894
  // early must close the live query instead of abandoning its transport.
2710
2895
  if (!settled)
@@ -2712,6 +2897,23 @@ async function* runTurn({ emergencySave, retainedToolPreviewChars, fileReadTrack
2712
2897
  }
2713
2898
  }
2714
2899
  catch (err) {
2900
+ if (isTurnInProgressError(err)) {
2901
+ // Nothing was begun: the conversation already has an active turn. Said
2902
+ // by name, with the turn and its state, because the ways out differ —
2903
+ // a paused turn is resumed or abandoned, a running one is waited for.
2904
+ yield {
2905
+ kind: 'error',
2906
+ message: `This conversation already has a ${err.state} turn (${err.activeTurnId}). ${err.state === 'running'
2907
+ ? 'Wait for it to finish.'
2908
+ : 'Resume it with /resume, or close it with /abandon.'}`,
2909
+ turnInProgress: {
2910
+ sessionId: String(err.sessionId),
2911
+ activeTurnId: String(err.activeTurnId),
2912
+ state: err.state,
2913
+ },
2914
+ };
2915
+ return;
2916
+ }
2715
2917
  yield {
2716
2918
  kind: 'error',
2717
2919
  message: err instanceof Error ? err.message : String(err),
@@ -2725,12 +2927,16 @@ async function* runTurn({ emergencySave, retainedToolPreviewChars, fileReadTrack
2725
2927
  * (`createReviewHandler`); what this application adds is the person to ask
2726
2928
  * and the session's "approve all" box, which the screen also reads.
2727
2929
  */
2728
- export function makeResumeHandler(approval, onPermission, mode = onPermission ? 'prompt' : 'auto', exempt = () => false) {
2930
+ export function makeResumeHandler(approval, onPermission, mode = onPermission ? 'prompt' : 'auto', exempt = () => false, escapePolicy = {}) {
2729
2931
  return createReviewHandler({
2730
2932
  mode,
2731
2933
  prompt: onPermission,
2732
2934
  exempt,
2733
2935
  remembered: approval,
2936
+ // Refused unless the operator wrote `sandbox.allowUnattendedEscape`: a
2937
+ // session with nobody to ask has nobody to consent to leaving the
2938
+ // sandbox, and `auto` is not consent to a command it never showed.
2939
+ unattendedSandboxEscape: escapePolicy.unattendedSandboxEscape ?? 'refuse',
2734
2940
  });
2735
2941
  }
2736
2942
  /**
@@ -2750,15 +2956,15 @@ export function promptExemptToolNames(registry) {
2750
2956
  /** A batch needs explicit approval when any call mutates state. */
2751
2957
  export const batchNeedsPrompt = batchNeedsReview;
2752
2958
  /**
2753
- * Translate one SDK `RunEvent` into the TUI's `AgentEvent` vocabulary, or
2959
+ * Translate one SDK `SessionEvent` into the TUI's `AgentEvent` vocabulary, or
2754
2960
  * `null` for events the chat surface doesn't render (iteration markers,
2755
- * token usage, checkpoints, plan/task lifecycle, …). Pure — unit-tested.
2961
+ * checkpoints, plan lifecycle, …). Pure — unit-tested.
2756
2962
  */
2757
2963
  export function toAgentEvent(event, presenter) {
2758
2964
  switch (event.type) {
2759
2965
  case 'hosted_tool': {
2760
2966
  const common = {
2761
- runId: event.runId,
2967
+ turnId: event.turnId,
2762
2968
  toolUseId: event.tool.id,
2763
2969
  toolName: 'web_search',
2764
2970
  };
@@ -2783,7 +2989,7 @@ export function toAgentEvent(event, presenter) {
2783
2989
  text: event.text,
2784
2990
  ...(event.textPart ? { textPart: event.textPart } : {}),
2785
2991
  ...(event.messageId ? { messageId: event.messageId } : {}),
2786
- ...(event.runId ? { runId: event.runId } : {}),
2992
+ ...(event.turnId ? { turnId: event.turnId } : {}),
2787
2993
  };
2788
2994
  case 'reasoning_started':
2789
2995
  // A redacted block has no text to show; the empty delta still says
@@ -2796,7 +3002,7 @@ export function toAgentEvent(event, presenter) {
2796
3002
  case 'tool_executing':
2797
3003
  return {
2798
3004
  kind: 'tool-start',
2799
- runId: event.runId,
3005
+ turnId: event.turnId,
2800
3006
  ...(event.toolName === 'wait_for_task' &&
2801
3007
  typeof event.input?.task_id === 'string'
2802
3008
  ? { taskId: event.input.task_id }
@@ -2818,7 +3024,7 @@ export function toAgentEvent(event, presenter) {
2818
3024
  case 'tool_progress':
2819
3025
  return {
2820
3026
  kind: 'tool-progress',
2821
- runId: event.runId,
3027
+ turnId: event.turnId,
2822
3028
  toolUseId: event.toolUseId,
2823
3029
  toolName: event.toolName,
2824
3030
  message: event.message,
@@ -2840,7 +3046,7 @@ export function toAgentEvent(event, presenter) {
2840
3046
  return {
2841
3047
  kind: 'tool-end',
2842
3048
  output: event.result,
2843
- runId: event.runId,
3049
+ turnId: event.turnId,
2844
3050
  toolUseId: event.toolUseId,
2845
3051
  toolName: event.toolName,
2846
3052
  isError: event.isError,
@@ -2862,6 +3068,8 @@ export function toAgentEvent(event, presenter) {
2862
3068
  // cannot ground, and a `0` here would ground a wrong one.
2863
3069
  return {
2864
3070
  kind: 'usage',
3071
+ sessionId: event.sessionId,
3072
+ ...(event.turnId ? { turnId: event.turnId } : {}),
2865
3073
  totalTokens: event.usage.totalTokens,
2866
3074
  ...(event.budget ? { budget: event.budget } : {}),
2867
3075
  cost: event.cost,
@@ -2920,7 +3128,7 @@ export function toAgentEvent(event, presenter) {
2920
3128
  subject: event.subject,
2921
3129
  status: event.status,
2922
3130
  };
2923
- case 'run_paused':
3131
+ case 'turn_paused':
2924
3132
  // A pause is not an error and not an invisible end. The checkpoint and
2925
3133
  // classification are the recovery surface; dropping this event made a
2926
3134
  // shell report success and let the interactive queue run on a premise
@@ -2928,26 +3136,28 @@ export function toAgentEvent(event, presenter) {
2928
3136
  return {
2929
3137
  kind: 'paused',
2930
3138
  ...(event.budget ? { budget: event.budget } : {}),
2931
- runId: String(event.runId),
3139
+ turnId: String(event.turnId),
2932
3140
  checkpointId: event.checkpointId,
2933
3141
  reason: event.reason,
2934
3142
  ...(event.failure ? { failure: event.failure } : {}),
2935
3143
  ...(event.providerError ? { providerError: event.providerError } : {}),
2936
3144
  ...(event.explanation ? { explanation: event.explanation } : {}),
2937
3145
  };
2938
- case 'run_completed':
2939
- // Carried through rather than dropped: `run_failed` fires only from
3146
+ case 'turn_completed':
3147
+ // Carried through rather than dropped: `turn_failed` fires only from
2940
3148
  // the throw path, so this event is also how a budget stop, a
2941
3149
  // timeout, a cancellation and a blocked output guardrail arrive. A
2942
- // consumer that reads this as success reports one for a run whose
3150
+ // consumer that reads this as success reports one for a turn whose
2943
3151
  // answer was refused.
2944
3152
  return {
2945
3153
  kind: 'done',
3154
+ sessionId: event.sessionId,
3155
+ turnId: event.turnId,
2946
3156
  text: event.result,
2947
3157
  ...(event.budget ? { budget: event.budget } : {}),
2948
3158
  ...(event.stopReason ? { stopReason: event.stopReason } : {}),
2949
3159
  };
2950
- case 'run_failed':
3160
+ case 'turn_failed':
2951
3161
  // Keep the compatibility string and the structure. Prefixing the
2952
3162
  // message with only the coarse `provider_error` code discarded the
2953
3163
  // stable explanation, retry delay and first-hand provider detail while
@@ -3057,7 +3267,7 @@ function describeCompactionFailure(event) {
3057
3267
  * the logs use.
3058
3268
  */
3059
3269
  const FALLBACK_REASONS = {
3060
- rate_limit: 'it rate limited this run and the retries did not clear it',
3270
+ rate_limit: 'it rate limited this turn and the retries did not clear it',
3061
3271
  overloaded: 'it was overloaded and the retries did not clear it',
3062
3272
  server_error: 'it kept failing and the retries did not clear it',
3063
3273
  timeout: 'it did not answer in time',
@@ -3067,7 +3277,7 @@ const FALLBACK_REASONS = {
3067
3277
  unknown: 'it failed in a way namzu could not classify',
3068
3278
  };
3069
3279
  /**
3070
- * The one line an operator reads when their run changes hands.
3280
+ * The one line an operator reads when their turn changes hands.
3071
3281
  *
3072
3282
  * Both members are named with their chain position, because naming only the
3073
3283
  * replacement leaves an operator with four declared members unable to tell
@@ -3322,7 +3532,7 @@ function emptySession(errorHint, errorKind = 'environment') {
3322
3532
  },
3323
3533
  // Throws rather than reporting `no-checkpoint`. A resume that reported
3324
3534
  // "there is nothing to continue" when the truth is "this session has no
3325
- // provider" would let a drainer mark every run in a queue as a dead end
3535
+ // provider" would let a drainer mark every turn in a queue as a dead end
3326
3536
  // and move on — an unavailable capability degrading a check into a
3327
3537
  // wrong answer, on the one path where the answer is destructive.
3328
3538
  resumeDurable: async () => {