@letrquan/book 0.2.0 → 0.4.0-beta.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 (177) hide show
  1. package/CHANGELOG.md +3771 -40
  2. package/README.md +135 -1311
  3. package/dist/additional-roots-VAWRQ625.js +32 -0
  4. package/dist/{app-MOXXWEOY.js → app-J7BVPSUH.js} +9648 -5509
  5. package/dist/app-J7BVPSUH.js.map +1 -0
  6. package/dist/chunk-2AAOF5B5.js +28 -0
  7. package/dist/chunk-2AAOF5B5.js.map +1 -0
  8. package/dist/chunk-32WBCUAN.js +660 -0
  9. package/dist/chunk-32WBCUAN.js.map +1 -0
  10. package/dist/chunk-4DKHLZRI.js +120 -0
  11. package/dist/chunk-4DKHLZRI.js.map +1 -0
  12. package/dist/{chunk-CAGOB2N4.js → chunk-62EEAUID.js} +6 -4
  13. package/dist/chunk-62EEAUID.js.map +1 -0
  14. package/dist/chunk-AHC7OU75.js +1124 -0
  15. package/dist/chunk-AHC7OU75.js.map +1 -0
  16. package/dist/{chunk-E75IPST5.js → chunk-AJZWLKLQ.js} +73 -70
  17. package/dist/chunk-AJZWLKLQ.js.map +1 -0
  18. package/dist/chunk-B5FC3MYZ.js +395 -0
  19. package/dist/chunk-B5FC3MYZ.js.map +1 -0
  20. package/dist/{chunk-KWHQ2XXL.js → chunk-DNFQTSMJ.js} +55 -13
  21. package/dist/chunk-DNFQTSMJ.js.map +1 -0
  22. package/dist/{chunk-J7MIZ5CA.js → chunk-E5WLQSKA.js} +719 -89
  23. package/dist/chunk-E5WLQSKA.js.map +1 -0
  24. package/dist/chunk-ECLC4CIJ.js +1181 -0
  25. package/dist/chunk-ECLC4CIJ.js.map +1 -0
  26. package/dist/chunk-EEX6QNXV.js +127 -0
  27. package/dist/chunk-EEX6QNXV.js.map +1 -0
  28. package/dist/chunk-EFXVSQ5J.js +931 -0
  29. package/dist/chunk-EFXVSQ5J.js.map +1 -0
  30. package/dist/{chunk-V4BOYR52.js → chunk-ENLB7NYV.js} +1304 -326
  31. package/dist/chunk-ENLB7NYV.js.map +1 -0
  32. package/dist/chunk-FPR7SKIE.js +6926 -0
  33. package/dist/chunk-FPR7SKIE.js.map +1 -0
  34. package/dist/{chunk-DGMQEI6Y.js → chunk-HFB74CLM.js} +30 -9
  35. package/dist/chunk-HFB74CLM.js.map +1 -0
  36. package/dist/{chunk-5RDRAO4B.js → chunk-HOZXKOE3.js} +37 -1
  37. package/dist/chunk-HOZXKOE3.js.map +1 -0
  38. package/dist/chunk-IDYWN37T.js +36 -0
  39. package/dist/chunk-IDYWN37T.js.map +1 -0
  40. package/dist/chunk-J4F2QLM2.js +273 -0
  41. package/dist/chunk-J4F2QLM2.js.map +1 -0
  42. package/dist/chunk-JGF5HLSI.js +1065 -0
  43. package/dist/chunk-JGF5HLSI.js.map +1 -0
  44. package/dist/{chunk-XIFFTATU.js → chunk-KDNWMKXV.js} +7166 -4237
  45. package/dist/chunk-KDNWMKXV.js.map +1 -0
  46. package/dist/chunk-KXAYUIKP.js +90 -0
  47. package/dist/chunk-KXAYUIKP.js.map +1 -0
  48. package/dist/{chunk-3U5IBM24.js → chunk-MEOB2DZA.js} +12 -2
  49. package/dist/chunk-MEOB2DZA.js.map +1 -0
  50. package/dist/{chunk-WKBO5R4O.js → chunk-MV5PQNIA.js} +2 -2
  51. package/dist/chunk-MV5PQNIA.js.map +1 -0
  52. package/dist/chunk-N2G4Q7ME.js +2672 -0
  53. package/dist/chunk-N2G4Q7ME.js.map +1 -0
  54. package/dist/chunk-OJBMDM5H.js +299 -0
  55. package/dist/chunk-OJBMDM5H.js.map +1 -0
  56. package/dist/{chunk-URLQAXTR.js → chunk-PTDC7ZUP.js} +2 -2
  57. package/dist/chunk-PTDC7ZUP.js.map +1 -0
  58. package/dist/{chunk-574C6MQR.js → chunk-REASC2QG.js} +173 -64
  59. package/dist/chunk-REASC2QG.js.map +1 -0
  60. package/dist/{chunk-7N4J557C.js → chunk-RT5SATF7.js} +1 -1
  61. package/dist/chunk-RT5SATF7.js.map +1 -0
  62. package/dist/chunk-SBTU5BEC.js +114 -0
  63. package/dist/chunk-SBTU5BEC.js.map +1 -0
  64. package/dist/{chunk-5KLVH3PY.js → chunk-SWPDKLNM.js} +5 -3
  65. package/dist/chunk-SWPDKLNM.js.map +1 -0
  66. package/dist/chunk-UCFJ43OS.js +520 -0
  67. package/dist/chunk-UCFJ43OS.js.map +1 -0
  68. package/dist/{chunk-VZQXX3WW.js → chunk-W7SLYS3M.js} +80 -16
  69. package/dist/chunk-W7SLYS3M.js.map +1 -0
  70. package/dist/{chunk-ANSJPYGI.js → chunk-WV3O5PGX.js} +3 -3
  71. package/dist/chunk-WV3O5PGX.js.map +1 -0
  72. package/dist/chunk-XHAO3K7I.js +18 -0
  73. package/dist/chunk-XHAO3K7I.js.map +1 -0
  74. package/dist/chunk-XJRJVXJR.js +1050 -0
  75. package/dist/chunk-XJRJVXJR.js.map +1 -0
  76. package/dist/chunk-YIL6HTJX.js +663 -0
  77. package/dist/chunk-YIL6HTJX.js.map +1 -0
  78. package/dist/{chunk-24AXE6SP.js → chunk-ZPSZZUWF.js} +1 -1
  79. package/dist/chunk-ZPSZZUWF.js.map +1 -0
  80. package/dist/{command-approvals-6Y57VCWZ.js → command-approvals-ZRY66LCW.js} +7 -5
  81. package/dist/context-C3RAP6K5.js +41 -0
  82. package/dist/{hook-approvals-CTUFKEJT.js → hook-approvals-DPQD5H65.js} +6 -5
  83. package/dist/index.js +947 -203
  84. package/dist/index.js.map +1 -1
  85. package/dist/{interactive-assets-CCAAWVKH.js → interactive-assets-73AQO5WP.js} +18 -9
  86. package/dist/interactive-assets-73AQO5WP.js.map +1 -0
  87. package/dist/job-runner.js +166 -111
  88. package/dist/job-runner.js.map +1 -1
  89. package/dist/job-supervisor.d.ts +2 -0
  90. package/dist/job-supervisor.js +121 -0
  91. package/dist/job-supervisor.js.map +1 -0
  92. package/dist/loader-DQNPDKOX.js +27 -0
  93. package/dist/{mcp-approvals-WNPH65BK.js → mcp-approvals-27LJYADQ.js} +6 -5
  94. package/dist/{mcp-config-YIJIES2C.js → mcp-config-GXSODNCM.js} +4 -3
  95. package/dist/memory-store-I6Z54VUN.js +51 -0
  96. package/dist/{model-window-store-YFVNXCIP.js → model-window-store-6VKXXTPJ.js} +9 -4
  97. package/dist/openai-compatible-IPUNBJ53.js +16 -0
  98. package/dist/permission-approvals-Q5IMFEZI.js +15 -0
  99. package/dist/permissions-7Y35X6HZ.js +62 -0
  100. package/dist/provider-approvals-5AGH3RVT.js +76 -0
  101. package/dist/sandbox-N2RY72GN.js +42 -0
  102. package/dist/{scrollback-LUKLIBVI.js → scrollback-GD76GNM4.js} +44 -26
  103. package/dist/scrollback-GD76GNM4.js.map +1 -0
  104. package/dist/sdk.d.ts +2739 -2069
  105. package/dist/sdk.js +77 -42
  106. package/dist/sdk.js.map +1 -1
  107. package/dist/settings-loader-KDBH45AQ.js +43 -0
  108. package/dist/settings-loader-KDBH45AQ.js.map +1 -0
  109. package/dist/{settings-removed-JWWPT4WA.js → settings-removed-3ATWJEF3.js} +2 -2
  110. package/dist/settings-removed-3ATWJEF3.js.map +1 -0
  111. package/dist/shell-selection-Z5WORZU4.js +31 -0
  112. package/dist/shell-selection-Z5WORZU4.js.map +1 -0
  113. package/package.json +25 -24
  114. package/dist/app-MOXXWEOY.js.map +0 -1
  115. package/dist/chunk-24AXE6SP.js.map +0 -1
  116. package/dist/chunk-3U5IBM24.js.map +0 -1
  117. package/dist/chunk-574C6MQR.js.map +0 -1
  118. package/dist/chunk-5GDZ22YP.js +0 -449
  119. package/dist/chunk-5GDZ22YP.js.map +0 -1
  120. package/dist/chunk-5KLVH3PY.js.map +0 -1
  121. package/dist/chunk-5RDRAO4B.js.map +0 -1
  122. package/dist/chunk-7N4J557C.js.map +0 -1
  123. package/dist/chunk-A7JLBK2Y.js +0 -3506
  124. package/dist/chunk-A7JLBK2Y.js.map +0 -1
  125. package/dist/chunk-ANSJPYGI.js.map +0 -1
  126. package/dist/chunk-BTZZUCLI.js +0 -264
  127. package/dist/chunk-BTZZUCLI.js.map +0 -1
  128. package/dist/chunk-C7D4NJ2D.js +0 -217
  129. package/dist/chunk-C7D4NJ2D.js.map +0 -1
  130. package/dist/chunk-CAGOB2N4.js.map +0 -1
  131. package/dist/chunk-DGMQEI6Y.js.map +0 -1
  132. package/dist/chunk-E75IPST5.js.map +0 -1
  133. package/dist/chunk-EYGD6X2T.js +0 -180
  134. package/dist/chunk-EYGD6X2T.js.map +0 -1
  135. package/dist/chunk-GTVB4HEM.js +0 -765
  136. package/dist/chunk-GTVB4HEM.js.map +0 -1
  137. package/dist/chunk-J7MIZ5CA.js.map +0 -1
  138. package/dist/chunk-KWHQ2XXL.js.map +0 -1
  139. package/dist/chunk-L6LK2QIQ.js +0 -657
  140. package/dist/chunk-L6LK2QIQ.js.map +0 -1
  141. package/dist/chunk-MAQXCUR4.js +0 -1007
  142. package/dist/chunk-MAQXCUR4.js.map +0 -1
  143. package/dist/chunk-S4XL7HOM.js +0 -402
  144. package/dist/chunk-S4XL7HOM.js.map +0 -1
  145. package/dist/chunk-URLQAXTR.js.map +0 -1
  146. package/dist/chunk-V4BOYR52.js.map +0 -1
  147. package/dist/chunk-VZQXX3WW.js.map +0 -1
  148. package/dist/chunk-WKBO5R4O.js.map +0 -1
  149. package/dist/chunk-XIFFTATU.js.map +0 -1
  150. package/dist/context-ZC5IFEAX.js +0 -25
  151. package/dist/interactive-assets-CCAAWVKH.js.map +0 -1
  152. package/dist/loader-4IDS2RIE.js +0 -15
  153. package/dist/openai-compatible-RHZUBHV6.js +0 -10
  154. package/dist/permission-approvals-76PVREFD.js +0 -14
  155. package/dist/permissions-CHCKTFQQ.js +0 -28
  156. package/dist/sandbox-CNVAQDA7.js +0 -23
  157. package/dist/scrollback-LUKLIBVI.js.map +0 -1
  158. package/dist/settings-loader-M2QP5TX2.js +0 -21
  159. package/dist/shell-selection-V6QAYXPX.js +0 -17
  160. package/patches/ink+6.8.0.patch +0 -13
  161. package/scripts/apply-ink-patch.mjs +0 -29
  162. package/scripts/ink-patch.mjs +0 -59
  163. package/scripts/verify-ink-patch.mjs +0 -11
  164. /package/dist/{command-approvals-6Y57VCWZ.js.map → additional-roots-VAWRQ625.js.map} +0 -0
  165. /package/dist/{context-ZC5IFEAX.js.map → command-approvals-ZRY66LCW.js.map} +0 -0
  166. /package/dist/{hook-approvals-CTUFKEJT.js.map → context-C3RAP6K5.js.map} +0 -0
  167. /package/dist/{loader-4IDS2RIE.js.map → hook-approvals-DPQD5H65.js.map} +0 -0
  168. /package/dist/{mcp-approvals-WNPH65BK.js.map → loader-DQNPDKOX.js.map} +0 -0
  169. /package/dist/{mcp-config-YIJIES2C.js.map → mcp-approvals-27LJYADQ.js.map} +0 -0
  170. /package/dist/{model-window-store-YFVNXCIP.js.map → mcp-config-GXSODNCM.js.map} +0 -0
  171. /package/dist/{openai-compatible-RHZUBHV6.js.map → memory-store-I6Z54VUN.js.map} +0 -0
  172. /package/dist/{permission-approvals-76PVREFD.js.map → model-window-store-6VKXXTPJ.js.map} +0 -0
  173. /package/dist/{permissions-CHCKTFQQ.js.map → openai-compatible-IPUNBJ53.js.map} +0 -0
  174. /package/dist/{sandbox-CNVAQDA7.js.map → permission-approvals-Q5IMFEZI.js.map} +0 -0
  175. /package/dist/{settings-loader-M2QP5TX2.js.map → permissions-7Y35X6HZ.js.map} +0 -0
  176. /package/dist/{settings-removed-JWWPT4WA.js.map → provider-approvals-5AGH3RVT.js.map} +0 -0
  177. /package/dist/{shell-selection-V6QAYXPX.js.map → sandbox-N2RY72GN.js.map} +0 -0
@@ -5,46 +5,107 @@ import {
5
5
  createAgentRunContext,
6
6
  createRunAmbientSnapshot,
7
7
  createTerminalOutcome,
8
+ isSpendlessUsage,
8
9
  isTerminalStatus,
9
10
  loadGitignore,
10
11
  runAgentLoop,
11
- runCompact,
12
- runHooks,
13
- runPostCompactHooks
14
- } from "./chunk-XIFFTATU.js";
12
+ seedShellIds
13
+ } from "./chunk-KDNWMKXV.js";
15
14
  import {
16
15
  DEFAULT_CONFIDENCE_THRESHOLD,
17
16
  REVIEW_LENSES,
17
+ addUsage,
18
+ applyCompactResult,
18
19
  buildReviewerPrompt,
19
20
  buildSingleReviewPrompt,
20
21
  clampConfidence,
22
+ createGoal,
21
23
  isReviewCategory,
22
24
  isReviewSeverity,
25
+ judgeCompaction,
26
+ judgedResult,
23
27
  loadReviewConfig,
24
28
  renderReviewConfigInstruction,
25
29
  renderReviewTarget,
26
- resolveReviewTarget
27
- } from "./chunk-A7JLBK2Y.js";
30
+ resolveCompactBudgets,
31
+ resolveReviewTarget,
32
+ restoreGoal,
33
+ runCompact,
34
+ runHooks,
35
+ runPostCompactHooks
36
+ } from "./chunk-FPR7SKIE.js";
28
37
  import {
38
+ observationKey,
39
+ observationPathFor,
29
40
  onAbort,
30
41
  throwIfAborted,
42
+ workspaceIdentity,
31
43
  yieldToEventLoop
32
- } from "./chunk-MAQXCUR4.js";
44
+ } from "./chunk-N2G4Q7ME.js";
33
45
  import {
34
- normalizeToolResult,
35
- observationKey,
36
- workspaceIdentity
37
- } from "./chunk-5GDZ22YP.js";
46
+ globWalkIgnore
47
+ } from "./chunk-XJRJVXJR.js";
48
+ import {
49
+ defaultGitRunner,
50
+ describeMissingObjects,
51
+ hardenedGitArgs,
52
+ noFetchGitEnv,
53
+ readRepositoryGitConfig
54
+ } from "./chunk-32WBCUAN.js";
55
+ import {
56
+ buildChildEnv
57
+ } from "./chunk-XHAO3K7I.js";
58
+ import {
59
+ findRepositoryRoot,
60
+ tryRealPath
61
+ } from "./chunk-SBTU5BEC.js";
62
+ import {
63
+ resolveAdditionalRoots
64
+ } from "./chunk-4DKHLZRI.js";
38
65
  import {
39
66
  createDebugLogger,
40
67
  createDebugLoggerWithCounter
41
- } from "./chunk-GTVB4HEM.js";
68
+ } from "./chunk-OJBMDM5H.js";
69
+ import {
70
+ isOutside,
71
+ normalizeToolResult,
72
+ resolveMentionReadablePath
73
+ } from "./chunk-YIL6HTJX.js";
74
+ import {
75
+ createUncGuard,
76
+ reachesUncPath,
77
+ uncGuardedWalkFs
78
+ } from "./chunk-EEX6QNXV.js";
79
+ import {
80
+ renderUntrustedText
81
+ } from "./chunk-WV3O5PGX.js";
82
+
83
+ // src/session-driver.ts
84
+ var SESSION_DRIVERS = ["human", "agent"];
85
+ function isSessionDriver(value) {
86
+ return SESSION_DRIVERS.includes(value);
87
+ }
88
+ function parseSessionDriver(value) {
89
+ const normalized = value?.trim().toLowerCase();
90
+ return isSessionDriver(normalized) ? normalized : void 0;
91
+ }
92
+ function resolveSessionDriver(input) {
93
+ const explicit = parseSessionDriver(input.explicit);
94
+ if (explicit) return { driver: explicit, signals: ["flag"] };
95
+ const fromEnv = parseSessionDriver(input.env.BOOK_SESSION_DRIVER);
96
+ if (fromEnv) return { driver: fromEnv, signals: ["BOOK_SESSION_DRIVER"] };
97
+ const signals = [];
98
+ const claudeCode = input.env.CLAUDECODE?.trim();
99
+ if (claudeCode && claudeCode !== "0") signals.push("CLAUDECODE");
100
+ if (input.env.AI_AGENT?.trim()) signals.push("AI_AGENT");
101
+ return { driver: signals.length > 0 ? "agent" : "human", signals };
102
+ }
42
103
 
43
104
  // src/session/name.ts
44
105
  var UNTITLED_SESSION_NAME = "Untitled session";
45
106
  var MAX_SESSION_NAME_LENGTH = 56;
46
107
  function deriveSessionName(prompt) {
47
- const normalized = prompt.replace(/\r?\n/g, " ").replace(/\s+/g, " ").trim().replace(/^[/!#>*_`~-]+\s*/, "").replace(/[.!?,;:]+$/, "").trim();
108
+ const normalized = prompt.replace(/^\s*`{3,}[^\n`]*\r?\n/, "").replace(/^[ \t]*`{3,}[\w+-]*[ \t\r]*$/gm, "").replace(/\r?\n/g, " ").replace(/\s+/g, " ").trim().replace(/^[/!#>*_~-]+\s*/, "").replace(/[.!?,;:]+$/, "").trim();
48
109
  if (!normalized) return UNTITLED_SESSION_NAME;
49
110
  const titled = normalized.replace(/^[a-z]/, (letter) => letter.toUpperCase());
50
111
  if (Array.from(titled).length <= MAX_SESSION_NAME_LENGTH) return titled;
@@ -85,6 +146,7 @@ var READ_REF_LIMIT = 8;
85
146
  var READ_TOTAL_CHARS = 16e3;
86
147
  var TOOL_RESULT_PREVIEW_CHARS = 4e3;
87
148
  var SESSION_INDEX_FILE = "session-index.json";
149
+ var HEADER_PROBE_BYTES = 16 * 1024;
88
150
  var JSONL_READ_BUFFER_BYTES = 64 * 1024;
89
151
  var IMAGE_STORAGE_KEY = /^[a-f0-9]{64}\.(?:png|jpg|gif|webp)$/;
90
152
  function imageSessionDirectory(root, sessionId) {
@@ -173,30 +235,37 @@ var SessionStore = class {
173
235
  }
174
236
  persistIndex(deletedIds = [], changedIds) {
175
237
  if (this.buildingIndex) return;
238
+ this.withIndexLock(() => this.writeIndexLocked(deletedIds, changedIds));
239
+ }
240
+ withIndexLock(action) {
176
241
  const lock = this.acquireIndexLock();
177
242
  try {
178
- const sessions = this.readPersistedIndexEntries();
179
- const ids = changedIds ?? Array.from(this.metadataIndex.keys());
180
- for (const id of ids) {
181
- const entry = this.metadataIndex.get(id);
182
- if (entry) sessions[id] = entry;
183
- }
184
- for (const id of deletedIds) delete sessions[id];
185
- const target = this.indexPath();
186
- const temporary = `${target}.${process.pid}.${crypto.randomUUID()}.tmp`;
187
- try {
188
- writeFileSync(temporary, JSON.stringify({ version: 1, sessions }), "utf-8");
189
- renameSync(temporary, target);
190
- this.indexMtimeMs = statSync(target).mtimeMs;
191
- this.metadataIndex.clear();
192
- for (const [id, entry] of Object.entries(sessions)) this.metadataIndex.set(id, entry);
193
- } finally {
194
- if (existsSync(temporary)) unlinkSync(temporary);
195
- }
243
+ return action();
196
244
  } finally {
197
245
  this.releaseIndexLock(lock);
198
246
  }
199
247
  }
248
+ /** The index write itself; the caller holds the index lock. */
249
+ writeIndexLocked(deletedIds, changedIds) {
250
+ const sessions = this.readPersistedIndexEntries();
251
+ const ids = changedIds ?? Array.from(this.metadataIndex.keys());
252
+ for (const id of ids) {
253
+ const entry = this.metadataIndex.get(id);
254
+ if (entry) sessions[id] = entry;
255
+ }
256
+ for (const id of deletedIds) delete sessions[id];
257
+ const target = this.indexPath();
258
+ const temporary = `${target}.${process.pid}.${crypto.randomUUID()}.tmp`;
259
+ try {
260
+ writeFileSync(temporary, JSON.stringify({ version: 1, sessions }), "utf-8");
261
+ renameSync(temporary, target);
262
+ this.indexMtimeMs = statSync(target).mtimeMs;
263
+ this.metadataIndex.clear();
264
+ for (const [id, entry] of Object.entries(sessions)) this.metadataIndex.set(id, entry);
265
+ } finally {
266
+ if (existsSync(temporary)) unlinkSync(temporary);
267
+ }
268
+ }
200
269
  acquireIndexLock() {
201
270
  const path = `${this.indexPath()}.lock`;
202
271
  for (let attempt = 0; attempt < 50; attempt++) {
@@ -274,7 +343,8 @@ var SessionStore = class {
274
343
  name: meta.name,
275
344
  createdAt: now,
276
345
  updatedAt: now,
277
- messageCount: 0
346
+ messageCount: 0,
347
+ ...meta.driver ? { driver: meta.driver } : {}
278
348
  }
279
349
  };
280
350
  appendFileSync(this.path(id), JSON.stringify(header) + "\n", "utf-8");
@@ -286,10 +356,10 @@ var SessionStore = class {
286
356
  appendFileSync(this.path(id), serialized, "utf-8");
287
357
  const cached = this.parsedSessions.get(id);
288
358
  if (cached) {
289
- const stat = statSync(this.path(id));
359
+ const stat2 = statSync(this.path(id));
290
360
  cached.records.push(record);
291
- cached.size = stat.size;
292
- cached.mtimeMs = stat.mtimeMs;
361
+ cached.size = stat2.size;
362
+ cached.mtimeMs = stat2.mtimeMs;
293
363
  cached.loaded = void 0;
294
364
  cached.activeEventIds = void 0;
295
365
  cached.activeRecords = void 0;
@@ -300,12 +370,12 @@ var SessionStore = class {
300
370
  readRecords(id) {
301
371
  const p = this.path(id);
302
372
  if (!existsSync(p)) throw new Error(`Session not found: ${id}`);
303
- const stat = statSync(p);
373
+ const stat2 = statSync(p);
304
374
  const cached = this.parsedSessions.get(id);
305
- if (cached && cached.size === stat.size && cached.mtimeMs === stat.mtimeMs) {
375
+ if (cached && cached.size === stat2.size && cached.mtimeMs === stat2.mtimeMs) {
306
376
  return cached.records;
307
377
  }
308
- const records = [];
378
+ const records2 = [];
309
379
  const descriptor = openSync(p, "r");
310
380
  const buffer = Buffer.allocUnsafe(JSONL_READ_BUFFER_BYTES);
311
381
  const decoder = new StringDecoder("utf8");
@@ -315,7 +385,7 @@ var SessionStore = class {
315
385
  lineNumber++;
316
386
  if (!line) return;
317
387
  try {
318
- records.push(JSON.parse(line));
388
+ records2.push(JSON.parse(line));
319
389
  } catch {
320
390
  log.warn("ignoring corrupt session record", { id, line: lineNumber });
321
391
  }
@@ -337,11 +407,11 @@ var SessionStore = class {
337
407
  closeSync(descriptor);
338
408
  }
339
409
  this.parsedSessions.set(id, {
340
- size: stat.size,
341
- mtimeMs: stat.mtimeMs,
342
- records
410
+ size: stat2.size,
411
+ mtimeMs: stat2.mtimeMs,
412
+ records: records2
343
413
  });
344
- return records;
414
+ return records2;
345
415
  }
346
416
  updateIndexAfterAppend(id, record) {
347
417
  const entry = this.metadataIndex.get(id);
@@ -356,10 +426,15 @@ var SessionStore = class {
356
426
  entry.meta.updatedAt = record.timestamp;
357
427
  if (data.kind === "session_meta_patch") {
358
428
  if (Object.prototype.hasOwnProperty.call(data, "name")) entry.meta.name = data.name;
429
+ if (isSessionDriver(data.driver)) {
430
+ entry.meta.driver = data.driver;
431
+ }
359
432
  } else if (record.type === "user") {
360
433
  entry.meta.messageCount++;
361
434
  entry.lastMessageRole = "user";
362
- if (!entry.meta.name && data.content) entry.meta.name = deriveSessionName(data.content);
435
+ if (!entry.meta.name && data.content && data.kind !== "agent-notification") {
436
+ entry.meta.name = deriveSessionName(data.content);
437
+ }
363
438
  } else if (record.type === "local") {
364
439
  entry.meta.messageCount++;
365
440
  entry.lastMessageRole = "assistant";
@@ -393,10 +468,10 @@ var SessionStore = class {
393
468
  });
394
469
  }
395
470
  load(id) {
396
- const records = this.readRecords(id);
471
+ const records2 = this.readRecords(id);
397
472
  const parsedCache = this.parsedSessions.get(id);
398
473
  if (parsedCache?.loaded) return cloneLoadedSession(parsedCache.loaded);
399
- const metaRec = records.find(
474
+ const metaRec = records2.find(
400
475
  (record) => record.data?.kind === "session_meta"
401
476
  );
402
477
  const storedMeta = metaRec?.data;
@@ -406,7 +481,8 @@ var SessionStore = class {
406
481
  createdAt: storedMeta?.createdAt ?? 0,
407
482
  updatedAt: storedMeta?.updatedAt ?? 0,
408
483
  messageCount: 0,
409
- ...storedMeta?.name === void 0 ? {} : { name: storedMeta.name }
484
+ ...storedMeta?.name === void 0 ? {} : { name: storedMeta.name },
485
+ ...isSessionDriver(storedMeta?.driver) ? { driver: storedMeta.driver } : {}
410
486
  };
411
487
  const transcript = [];
412
488
  let contextHistory = [];
@@ -418,30 +494,28 @@ var SessionStore = class {
418
494
  const checkpointByUserEventId = /* @__PURE__ */ new Map();
419
495
  const snapshotIds = /* @__PURE__ */ new Set();
420
496
  let latestPlan;
497
+ let latestGoal;
421
498
  let carriedUsage;
422
499
  const carriedModels = /* @__PURE__ */ new Set();
423
500
  let count = 0;
424
- for (let lineIndex = 0; lineIndex < records.length; lineIndex++) {
425
- const record = records[lineIndex];
501
+ for (let lineIndex = 0; lineIndex < records2.length; lineIndex++) {
502
+ const record = records2[lineIndex];
426
503
  const eventId = record.eventId ?? `legacy-line-${lineIndex + 1}`;
427
504
  const data = record.data;
428
505
  if (data.kind === "session_meta_patch") {
429
506
  if (Object.prototype.hasOwnProperty.call(data, "name")) meta.name = data.name;
507
+ const driver = data.driver;
508
+ if (isSessionDriver(driver)) meta.driver = driver;
430
509
  continue;
431
510
  }
432
511
  if (data.kind === "session_meta" || data.kind === "session_touch") continue;
433
512
  if (record.type === "usage") {
434
513
  const entry = record.data;
435
514
  if (entry?.version === 1 && entry.usage) {
436
- carriedUsage = {
437
- promptTokens: (carriedUsage?.promptTokens ?? 0) + (entry.usage.promptTokens ?? 0),
438
- completionTokens: (carriedUsage?.completionTokens ?? 0) + (entry.usage.completionTokens ?? 0),
439
- totalTokens: (carriedUsage?.totalTokens ?? 0) + (entry.usage.totalTokens ?? 0),
440
- cacheCreationInputTokens: (carriedUsage?.cacheCreationInputTokens ?? 0) + (entry.usage.cacheCreationInputTokens ?? 0),
441
- cacheReadInputTokens: (carriedUsage?.cacheReadInputTokens ?? 0) + (entry.usage.cacheReadInputTokens ?? 0)
442
- };
515
+ carriedUsage = addUsage(carriedUsage ?? null, entry.usage);
443
516
  const model = entry.responseModel ?? entry.requestedModel;
444
517
  if (model) carriedModels.add(model);
518
+ for (const spent of entry.models ?? []) carriedModels.add(spent);
445
519
  }
446
520
  continue;
447
521
  }
@@ -450,6 +524,11 @@ var SessionStore = class {
450
524
  if (plan?.version === 1) latestPlan = plan;
451
525
  continue;
452
526
  }
527
+ if (record.type === "goal") {
528
+ const goal = record.data;
529
+ if (goal?.version === 1) latestGoal = goal;
530
+ continue;
531
+ }
453
532
  if (record.type === "turn_checkpoint") {
454
533
  const checkpoint = record.data;
455
534
  if (checkpoint?.version !== 1 || !checkpoint.checkpointId || !checkpoint.userEventId || typeof checkpoint.prompt !== "string") {
@@ -460,6 +539,7 @@ var SessionStore = class {
460
539
  id: checkpoint.checkpointId,
461
540
  userEventId: checkpoint.userEventId,
462
541
  prompt: checkpoint.prompt,
542
+ ...checkpoint.promptTyped === true ? { promptTyped: true } : {},
463
543
  attachments: checkpoint.attachments,
464
544
  timestamp: record.timestamp,
465
545
  ...checkpoint.checkpoint,
@@ -601,6 +681,7 @@ var SessionStore = class {
601
681
  toolCalls: data.toolCalls,
602
682
  toolResults: normalizePersistedToolResults(data.toolResults),
603
683
  fileObservations: data.fileObservations,
684
+ hostNotice: data.hostNotice,
604
685
  timestamp: record.timestamp
605
686
  };
606
687
  transcript.push(message);
@@ -639,11 +720,11 @@ var SessionStore = class {
639
720
  }
640
721
  }
641
722
  }
642
- if (records.length) meta.updatedAt = records[records.length - 1].timestamp;
723
+ if (records2.length) meta.updatedAt = records2[records2.length - 1].timestamp;
643
724
  meta.messageCount = count;
644
725
  if (!meta.name) {
645
726
  const firstUserPrompt = transcript.find(
646
- (message) => message.role === "user" && message.kind !== "local"
727
+ (message) => message.role === "user" && message.kind !== "local" && message.kind !== "agent-notification"
647
728
  )?.content;
648
729
  if (firstUserPrompt) meta.name = deriveSessionName(firstUserPrompt);
649
730
  }
@@ -658,6 +739,7 @@ var SessionStore = class {
658
739
  activeEventIds,
659
740
  meta,
660
741
  plan: latestPlan,
742
+ goal: latestGoal,
661
743
  carriedUsage,
662
744
  carriedModels: [...carriedModels],
663
745
  history: contextHistory
@@ -780,9 +862,9 @@ var SessionStore = class {
780
862
  activeRecordMap(id) {
781
863
  const cache = this.parsedSessions.get(id);
782
864
  if (cache?.recordsByActiveId) return cache.recordsByActiveId;
783
- const records = this.activeRecordIndex(id);
865
+ const records2 = this.activeRecordIndex(id);
784
866
  const refreshed = this.parsedSessions.get(id);
785
- const byId = new Map(records.map((record) => [record.eventId, record]));
867
+ const byId = new Map(records2.map((record) => [record.eventId, record]));
786
868
  if (refreshed) refreshed.recordsByActiveId = byId;
787
869
  return byId;
788
870
  }
@@ -805,9 +887,15 @@ var SessionStore = class {
805
887
  }
806
888
  return references;
807
889
  }
808
- mostRecentInCwd(cwd) {
890
+ /**
891
+ * The newest session in `cwd` that holds a conversation. A launch that ends
892
+ * before its first prompt leaves a session with no messages, and that is never
893
+ * the conversation `--continue` means (#428). `accept` replaces that default test
894
+ * for a caller with a wider idea of a live session, such as `book status`.
895
+ */
896
+ mostRecentInCwd(cwd, accept = (meta) => meta.messageCount > 0) {
809
897
  const normalized = normalizeWorkspace(cwd);
810
- return this.list().find((meta) => meta.cwd === normalized);
898
+ return this.list().find((meta) => meta.cwd === normalized && accept(meta));
811
899
  }
812
900
  findByName(name) {
813
901
  return this.list().find((meta) => meta.name === name);
@@ -824,19 +912,13 @@ var SessionStore = class {
824
912
  if (preserveIds.has(id)) continue;
825
913
  try {
826
914
  if (entry.meta.updatedAt < cutoff) {
827
- unlinkSync(this.path(id));
828
- rmSync(imageSessionDirectory(this.root, id), { recursive: true, force: true });
829
- this.metadataIndex.delete(id);
830
- this.parsedSessions.delete(id);
915
+ this.discardSession(id);
831
916
  removedIds.push(id);
832
917
  removed++;
833
918
  }
834
919
  } catch {
835
920
  if (existsSync(this.path(id)) && statSync(this.path(id)).mtimeMs < cutoff) {
836
- unlinkSync(this.path(id));
837
- rmSync(imageSessionDirectory(this.root, id), { recursive: true, force: true });
838
- this.metadataIndex.delete(id);
839
- this.parsedSessions.delete(id);
921
+ this.discardSession(id);
840
922
  removedIds.push(id);
841
923
  removed++;
842
924
  }
@@ -845,7 +927,85 @@ var SessionStore = class {
845
927
  if (removedIds.length > 0) this.persistIndex(removedIds, []);
846
928
  return removed;
847
929
  }
930
+ /**
931
+ * Remove one session this process created if nothing was ever recorded in it: its file
932
+ * holds nothing but the bare header `create` wrote (no turn, no run, no patch, no name).
933
+ * A name is something a user typed (`--name`), so a named session is kept. The decision
934
+ * is made from the stored file, under the index lock, and the file and its index entry go
935
+ * together. A file another process still holds (EBUSY, EPERM on Windows) is left in place.
936
+ * Once the file is gone the session is gone: an attachment directory that will not go, or an
937
+ * index file that cannot be rewritten, no longer undoes that (a later index rebuild drops the
938
+ * stale entry). Returns whether the session was removed.
939
+ */
940
+ removeUntouched(id) {
941
+ if (!existsSync(this.path(id))) return false;
942
+ this.ensureIndex();
943
+ const entry = this.metadataIndex.get(id);
944
+ if (entry && (entry.meta.messageCount > 0 || entry.meta.name)) return false;
945
+ try {
946
+ return this.withIndexLock(() => {
947
+ if (!this.isUntouched(id)) return false;
948
+ try {
949
+ this.discardSession(id);
950
+ } catch (error) {
951
+ if (!isErrno(error, ["ENOENT"])) throw error;
952
+ this.dropSession(id);
953
+ }
954
+ try {
955
+ this.writeIndexLocked([id], []);
956
+ } catch (error) {
957
+ if (!isErrno(error, ["ENOENT", "EBUSY", "EPERM"])) throw error;
958
+ log.warn("could not rewrite the session index after removing a session", { id });
959
+ }
960
+ return true;
961
+ });
962
+ } catch (error) {
963
+ if (isErrno(error, ["ENOENT", "EBUSY", "EPERM"])) return false;
964
+ throw error;
965
+ }
966
+ }
967
+ isUntouched(id) {
968
+ if (statSync(this.path(id)).size > HEADER_PROBE_BYTES) return false;
969
+ const lines = readFileSync(this.path(id), "utf-8").split("\n").filter(Boolean);
970
+ if (lines.length === 0) return true;
971
+ if (lines.length > 1) return false;
972
+ try {
973
+ const header = JSON.parse(lines[0]);
974
+ const data = header.data ?? {};
975
+ return header.type === "session_meta" && data.kind === "session_meta" && !data.name;
976
+ } catch {
977
+ return false;
978
+ }
979
+ }
980
+ /** Remove one session's file, attachments and cached state. The index file is not rewritten. */
981
+ discardSession(id) {
982
+ this.unlinkSessionFile(id);
983
+ this.dropSession(id);
984
+ }
985
+ unlinkSessionFile(id) {
986
+ unlinkSync(this.path(id));
987
+ }
988
+ /** What remains once the session file is gone. Attachments are best-effort. */
989
+ dropSession(id) {
990
+ try {
991
+ this.removeAttachments(id);
992
+ } catch (error) {
993
+ log.warn("could not remove a session attachment directory", {
994
+ id,
995
+ error: error instanceof Error ? error.message : String(error)
996
+ });
997
+ }
998
+ this.metadataIndex.delete(id);
999
+ this.parsedSessions.delete(id);
1000
+ }
1001
+ removeAttachments(id) {
1002
+ rmSync(imageSessionDirectory(this.root, id), { recursive: true, force: true });
1003
+ }
848
1004
  };
1005
+ function isErrno(error, codes) {
1006
+ const code = error?.code;
1007
+ return code !== void 0 && codes.includes(code);
1008
+ }
849
1009
  function cloneLoadedSession(session) {
850
1010
  const contextHistory = [...session.contextHistory];
851
1011
  return {
@@ -860,6 +1020,7 @@ function cloneLoadedSession(session) {
860
1020
  todos: session.plan.todos?.map((todo) => ({ ...todo })),
861
1021
  tasks: session.plan.tasks?.map((task) => ({ ...task }))
862
1022
  } : void 0,
1023
+ goal: session.goal ? { version: 1, goal: session.goal.goal ? { ...session.goal.goal } : null } : void 0,
863
1024
  carriedUsage: session.carriedUsage ? { ...session.carriedUsage } : void 0,
864
1025
  carriedModels: session.carriedModels ? [...session.carriedModels] : void 0,
865
1026
  history: contextHistory
@@ -942,7 +1103,10 @@ function reviewRunnerFor(manager, attribution = {}) {
942
1103
  rootRunId: attribution.rootRunId,
943
1104
  parentRunId: attribution.parentRunId,
944
1105
  parentSessionId: attribution.parentSessionId,
945
- notifyParentOnCompletion: false
1106
+ notifyParentOnCompletion: false,
1107
+ // The report dies with the process, so a restart must not re-run a reviewer
1108
+ // whose result nobody would receive.
1109
+ resumeAfterRestart: false
946
1110
  });
947
1111
  return {
948
1112
  id: record.id,
@@ -980,24 +1144,25 @@ function fixRunnerFor(manager, attribution = {}) {
980
1144
  const evidence = (await manager.listEvidence({ ids: [evidenceId] }))[0];
981
1145
  return evidence ? evidenceProjection(evidence) : void 0;
982
1146
  },
983
- async apply(agentId, evidenceId) {
984
- const result = await manager.apply(agentId, evidenceId);
1147
+ async apply(agentId, evidenceId, options) {
1148
+ const result = await manager.apply(agentId, evidenceId, options);
985
1149
  return { status: result.status, commit: result.commit, error: result.error };
986
1150
  }
987
1151
  };
988
1152
  }
989
1153
 
990
1154
  // src/review/json.ts
991
- function extractJsonObject(text) {
992
- const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
993
- const candidate = fenced?.[1] ?? text;
994
- const start = candidate.indexOf("{");
1155
+ function firstObjectIn(text) {
1156
+ return objectIn(text, 0)?.slice;
1157
+ }
1158
+ function objectIn(text, from) {
1159
+ const start = text.indexOf("{", from);
995
1160
  if (start === -1) return void 0;
996
1161
  let depth = 0;
997
1162
  let inString = false;
998
1163
  let escaped = false;
999
- for (let index = start; index < candidate.length; index++) {
1000
- const char = candidate[index];
1164
+ for (let index = start; index < text.length; index++) {
1165
+ const char = text[index];
1001
1166
  if (inString) {
1002
1167
  if (escaped) escaped = false;
1003
1168
  else if (char === "\\") escaped = true;
@@ -1006,10 +1171,47 @@ function extractJsonObject(text) {
1006
1171
  }
1007
1172
  if (char === '"') inString = true;
1008
1173
  else if (char === "{") depth++;
1009
- else if (char === "}" && --depth === 0) return candidate.slice(start, index + 1);
1174
+ else if (char === "}" && --depth === 0) {
1175
+ return { start, end: index + 1, slice: text.slice(start, index + 1) };
1176
+ }
1010
1177
  }
1011
1178
  return void 0;
1012
1179
  }
1180
+ function fenceRuns(text) {
1181
+ const runs = [];
1182
+ for (let index = text.indexOf("```"); index !== -1; index = text.indexOf("```", index + 3)) {
1183
+ runs.push(index);
1184
+ }
1185
+ return runs;
1186
+ }
1187
+ function fenceContent(text, open, close) {
1188
+ return text.slice(open + 3, close).replace(/^json\s*/i, "");
1189
+ }
1190
+ function isJsonObject(candidate) {
1191
+ try {
1192
+ const parsed = JSON.parse(candidate);
1193
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed);
1194
+ } catch {
1195
+ return false;
1196
+ }
1197
+ }
1198
+ function extractJsonObject(text) {
1199
+ const runs = fenceRuns(text);
1200
+ const whole = objectIn(text, 0);
1201
+ if (whole !== void 0 && isJsonObject(whole.slice) && runs.every((run) => run >= whole.start && run < whole.end)) {
1202
+ return whole.slice;
1203
+ }
1204
+ let firstBalanced;
1205
+ for (let open = 0; open < runs.length; open++) {
1206
+ for (let close = open + 1; close < runs.length; close++) {
1207
+ const found = firstObjectIn(fenceContent(text, runs[open], runs[close]));
1208
+ if (found === void 0) continue;
1209
+ if (isJsonObject(found)) return found;
1210
+ firstBalanced ??= found;
1211
+ }
1212
+ }
1213
+ return firstBalanced ?? firstObjectIn(text);
1214
+ }
1013
1215
  function parseJsonObject(text) {
1014
1216
  const candidate = extractJsonObject(text);
1015
1217
  if (!candidate) return void 0;
@@ -1207,6 +1409,29 @@ function rankFindings(findings) {
1207
1409
  function filterLowConfidence(findings, threshold = DEFAULT_CONFIDENCE_THRESHOLD) {
1208
1410
  return findings.filter((finding) => finding.confidence >= threshold);
1209
1411
  }
1412
+ function isStanding(finding) {
1413
+ return finding.verification === void 0 || finding.verification === "confirmed";
1414
+ }
1415
+ function deriveReviewVerdict(input) {
1416
+ const entries = [...input.coverage.reviewers, input.coverage.verifier].filter(
1417
+ (entry) => entry !== void 0
1418
+ );
1419
+ if (entries.some((entry) => entry.status !== "completed")) {
1420
+ return { verdict: "inconclusive", reason: "incomplete-coverage" };
1421
+ }
1422
+ const standing = input.findings.filter(isStanding);
1423
+ if (standing.some((finding) => finding.severity === "critical")) {
1424
+ return { verdict: "blocking", reason: "standing-findings" };
1425
+ }
1426
+ if (standing.length > 0) return { verdict: "recommend", reason: "standing-findings" };
1427
+ if (input.findings.length > 0) {
1428
+ return { verdict: "inconclusive", reason: "unverified-findings" };
1429
+ }
1430
+ if (input.reviewerVerdicts?.some((verdict) => verdict === "inconclusive")) {
1431
+ return { verdict: "inconclusive", reason: "reviewer-inconclusive" };
1432
+ }
1433
+ return { verdict: "clean", reason: "clean" };
1434
+ }
1210
1435
 
1211
1436
  // src/review/verify-findings.ts
1212
1437
  function buildVerificationPrompt(findings, config = {}, target) {
@@ -1370,13 +1595,23 @@ async function runSingleReview(runner, scope, workspace, options = {}) {
1370
1595
  rankFindings(filterLowConfidence(dedupeFindings(parsed.report.findings)))
1371
1596
  );
1372
1597
  const coverage = passCoverage("single", structured, parsed.droppedFindings, findings.length);
1598
+ const reviewerEntries = [coverage];
1599
+ const reportCoverage = { reviewers: reviewerEntries };
1600
+ const { verdict, reason } = deriveReviewVerdict({
1601
+ findings,
1602
+ coverage: reportCoverage,
1603
+ // A completed, structured pass whose own verdict is `inconclusive` is the
1604
+ // one reviewer signal that is not already a coverage entry.
1605
+ reviewerVerdicts: structured ? [parsed.report.verdict] : []
1606
+ });
1373
1607
  const report = {
1374
- verdict: coverage.status === "completed" ? parsed.report.verdict : "inconclusive",
1608
+ verdict,
1375
1609
  findings,
1376
- coverage: { reviewers: [coverage] }
1610
+ coverage: reportCoverage
1377
1611
  };
1378
1612
  const text = [
1379
- renderCoverageWarnings([coverage]),
1613
+ renderCoverageWarnings(reviewerEntries),
1614
+ renderVerdictReason("Review", reason),
1380
1615
  renderReviewReport(report),
1381
1616
  coverage.status === "completed" ? "" : renderRawOutput("reviewer", settled.result)
1382
1617
  ].filter(Boolean).join("\n\n");
@@ -1414,6 +1649,17 @@ function renderCoverageWarnings(entries) {
1414
1649
  })
1415
1650
  ].join("\n");
1416
1651
  }
1652
+ function renderVerdictReason(prefix, reason) {
1653
+ const explanation = {
1654
+ "incomplete-coverage": "",
1655
+ "standing-findings": "",
1656
+ "unverified-findings": "the verifier could neither confirm nor reject the remaining findings.",
1657
+ "reviewer-inconclusive": "a reviewer reported its review as inconclusive.",
1658
+ clean: ""
1659
+ };
1660
+ const text = explanation[reason];
1661
+ return text ? `${prefix}: ${text}` : "";
1662
+ }
1417
1663
  async function runDeepReview(runner, scope, workspace, options = {}) {
1418
1664
  if (scope.error) throw new Error(scope.error);
1419
1665
  const target = await resolveReviewTarget(workspace, scope);
@@ -1514,22 +1760,22 @@ async function runDeepReview(runner, scope, workspace, options = {}) {
1514
1760
  reviewerCoverage.sort(
1515
1761
  (left, right) => REVIEW_LENSES.findIndex((lens) => lens.id === left.id) - REVIEW_LENSES.findIndex((lens) => lens.id === right.id)
1516
1762
  );
1517
- const completeDiscovery = reviewerCoverage.every((entry) => entry.status === "completed");
1518
1763
  const candidates = reindex(rankFindings(filterLowConfidence(dedupeFindings(rawFindings))));
1519
1764
  if (candidates.length === 0) {
1765
+ const reportCoverage2 = { reviewers: reviewerCoverage };
1766
+ const { verdict: verdict2, reason: reason2 } = deriveReviewVerdict({
1767
+ findings: [],
1768
+ coverage: reportCoverage2,
1769
+ reviewerVerdicts
1770
+ });
1520
1771
  const report2 = {
1521
- verdict: completeDiscovery && reviewerVerdicts.every((verdict2) => verdict2 === "clean") ? "clean" : "inconclusive",
1772
+ verdict: verdict2,
1522
1773
  findings: [],
1523
- coverage: { reviewers: reviewerCoverage }
1774
+ coverage: reportCoverage2
1524
1775
  };
1525
1776
  const warning2 = renderCoverageWarnings(reviewerCoverage);
1526
- return {
1527
- target,
1528
- report: report2,
1529
- text: [warning2 || "Deep review complete: no confirmed findings.", ...rawOutputs].join(
1530
- "\n\n"
1531
- )
1532
- };
1777
+ const summary = warning2 || (reason2 === "clean" ? "Deep review complete: no confirmed findings." : renderVerdictReason("Deep review", reason2));
1778
+ return { target, report: report2, text: [summary, ...rawOutputs].join("\n\n") };
1533
1779
  }
1534
1780
  if (options.signal?.aborted) return cancelledDeepResult(target);
1535
1781
  const verifier = await spawnTracked(
@@ -1556,20 +1802,30 @@ async function runDeepReview(runner, scope, workspace, options = {}) {
1556
1802
  }
1557
1803
  const verified = applyVerification(candidates, verdicts);
1558
1804
  const ranked = rankFindings(verified);
1559
- const completeCoverage = completeDiscovery && verifierCoverage.status === "completed";
1560
- const verdict = !completeCoverage ? "inconclusive" : ranked.some(
1561
- (finding) => finding.verification === "confirmed" && finding.severity === "critical"
1562
- ) ? "blocking" : ranked.some((finding) => finding.verification === "confirmed") ? "recommend" : ranked.length > 0 ? "inconclusive" : "clean";
1805
+ const reportCoverage = {
1806
+ reviewers: reviewerCoverage,
1807
+ verifier: verifierCoverage
1808
+ };
1809
+ const { verdict, reason } = deriveReviewVerdict({
1810
+ findings: ranked,
1811
+ coverage: reportCoverage,
1812
+ reviewerVerdicts
1813
+ });
1563
1814
  const report = {
1564
1815
  verdict,
1565
1816
  findings: ranked,
1566
- coverage: { reviewers: reviewerCoverage, verifier: verifierCoverage }
1817
+ coverage: reportCoverage
1567
1818
  };
1568
1819
  const warning = renderCoverageWarnings([...reviewerCoverage, verifierCoverage]);
1569
1820
  return {
1570
1821
  target,
1571
1822
  report,
1572
- text: [warning, renderReviewReport(report), ...rawOutputs].filter(Boolean).join("\n\n")
1823
+ text: [
1824
+ warning,
1825
+ renderVerdictReason("Deep review", reason),
1826
+ renderReviewReport(report),
1827
+ ...rawOutputs
1828
+ ].filter(Boolean).join("\n\n")
1573
1829
  };
1574
1830
  } catch (error) {
1575
1831
  await stopNonTerminal(runner, spawned);
@@ -1684,7 +1940,15 @@ async function applyReviewFixes(runner, confirmed, options = {}) {
1684
1940
  );
1685
1941
  continue;
1686
1942
  }
1687
- const applied = await runner.apply(patcher.id, candidate.id);
1943
+ let applied;
1944
+ try {
1945
+ applied = await runner.apply(patcher.id, candidate.id, { signal: options.signal });
1946
+ } catch (error) {
1947
+ if (!options.signal?.aborted) throw error;
1948
+ result.cancelled = true;
1949
+ result.messages.push(`Stopped while applying ${finding.file}: ${REVIEW_CANCELLED}.`);
1950
+ break;
1951
+ }
1688
1952
  if (applied.status === "applied") {
1689
1953
  result.applied++;
1690
1954
  result.messages.push(
@@ -1857,12 +2121,19 @@ function reduceAgentSessionSnapshot(snapshot, event) {
1857
2121
  case "agent_permission":
1858
2122
  case "agent_persistence":
1859
2123
  case "skill_lifecycle":
2124
+ case "notice":
2125
+ case "goal_status":
1860
2126
  case "background_job_start":
1861
2127
  case "background_job_update":
1862
2128
  case "background_job_output":
1863
2129
  case "background_job_result":
1864
2130
  case "background_job_dismiss":
1865
2131
  return snapshot;
2132
+ // `agent_notice` is a child's own line for the operator (#305): it is not part of the
2133
+ // child's record and not part of the parent's transcript, so the snapshot drops it and
2134
+ // the TUI reads it from the manager subscription instead (see `useManagedAgents`).
2135
+ case "agent_notice":
2136
+ return snapshot;
1866
2137
  case "evidence_update":
1867
2138
  return { ...snapshot, evidence: upsertById(snapshot.evidence, event.evidence) };
1868
2139
  case "attempt_discarded":
@@ -1900,6 +2171,10 @@ function reduceAgentSessionSnapshot(snapshot, event) {
1900
2171
  }
1901
2172
 
1902
2173
  // src/session/resolve.ts
2174
+ var log2 = createDebugLogger("session:resolve");
2175
+ function recordResumeDriver(store, sessionId, recorded, driver) {
2176
+ if (driver === "agent" && recorded !== "agent") store.patchMeta(sessionId, { driver: "agent" });
2177
+ }
1903
2178
  function selectSession(store, selector, cwd) {
1904
2179
  const exact = store.findByName(selector) ?? store.findById(selector);
1905
2180
  if (exact) return exact;
@@ -1940,6 +2215,25 @@ function persistHistory(store, sessionId, history) {
1940
2215
  });
1941
2216
  }
1942
2217
  }
2218
+ function discardUntouchedSession(store, launch) {
2219
+ if (!store || !launch.created) return false;
2220
+ try {
2221
+ return store.removeUntouched?.(launch.sessionId) ?? false;
2222
+ } catch (error) {
2223
+ log2.warn("could not remove the untouched session", {
2224
+ error: error instanceof Error ? error.message : String(error)
2225
+ });
2226
+ return false;
2227
+ }
2228
+ }
2229
+ function discardUntouchedSessions(store, launch, createdSessionIds) {
2230
+ let removed = 0;
2231
+ if (discardUntouchedSession(store, launch)) removed++;
2232
+ for (const sessionId of createdSessionIds) {
2233
+ if (discardUntouchedSession(store, { sessionId, created: true })) removed++;
2234
+ }
2235
+ return removed;
2236
+ }
1943
2237
  function resolveSessionBootstrap(store, options) {
1944
2238
  if (!store) {
1945
2239
  if (options.resume || options.continue) {
@@ -1970,7 +2264,8 @@ function resolveSessionBootstrap(store, options) {
1970
2264
  const sessionId2 = store.create({
1971
2265
  id: options.sessionId,
1972
2266
  cwd: options.cwd,
1973
- name: options.sessionName
2267
+ name: options.sessionName,
2268
+ driver: options.driver
1974
2269
  });
1975
2270
  return {
1976
2271
  sessionId: sessionId2,
@@ -1992,7 +2287,8 @@ function resolveSessionBootstrap(store, options) {
1992
2287
  if (selected) {
1993
2288
  const loaded = store.load(selected.id);
1994
2289
  if (options.forkSession) {
1995
- const sessionId2 = store.fork ? store.fork(selected.id, { cwd: options.cwd, name: options.sessionName }) : store.create({ cwd: options.cwd, name: options.sessionName });
2290
+ const driver = loaded.meta.driver === "agent" ? "agent" : options.driver;
2291
+ const sessionId2 = store.fork ? store.fork(selected.id, { cwd: options.cwd, name: options.sessionName, driver }) : store.create({ cwd: options.cwd, name: options.sessionName, driver });
1996
2292
  if (!store.fork) persistHistory(store, sessionId2, loaded.transcript);
1997
2293
  return {
1998
2294
  sessionId: sessionId2,
@@ -2004,12 +2300,14 @@ function resolveSessionBootstrap(store, options) {
2004
2300
  rewindTargets: loaded.rewindTargets,
2005
2301
  activeEventIds: loaded.activeEventIds,
2006
2302
  plan: loaded.plan,
2303
+ goal: loaded.goal,
2007
2304
  source: "resume",
2008
2305
  persisted: true,
2009
2306
  created: true
2010
2307
  };
2011
2308
  }
2012
2309
  store.touch(selected.id);
2310
+ recordResumeDriver(store, selected.id, loaded.meta.driver, options.driver);
2013
2311
  return {
2014
2312
  sessionId: selected.id,
2015
2313
  sessionName: loaded.meta.name,
@@ -2020,6 +2318,7 @@ function resolveSessionBootstrap(store, options) {
2020
2318
  rewindTargets: loaded.rewindTargets,
2021
2319
  activeEventIds: loaded.activeEventIds,
2022
2320
  plan: loaded.plan,
2321
+ goal: loaded.goal,
2023
2322
  carriedUsage: loaded.carriedUsage,
2024
2323
  carriedModels: loaded.carriedModels,
2025
2324
  source: "resume",
@@ -2027,7 +2326,11 @@ function resolveSessionBootstrap(store, options) {
2027
2326
  created: false
2028
2327
  };
2029
2328
  }
2030
- const sessionId = store.create({ cwd: options.cwd, name: options.sessionName });
2329
+ const sessionId = store.create({
2330
+ cwd: options.cwd,
2331
+ name: options.sessionName,
2332
+ driver: options.driver
2333
+ });
2031
2334
  return {
2032
2335
  sessionId,
2033
2336
  sessionName: options.sessionName,
@@ -2044,8 +2347,9 @@ function resolveSessionBootstrap(store, options) {
2044
2347
  }
2045
2348
 
2046
2349
  // src/input/file-mentions.ts
2350
+ import { stat } from "fs/promises";
2047
2351
  import { isAbsolute, relative, resolve as resolve2 } from "path";
2048
- import fg from "fast-glob";
2352
+ import { glob } from "tinyglobby";
2049
2353
  var DEFAULT_IGNORE = [
2050
2354
  "**/.git/**",
2051
2355
  "**/node_modules/**",
@@ -2066,9 +2370,8 @@ function resolveWorkspaceMentionPath(workspace, mentionPath) {
2066
2370
  const normalized = mentionPath.replace(/\\/g, "/");
2067
2371
  const candidate = isAbsolute(normalized) ? normalized : resolve2(root, normalized);
2068
2372
  const filePath = resolve2(candidate);
2069
- const rel = relative(root, filePath);
2070
- if (rel.startsWith("..") || isAbsolute(rel)) return null;
2071
- return { filePath, relativePath: normalizeMentionPath(rel) };
2373
+ if (isOutside(root, filePath)) return null;
2374
+ return { filePath, relativePath: normalizeMentionPath(relative(root, filePath)) };
2072
2375
  }
2073
2376
  function findActiveFileMention(input) {
2074
2377
  for (let i = input.length - 1; i >= 0; i--) {
@@ -2090,21 +2393,50 @@ function replaceActiveFileMention(input, mention, replacementPath) {
2090
2393
  const mentionText = needsQuotes ? `@"${replacementPath}" ` : `@${replacementPath} `;
2091
2394
  return input.slice(0, mention.start) + mentionText + input.slice(mention.end);
2092
2395
  }
2396
+ async function fileSizeBytes(filePath) {
2397
+ try {
2398
+ return (await stat(filePath)).size;
2399
+ } catch {
2400
+ return 0;
2401
+ }
2402
+ }
2403
+ function withMissingAncestors(entries) {
2404
+ const reported = new Set(entries.filter((entry) => entry.endsWith("/")));
2405
+ const missing = [];
2406
+ for (const entry of entries) {
2407
+ const path = entry.endsWith("/") ? entry.slice(0, -1) : entry;
2408
+ for (let cut = path.lastIndexOf("/"); cut > 0; cut = path.lastIndexOf("/", cut - 1)) {
2409
+ const ancestor = path.slice(0, cut + 1);
2410
+ if (reported.has(ancestor)) break;
2411
+ reported.add(ancestor);
2412
+ missing.push(ancestor);
2413
+ }
2414
+ }
2415
+ return missing.length > 0 ? [...entries, ...missing] : entries;
2416
+ }
2093
2417
  async function getFileMentionCandidates(workspace, query, limit = 50, signal) {
2418
+ const reachesUnc = createUncGuard();
2419
+ if (reachesUnc(workspace)) return [];
2094
2420
  const normalizedQuery = normalizeMentionPath(query).toLowerCase();
2095
2421
  const gitignore = loadGitignore(workspace).patterns;
2096
- const ignore = [...DEFAULT_IGNORE, ...gitignore];
2422
+ const ignore = globWalkIgnore([...DEFAULT_IGNORE, ...gitignore]);
2097
2423
  let entries;
2098
2424
  try {
2099
- entries = await fg("**/*", {
2100
- cwd: workspace,
2101
- dot: true,
2102
- onlyFiles: false,
2103
- unique: true,
2104
- ignore,
2105
- objectMode: true,
2106
- stats: true
2107
- });
2425
+ entries = withMissingAncestors(
2426
+ await glob("**/*", {
2427
+ cwd: workspace,
2428
+ dot: true,
2429
+ onlyFiles: false,
2430
+ ignore,
2431
+ // A pattern naming a directory returns that directory, not everything inside it — which is
2432
+ // the whole point of walking for mentions.
2433
+ expandDirectories: false,
2434
+ // The walk follows links: on Windows each one is asked about first, so none to a share is
2435
+ // resolved.
2436
+ fs: uncGuardedWalkFs(workspace, reachesUnc),
2437
+ signal
2438
+ })
2439
+ );
2108
2440
  } catch {
2109
2441
  return [];
2110
2442
  }
@@ -2112,7 +2444,8 @@ async function getFileMentionCandidates(workspace, query, limit = 50, signal) {
2112
2444
  const scored = [];
2113
2445
  for (let index = 0; index < entries.length; index++) {
2114
2446
  const entry = entries[index];
2115
- const display = normalizeMentionPath(entry.path);
2447
+ const isDirectory = entry.endsWith("/");
2448
+ const display = normalizeMentionPath(isDirectory ? entry.slice(0, -1) : entry);
2116
2449
  const lower = display.toLowerCase();
2117
2450
  const base = lower.split("/").pop() ?? lower;
2118
2451
  let score = null;
@@ -2122,11 +2455,10 @@ async function getFileMentionCandidates(workspace, query, limit = 50, signal) {
2122
2455
  else if (base.startsWith(normalizedQuery)) score = 2;
2123
2456
  else if (lower.includes(normalizedQuery)) score = 4;
2124
2457
  if (score !== null) {
2125
- const kind = entry.dirent.isDirectory() ? "directory" : "file";
2126
2458
  scored.push({
2127
- path: kind === "directory" ? `${display}/` : display,
2128
- kind,
2129
- desc: kind === "directory" ? "directory" : `${entry.stats?.size ?? 0} bytes`,
2459
+ path: isDirectory ? `${display}/` : display,
2460
+ kind: isDirectory ? "directory" : "file",
2461
+ desc: "",
2130
2462
  score
2131
2463
  });
2132
2464
  }
@@ -2138,7 +2470,18 @@ async function getFileMentionCandidates(workspace, query, limit = 50, signal) {
2138
2470
  return a.path.localeCompare(b.path);
2139
2471
  });
2140
2472
  throwIfAborted(signal);
2141
- return scored.slice(0, limit).map(({ score: _score, ...item }) => item);
2473
+ const listed = scored.slice(0, limit);
2474
+ const sizes = await Promise.all(
2475
+ listed.map(
2476
+ (candidate) => candidate.kind === "directory" ? 0 : fileSizeBytes(resolve2(workspace, candidate.path))
2477
+ )
2478
+ );
2479
+ throwIfAborted(signal);
2480
+ return listed.map(({ score: _score, path, kind }, index) => ({
2481
+ path,
2482
+ kind,
2483
+ desc: kind === "directory" ? "directory" : `${sizes[index]} bytes`
2484
+ }));
2142
2485
  }
2143
2486
 
2144
2487
  // src/session/lifecycle.ts
@@ -2154,15 +2497,96 @@ async function runSessionEnd(config, sessionId, reason, options) {
2154
2497
  await runHooks(
2155
2498
  config.settings.hooks.SessionEnd,
2156
2499
  "SessionEnd",
2157
- { workspace: config.workspace, event: "SessionEnd", sessionId, reason },
2500
+ {
2501
+ workspace: config.workspace,
2502
+ event: "SessionEnd",
2503
+ sessionId,
2504
+ reason,
2505
+ status: options?.endOutcome?.status,
2506
+ stopReason: options?.endOutcome?.reason
2507
+ },
2158
2508
  options
2159
2509
  );
2160
2510
  }
2161
2511
 
2162
2512
  // src/input/input-expansion.ts
2163
- import { existsSync as existsSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
2513
+ import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
2164
2514
  import { exec } from "child_process";
2165
2515
  import { createHash as createHash2 } from "crypto";
2516
+ import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve3 } from "path";
2517
+
2518
+ // src/input/committed-files.ts
2519
+ var REPOSITORY_SELECTION = {
2520
+ GIT_DIR: void 0,
2521
+ GIT_WORK_TREE: void 0,
2522
+ GIT_INDEX_FILE: void 0,
2523
+ GIT_OBJECT_DIRECTORY: void 0,
2524
+ GIT_ALTERNATE_OBJECT_DIRECTORIES: void 0,
2525
+ GIT_COMMON_DIR: void 0,
2526
+ GIT_NAMESPACE: void 0,
2527
+ GIT_PREFIX: void 0
2528
+ };
2529
+ var GIT_ENV = { ...REPOSITORY_SELECTION, ...noFetchGitEnv() };
2530
+ var GIT_TIMEOUT_MS = 1e4;
2531
+ function records(stdout) {
2532
+ return stdout.split("\0").filter(Boolean).flatMap((record) => {
2533
+ const tab = record.indexOf(" ");
2534
+ if (tab === -1) return [];
2535
+ return [{ fields: record.slice(0, tab).split(" "), path: record.slice(tab + 1) }];
2536
+ });
2537
+ }
2538
+ async function committedFileStates(repoRoot, paths, signal) {
2539
+ const states = /* @__PURE__ */ new Map();
2540
+ if (paths.length === 0) return states;
2541
+ const { pins, partialClone } = await readRepositoryGitConfig(repoRoot, void 0, {
2542
+ signal,
2543
+ timeoutMs: GIT_TIMEOUT_MS,
2544
+ env: GIT_ENV
2545
+ });
2546
+ const git = async (args) => {
2547
+ try {
2548
+ return (await defaultGitRunner(hardenedGitArgs([...pins, "--literal-pathspecs", ...args]), {
2549
+ cwd: repoRoot,
2550
+ signal,
2551
+ timeoutMs: GIT_TIMEOUT_MS,
2552
+ env: GIT_ENV
2553
+ })).stdout;
2554
+ } catch (error) {
2555
+ if (!(error instanceof Error)) throw error;
2556
+ const message = describeMissingObjects(error.message, partialClone);
2557
+ throw message === error.message ? error : new Error(message, { cause: error });
2558
+ }
2559
+ };
2560
+ const settled = await Promise.allSettled([
2561
+ git(["ls-tree", "-r", "-z", "--full-tree", "HEAD", "--", ...paths]),
2562
+ git(["ls-files", "-z", "--stage", "--full-name", "--", ...paths]),
2563
+ git(["hash-object", "--", ...paths])
2564
+ ]);
2565
+ const [tree, index, hashes] = settled.map((result) => {
2566
+ if (result.status === "rejected") throw result.reason;
2567
+ return result.value;
2568
+ });
2569
+ const committed = /* @__PURE__ */ new Map();
2570
+ for (const { fields, path } of records(tree)) {
2571
+ if ((fields[0] === "100644" || fields[0] === "100755") && fields[1] === "blob") {
2572
+ committed.set(path, fields[2]);
2573
+ }
2574
+ }
2575
+ const staged = /* @__PURE__ */ new Map();
2576
+ for (const { fields, path } of records(index)) {
2577
+ if (fields[2] === "0") staged.set(path, fields[1]);
2578
+ }
2579
+ const worktree = hashes.split(/\r?\n/);
2580
+ paths.forEach((path, position) => {
2581
+ const blob = committed.get(path);
2582
+ if (blob === void 0) states.set(path, "untracked");
2583
+ else if (staged.get(path) !== blob || worktree[position] !== blob) states.set(path, "changed");
2584
+ else states.set(path, "unchanged");
2585
+ });
2586
+ return states;
2587
+ }
2588
+
2589
+ // src/input/input-expansion.ts
2166
2590
  var AT_MENTION_CHAR_LIMIT = 2e4;
2167
2591
  function isMentionBoundary2(input, index) {
2168
2592
  if (index === 0) return true;
@@ -2173,11 +2597,76 @@ function splitTrailingPunctuation(token) {
2173
2597
  while (end > 0 && /[.,;:!?)]/.test(token[end - 1])) end--;
2174
2598
  return { path: token.slice(0, end), trailing: token.slice(end) };
2175
2599
  }
2176
- function findMentionTokens(input) {
2600
+ function codeRanges(input) {
2601
+ const fenced = fencedCodeRanges(input);
2602
+ const inline = [];
2603
+ let cursor = 0;
2604
+ for (const [start, end] of fenced) {
2605
+ collectParagraphInlineCodeSpans(input.slice(cursor, start), cursor, inline);
2606
+ cursor = end;
2607
+ }
2608
+ collectParagraphInlineCodeSpans(input.slice(cursor), cursor, inline);
2609
+ return [...fenced, ...inline];
2610
+ }
2611
+ function collectParagraphInlineCodeSpans(segment, base, ranges) {
2612
+ const blankLine = /\n[ \t]*\r?\n/g;
2613
+ let start = 0;
2614
+ let separator;
2615
+ while ((separator = blankLine.exec(segment)) !== null) {
2616
+ collectInlineCodeSpans(segment.slice(start, separator.index), base + start, ranges);
2617
+ start = separator.index + separator[0].length;
2618
+ }
2619
+ collectInlineCodeSpans(segment.slice(start), base + start, ranges);
2620
+ }
2621
+ function fencedCodeRanges(input) {
2622
+ const ranges = [];
2623
+ let fence = null;
2624
+ let offset = 0;
2625
+ while (offset < input.length) {
2626
+ const newline = input.indexOf("\n", offset);
2627
+ const lineEnd = newline === -1 ? input.length : newline;
2628
+ const rangeEnd = newline === -1 ? input.length : newline + 1;
2629
+ const line = input.slice(offset, lineEnd).replace(/\r$/, "");
2630
+ if (fence) {
2631
+ const close = /^ {0,3}(`{3,}|~{3,})[ \t]*$/.exec(line);
2632
+ if (close && close[1][0] === fence.char && close[1].length >= fence.length) {
2633
+ ranges.push([fence.start, rangeEnd]);
2634
+ fence = null;
2635
+ }
2636
+ } else {
2637
+ const open = /^ {0,3}(?:(`{3,})(?!.*`)|(~{3,}))/.exec(line);
2638
+ if (open) {
2639
+ const run = open[1] ?? open[2];
2640
+ fence = { start: offset, char: run[0], length: run.length };
2641
+ }
2642
+ }
2643
+ offset = rangeEnd;
2644
+ }
2645
+ if (fence) ranges.push([fence.start, input.length]);
2646
+ return ranges;
2647
+ }
2648
+ function collectInlineCodeSpans(text, base, ranges) {
2649
+ const runs = [...text.matchAll(/`+/g)];
2650
+ for (let i = 0; i < runs.length; i++) {
2651
+ const length = runs[i][0].length;
2652
+ for (let j = i + 1; j < runs.length; j++) {
2653
+ if (runs[j][0].length !== length) continue;
2654
+ ranges.push([base + (runs[i].index ?? 0), base + (runs[j].index ?? 0) + length]);
2655
+ i = j;
2656
+ break;
2657
+ }
2658
+ }
2659
+ }
2660
+ function isInsideRanges(ranges, index) {
2661
+ return ranges.some(([start, end]) => index >= start && index < end);
2662
+ }
2663
+ function findMentionTokens(input, excluded = []) {
2664
+ if (!input.includes("@")) return [];
2177
2665
  const tokens = [];
2666
+ const code = codeRanges(input);
2178
2667
  let i = 0;
2179
2668
  while (i < input.length) {
2180
- if (input[i] !== "@" || !isMentionBoundary2(input, i)) {
2669
+ if (input[i] !== "@" || !isMentionBoundary2(input, i) || isInsideRanges(code, i) || isInsideRanges(excluded, i)) {
2181
2670
  i++;
2182
2671
  continue;
2183
2672
  }
@@ -2233,94 +2722,198 @@ function formatMentionError(filePath, reason) {
2233
2722
  function looksBinary(content) {
2234
2723
  return content.includes("\0");
2235
2724
  }
2236
- function expandMention(filePath, workspace) {
2237
- const resolved = resolveWorkspaceMentionPath(workspace, filePath);
2238
- if (!resolved) return formatMentionError(filePath, "path is outside the workspace");
2239
- if (!existsSync2(resolved.filePath)) return formatMentionError(filePath, "file not found");
2725
+ var REFUSAL_TEXT = {
2726
+ outside: "it is outside the workspace and additionalDirectories",
2727
+ unc: "it is, or links to, a network (UNC) path",
2728
+ "no-repository": "a project command inlines only files committed to git, and there is no repository",
2729
+ untracked: "a project command inlines only files committed to git, and git does not track it",
2730
+ changed: "a project command inlines only committed files without local changes, and it has some",
2731
+ "git-failed": "a project command inlines only committed files, and git could not check it"
2732
+ };
2733
+ function formatRefusedMentionNotice(refused) {
2734
+ return `\u26A0 Left @${renderUntrustedText(refused.path, 400)} as written: ${REFUSAL_TEXT[refused.reason]}.`;
2735
+ }
2736
+ function mentionTarget(workspace, mentionPath, additionalRoots) {
2737
+ const inWorkspace = resolveWorkspaceMentionPath(workspace, mentionPath);
2738
+ if (inWorkspace) return inWorkspace;
2739
+ const normalized = mentionPath.replace(/\\/g, "/");
2740
+ if (!isAbsolute2(normalized)) return null;
2741
+ const filePath = resolve3(normalized);
2742
+ if (!additionalRoots.some((root) => !isOutside(root, filePath))) return null;
2743
+ return { filePath, relativePath: normalizeMentionPath(relative2(resolve3(workspace), filePath)) };
2744
+ }
2745
+ function decideMention(workspace, mentionPath, additionalRoots) {
2746
+ const target = mentionTarget(workspace, mentionPath, additionalRoots);
2747
+ if (!target) return null;
2748
+ if (reachesUncPath(target.filePath)) return { kind: "refused", reason: "unc" };
2749
+ const realPath = tryRealPath(target.filePath);
2750
+ if (realPath === void 0) return null;
2751
+ let regular;
2752
+ try {
2753
+ const stat2 = statSync2(realPath);
2754
+ if (stat2.isDirectory()) return null;
2755
+ regular = stat2.isFile();
2756
+ } catch {
2757
+ return null;
2758
+ }
2759
+ const readable = resolveMentionReadablePath(
2760
+ { workspaceRoot: workspace, additionalRoots },
2761
+ realPath
2762
+ );
2763
+ if (!("path" in readable)) {
2764
+ return { kind: "refused", reason: readable.reason === "unc" ? "unc" : "outside" };
2765
+ }
2766
+ return { kind: "file", realPath, relativePath: target.relativePath, regular };
2767
+ }
2768
+ async function applyProjectCommandRule(workspace, decisions, signal) {
2769
+ const files = decisions.filter(
2770
+ (decision) => decision?.kind === "file"
2771
+ );
2772
+ if (files.length === 0) return decisions;
2773
+ const refuse = (reason) => decisions.map(
2774
+ (decision) => decision?.kind === "file" ? { kind: "refused", reason } : decision
2775
+ );
2776
+ const repoRoot = findRepositoryRoot(workspace);
2777
+ if (repoRoot === void 0) return refuse("no-repository");
2778
+ const inRepository = (realPath) => isOutside(repoRoot, realPath) ? void 0 : relative2(repoRoot, realPath).replace(/\\/g, "/");
2779
+ const paths = /* @__PURE__ */ new Set();
2780
+ for (const file of files) {
2781
+ const path = file.regular ? inRepository(file.realPath) : void 0;
2782
+ if (path !== void 0) paths.add(path);
2783
+ }
2784
+ let states;
2785
+ try {
2786
+ states = await committedFileStates(repoRoot, [...paths], signal);
2787
+ } catch {
2788
+ states = /* @__PURE__ */ new Map();
2789
+ for (const path of paths) {
2790
+ try {
2791
+ states.set(path, (await committedFileStates(repoRoot, [path], signal)).get(path));
2792
+ } catch {
2793
+ states.set(path, "git-failed");
2794
+ }
2795
+ }
2796
+ }
2797
+ return decisions.map((decision) => {
2798
+ if (decision?.kind !== "file") return decision;
2799
+ const path = inRepository(decision.realPath);
2800
+ const state = path === void 0 ? "untracked" : states.get(path) ?? "untracked";
2801
+ return state === "unchanged" ? decision : { kind: "refused", reason: state };
2802
+ });
2803
+ }
2804
+ function inlineMention(token, decision, scope) {
2805
+ if (decision?.kind !== "file") return { text: null };
2806
+ if (!decision.regular) {
2807
+ return { text: formatMentionError(decision.relativePath, "path is not a regular file") };
2808
+ }
2809
+ let bytes;
2240
2810
  try {
2241
- const stat = statSync2(resolved.filePath);
2242
- if (stat.isDirectory()) return formatMentionError(resolved.relativePath, "path is a directory");
2243
- if (!stat.isFile())
2244
- return formatMentionError(resolved.relativePath, "path is not a regular file");
2245
- const content = readFileSync2(resolved.filePath, "utf-8");
2246
- if (looksBinary(content))
2247
- return formatMentionError(resolved.relativePath, "file appears to be binary");
2248
- const truncated = content.length > AT_MENTION_CHAR_LIMIT;
2249
- const body = truncated ? content.slice(0, AT_MENTION_CHAR_LIMIT) : content;
2250
- const suffix = truncated ? `
2811
+ bytes = readFileSync2(decision.realPath);
2812
+ } catch (error) {
2813
+ const message = error instanceof Error ? error.message : String(error);
2814
+ return { text: formatMentionError(token.path, message.slice(0, 200) || "unable to read file") };
2815
+ }
2816
+ const observation = {
2817
+ // Filed the way Read and Edit file and look a file up (`observationPathFor`): canonical, so a
2818
+ // mention through a link, or of a file in an `additionalDirectories` entry, counts as seen.
2819
+ path: observationPathFor(scope.workspace, decision.realPath),
2820
+ workspaceId: scope.workspaceId,
2821
+ sha256: createHash2("sha256").update(bytes).digest("hex"),
2822
+ byteSize: bytes.byteLength,
2823
+ operation: "mention",
2824
+ sourceRef: scope.sourceRef,
2825
+ timestamp: Date.now()
2826
+ };
2827
+ const content = bytes.toString("utf-8");
2828
+ if (looksBinary(content)) {
2829
+ return {
2830
+ text: formatMentionError(decision.relativePath, "file appears to be binary"),
2831
+ observation
2832
+ };
2833
+ }
2834
+ const truncated = content.length > AT_MENTION_CHAR_LIMIT;
2835
+ const body = truncated ? content.slice(0, AT_MENTION_CHAR_LIMIT) : content;
2836
+ const suffix = truncated ? `
2251
2837
 
2252
2838
  [File truncated at ${AT_MENTION_CHAR_LIMIT} characters; use the Read tool for more.]` : "";
2253
- return `
2254
- Contents of ${resolved.relativePath}:
2839
+ return {
2840
+ text: `
2841
+ Contents of ${decision.relativePath}:
2255
2842
 
2256
2843
  \`\`\`
2257
2844
  ${body}${suffix}
2258
2845
  \`\`\`
2259
- `;
2260
- } catch (error) {
2261
- const message = error instanceof Error ? error.message : String(error);
2262
- return formatMentionError(filePath, message.slice(0, 200) || "unable to read file");
2263
- }
2846
+ `,
2847
+ observation
2848
+ };
2264
2849
  }
2265
- function expandAtMentions(input, workspace) {
2266
- const tokens = findMentionTokens(input);
2267
- if (tokens.length === 0) return input;
2268
- let output = "";
2269
- let cursor = 0;
2270
- for (const token of tokens) {
2271
- output += input.slice(cursor, token.start);
2272
- output += expandMention(token.path, workspace);
2273
- output += token.trailing;
2274
- cursor = token.end + token.trailing.length;
2275
- }
2276
- output += input.slice(cursor);
2277
- return output;
2850
+ function mentionTokenRanges(input) {
2851
+ return findMentionTokens(input).map((token) => [token.start, token.end]);
2278
2852
  }
2279
- function collectAtMentionObservations(input, workspace, sourceRef) {
2280
- const workspaceId = workspaceIdentity(workspace);
2281
- const observations = [];
2282
- for (const token of findMentionTokens(input)) {
2283
- const resolved = resolveWorkspaceMentionPath(workspace, token.path);
2284
- if (!resolved || !existsSync2(resolved.filePath)) continue;
2285
- try {
2286
- const info = statSync2(resolved.filePath);
2287
- if (!info.isFile()) continue;
2288
- const bytes = readFileSync2(resolved.filePath);
2289
- observations.push({
2290
- path: resolved.relativePath,
2291
- workspaceId,
2292
- sha256: createHash2("sha256").update(bytes).digest("hex"),
2293
- byteSize: bytes.byteLength,
2294
- operation: "mention",
2295
- sourceRef,
2296
- timestamp: Date.now()
2297
- });
2298
- } catch {
2299
- }
2300
- }
2301
- return observations;
2853
+ function findShellLines(input) {
2854
+ const fenced = fencedCodeRanges(input);
2855
+ return [...input.matchAll(/^!(\S.*)$/gm)].filter((match) => !isInsideRanges(fenced, match.index ?? 0)).map((match) => {
2856
+ const start = match.index ?? 0;
2857
+ return { start, end: start + match[0].length, command: match[1] };
2858
+ });
2302
2859
  }
2303
- async function expandShellCommands(input, workspace, signal) {
2304
- const matches = [...input.matchAll(/^!(\S.*)$/gm)];
2305
- if (matches.length === 0) return input;
2860
+ function shellLineRanges(lines) {
2861
+ return lines.map((line) => [line.start, line.end]);
2862
+ }
2863
+ async function expandUserInputDetailed(input, workspace, options = {}) {
2864
+ const shellLines = options.expandShell && !options.projectCommand ? findShellLines(input) : [];
2865
+ const tokens = findMentionTokens(input, shellLineRanges(shellLines));
2866
+ if (shellLines.length === 0 && tokens.length === 0) {
2867
+ return { text: input, observations: [], refused: [] };
2868
+ }
2869
+ const additionalRoots = options.additionalRoots ?? [];
2870
+ const decide = (token) => decideMention(workspace, token.path, additionalRoots);
2871
+ const decided = options.projectCommand ? await applyProjectCommandRule(workspace, tokens.map(decide), options.signal) : void 0;
2872
+ const scope = {
2873
+ workspace,
2874
+ workspaceId: workspaceIdentity(workspace),
2875
+ sourceRef: options.sourceRef ?? ""
2876
+ };
2877
+ const observations = [];
2878
+ const refused = [];
2879
+ const edits = [
2880
+ ...shellLines.map((line) => ({
2881
+ start: line.start,
2882
+ end: line.end,
2883
+ apply: () => executeShellExpansion(line.command, workspace, options.signal)
2884
+ })),
2885
+ ...tokens.map((token, index) => ({
2886
+ start: token.start,
2887
+ end: token.end,
2888
+ apply: () => {
2889
+ const decision = decided ? decided[index] : decide(token);
2890
+ if (decision?.kind === "refused")
2891
+ refused.push({ path: token.path, reason: decision.reason });
2892
+ const inlined = inlineMention(token, decision, scope);
2893
+ if (inlined.observation) observations.push(inlined.observation);
2894
+ return inlined.text ?? token.raw;
2895
+ }
2896
+ }))
2897
+ ].sort((left, right) => left.start - right.start);
2306
2898
  let output = "";
2307
2899
  let cursor = 0;
2308
- for (const match of matches) {
2309
- const index = match.index ?? 0;
2310
- const command = match[1];
2311
- output += input.slice(cursor, index);
2312
- output += await executeShellExpansion(command, workspace, signal);
2313
- cursor = index + match[0].length;
2900
+ for (const edit of edits) {
2901
+ output += input.slice(cursor, edit.start);
2902
+ output += await edit.apply();
2903
+ cursor = edit.end;
2314
2904
  }
2315
- return output + input.slice(cursor);
2905
+ return { text: output + input.slice(cursor), observations, refused };
2316
2906
  }
2317
2907
  function executeShellExpansion(command, workspace, signal) {
2318
- return new Promise((resolve3) => {
2908
+ return new Promise((resolve4) => {
2319
2909
  exec(
2320
2910
  command,
2321
2911
  {
2322
2912
  cwd: workspace,
2323
2913
  encoding: "utf8",
2914
+ // The `!command` the user typed is theirs, so it must not inherit the NODE_ENV Book
2915
+ // defaulted for its own renderer.
2916
+ env: buildChildEnv(),
2324
2917
  timeout: 1e4,
2325
2918
  maxBuffer: 1024 * 1024,
2326
2919
  signal,
@@ -2329,18 +2922,18 @@ function executeShellExpansion(command, workspace, signal) {
2329
2922
  (error, stdout) => {
2330
2923
  if (error) {
2331
2924
  const message = error.message.slice(0, 200) || "unknown error";
2332
- resolve3(`(command '${command}' failed: ${message})`);
2925
+ resolve4(`(command '${command}' failed: ${message})`);
2333
2926
  return;
2334
2927
  }
2335
2928
  const value = stdout.trim();
2336
- resolve3(value || `(command '${command}' produced no output)`);
2929
+ resolve4(value || `(command '${command}' produced no output)`);
2337
2930
  }
2338
2931
  );
2339
2932
  });
2340
2933
  }
2341
2934
 
2342
2935
  // src/session/agent-interactions.ts
2343
- var log2 = createDebugLoggerWithCounter("session:interactions");
2936
+ var log3 = createDebugLoggerWithCounter("session:interactions");
2344
2937
  function snapshotOf(permission, planApproval, userQuestions, elicitations) {
2345
2938
  return Object.freeze({
2346
2939
  pendingPermission: permission ? Object.freeze({ toolCall: permission.toolCall }) : null,
@@ -2369,11 +2962,11 @@ var AgentInteractionController = class {
2369
2962
  return () => this.listeners.delete(listener);
2370
2963
  }
2371
2964
  requestPermission(toolCall) {
2372
- return new Promise((resolve3) => {
2373
- const request = { toolCall, resolve: resolve3 };
2965
+ return new Promise((resolve4) => {
2966
+ const request = { toolCall, resolve: resolve4 };
2374
2967
  if (this.pendingPermission) {
2375
2968
  this.pendingPermissionQueue.push(request);
2376
- log2.event("permission:queued", {
2969
+ log3.event("permission:queued", {
2377
2970
  tool: toolCall.name,
2378
2971
  id: toolCall.id,
2379
2972
  queueLength: this.pendingPermissionQueue.length
@@ -2381,19 +2974,19 @@ var AgentInteractionController = class {
2381
2974
  } else {
2382
2975
  this.pendingPermission = request;
2383
2976
  this.publish();
2384
- log2.event("permission:pending", { tool: toolCall.name, id: toolCall.id });
2977
+ log3.event("permission:pending", { tool: toolCall.name, id: toolCall.id });
2385
2978
  }
2386
2979
  });
2387
2980
  }
2388
2981
  settlePermission(result, via) {
2389
2982
  const pending = this.pendingPermission;
2390
2983
  if (!pending) {
2391
- log2.event("permission:settled:noop", { reason: "no-pending", result, via });
2984
+ log3.event("permission:settled:noop", { reason: "no-pending", result, via });
2392
2985
  return false;
2393
2986
  }
2394
2987
  this.pendingPermission = this.pendingPermissionQueue.shift() ?? null;
2395
2988
  this.publish();
2396
- log2.event("permission:settled", {
2989
+ log3.event("permission:settled", {
2397
2990
  tool: pending.toolCall.name,
2398
2991
  id: pending.toolCall.id,
2399
2992
  result,
@@ -2411,27 +3004,27 @@ var AgentInteractionController = class {
2411
3004
  this.pendingPermission = null;
2412
3005
  this.pendingPermissionQueue = [];
2413
3006
  this.publish();
2414
- for (const request of pending) request.resolve("deny");
2415
- log2.event("permission:cancelled-all", { via, count: pending.length });
3007
+ for (const request of pending) request.resolve({ result: "deny", reason: "dismissed" });
3008
+ log3.event("permission:cancelled-all", { via, count: pending.length });
2416
3009
  return true;
2417
3010
  }
2418
3011
  requestPlanApproval(plan) {
2419
3012
  if (this.pendingPlanApproval) this.settlePlanApproval("reject", "superseded");
2420
- return new Promise((resolve3) => {
2421
- this.pendingPlanApproval = { plan, resolve: resolve3 };
3013
+ return new Promise((resolve4) => {
3014
+ this.pendingPlanApproval = { plan, resolve: resolve4 };
2422
3015
  this.publish();
2423
- log2.event("plan-approval:pending", { len: plan.length });
3016
+ log3.event("plan-approval:pending", { len: plan.length });
2424
3017
  });
2425
3018
  }
2426
3019
  settlePlanApproval(result, via) {
2427
3020
  const pending = this.pendingPlanApproval;
2428
3021
  if (!pending) {
2429
- log2.event("plan-approval:settled:noop", { reason: "no-pending", result, via });
3022
+ log3.event("plan-approval:settled:noop", { reason: "no-pending", result, via });
2430
3023
  return false;
2431
3024
  }
2432
3025
  this.pendingPlanApproval = null;
2433
3026
  this.publish();
2434
- log2.event("plan-approval:settled", { result, via, len: pending.plan.length });
3027
+ log3.event("plan-approval:settled", { result, via, len: pending.plan.length });
2435
3028
  pending.resolve(result);
2436
3029
  return true;
2437
3030
  }
@@ -2442,10 +3035,10 @@ var AgentInteractionController = class {
2442
3035
  message: `Duplicate user question request: ${request.id}`
2443
3036
  });
2444
3037
  }
2445
- return new Promise((resolve3) => {
2446
- this.pendingUserQuestions = [...this.pendingUserQuestions, { request, resolve: resolve3 }];
3038
+ return new Promise((resolve4) => {
3039
+ this.pendingUserQuestions = [...this.pendingUserQuestions, { request, resolve: resolve4 }];
2447
3040
  this.publish();
2448
- log2.event("user-question:pending", {
3041
+ log3.event("user-question:pending", {
2449
3042
  id: request.id,
2450
3043
  count: request.questions.length,
2451
3044
  queueLength: this.pendingUserQuestions.length,
@@ -2456,14 +3049,14 @@ var AgentInteractionController = class {
2456
3049
  settleUserQuestion(result, via, requestId) {
2457
3050
  const index = requestId ? this.pendingUserQuestions.findIndex((entry) => entry.request.id === requestId) : 0;
2458
3051
  if (index < 0 || this.pendingUserQuestions.length === 0) {
2459
- log2.event("user-question:settled:noop", { reason: "no-pending", via, requestId });
3052
+ log3.event("user-question:settled:noop", { reason: "no-pending", via, requestId });
2460
3053
  return false;
2461
3054
  }
2462
3055
  const next = [...this.pendingUserQuestions];
2463
3056
  const [pending] = next.splice(index, 1);
2464
3057
  this.pendingUserQuestions = next;
2465
3058
  this.publish();
2466
- log2.event("user-question:settled", {
3059
+ log3.event("user-question:settled", {
2467
3060
  id: pending.request.id,
2468
3061
  action: result.action,
2469
3062
  via,
@@ -2480,7 +3073,7 @@ var AgentInteractionController = class {
2480
3073
  for (const entry of pending) {
2481
3074
  entry.resolve({ action: "cancel", message: `Question cancelled via ${via}.` });
2482
3075
  }
2483
- log2.event("user-question:cancelled-all", { via, count: pending.length });
3076
+ log3.event("user-question:cancelled-all", { via, count: pending.length });
2484
3077
  return pending.length;
2485
3078
  }
2486
3079
  /**
@@ -2491,10 +3084,10 @@ var AgentInteractionController = class {
2491
3084
  if (this.pendingElicitations.some((entry) => entry.request.id === request.id)) {
2492
3085
  return Promise.resolve({ action: "cancel" });
2493
3086
  }
2494
- return new Promise((resolve3) => {
2495
- this.pendingElicitations = [...this.pendingElicitations, { request, resolve: resolve3 }];
3087
+ return new Promise((resolve4) => {
3088
+ this.pendingElicitations = [...this.pendingElicitations, { request, resolve: resolve4 }];
2496
3089
  this.publish();
2497
- log2.event("elicitation:pending", {
3090
+ log3.event("elicitation:pending", {
2498
3091
  id: request.id,
2499
3092
  server: request.server,
2500
3093
  fields: request.fields.length,
@@ -2505,14 +3098,14 @@ var AgentInteractionController = class {
2505
3098
  settleElicitation(result, via, requestId) {
2506
3099
  const index = requestId ? this.pendingElicitations.findIndex((entry) => entry.request.id === requestId) : 0;
2507
3100
  if (index < 0 || this.pendingElicitations.length === 0) {
2508
- log2.event("elicitation:settled:noop", { reason: "no-pending", via, requestId });
3101
+ log3.event("elicitation:settled:noop", { reason: "no-pending", via, requestId });
2509
3102
  return false;
2510
3103
  }
2511
3104
  const next = [...this.pendingElicitations];
2512
3105
  const [pending] = next.splice(index, 1);
2513
3106
  this.pendingElicitations = next;
2514
3107
  this.publish();
2515
- log2.event("elicitation:settled", {
3108
+ log3.event("elicitation:settled", {
2516
3109
  id: pending.request.id,
2517
3110
  action: result.action,
2518
3111
  via,
@@ -2527,7 +3120,7 @@ var AgentInteractionController = class {
2527
3120
  this.pendingElicitations = [];
2528
3121
  this.publish();
2529
3122
  for (const entry of pending) entry.resolve({ action: "cancel" });
2530
- log2.event("elicitation:cancelled-all", { via, count: pending.length });
3123
+ log3.event("elicitation:cancelled-all", { via, count: pending.length });
2531
3124
  return pending.length;
2532
3125
  }
2533
3126
  cancelAll(via) {
@@ -2594,21 +3187,22 @@ var AgentSessionOperations = class {
2594
3187
  };
2595
3188
 
2596
3189
  // src/session/agent-session.ts
2597
- function subtractUsage(total, already) {
2598
- const at = (left, right) => Math.max(0, (left ?? 0) - (right ?? 0));
2599
- return {
2600
- promptTokens: at(total.promptTokens, already.promptTokens),
2601
- completionTokens: at(total.completionTokens, already.completionTokens),
2602
- totalTokens: at(total.totalTokens, already.totalTokens),
2603
- cacheReadInputTokens: at(total.cacheReadInputTokens, already.cacheReadInputTokens),
2604
- cacheCreationInputTokens: at(total.cacheCreationInputTokens, already.cacheCreationInputTokens)
2605
- };
3190
+ var log4 = createDebugLogger("session:usage");
3191
+ function persistGoal(target, goal) {
3192
+ if (!target.timelineStore || !target.sessionId) return;
3193
+ const data = { version: 1, goal: goal ? { ...goal } : null };
3194
+ target.timelineStore.append(target.sessionId, {
3195
+ type: "goal",
3196
+ timestamp: Date.now(),
3197
+ data
3198
+ });
2606
3199
  }
2607
3200
  var AgentSession = class {
2608
3201
  interactions = new AgentInteractionController();
2609
3202
  operations = new AgentSessionOperations();
2610
3203
  runLoop;
2611
3204
  compactRunner;
3205
+ judgeRunner;
2612
3206
  postCompactHooksRunner;
2613
3207
  sessionStartRunner;
2614
3208
  sessionEndRunner;
@@ -2616,12 +3210,27 @@ var AgentSession = class {
2616
3210
  listeners = /* @__PURE__ */ new Set();
2617
3211
  runGeneration = 0;
2618
3212
  lifecycleStartedSessionId;
3213
+ lifecycleStart;
2619
3214
  lifecycleEndedSessionId;
3215
+ lifecycleEnd;
2620
3216
  runtime;
2621
3217
  registryFactory;
3218
+ /**
3219
+ * Every root this session has run, and where its spend is written (#336).
3220
+ *
3221
+ * `onUsage` persists what a run's own responses report, which leaves two
3222
+ * shapes of spend with nowhere to go: whatever a managed child spent after the
3223
+ * root's last response, and a root whose model was never called at all — a
3224
+ * `/review` the host performed. Neither reaches a store on the way out, so the
3225
+ * process ends holding tokens no later process restores, and `--max-budget-usd`
3226
+ * re-authorises them. Registered by `run()` and by hosts that create a root
3227
+ * themselves (`trackRunUsage`), and drained by `dispose`.
3228
+ */
3229
+ usageTargets = /* @__PURE__ */ new Map();
2622
3230
  constructor(dependencies = {}) {
2623
3231
  this.runLoop = dependencies.runLoop ?? runAgentLoop;
2624
3232
  this.compactRunner = dependencies.compactRunner ?? runCompact;
3233
+ this.judgeRunner = dependencies.judgeRunner ?? judgeCompaction;
2625
3234
  this.postCompactHooksRunner = dependencies.postCompactHooksRunner ?? runPostCompactHooks;
2626
3235
  this.sessionStartRunner = dependencies.sessionStartRunner ?? runSessionStart;
2627
3236
  this.sessionEndRunner = dependencies.sessionEndRunner ?? runSessionEnd;
@@ -2674,6 +3283,11 @@ var AgentSession = class {
2674
3283
  contextMessage: request.contextMessage,
2675
3284
  userMessage,
2676
3285
  sessionName: request.sessionName,
3286
+ sessionNameSource: request.sessionNameSource,
3287
+ sessionNameExact: request.sessionNameExact,
3288
+ expandShellInput: request.expandShellInput,
3289
+ projectCommand: request.projectCommand,
3290
+ typedText: request.typedText,
2677
3291
  snapshotStore: request.snapshotStore,
2678
3292
  timelineStore: request.timelineStore,
2679
3293
  signal: control.signal,
@@ -2719,6 +3333,7 @@ var AgentSession = class {
2719
3333
  ...request.options,
2720
3334
  runtime,
2721
3335
  displayMessage: request.displayMessage,
3336
+ typedText: request.typedText,
2722
3337
  userMessageId: userMessage.id,
2723
3338
  userMessageTimestamp: userMessage.timestamp,
2724
3339
  userFileObservations: userMessage.fileObservations,
@@ -2766,9 +3381,61 @@ var AgentSession = class {
2766
3381
  getRuntime() {
2767
3382
  return this.runtime;
2768
3383
  }
3384
+ /** The session's `/goal`, if one is set. */
3385
+ getGoal() {
3386
+ return this.runtime.activeGoal;
3387
+ }
3388
+ /**
3389
+ * Set the session's goal, replacing any earlier one. The next time the agent
3390
+ * stops without calling a tool, the loop checks the condition (`agent/goal.ts`).
3391
+ * Taking effect on the runtime means a goal set mid-run applies to that run.
3392
+ */
3393
+ setGoal(condition, target = {}) {
3394
+ const goal = createGoal(condition);
3395
+ this.runtime.activeGoal = goal;
3396
+ persistGoal(target, goal);
3397
+ return goal;
3398
+ }
3399
+ /**
3400
+ * Carry a goal into a conversation that started fresh -- a plan handoff reseeds a new
3401
+ * session to implement the plan the goal was guiding -- keeping its progress.
3402
+ */
3403
+ adoptGoal(goal, target = {}) {
3404
+ this.runtime.activeGoal = { ...goal };
3405
+ persistGoal(target, this.runtime.activeGoal);
3406
+ }
3407
+ /** Clear the session's goal. Returns the goal that was cleared, if there was one. */
3408
+ clearGoal(target = {}) {
3409
+ const goal = this.runtime.activeGoal;
3410
+ if (!goal) return void 0;
3411
+ this.runtime.activeGoal = void 0;
3412
+ persistGoal(target, null);
3413
+ return goal;
3414
+ }
3415
+ /** Put back an unmet goal a resumed session recorded. Call after the runtime it belongs on is installed. */
3416
+ restoreGoal(record) {
3417
+ this.runtime.activeGoal = restoreGoal(record);
3418
+ }
3419
+ /**
3420
+ * Stop the outgoing runtime, write what it spent, and install its replacement.
3421
+ *
3422
+ * The one place a runtime goes away, so it is where its spend is written: the
3423
+ * children are stopped first, so a managed agent charged during the teardown is
3424
+ * in the figures the flush reads, and its targets are released with it rather
3425
+ * than walked by every later sweep. A `/resume` or `/rewind` that reached the
3426
+ * replacement without this left the outgoing runtime's unpersisted spend behind
3427
+ * with it, and the resumed session restored a carry short of what it had cost.
3428
+ */
2769
3429
  replaceRuntime(options = {}, via = "session_transition") {
2770
- this.runtime.dispose(via);
2771
- this.runtime = new SessionRuntime(options);
3430
+ const outgoing = this.runtime;
3431
+ outgoing.agentManager?.handOffLateApplications(() => this.runtime.agentManager);
3432
+ outgoing.dispose(via);
3433
+ this.releaseRunUsage(outgoing);
3434
+ this.runtime = new SessionRuntime({
3435
+ ...options,
3436
+ // The managers this session disposed, the outgoing one included, own nothing (#568).
3437
+ disposedAgentInstanceIds: options.disposedAgentInstanceIds ?? outgoing.disposedAgentInstanceIds
3438
+ });
2772
3439
  return this.runtime;
2773
3440
  }
2774
3441
  subscribe(listener) {
@@ -2793,16 +3460,165 @@ var AgentSession = class {
2793
3460
  this.operations.reset({ bookTerminalReason: "session_disposed" });
2794
3461
  this.runGeneration++;
2795
3462
  this.runtime.dispose(via);
3463
+ this.releaseRunUsage(this.runtime);
3464
+ }
3465
+ /**
3466
+ * Record that `rootRunId`'s spend belongs in `target`, so the flush at the end
3467
+ * of the session can persist what no response of that root reported.
3468
+ *
3469
+ * `run()` registers every root it runs. A host that starts a root itself and
3470
+ * never sends a turn — a print-mode `/review`, whose reviewer agents spend
3471
+ * under a root the root model is never asked for — registers it here, or its
3472
+ * spend leaves the process with it. Re-registering a root replaces the target,
3473
+ * so a run under an existing root keeps writing to where that root writes.
3474
+ */
3475
+ trackRunUsage(rootRunId, target) {
3476
+ this.usageTargets.set(rootRunId, { target, runtime: this.runtime });
3477
+ }
3478
+ /**
3479
+ * Register a compaction's root the way `run` registers a turn's.
3480
+ *
3481
+ * A compactor's model calls are charged to a run context, and no turn of that
3482
+ * root may ever run — a manual `/compact` mints a root of its own, and a send
3483
+ * cancelled between the host's auto-compact in `beforePrepare` and `run()`
3484
+ * never reaches `run`'s own registration. Without this, that spend is charged
3485
+ * to a root no flush knows about, and leaves the process unwritten.
3486
+ */
3487
+ registerUsageTarget(request, runtime) {
3488
+ if (!request.runContext || !request.timelineStore || !request.sessionId) return;
3489
+ this.usageTargets.set(request.runContext.rootRunId, {
3490
+ target: { sessionId: request.sessionId, timelineStore: request.timelineStore },
3491
+ runtime
3492
+ });
3493
+ }
3494
+ /**
3495
+ * Write what a root has spent and no `usage` record covers, as one record
3496
+ * named after the response the host is being told about.
3497
+ *
3498
+ * The `onUsage` writer, and the reason it is one function rather than a
3499
+ * closure: every host reaches it the same way, so the watermark it moves and
3500
+ * the ones `flushRunUsage` moves are the same figure, written once. Cost is
3501
+ * not stored — pricing changes between processes, so it is re-derived from the
3502
+ * tokens at bootstrap, deliberately at the most expensive model involved.
3503
+ *
3504
+ * The watermark moves only where a record really lands. A turn that reaches no
3505
+ * store — a session that moved on, a run with nowhere to write — leaves its
3506
+ * spend unpersisted instead, for the next writer under this root to record;
3507
+ * counted as written, no record would ever hold it and no restart would
3508
+ * restore it.
3509
+ */
3510
+ persistReportedUsage(rootRunId, reported, metadata) {
3511
+ const entry = this.usageTargets.get(rootRunId);
3512
+ if (!entry) return;
3513
+ const { target, runtime } = entry;
3514
+ const recordUsage = runtime.runAccounting.peekUnpersistedUsage(rootRunId) ?? reported;
3515
+ if (isSpendlessUsage(recordUsage)) return;
3516
+ const store = target.isCurrent?.() === false ? void 0 : target.timelineStore;
3517
+ if (!store) return;
3518
+ store.append(target.sessionId, {
3519
+ type: "usage",
3520
+ timestamp: Date.now(),
3521
+ data: {
3522
+ version: 1,
3523
+ usage: recordUsage,
3524
+ // The response that triggered this write, which is not the same thing as
3525
+ // the models the delta covers: a dearer child may have finished before it.
3526
+ requestedModel: metadata?.requestedModel,
3527
+ responseModel: metadata?.responseModel,
3528
+ models: runtime.runAccounting.modelsFor(rootRunId)
3529
+ }
3530
+ });
3531
+ runtime.runAccounting.commitPersistedUsage(rootRunId);
3532
+ }
3533
+ /**
3534
+ * Write what a root has spent and no `usage` record covers, if it can be
3535
+ * written at all.
3536
+ *
3537
+ * One record for the whole delta, so what this writes and what it then commits
3538
+ * are the same figure by construction. It is named after the dearest model the
3539
+ * root has spent on rather than after any one of them: `carriedCostUsd`
3540
+ * (`headless.ts`) prices a restored carry at the most expensive model in
3541
+ * `carriedModels` regardless of which record said what, and the records do not
3542
+ * attribute tokens to models, so one name is all the restored pool can be said
3543
+ * to have cost.
3544
+ *
3545
+ * `requireCurrent` is the end of a run, where the target's own currency still
3546
+ * decides whether it is the session to write to. The end-of-session sweep does
3547
+ * not pass it: by then every operation lease has been released, so consulting
3548
+ * one would skip every target the TUI ever registered — which is where a
3549
+ * background agent's late spend goes.
3550
+ *
3551
+ * A store that throws is contained here. This runs from a run's `finally` and
3552
+ * from the runtime swap, so an escaping filesystem error would replace a
3553
+ * completed run's outcome, abort the sweep part-way, and leave the session
3554
+ * installed on a disposed runtime. Nothing was written, so the watermark stays
3555
+ * where it was and the spend remains owed to the next writer that can.
3556
+ */
3557
+ flushRunUsage(rootRunId, requireCurrent) {
3558
+ const entry = this.usageTargets.get(rootRunId);
3559
+ if (!entry) return;
3560
+ const { target, runtime } = entry;
3561
+ const recordUsage = runtime.runAccounting.peekUnpersistedUsage(rootRunId);
3562
+ if (!recordUsage || isSpendlessUsage(recordUsage)) return;
3563
+ const store = requireCurrent && target.isCurrent?.() === false ? void 0 : target.timelineStore;
3564
+ if (!store) return;
3565
+ try {
3566
+ store.append(target.sessionId, {
3567
+ type: "usage",
3568
+ timestamp: Date.now(),
3569
+ data: {
3570
+ version: 1,
3571
+ usage: recordUsage,
3572
+ requestedModel: runtime.runAccounting.dearestModel(rootRunId),
3573
+ models: runtime.runAccounting.modelsFor(rootRunId)
3574
+ }
3575
+ });
3576
+ runtime.runAccounting.commitPersistedUsage(rootRunId);
3577
+ } catch (error) {
3578
+ log4.warn("usage record could not be appended; spend left unpersisted", {
3579
+ rootRunId,
3580
+ sessionId: target.sessionId,
3581
+ error: error instanceof Error ? error.message : String(error)
3582
+ });
3583
+ }
3584
+ }
3585
+ /**
3586
+ * Write what the roots of a runtime that is going away spent, and drop their
3587
+ * targets: nothing can be charged to a root whose runtime is gone, so keeping
3588
+ * its target would only make every later sweep walk it.
3589
+ *
3590
+ * Whatever lease a target carries is ignored — a `send()` lease has been
3591
+ * released by the time any of these runs, so consulting one would skip every
3592
+ * target the TUI ever made.
3593
+ */
3594
+ releaseRunUsage(runtime) {
3595
+ for (const [rootRunId, entry] of [...this.usageTargets.entries()]) {
3596
+ if (entry.runtime !== runtime) continue;
3597
+ this.flushRunUsage(rootRunId, false);
3598
+ this.usageTargets.delete(rootRunId);
3599
+ }
2796
3600
  }
2797
3601
  async startLifecycle(config, sessionId, source, options) {
2798
- if (this.lifecycleStartedSessionId === sessionId) return;
3602
+ if (this.lifecycleStartedSessionId === sessionId) {
3603
+ await this.lifecycleStart?.catch(() => void 0);
3604
+ return;
3605
+ }
2799
3606
  this.lifecycleStartedSessionId = sessionId;
2800
- await this.sessionStartRunner(config, sessionId, source, options);
3607
+ this.lifecycleStart = this.sessionStartRunner(config, sessionId, source, options);
3608
+ await this.lifecycleStart;
2801
3609
  }
3610
+ // A second call for a session that is already ending waits for the SessionEnd in flight
3611
+ // instead of returning at once, which let a second exit or a `/clear` racing an exit move on
3612
+ // while the hooks were still running. The first caller reports a failed SessionEnd; a caller that
3613
+ // only waited for it does not report the same failure a second time.
2802
3614
  async endLifecycle(config, sessionId, reason, options) {
2803
- if (this.lifecycleEndedSessionId === sessionId) return;
3615
+ if (this.lifecycleEndedSessionId === sessionId) {
3616
+ await this.lifecycleEnd?.catch(() => void 0);
3617
+ return;
3618
+ }
2804
3619
  this.lifecycleEndedSessionId = sessionId;
2805
- await this.sessionEndRunner(config, sessionId, reason, options);
3620
+ this.lifecycleEnd = this.sessionEndRunner(config, sessionId, reason, options);
3621
+ await this.lifecycleEnd;
2806
3622
  }
2807
3623
  async clearSession(request) {
2808
3624
  request.onTransitionStart?.();
@@ -2811,9 +3627,20 @@ var AgentSession = class {
2811
3627
  if (request.previousName && request.store) {
2812
3628
  request.store.patchMeta(request.currentSessionId, { name: request.previousName });
2813
3629
  }
2814
- const sessionId = request.store ? request.store.create({ cwd: request.config.workspace }) : request.timelineStore?.create({ cwd: request.config.workspace }) ?? crypto.randomUUID();
3630
+ const sessionId = request.store ? request.store.create({
3631
+ cwd: request.config.workspace,
3632
+ driver: request.config.sessionDriver
3633
+ }) : request.timelineStore?.create({
3634
+ cwd: request.config.workspace,
3635
+ driver: request.config.sessionDriver
3636
+ }) ?? crypto.randomUUID();
3637
+ if (request.store) request.createdSessions?.add(sessionId);
2815
3638
  if (request.store && request.timelineStore && request.timelineStore !== request.store) {
2816
- request.timelineStore.create({ id: sessionId, cwd: request.config.workspace });
3639
+ request.timelineStore.create({
3640
+ id: sessionId,
3641
+ cwd: request.config.workspace,
3642
+ driver: request.config.sessionDriver
3643
+ });
2817
3644
  }
2818
3645
  const bootstrap = emptySessionBootstrap(
2819
3646
  sessionId,
@@ -2839,6 +3666,12 @@ var AgentSession = class {
2839
3666
  this.operations.cancel({ bookTerminalReason: "session_replaced" });
2840
3667
  await this.endLifecycle(request.config, request.currentSessionId, "resume");
2841
3668
  request.store.touch(selected.id);
3669
+ recordResumeDriver(
3670
+ request.store,
3671
+ selected.id,
3672
+ loaded.meta.driver,
3673
+ request.config.sessionDriver
3674
+ );
2842
3675
  const bootstrap = {
2843
3676
  sessionId: selected.id,
2844
3677
  sessionName: loaded.meta.name,
@@ -2852,11 +3685,14 @@ var AgentSession = class {
2852
3685
  // user who wrote a plan, switched away and came back resumed a half-finished
2853
3686
  // objective with an empty task list and no notice that it had been dropped.
2854
3687
  plan: loaded.plan,
3688
+ goal: loaded.goal,
2855
3689
  source: "resume",
2856
3690
  persisted: true,
2857
3691
  created: false
2858
3692
  };
2859
3693
  this.reset("session-resume");
3694
+ const resumed = this.replaceRuntime({}, "session-resume");
3695
+ seedShellIds(resumed.backgroundShells, loaded.transcript, loaded.contextHistory);
2860
3696
  request.onTransition?.(bootstrap);
2861
3697
  await this.startLifecycle(request.config, selected.id, "resume");
2862
3698
  return { status: "transitioned", bootstrap };
@@ -2877,10 +3713,13 @@ var AgentSession = class {
2877
3713
  gitHead: capture.gitHead,
2878
3714
  codeUnavailableReason: capture.reason
2879
3715
  };
3716
+ const rewindPrompt = request.typedText ?? request.displayMessage;
3717
+ const promptTyped = request.typedText !== void 0;
2880
3718
  const rewindTarget = {
2881
3719
  id: checkpointId,
2882
3720
  userEventId: request.userMessage.id,
2883
- prompt: request.displayMessage,
3721
+ prompt: rewindPrompt,
3722
+ ...promptTyped ? { promptTyped } : {},
2884
3723
  attachments: request.userMessage.attachments,
2885
3724
  timestamp: checkpointTimestamp,
2886
3725
  ...checkpoint,
@@ -2894,23 +3733,32 @@ var AgentSession = class {
2894
3733
  version: 1,
2895
3734
  checkpointId,
2896
3735
  userEventId: request.userMessage.id,
2897
- prompt: request.displayMessage,
3736
+ prompt: rewindPrompt,
3737
+ ...promptTyped ? { promptTyped } : {},
2898
3738
  attachments: request.userMessage.attachments,
2899
3739
  checkpoint
2900
3740
  }
2901
3741
  });
2902
- const { contextMessage, sessionName } = await this.recordUserMessage(request);
2903
- return { status: "prepared", contextMessage, rewindTarget, sessionName };
3742
+ const { contextMessage, sessionName, mentionNotices } = await this.recordUserMessage(request);
3743
+ return { status: "prepared", contextMessage, rewindTarget, sessionName, mentionNotices };
2904
3744
  }
2905
3745
  async recordUserMessage(request) {
2906
- const expandedMentions = request.contextMessage === void 0 ? expandAtMentions(request.displayMessage, request.config.workspace) : request.contextMessage;
2907
- const contextMessage = request.contextMessage !== void 0 || request.expandShellInput === false ? expandedMentions : await expandShellCommands(expandedMentions, request.config.workspace, request.signal);
3746
+ const expansion = request.contextMessage !== void 0 ? void 0 : await expandUserInputDetailed(request.displayMessage, request.config.workspace, {
3747
+ expandShell: request.expandShellInput === true,
3748
+ signal: request.signal,
3749
+ sourceRef: request.userMessage.id,
3750
+ // The roots Read serves besides the workspace, resolved as the agent loop does, and
3751
+ // only for text that can hold a mention at all.
3752
+ additionalRoots: request.displayMessage.includes("@") ? resolveAdditionalRoots(
3753
+ request.config.workspace,
3754
+ request.config.settings.additionalDirectories
3755
+ ) : [],
3756
+ projectCommand: request.projectCommand
3757
+ });
3758
+ const contextMessage = expansion?.text ?? request.contextMessage ?? request.displayMessage;
3759
+ const mentionNotices = (expansion?.refused ?? []).map(formatRefusedMentionNotice);
2908
3760
  request.userMessage.contextContent = contextMessage === request.displayMessage ? void 0 : contextMessage;
2909
- request.userMessage.fileObservations = request.contextMessage === void 0 ? collectAtMentionObservations(
2910
- request.displayMessage,
2911
- request.config.workspace,
2912
- request.userMessage.id
2913
- ) : [];
3761
+ request.userMessage.fileObservations = expansion?.observations ?? [];
2914
3762
  const observationLedger = (request.runtime ?? this.runtime).fileObservationLedger;
2915
3763
  for (const observation of request.userMessage.fileObservations) {
2916
3764
  observationLedger.set(observationKey(observation.workspaceId, observation.path), observation);
@@ -2934,40 +3782,139 @@ var AgentSession = class {
2934
3782
  }
2935
3783
  });
2936
3784
  if (request.timelineStore) request.onUserMessagePersisted?.();
2937
- const sessionName = request.sessionName?.trim() || deriveSessionName(request.displayMessage);
3785
+ if (request.userMessage.kind === "agent-notification") {
3786
+ return { contextMessage, sessionName: request.sessionName, mentionNotices };
3787
+ }
3788
+ const sessionName = request.sessionName?.trim() || request.sessionNameExact?.trim() || deriveSessionName(request.sessionNameSource ?? request.displayMessage);
2938
3789
  if (!request.sessionName?.trim()) {
2939
3790
  request.timelineStore?.patchMeta?.(request.sessionId, { name: sessionName });
2940
3791
  }
2941
- return { contextMessage, sessionName };
3792
+ return { contextMessage, sessionName, mentionNotices };
2942
3793
  }
2943
3794
  async compact(request) {
2944
3795
  const runtime = request.runtime ?? this.runtime;
2945
3796
  if (request.runContext) runtime.runAccounting.startRoot(request.runContext);
3797
+ this.registerUsageTarget(request, runtime);
2946
3798
  const result = await this.compactRunner(request.config, request.history, {
2947
- ...request.options,
2948
- sessionId: request.sessionId,
2949
- beforeModelCall: request.runContext ? (model) => {
2950
- const requestCheck = request.options.beforeModelCall?.(model);
2951
- if (requestCheck && !requestCheck.allowed) return requestCheck;
2952
- return runtime.runAccounting.checkBeforeModelCall(request.runContext.rootRunId, model);
2953
- } : request.options.beforeModelCall,
2954
- onUsage: request.runContext ? (usage, metadata) => {
2955
- runtime.runAccounting.record(request.runContext, usage, metadata);
2956
- request.options.onUsage?.(usage, metadata);
2957
- } : request.options.onUsage,
2958
- onUsageMissing: request.runContext ? (metadata) => {
2959
- runtime.runAccounting.markUsageUnknown(
2960
- request.runContext,
2961
- metadata,
2962
- "compaction_usage"
2963
- );
2964
- request.options.onUsageMissing?.(metadata);
2965
- } : request.options.onUsageMissing
3799
+ ...this.accountedOptions(request.options, request.runContext, runtime, request.onUsage),
3800
+ sessionId: request.sessionId
3801
+ });
3802
+ if (result.status !== "compacted") return { result };
3803
+ if (request.isCurrent?.() === false) return { result };
3804
+ return this.commitCompactResult(result, request);
3805
+ }
3806
+ /**
3807
+ * Deferred compaction (`plans/async-compaction-plan.md`): the reducer half of
3808
+ * `compact`, on a snapshot, without the record, the boundary or the
3809
+ * PostCompact hooks. PreCompact hooks still run -- they are the refusal
3810
+ * point, and the suspect-input scan they see is the snapshot's.
3811
+ */
3812
+ async prepareCompact(request) {
3813
+ const runtime = request.runtime ?? this.runtime;
3814
+ if (request.runContext) runtime.runAccounting.startRoot(request.runContext);
3815
+ this.registerUsageTarget(request, runtime);
3816
+ const result = await this.compactRunner(request.config, request.history, {
3817
+ ...this.accountedOptions(request.options, request.runContext, runtime, request.onUsage),
3818
+ sessionId: request.sessionId
3819
+ });
3820
+ if (result.status !== "compacted") return { status: result.status, result };
3821
+ return {
3822
+ status: "prepared",
3823
+ prepared: {
3824
+ snapshot: [...request.history],
3825
+ result,
3826
+ trigger: request.options.trigger,
3827
+ preContextTokens: request.options.preContextTokens
3828
+ }
3829
+ };
3830
+ }
3831
+ /**
3832
+ * The other half: apply a prepared compaction to the history as it stands,
3833
+ * ask the judge whether the checkpoint holds what the steps taken meanwhile
3834
+ * relied on, and only then write the record and run the hooks. A rejected
3835
+ * or inapplicable checkpoint is dropped, and the caller falls back to a
3836
+ * synchronous compaction.
3837
+ */
3838
+ async commitCompact(request) {
3839
+ const runtime = request.runtime ?? this.runtime;
3840
+ const { prepared } = request;
3841
+ const applied = applyCompactResult(prepared.result, prepared.snapshot, request.history, {
3842
+ toolResultMaxTokens: resolveCompactBudgets(request.config).retainedToolResultMaxTokens
3843
+ });
3844
+ if (!applied) {
3845
+ return {
3846
+ result: {
3847
+ status: "skipped",
3848
+ reason: "not-applicable",
3849
+ message: "The history no longer extends the snapshot the reducer read."
3850
+ }
3851
+ };
3852
+ }
3853
+ const snapshotIds = new Set(prepared.snapshot.map((message) => message.id));
3854
+ const delta = request.history.filter((message) => !snapshotIds.has(message.id));
3855
+ if (request.runContext) runtime.runAccounting.startRoot(request.runContext);
3856
+ this.registerUsageTarget(request, runtime);
3857
+ const accounted = this.accountedOptions(
3858
+ { ...request.options, trigger: prepared.trigger },
3859
+ request.runContext,
3860
+ runtime,
3861
+ request.onUsage
3862
+ );
3863
+ const judge = await this.judgeRunner(request.config, applied, delta, {
3864
+ signal: accounted.signal,
3865
+ provider: accounted.provider,
3866
+ beforeModelCall: accounted.beforeModelCall,
3867
+ onUsage: accounted.onUsage,
3868
+ onUsageMissing: accounted.onUsageMissing
2966
3869
  });
3870
+ if (accounted.signal?.aborted || judge.note === "aborted") {
3871
+ return { result: { status: "failed", reason: "aborted", error: "Compaction aborted." } };
3872
+ }
3873
+ const result = judgedResult(applied, judge);
2967
3874
  if (result.status !== "compacted") return { result };
2968
3875
  if (request.isCurrent?.() === false) return { result };
3876
+ return this.commitCompactResult(result, {
3877
+ config: request.config,
3878
+ history: request.history,
3879
+ sessionId: request.sessionId,
3880
+ transcriptOrdinal: request.transcriptOrdinal,
3881
+ options: { ...request.options, trigger: prepared.trigger },
3882
+ runContext: request.runContext,
3883
+ runtime,
3884
+ timelineStore: request.timelineStore,
3885
+ isCurrent: request.isCurrent,
3886
+ onCommitted: request.onCommitted
3887
+ });
3888
+ }
3889
+ /**
3890
+ * The compactor's model calls charged to the run, the way `compact` has always charged
3891
+ * them, and reported to the host's own session bill when it keeps one (#370).
3892
+ */
3893
+ accountedOptions(options, runContext, runtime, onUsage) {
3894
+ return {
3895
+ ...options,
3896
+ beforeModelCall: runContext ? (model) => {
3897
+ const requestCheck = options.beforeModelCall?.(model);
3898
+ if (requestCheck && !requestCheck.allowed) return requestCheck;
3899
+ return runtime.runAccounting.checkBeforeModelCall(runContext.rootRunId, model);
3900
+ } : options.beforeModelCall,
3901
+ // Left exactly as it was when there is nothing to chain: a compaction with
3902
+ // no run context and no host callback hands the runner its own options back.
3903
+ onUsage: runContext || onUsage ? (usage, metadata) => {
3904
+ if (runContext) runtime.runAccounting.record(runContext, usage, metadata);
3905
+ options.onUsage?.(usage, metadata);
3906
+ onUsage?.(usage);
3907
+ } : options.onUsage,
3908
+ onUsageMissing: runContext ? (metadata) => {
3909
+ runtime.runAccounting.markUsageUnknown(runContext, metadata, "compaction_usage");
3910
+ options.onUsageMissing?.(metadata);
3911
+ } : options.onUsageMissing
3912
+ };
3913
+ }
3914
+ /** The record, the boundary, `onCommitted` and the PostCompact hooks for a compacted result. */
3915
+ async commitCompactResult(result, request) {
2969
3916
  const timestamp = Date.now();
2970
- const boundary = {
3917
+ const recorded = {
2971
3918
  id: result.compactId,
2972
3919
  trigger: result.trigger,
2973
3920
  transcriptOrdinal: request.transcriptOrdinal,
@@ -2977,7 +3924,13 @@ var AgentSession = class {
2977
3924
  postContextTokens: result.postContextTokens,
2978
3925
  generation: result.generation,
2979
3926
  checkpointVersion: 2,
2980
- timestamp
3927
+ timestamp,
3928
+ carriedCount: result.carriedCount
3929
+ };
3930
+ const boundary = {
3931
+ ...recorded,
3932
+ ...result.degraded ? { degraded: true } : {},
3933
+ ...result.warning ? { warning: result.warning } : {}
2981
3934
  };
2982
3935
  const data = {
2983
3936
  version: 2,
@@ -2990,14 +3943,16 @@ var AgentSession = class {
2990
3943
  preContextTokens: result.preContextTokens,
2991
3944
  postContextTokens: result.postContextTokens,
2992
3945
  replacementHistory: result.replacementHistory,
2993
- boundary,
3946
+ boundary: recorded,
2994
3947
  throughEventRef: result.throughEventRef,
2995
3948
  summarizedCount: result.summarizedCount,
2996
3949
  retainedCount: result.retainedCount,
3950
+ carriedCount: result.carriedCount,
2997
3951
  strategy: result.strategy,
2998
3952
  modelCalls: result.modelCalls,
2999
3953
  degraded: result.degraded,
3000
- warning: result.warning
3954
+ warning: result.warning,
3955
+ ...result.judge ? { judge: result.judge } : {}
3001
3956
  };
3002
3957
  if (request.timelineStore && request.sessionId) {
3003
3958
  request.timelineStore.append(request.sessionId, {
@@ -3012,8 +3967,7 @@ var AgentSession = class {
3012
3967
  trigger: result.trigger,
3013
3968
  sessionId: request.sessionId,
3014
3969
  focus: request.options.focus,
3015
- onHookEvent: request.options.onHookEvent,
3016
- signal: request.options.signal
3970
+ onHookEvent: request.options.onHookEvent
3017
3971
  });
3018
3972
  return { result, boundary };
3019
3973
  }
@@ -3049,6 +4003,14 @@ var AgentSession = class {
3049
4003
  allowedTools: request.options?.allowedTools
3050
4004
  })
3051
4005
  );
4006
+ this.usageTargets.set(runContext.rootRunId, {
4007
+ target: {
4008
+ sessionId: request.sessionId,
4009
+ timelineStore: request.timelineStore,
4010
+ isCurrent: request.isCurrent
4011
+ },
4012
+ runtime
4013
+ });
3052
4014
  const finalizeOutcome = (outcome) => {
3053
4015
  if (!terminalOutcome) {
3054
4016
  terminalOutcome = outcome;
@@ -3066,13 +4028,6 @@ var AgentSession = class {
3066
4028
  emit({ type: "session", sessionId: request.sessionId });
3067
4029
  const unsubscribeShellEvents = runtime.shellManager.subscribe((event) => emit(event));
3068
4030
  try {
3069
- let persistedUsage = {
3070
- promptTokens: 0,
3071
- completionTokens: 0,
3072
- totalTokens: 0,
3073
- cacheReadInputTokens: 0,
3074
- cacheCreationInputTokens: 0
3075
- };
3076
4031
  let lastPlanSignature = "";
3077
4032
  const persistPlan = () => {
3078
4033
  if (!request.timelineStore) return;
@@ -3094,6 +4049,15 @@ var AgentSession = class {
3094
4049
  data
3095
4050
  });
3096
4051
  };
4052
+ let lastGoalSignature = "";
4053
+ const persistRunGoal = () => {
4054
+ if (request.isCurrent?.() === false) return;
4055
+ const goal = runtime.activeGoal ?? null;
4056
+ const signature = JSON.stringify(goal);
4057
+ if (signature === lastGoalSignature) return;
4058
+ lastGoalSignature = signature;
4059
+ persistGoal({ sessionId: request.sessionId, timelineStore: request.timelineStore }, goal);
4060
+ };
3097
4061
  const baseLoopCallbacks = {
3098
4062
  onText: (content) => {
3099
4063
  streamedAssistantText += content;
@@ -3106,6 +4070,12 @@ var AgentSession = class {
3106
4070
  },
3107
4071
  onToolCall: (toolCall) => emit({ type: "tool_use", toolCall }),
3108
4072
  onToolResult: (toolResult) => emit({ type: "tool_result", toolResult }),
4073
+ onNotice: (message) => emit({ type: "notice", message }),
4074
+ onGoalStatus: (event) => {
4075
+ persistRunGoal();
4076
+ emit({ type: "goal_status", event });
4077
+ },
4078
+ onAuxiliaryUsage: callbacks.onAuxiliaryUsage,
3109
4079
  onError: (error) => {
3110
4080
  emittedError = error;
3111
4081
  emit({ type: "error", error });
@@ -3117,7 +4087,9 @@ var AgentSession = class {
3117
4087
  finalizeOutcome(outcome2);
3118
4088
  },
3119
4089
  onPermissionRequired: (toolCall) => {
3120
- if (request.isCurrent?.() === false) return Promise.resolve("deny");
4090
+ if (request.isCurrent?.() === false) {
4091
+ return Promise.resolve({ result: "deny", reason: "dismissed" });
4092
+ }
3121
4093
  return callbacks.onPermissionRequired ? callbacks.onPermissionRequired(toolCall) : this.interactions.requestPermission(toolCall);
3122
4094
  },
3123
4095
  onPlanApprovalRequired: (plan) => {
@@ -3139,32 +4111,21 @@ var AgentSession = class {
3139
4111
  },
3140
4112
  onUsage: (nextUsage, metadata) => {
3141
4113
  usage = nextUsage;
3142
- const inclusive = request.runContext ? runtime.runAccounting.snapshotRoot(request.runContext.rootRunId).inclusiveUsage : null;
3143
- const recordUsage = inclusive ? subtractUsage(inclusive, persistedUsage) : nextUsage;
3144
- if (inclusive) persistedUsage = inclusive;
3145
- if (recordUsage.totalTokens <= 0 && recordUsage.promptTokens <= 0) return;
3146
- if (request.isCurrent?.() !== false) {
3147
- request.timelineStore?.append(request.sessionId, {
3148
- type: "usage",
3149
- timestamp: Date.now(),
3150
- data: {
3151
- version: 1,
3152
- usage: recordUsage,
3153
- requestedModel: metadata?.requestedModel,
3154
- responseModel: metadata?.responseModel
3155
- }
3156
- });
3157
- }
4114
+ this.persistReportedUsage(runContext.rootRunId, nextUsage, metadata);
3158
4115
  callbacks.onUsage?.(nextUsage, metadata);
3159
4116
  },
3160
4117
  getMode: callbacks.getMode,
3161
4118
  onModeChange: callbacks.onModeChange,
4119
+ // The root run's only: a managed child and a Task subagent run the loop
4120
+ // with their own callbacks, and neither announces calls to a stream the
4121
+ // host is reading for them.
4122
+ beforeToolExecution: callbacks.beforeToolExecution,
3162
4123
  onPlanHandoff: callbacks.onPlanHandoff,
3163
- // Kept under Zero-Mem: `loopConfig` already sets `autoCompactEnabled:
3164
- // false`, which gates the loop's two routine compaction paths, and the
3165
- // context-overflow path at the bottom of the turn is deliberately not
3166
- // gated by it. Nulling the callback disabled that recovery too.
4124
+ // Passed through as is: the loop gates every compaction site on
4125
+ // `autoCompactEnabled`, the context-overflow recovery included.
3167
4126
  onCompact: callbacks.onCompact,
4127
+ prepareCompact: callbacks.prepareCompact,
4128
+ commitCompact: callbacks.commitCompact,
3168
4129
  onAssistantMessageComplete: (message) => {
3169
4130
  if (request.isCurrent?.() === false) return;
3170
4131
  request.timelineStore?.append(request.sessionId, {
@@ -3180,7 +4141,10 @@ var AgentSession = class {
3180
4141
  kind: message.kind ?? "conversation",
3181
4142
  toolCalls: message.toolCalls,
3182
4143
  toolResults: message.toolResults,
3183
- fileObservations: message.fileObservations
4144
+ fileObservations: message.fileObservations,
4145
+ // Without this a resumed session reads a host notice as the model's
4146
+ // own reply: `finalAnswerText` and the answer walk both key on it.
4147
+ hostNotice: message.hostNotice
3184
4148
  }
3185
4149
  });
3186
4150
  callbacks.onAssistantMessageComplete?.(message);
@@ -3213,7 +4177,10 @@ var AgentSession = class {
3213
4177
  id: message.id,
3214
4178
  content: message.content,
3215
4179
  kind: message.kind ?? "conversation",
3216
- includeInContext: message.includeInContext ?? true
4180
+ includeInContext: message.includeInContext ?? true,
4181
+ // A host-appended prompt stays host-written across a reload: the ledger,
4182
+ // Carried Turns key on it.
4183
+ derivedContent: message.derivedContent
3217
4184
  }
3218
4185
  });
3219
4186
  },
@@ -3224,21 +4191,27 @@ var AgentSession = class {
3224
4191
  onHookEvent: callbacks.onHookEvent,
3225
4192
  onAgentEvent: (event) => emit(event)
3226
4193
  };
3227
- const messages = await this.runLoop(
3228
- request.config,
3229
- request.registry,
3230
- request.prompt,
3231
- effectiveHistory,
3232
- baseLoopCallbacks,
3233
- request.mode,
3234
- {
3235
- ...request.options,
3236
- runtime,
3237
- runContext,
3238
- resolveAttachment: request.options?.resolveAttachment ?? (request.timelineStore?.readImageAttachment ? (attachment) => request.timelineStore.readImageAttachment(request.sessionId, attachment) : void 0),
3239
- signal: request.signal
3240
- }
3241
- );
4194
+ let messages;
4195
+ try {
4196
+ messages = await this.runLoop(
4197
+ request.config,
4198
+ request.registry,
4199
+ request.prompt,
4200
+ effectiveHistory,
4201
+ baseLoopCallbacks,
4202
+ request.mode,
4203
+ {
4204
+ ...request.options,
4205
+ runtime,
4206
+ runContext,
4207
+ resolveAttachment: request.options?.resolveAttachment ?? (request.timelineStore?.readImageAttachment ? (attachment) => request.timelineStore.readImageAttachment(request.sessionId, attachment) : void 0),
4208
+ signal: request.signal
4209
+ }
4210
+ );
4211
+ } finally {
4212
+ this.flushRunUsage(runContext.rootRunId, true);
4213
+ if (runtime.activeGoal) persistRunGoal();
4214
+ }
3242
4215
  const partialOutput = streamedAssistantText.length > 0 || messages.slice(effectiveHistory.length).some((message) => message.role === "assistant");
3243
4216
  const outcome = finalizeOutcome(
3244
4217
  terminalOutcome ?? (request.signal?.aborted ? classifyAbortReason(request.signal.reason, partialOutput) : emittedError ? createTerminalOutcome("failed", "provider_error", {
@@ -3287,10 +4260,10 @@ async function captureSnapshot(snapshotStore) {
3287
4260
  return { ok: false, reason: "Filesystem checkpoint storage is unavailable." };
3288
4261
  }
3289
4262
  if (snapshotStore.captureAsync) return snapshotStore.captureAsync();
3290
- return new Promise((resolve3, reject) => {
4263
+ return new Promise((resolve4, reject) => {
3291
4264
  setTimeout(() => {
3292
4265
  try {
3293
- resolve3(snapshotStore.capture());
4266
+ resolve4(snapshotStore.capture());
3294
4267
  } catch (error) {
3295
4268
  reject(error);
3296
4269
  }
@@ -3481,6 +4454,8 @@ function validateElicitationContent(fields, content) {
3481
4454
  }
3482
4455
 
3483
4456
  export {
4457
+ parseSessionDriver,
4458
+ resolveSessionDriver,
3484
4459
  displaySessionName,
3485
4460
  normalizeWorkspace,
3486
4461
  SessionStore,
@@ -3491,8 +4466,11 @@ export {
3491
4466
  findActiveFileMention,
3492
4467
  replaceActiveFileMention,
3493
4468
  getFileMentionCandidates,
4469
+ mentionTokenRanges,
3494
4470
  createAgentSessionSnapshot,
3495
4471
  reduceAgentSessionSnapshot,
4472
+ discardUntouchedSession,
4473
+ discardUntouchedSessions,
3496
4474
  resolveSessionBootstrap,
3497
4475
  AgentSession,
3498
4476
  toElicitationFields,
@@ -3501,4 +4479,4 @@ export {
3501
4479
  coerceElicitationValue,
3502
4480
  validateElicitationContent
3503
4481
  };
3504
- //# sourceMappingURL=chunk-V4BOYR52.js.map
4482
+ //# sourceMappingURL=chunk-ENLB7NYV.js.map