@letrquan/book 0.2.0 → 0.3.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 (157) hide show
  1. package/CHANGELOG.md +2747 -40
  2. package/README.md +132 -1311
  3. package/dist/additional-roots-H7UNAEHS.js +31 -0
  4. package/dist/{app-MOXXWEOY.js → app-GE4OHJOC.js} +5740 -4045
  5. package/dist/app-GE4OHJOC.js.map +1 -0
  6. package/dist/{chunk-5GDZ22YP.js → chunk-224FJZLH.js} +99 -108
  7. package/dist/chunk-224FJZLH.js.map +1 -0
  8. package/dist/chunk-2AAOF5B5.js +28 -0
  9. package/dist/chunk-2AAOF5B5.js.map +1 -0
  10. package/dist/{chunk-CAGOB2N4.js → chunk-4AS7LKKQ.js} +6 -4
  11. package/dist/chunk-4AS7LKKQ.js.map +1 -0
  12. package/dist/chunk-525CAZPY.js +120 -0
  13. package/dist/chunk-525CAZPY.js.map +1 -0
  14. package/dist/{chunk-URLQAXTR.js → chunk-56SIEMDX.js} +2 -2
  15. package/dist/chunk-56SIEMDX.js.map +1 -0
  16. package/dist/{chunk-XIFFTATU.js → chunk-5YHTKWJN.js} +5420 -4031
  17. package/dist/chunk-5YHTKWJN.js.map +1 -0
  18. package/dist/chunk-62QL5QUP.js +283 -0
  19. package/dist/chunk-62QL5QUP.js.map +1 -0
  20. package/dist/{chunk-KWHQ2XXL.js → chunk-67UZEWWF.js} +3 -3
  21. package/dist/chunk-67UZEWWF.js.map +1 -0
  22. package/dist/chunk-6OQQOMZR.js +785 -0
  23. package/dist/chunk-6OQQOMZR.js.map +1 -0
  24. package/dist/{chunk-GTVB4HEM.js → chunk-AQEQX4FF.js} +344 -315
  25. package/dist/chunk-AQEQX4FF.js.map +1 -0
  26. package/dist/chunk-C7ADUNPR.js +273 -0
  27. package/dist/chunk-C7ADUNPR.js.map +1 -0
  28. package/dist/{chunk-E75IPST5.js → chunk-EFJ2NH6E.js} +12 -58
  29. package/dist/chunk-EFJ2NH6E.js.map +1 -0
  30. package/dist/{chunk-5KLVH3PY.js → chunk-HQI7NYGX.js} +5 -3
  31. package/dist/chunk-HQI7NYGX.js.map +1 -0
  32. package/dist/chunk-IME7R2UI.js +6541 -0
  33. package/dist/chunk-IME7R2UI.js.map +1 -0
  34. package/dist/chunk-J4F2QLM2.js +273 -0
  35. package/dist/chunk-J4F2QLM2.js.map +1 -0
  36. package/dist/{chunk-MAQXCUR4.js → chunk-J62VZYOB.js} +444 -112
  37. package/dist/chunk-J62VZYOB.js.map +1 -0
  38. package/dist/chunk-JTPOI3KU.js +826 -0
  39. package/dist/chunk-JTPOI3KU.js.map +1 -0
  40. package/dist/{chunk-WKBO5R4O.js → chunk-KBQS35OV.js} +2 -2
  41. package/dist/chunk-KBQS35OV.js.map +1 -0
  42. package/dist/chunk-KXAYUIKP.js +90 -0
  43. package/dist/chunk-KXAYUIKP.js.map +1 -0
  44. package/dist/{chunk-VZQXX3WW.js → chunk-QYBD22H5.js} +1 -1
  45. package/dist/chunk-QYBD22H5.js.map +1 -0
  46. package/dist/{chunk-V4BOYR52.js → chunk-RFJ6Q6LE.js} +763 -198
  47. package/dist/chunk-RFJ6Q6LE.js.map +1 -0
  48. package/dist/{chunk-7N4J557C.js → chunk-RT5SATF7.js} +1 -1
  49. package/dist/chunk-RT5SATF7.js.map +1 -0
  50. package/dist/{chunk-C7D4NJ2D.js → chunk-S4JMID4G.js} +68 -26
  51. package/dist/chunk-S4JMID4G.js.map +1 -0
  52. package/dist/{chunk-ANSJPYGI.js → chunk-SOW4WRQP.js} +3 -3
  53. package/dist/chunk-SOW4WRQP.js.map +1 -0
  54. package/dist/{chunk-J7MIZ5CA.js → chunk-TLX3H2CL.js} +518 -79
  55. package/dist/chunk-TLX3H2CL.js.map +1 -0
  56. package/dist/{chunk-DGMQEI6Y.js → chunk-VPNLCMVR.js} +22 -9
  57. package/dist/chunk-VPNLCMVR.js.map +1 -0
  58. package/dist/{chunk-BTZZUCLI.js → chunk-W3XFHEF4.js} +173 -29
  59. package/dist/chunk-W3XFHEF4.js.map +1 -0
  60. package/dist/{chunk-574C6MQR.js → chunk-X4NJT436.js} +130 -60
  61. package/dist/chunk-X4NJT436.js.map +1 -0
  62. package/dist/chunk-X66I2NQO.js +137 -0
  63. package/dist/chunk-X66I2NQO.js.map +1 -0
  64. package/dist/chunk-XHAO3K7I.js +18 -0
  65. package/dist/chunk-XHAO3K7I.js.map +1 -0
  66. package/dist/{chunk-3U5IBM24.js → chunk-Y2VHN7TC.js} +2 -2
  67. package/dist/chunk-Y2VHN7TC.js.map +1 -0
  68. package/dist/chunk-YC6W4EJH.js +947 -0
  69. package/dist/chunk-YC6W4EJH.js.map +1 -0
  70. package/dist/{chunk-24AXE6SP.js → chunk-ZPSZZUWF.js} +1 -1
  71. package/dist/chunk-ZPSZZUWF.js.map +1 -0
  72. package/dist/{command-approvals-6Y57VCWZ.js → command-approvals-VBIU6BLM.js} +6 -5
  73. package/dist/context-CBOBDY5Q.js +38 -0
  74. package/dist/{hook-approvals-CTUFKEJT.js → hook-approvals-ZVT4V7IM.js} +5 -5
  75. package/dist/index.js +996 -155
  76. package/dist/index.js.map +1 -1
  77. package/dist/{interactive-assets-CCAAWVKH.js → interactive-assets-T4I4SEHU.js} +13 -9
  78. package/dist/interactive-assets-T4I4SEHU.js.map +1 -0
  79. package/dist/job-runner.js +159 -111
  80. package/dist/job-runner.js.map +1 -1
  81. package/dist/job-supervisor.d.ts +2 -0
  82. package/dist/job-supervisor.js +112 -0
  83. package/dist/job-supervisor.js.map +1 -0
  84. package/dist/loader-S77OVTHH.js +18 -0
  85. package/dist/{mcp-approvals-WNPH65BK.js → mcp-approvals-HJ2VDYMC.js} +5 -5
  86. package/dist/{mcp-config-YIJIES2C.js → mcp-config-RNB5LEVE.js} +3 -3
  87. package/dist/memory-store-DZUT5HUH.js +75 -0
  88. package/dist/{model-window-store-YFVNXCIP.js → model-window-store-MTSQ5JDJ.js} +8 -4
  89. package/dist/openai-compatible-2OB54YST.js +14 -0
  90. package/dist/{permission-approvals-76PVREFD.js → permission-approvals-YLV6UC5W.js} +5 -5
  91. package/dist/permissions-DHOKF3W2.js +45 -0
  92. package/dist/sandbox-WRYTZH3D.js +41 -0
  93. package/dist/{scrollback-LUKLIBVI.js → scrollback-35ME2GFX.js} +32 -22
  94. package/dist/scrollback-35ME2GFX.js.map +1 -0
  95. package/dist/sdk.d.ts +2219 -2361
  96. package/dist/sdk.js +53 -38
  97. package/dist/sdk.js.map +1 -1
  98. package/dist/settings-loader-JUNPGAAM.js +35 -0
  99. package/dist/settings-loader-JUNPGAAM.js.map +1 -0
  100. package/dist/{shell-selection-V6QAYXPX.js → shell-selection-CDHGJ4GC.js} +2 -2
  101. package/dist/shell-selection-CDHGJ4GC.js.map +1 -0
  102. package/package.json +23 -22
  103. package/dist/app-MOXXWEOY.js.map +0 -1
  104. package/dist/chunk-24AXE6SP.js.map +0 -1
  105. package/dist/chunk-3U5IBM24.js.map +0 -1
  106. package/dist/chunk-574C6MQR.js.map +0 -1
  107. package/dist/chunk-5GDZ22YP.js.map +0 -1
  108. package/dist/chunk-5KLVH3PY.js.map +0 -1
  109. package/dist/chunk-7N4J557C.js.map +0 -1
  110. package/dist/chunk-A7JLBK2Y.js +0 -3506
  111. package/dist/chunk-A7JLBK2Y.js.map +0 -1
  112. package/dist/chunk-ANSJPYGI.js.map +0 -1
  113. package/dist/chunk-BTZZUCLI.js.map +0 -1
  114. package/dist/chunk-C7D4NJ2D.js.map +0 -1
  115. package/dist/chunk-CAGOB2N4.js.map +0 -1
  116. package/dist/chunk-DGMQEI6Y.js.map +0 -1
  117. package/dist/chunk-E75IPST5.js.map +0 -1
  118. package/dist/chunk-EYGD6X2T.js +0 -180
  119. package/dist/chunk-EYGD6X2T.js.map +0 -1
  120. package/dist/chunk-GTVB4HEM.js.map +0 -1
  121. package/dist/chunk-J7MIZ5CA.js.map +0 -1
  122. package/dist/chunk-KWHQ2XXL.js.map +0 -1
  123. package/dist/chunk-L6LK2QIQ.js +0 -657
  124. package/dist/chunk-L6LK2QIQ.js.map +0 -1
  125. package/dist/chunk-MAQXCUR4.js.map +0 -1
  126. package/dist/chunk-S4XL7HOM.js +0 -402
  127. package/dist/chunk-S4XL7HOM.js.map +0 -1
  128. package/dist/chunk-URLQAXTR.js.map +0 -1
  129. package/dist/chunk-V4BOYR52.js.map +0 -1
  130. package/dist/chunk-VZQXX3WW.js.map +0 -1
  131. package/dist/chunk-WKBO5R4O.js.map +0 -1
  132. package/dist/chunk-XIFFTATU.js.map +0 -1
  133. package/dist/context-ZC5IFEAX.js +0 -25
  134. package/dist/interactive-assets-CCAAWVKH.js.map +0 -1
  135. package/dist/loader-4IDS2RIE.js +0 -15
  136. package/dist/openai-compatible-RHZUBHV6.js +0 -10
  137. package/dist/permissions-CHCKTFQQ.js +0 -28
  138. package/dist/sandbox-CNVAQDA7.js +0 -23
  139. package/dist/scrollback-LUKLIBVI.js.map +0 -1
  140. package/dist/settings-loader-M2QP5TX2.js +0 -21
  141. package/patches/ink+6.8.0.patch +0 -13
  142. package/scripts/apply-ink-patch.mjs +0 -29
  143. package/scripts/ink-patch.mjs +0 -59
  144. package/scripts/verify-ink-patch.mjs +0 -11
  145. /package/dist/{command-approvals-6Y57VCWZ.js.map → additional-roots-H7UNAEHS.js.map} +0 -0
  146. /package/dist/{context-ZC5IFEAX.js.map → command-approvals-VBIU6BLM.js.map} +0 -0
  147. /package/dist/{hook-approvals-CTUFKEJT.js.map → context-CBOBDY5Q.js.map} +0 -0
  148. /package/dist/{loader-4IDS2RIE.js.map → hook-approvals-ZVT4V7IM.js.map} +0 -0
  149. /package/dist/{mcp-approvals-WNPH65BK.js.map → loader-S77OVTHH.js.map} +0 -0
  150. /package/dist/{mcp-config-YIJIES2C.js.map → mcp-approvals-HJ2VDYMC.js.map} +0 -0
  151. /package/dist/{model-window-store-YFVNXCIP.js.map → mcp-config-RNB5LEVE.js.map} +0 -0
  152. /package/dist/{openai-compatible-RHZUBHV6.js.map → memory-store-DZUT5HUH.js.map} +0 -0
  153. /package/dist/{permission-approvals-76PVREFD.js.map → model-window-store-MTSQ5JDJ.js.map} +0 -0
  154. /package/dist/{permissions-CHCKTFQQ.js.map → openai-compatible-2OB54YST.js.map} +0 -0
  155. /package/dist/{sandbox-CNVAQDA7.js.map → permission-approvals-YLV6UC5W.js.map} +0 -0
  156. /package/dist/{settings-loader-M2QP5TX2.js.map → permissions-DHOKF3W2.js.map} +0 -0
  157. /package/dist/{shell-selection-V6QAYXPX.js.map → sandbox-WRYTZH3D.js.map} +0 -0
@@ -5,40 +5,73 @@ import {
5
5
  createAgentRunContext,
6
6
  createRunAmbientSnapshot,
7
7
  createTerminalOutcome,
8
+ isSpendlessUsage,
8
9
  isTerminalStatus,
9
10
  loadGitignore,
10
- runAgentLoop,
11
- runCompact,
12
- runHooks,
13
- runPostCompactHooks
14
- } from "./chunk-XIFFTATU.js";
11
+ runAgentLoop
12
+ } from "./chunk-5YHTKWJN.js";
15
13
  import {
16
14
  DEFAULT_CONFIDENCE_THRESHOLD,
17
15
  REVIEW_LENSES,
16
+ addUsage,
17
+ applyCompactResult,
18
18
  buildReviewerPrompt,
19
19
  buildSingleReviewPrompt,
20
20
  clampConfidence,
21
21
  isReviewCategory,
22
22
  isReviewSeverity,
23
+ judgeCompaction,
24
+ judgedResult,
23
25
  loadReviewConfig,
24
26
  renderReviewConfigInstruction,
25
27
  renderReviewTarget,
26
- resolveReviewTarget
27
- } from "./chunk-A7JLBK2Y.js";
28
+ resolveCompactBudgets,
29
+ resolveReviewTarget,
30
+ runCompact,
31
+ runHooks,
32
+ runPostCompactHooks
33
+ } from "./chunk-IME7R2UI.js";
28
34
  import {
35
+ observationKey,
29
36
  onAbort,
30
37
  throwIfAborted,
38
+ workspaceIdentity,
31
39
  yieldToEventLoop
32
- } from "./chunk-MAQXCUR4.js";
40
+ } from "./chunk-J62VZYOB.js";
33
41
  import {
34
- normalizeToolResult,
35
- observationKey,
36
- workspaceIdentity
37
- } from "./chunk-5GDZ22YP.js";
42
+ globWalkIgnore
43
+ } from "./chunk-JTPOI3KU.js";
44
+ import {
45
+ buildChildEnv
46
+ } from "./chunk-XHAO3K7I.js";
38
47
  import {
39
48
  createDebugLogger,
40
49
  createDebugLoggerWithCounter
41
- } from "./chunk-GTVB4HEM.js";
50
+ } from "./chunk-62QL5QUP.js";
51
+ import {
52
+ normalizeToolResult
53
+ } from "./chunk-224FJZLH.js";
54
+
55
+ // src/session-driver.ts
56
+ var SESSION_DRIVERS = ["human", "agent"];
57
+ function isSessionDriver(value) {
58
+ return SESSION_DRIVERS.includes(value);
59
+ }
60
+ function parseSessionDriver(value) {
61
+ const normalized = value?.trim().toLowerCase();
62
+ return isSessionDriver(normalized) ? normalized : void 0;
63
+ }
64
+ function resolveSessionDriver(input) {
65
+ const explicit = parseSessionDriver(input.explicit);
66
+ if (explicit) return { driver: explicit, signals: ["flag"] };
67
+ const fromEnv = parseSessionDriver(input.env.BOOK_SESSION_DRIVER);
68
+ if (fromEnv) return { driver: fromEnv, signals: ["BOOK_SESSION_DRIVER"] };
69
+ const signals = [];
70
+ const claudeCode = input.env.CLAUDECODE?.trim();
71
+ if (claudeCode && claudeCode !== "0") signals.push("CLAUDECODE");
72
+ if (input.env.AI_AGENT?.trim()) signals.push("AI_AGENT");
73
+ return { driver: signals.length > 0 ? "agent" : "human", signals };
74
+ }
42
75
 
43
76
  // src/session/name.ts
44
77
  var UNTITLED_SESSION_NAME = "Untitled session";
@@ -274,7 +307,8 @@ var SessionStore = class {
274
307
  name: meta.name,
275
308
  createdAt: now,
276
309
  updatedAt: now,
277
- messageCount: 0
310
+ messageCount: 0,
311
+ ...meta.driver ? { driver: meta.driver } : {}
278
312
  }
279
313
  };
280
314
  appendFileSync(this.path(id), JSON.stringify(header) + "\n", "utf-8");
@@ -286,10 +320,10 @@ var SessionStore = class {
286
320
  appendFileSync(this.path(id), serialized, "utf-8");
287
321
  const cached = this.parsedSessions.get(id);
288
322
  if (cached) {
289
- const stat = statSync(this.path(id));
323
+ const stat2 = statSync(this.path(id));
290
324
  cached.records.push(record);
291
- cached.size = stat.size;
292
- cached.mtimeMs = stat.mtimeMs;
325
+ cached.size = stat2.size;
326
+ cached.mtimeMs = stat2.mtimeMs;
293
327
  cached.loaded = void 0;
294
328
  cached.activeEventIds = void 0;
295
329
  cached.activeRecords = void 0;
@@ -300,9 +334,9 @@ var SessionStore = class {
300
334
  readRecords(id) {
301
335
  const p = this.path(id);
302
336
  if (!existsSync(p)) throw new Error(`Session not found: ${id}`);
303
- const stat = statSync(p);
337
+ const stat2 = statSync(p);
304
338
  const cached = this.parsedSessions.get(id);
305
- if (cached && cached.size === stat.size && cached.mtimeMs === stat.mtimeMs) {
339
+ if (cached && cached.size === stat2.size && cached.mtimeMs === stat2.mtimeMs) {
306
340
  return cached.records;
307
341
  }
308
342
  const records = [];
@@ -337,8 +371,8 @@ var SessionStore = class {
337
371
  closeSync(descriptor);
338
372
  }
339
373
  this.parsedSessions.set(id, {
340
- size: stat.size,
341
- mtimeMs: stat.mtimeMs,
374
+ size: stat2.size,
375
+ mtimeMs: stat2.mtimeMs,
342
376
  records
343
377
  });
344
378
  return records;
@@ -356,6 +390,9 @@ var SessionStore = class {
356
390
  entry.meta.updatedAt = record.timestamp;
357
391
  if (data.kind === "session_meta_patch") {
358
392
  if (Object.prototype.hasOwnProperty.call(data, "name")) entry.meta.name = data.name;
393
+ if (isSessionDriver(data.driver)) {
394
+ entry.meta.driver = data.driver;
395
+ }
359
396
  } else if (record.type === "user") {
360
397
  entry.meta.messageCount++;
361
398
  entry.lastMessageRole = "user";
@@ -406,7 +443,8 @@ var SessionStore = class {
406
443
  createdAt: storedMeta?.createdAt ?? 0,
407
444
  updatedAt: storedMeta?.updatedAt ?? 0,
408
445
  messageCount: 0,
409
- ...storedMeta?.name === void 0 ? {} : { name: storedMeta.name }
446
+ ...storedMeta?.name === void 0 ? {} : { name: storedMeta.name },
447
+ ...isSessionDriver(storedMeta?.driver) ? { driver: storedMeta.driver } : {}
410
448
  };
411
449
  const transcript = [];
412
450
  let contextHistory = [];
@@ -427,21 +465,18 @@ var SessionStore = class {
427
465
  const data = record.data;
428
466
  if (data.kind === "session_meta_patch") {
429
467
  if (Object.prototype.hasOwnProperty.call(data, "name")) meta.name = data.name;
468
+ const driver = data.driver;
469
+ if (isSessionDriver(driver)) meta.driver = driver;
430
470
  continue;
431
471
  }
432
472
  if (data.kind === "session_meta" || data.kind === "session_touch") continue;
433
473
  if (record.type === "usage") {
434
474
  const entry = record.data;
435
475
  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
- };
476
+ carriedUsage = addUsage(carriedUsage ?? null, entry.usage);
443
477
  const model = entry.responseModel ?? entry.requestedModel;
444
478
  if (model) carriedModels.add(model);
479
+ for (const spent of entry.models ?? []) carriedModels.add(spent);
445
480
  }
446
481
  continue;
447
482
  }
@@ -601,6 +636,7 @@ var SessionStore = class {
601
636
  toolCalls: data.toolCalls,
602
637
  toolResults: normalizePersistedToolResults(data.toolResults),
603
638
  fileObservations: data.fileObservations,
639
+ hostNotice: data.hostNotice,
604
640
  timestamp: record.timestamp
605
641
  };
606
642
  transcript.push(message);
@@ -942,7 +978,10 @@ function reviewRunnerFor(manager, attribution = {}) {
942
978
  rootRunId: attribution.rootRunId,
943
979
  parentRunId: attribution.parentRunId,
944
980
  parentSessionId: attribution.parentSessionId,
945
- notifyParentOnCompletion: false
981
+ notifyParentOnCompletion: false,
982
+ // The report dies with the process, so a restart must not re-run a reviewer
983
+ // whose result nobody would receive.
984
+ resumeAfterRestart: false
946
985
  });
947
986
  return {
948
987
  id: record.id,
@@ -988,16 +1027,17 @@ function fixRunnerFor(manager, attribution = {}) {
988
1027
  }
989
1028
 
990
1029
  // 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("{");
1030
+ function firstObjectIn(text) {
1031
+ return objectIn(text, 0)?.slice;
1032
+ }
1033
+ function objectIn(text, from) {
1034
+ const start = text.indexOf("{", from);
995
1035
  if (start === -1) return void 0;
996
1036
  let depth = 0;
997
1037
  let inString = false;
998
1038
  let escaped = false;
999
- for (let index = start; index < candidate.length; index++) {
1000
- const char = candidate[index];
1039
+ for (let index = start; index < text.length; index++) {
1040
+ const char = text[index];
1001
1041
  if (inString) {
1002
1042
  if (escaped) escaped = false;
1003
1043
  else if (char === "\\") escaped = true;
@@ -1006,10 +1046,47 @@ function extractJsonObject(text) {
1006
1046
  }
1007
1047
  if (char === '"') inString = true;
1008
1048
  else if (char === "{") depth++;
1009
- else if (char === "}" && --depth === 0) return candidate.slice(start, index + 1);
1049
+ else if (char === "}" && --depth === 0) {
1050
+ return { start, end: index + 1, slice: text.slice(start, index + 1) };
1051
+ }
1010
1052
  }
1011
1053
  return void 0;
1012
1054
  }
1055
+ function fenceRuns(text) {
1056
+ const runs = [];
1057
+ for (let index = text.indexOf("```"); index !== -1; index = text.indexOf("```", index + 3)) {
1058
+ runs.push(index);
1059
+ }
1060
+ return runs;
1061
+ }
1062
+ function fenceContent(text, open, close) {
1063
+ return text.slice(open + 3, close).replace(/^json\s*/i, "");
1064
+ }
1065
+ function isJsonObject(candidate) {
1066
+ try {
1067
+ const parsed = JSON.parse(candidate);
1068
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed);
1069
+ } catch {
1070
+ return false;
1071
+ }
1072
+ }
1073
+ function extractJsonObject(text) {
1074
+ const runs = fenceRuns(text);
1075
+ const whole = objectIn(text, 0);
1076
+ if (whole !== void 0 && isJsonObject(whole.slice) && runs.every((run) => run >= whole.start && run < whole.end)) {
1077
+ return whole.slice;
1078
+ }
1079
+ let firstBalanced;
1080
+ for (let open = 0; open < runs.length; open++) {
1081
+ for (let close = open + 1; close < runs.length; close++) {
1082
+ const found = firstObjectIn(fenceContent(text, runs[open], runs[close]));
1083
+ if (found === void 0) continue;
1084
+ if (isJsonObject(found)) return found;
1085
+ firstBalanced ??= found;
1086
+ }
1087
+ }
1088
+ return firstBalanced ?? firstObjectIn(text);
1089
+ }
1013
1090
  function parseJsonObject(text) {
1014
1091
  const candidate = extractJsonObject(text);
1015
1092
  if (!candidate) return void 0;
@@ -1207,6 +1284,29 @@ function rankFindings(findings) {
1207
1284
  function filterLowConfidence(findings, threshold = DEFAULT_CONFIDENCE_THRESHOLD) {
1208
1285
  return findings.filter((finding) => finding.confidence >= threshold);
1209
1286
  }
1287
+ function isStanding(finding) {
1288
+ return finding.verification === void 0 || finding.verification === "confirmed";
1289
+ }
1290
+ function deriveReviewVerdict(input) {
1291
+ const entries = [...input.coverage.reviewers, input.coverage.verifier].filter(
1292
+ (entry) => entry !== void 0
1293
+ );
1294
+ if (entries.some((entry) => entry.status !== "completed")) {
1295
+ return { verdict: "inconclusive", reason: "incomplete-coverage" };
1296
+ }
1297
+ const standing = input.findings.filter(isStanding);
1298
+ if (standing.some((finding) => finding.severity === "critical")) {
1299
+ return { verdict: "blocking", reason: "standing-findings" };
1300
+ }
1301
+ if (standing.length > 0) return { verdict: "recommend", reason: "standing-findings" };
1302
+ if (input.findings.length > 0) {
1303
+ return { verdict: "inconclusive", reason: "unverified-findings" };
1304
+ }
1305
+ if (input.reviewerVerdicts?.some((verdict) => verdict === "inconclusive")) {
1306
+ return { verdict: "inconclusive", reason: "reviewer-inconclusive" };
1307
+ }
1308
+ return { verdict: "clean", reason: "clean" };
1309
+ }
1210
1310
 
1211
1311
  // src/review/verify-findings.ts
1212
1312
  function buildVerificationPrompt(findings, config = {}, target) {
@@ -1370,13 +1470,23 @@ async function runSingleReview(runner, scope, workspace, options = {}) {
1370
1470
  rankFindings(filterLowConfidence(dedupeFindings(parsed.report.findings)))
1371
1471
  );
1372
1472
  const coverage = passCoverage("single", structured, parsed.droppedFindings, findings.length);
1473
+ const reviewerEntries = [coverage];
1474
+ const reportCoverage = { reviewers: reviewerEntries };
1475
+ const { verdict, reason } = deriveReviewVerdict({
1476
+ findings,
1477
+ coverage: reportCoverage,
1478
+ // A completed, structured pass whose own verdict is `inconclusive` is the
1479
+ // one reviewer signal that is not already a coverage entry.
1480
+ reviewerVerdicts: structured ? [parsed.report.verdict] : []
1481
+ });
1373
1482
  const report = {
1374
- verdict: coverage.status === "completed" ? parsed.report.verdict : "inconclusive",
1483
+ verdict,
1375
1484
  findings,
1376
- coverage: { reviewers: [coverage] }
1485
+ coverage: reportCoverage
1377
1486
  };
1378
1487
  const text = [
1379
- renderCoverageWarnings([coverage]),
1488
+ renderCoverageWarnings(reviewerEntries),
1489
+ renderVerdictReason("Review", reason),
1380
1490
  renderReviewReport(report),
1381
1491
  coverage.status === "completed" ? "" : renderRawOutput("reviewer", settled.result)
1382
1492
  ].filter(Boolean).join("\n\n");
@@ -1414,6 +1524,17 @@ function renderCoverageWarnings(entries) {
1414
1524
  })
1415
1525
  ].join("\n");
1416
1526
  }
1527
+ function renderVerdictReason(prefix, reason) {
1528
+ const explanation = {
1529
+ "incomplete-coverage": "",
1530
+ "standing-findings": "",
1531
+ "unverified-findings": "the verifier could neither confirm nor reject the remaining findings.",
1532
+ "reviewer-inconclusive": "a reviewer reported its review as inconclusive.",
1533
+ clean: ""
1534
+ };
1535
+ const text = explanation[reason];
1536
+ return text ? `${prefix}: ${text}` : "";
1537
+ }
1417
1538
  async function runDeepReview(runner, scope, workspace, options = {}) {
1418
1539
  if (scope.error) throw new Error(scope.error);
1419
1540
  const target = await resolveReviewTarget(workspace, scope);
@@ -1514,22 +1635,22 @@ async function runDeepReview(runner, scope, workspace, options = {}) {
1514
1635
  reviewerCoverage.sort(
1515
1636
  (left, right) => REVIEW_LENSES.findIndex((lens) => lens.id === left.id) - REVIEW_LENSES.findIndex((lens) => lens.id === right.id)
1516
1637
  );
1517
- const completeDiscovery = reviewerCoverage.every((entry) => entry.status === "completed");
1518
1638
  const candidates = reindex(rankFindings(filterLowConfidence(dedupeFindings(rawFindings))));
1519
1639
  if (candidates.length === 0) {
1640
+ const reportCoverage2 = { reviewers: reviewerCoverage };
1641
+ const { verdict: verdict2, reason: reason2 } = deriveReviewVerdict({
1642
+ findings: [],
1643
+ coverage: reportCoverage2,
1644
+ reviewerVerdicts
1645
+ });
1520
1646
  const report2 = {
1521
- verdict: completeDiscovery && reviewerVerdicts.every((verdict2) => verdict2 === "clean") ? "clean" : "inconclusive",
1647
+ verdict: verdict2,
1522
1648
  findings: [],
1523
- coverage: { reviewers: reviewerCoverage }
1649
+ coverage: reportCoverage2
1524
1650
  };
1525
1651
  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
- };
1652
+ const summary = warning2 || (reason2 === "clean" ? "Deep review complete: no confirmed findings." : renderVerdictReason("Deep review", reason2));
1653
+ return { target, report: report2, text: [summary, ...rawOutputs].join("\n\n") };
1533
1654
  }
1534
1655
  if (options.signal?.aborted) return cancelledDeepResult(target);
1535
1656
  const verifier = await spawnTracked(
@@ -1556,20 +1677,30 @@ async function runDeepReview(runner, scope, workspace, options = {}) {
1556
1677
  }
1557
1678
  const verified = applyVerification(candidates, verdicts);
1558
1679
  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";
1680
+ const reportCoverage = {
1681
+ reviewers: reviewerCoverage,
1682
+ verifier: verifierCoverage
1683
+ };
1684
+ const { verdict, reason } = deriveReviewVerdict({
1685
+ findings: ranked,
1686
+ coverage: reportCoverage,
1687
+ reviewerVerdicts
1688
+ });
1563
1689
  const report = {
1564
1690
  verdict,
1565
1691
  findings: ranked,
1566
- coverage: { reviewers: reviewerCoverage, verifier: verifierCoverage }
1692
+ coverage: reportCoverage
1567
1693
  };
1568
1694
  const warning = renderCoverageWarnings([...reviewerCoverage, verifierCoverage]);
1569
1695
  return {
1570
1696
  target,
1571
1697
  report,
1572
- text: [warning, renderReviewReport(report), ...rawOutputs].filter(Boolean).join("\n\n")
1698
+ text: [
1699
+ warning,
1700
+ renderVerdictReason("Deep review", reason),
1701
+ renderReviewReport(report),
1702
+ ...rawOutputs
1703
+ ].filter(Boolean).join("\n\n")
1573
1704
  };
1574
1705
  } catch (error) {
1575
1706
  await stopNonTerminal(runner, spawned);
@@ -1857,12 +1988,18 @@ function reduceAgentSessionSnapshot(snapshot, event) {
1857
1988
  case "agent_permission":
1858
1989
  case "agent_persistence":
1859
1990
  case "skill_lifecycle":
1991
+ case "notice":
1860
1992
  case "background_job_start":
1861
1993
  case "background_job_update":
1862
1994
  case "background_job_output":
1863
1995
  case "background_job_result":
1864
1996
  case "background_job_dismiss":
1865
1997
  return snapshot;
1998
+ // `agent_notice` is a child's own line for the operator (#305): it is not part of the
1999
+ // child's record and not part of the parent's transcript, so the snapshot drops it and
2000
+ // the TUI reads it from the manager subscription instead (see `useManagedAgents`).
2001
+ case "agent_notice":
2002
+ return snapshot;
1866
2003
  case "evidence_update":
1867
2004
  return { ...snapshot, evidence: upsertById(snapshot.evidence, event.evidence) };
1868
2005
  case "attempt_discarded":
@@ -1900,6 +2037,9 @@ function reduceAgentSessionSnapshot(snapshot, event) {
1900
2037
  }
1901
2038
 
1902
2039
  // src/session/resolve.ts
2040
+ function recordResumeDriver(store, sessionId, recorded, driver) {
2041
+ if (driver === "agent" && recorded !== "agent") store.patchMeta(sessionId, { driver: "agent" });
2042
+ }
1903
2043
  function selectSession(store, selector, cwd) {
1904
2044
  const exact = store.findByName(selector) ?? store.findById(selector);
1905
2045
  if (exact) return exact;
@@ -1970,7 +2110,8 @@ function resolveSessionBootstrap(store, options) {
1970
2110
  const sessionId2 = store.create({
1971
2111
  id: options.sessionId,
1972
2112
  cwd: options.cwd,
1973
- name: options.sessionName
2113
+ name: options.sessionName,
2114
+ driver: options.driver
1974
2115
  });
1975
2116
  return {
1976
2117
  sessionId: sessionId2,
@@ -1992,7 +2133,8 @@ function resolveSessionBootstrap(store, options) {
1992
2133
  if (selected) {
1993
2134
  const loaded = store.load(selected.id);
1994
2135
  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 });
2136
+ const driver = loaded.meta.driver === "agent" ? "agent" : options.driver;
2137
+ 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
2138
  if (!store.fork) persistHistory(store, sessionId2, loaded.transcript);
1997
2139
  return {
1998
2140
  sessionId: sessionId2,
@@ -2010,6 +2152,7 @@ function resolveSessionBootstrap(store, options) {
2010
2152
  };
2011
2153
  }
2012
2154
  store.touch(selected.id);
2155
+ recordResumeDriver(store, selected.id, loaded.meta.driver, options.driver);
2013
2156
  return {
2014
2157
  sessionId: selected.id,
2015
2158
  sessionName: loaded.meta.name,
@@ -2027,7 +2170,11 @@ function resolveSessionBootstrap(store, options) {
2027
2170
  created: false
2028
2171
  };
2029
2172
  }
2030
- const sessionId = store.create({ cwd: options.cwd, name: options.sessionName });
2173
+ const sessionId = store.create({
2174
+ cwd: options.cwd,
2175
+ name: options.sessionName,
2176
+ driver: options.driver
2177
+ });
2031
2178
  return {
2032
2179
  sessionId,
2033
2180
  sessionName: options.sessionName,
@@ -2044,8 +2191,9 @@ function resolveSessionBootstrap(store, options) {
2044
2191
  }
2045
2192
 
2046
2193
  // src/input/file-mentions.ts
2194
+ import { stat } from "fs/promises";
2047
2195
  import { isAbsolute, relative, resolve as resolve2 } from "path";
2048
- import fg from "fast-glob";
2196
+ import { glob } from "tinyglobby";
2049
2197
  var DEFAULT_IGNORE = [
2050
2198
  "**/.git/**",
2051
2199
  "**/node_modules/**",
@@ -2090,21 +2238,45 @@ function replaceActiveFileMention(input, mention, replacementPath) {
2090
2238
  const mentionText = needsQuotes ? `@"${replacementPath}" ` : `@${replacementPath} `;
2091
2239
  return input.slice(0, mention.start) + mentionText + input.slice(mention.end);
2092
2240
  }
2241
+ async function fileSizeBytes(filePath) {
2242
+ try {
2243
+ return (await stat(filePath)).size;
2244
+ } catch {
2245
+ return 0;
2246
+ }
2247
+ }
2248
+ function withMissingAncestors(entries) {
2249
+ const reported = new Set(entries.filter((entry) => entry.endsWith("/")));
2250
+ const missing = [];
2251
+ for (const entry of entries) {
2252
+ const path = entry.endsWith("/") ? entry.slice(0, -1) : entry;
2253
+ for (let cut = path.lastIndexOf("/"); cut > 0; cut = path.lastIndexOf("/", cut - 1)) {
2254
+ const ancestor = path.slice(0, cut + 1);
2255
+ if (reported.has(ancestor)) break;
2256
+ reported.add(ancestor);
2257
+ missing.push(ancestor);
2258
+ }
2259
+ }
2260
+ return missing.length > 0 ? [...entries, ...missing] : entries;
2261
+ }
2093
2262
  async function getFileMentionCandidates(workspace, query, limit = 50, signal) {
2094
2263
  const normalizedQuery = normalizeMentionPath(query).toLowerCase();
2095
2264
  const gitignore = loadGitignore(workspace).patterns;
2096
- const ignore = [...DEFAULT_IGNORE, ...gitignore];
2265
+ const ignore = globWalkIgnore([...DEFAULT_IGNORE, ...gitignore]);
2097
2266
  let entries;
2098
2267
  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
- });
2268
+ entries = withMissingAncestors(
2269
+ await glob("**/*", {
2270
+ cwd: workspace,
2271
+ dot: true,
2272
+ onlyFiles: false,
2273
+ ignore,
2274
+ // A pattern naming a directory returns that directory, not everything inside it — which is
2275
+ // the whole point of walking for mentions.
2276
+ expandDirectories: false,
2277
+ signal
2278
+ })
2279
+ );
2108
2280
  } catch {
2109
2281
  return [];
2110
2282
  }
@@ -2112,7 +2284,8 @@ async function getFileMentionCandidates(workspace, query, limit = 50, signal) {
2112
2284
  const scored = [];
2113
2285
  for (let index = 0; index < entries.length; index++) {
2114
2286
  const entry = entries[index];
2115
- const display = normalizeMentionPath(entry.path);
2287
+ const isDirectory = entry.endsWith("/");
2288
+ const display = normalizeMentionPath(isDirectory ? entry.slice(0, -1) : entry);
2116
2289
  const lower = display.toLowerCase();
2117
2290
  const base = lower.split("/").pop() ?? lower;
2118
2291
  let score = null;
@@ -2122,11 +2295,10 @@ async function getFileMentionCandidates(workspace, query, limit = 50, signal) {
2122
2295
  else if (base.startsWith(normalizedQuery)) score = 2;
2123
2296
  else if (lower.includes(normalizedQuery)) score = 4;
2124
2297
  if (score !== null) {
2125
- const kind = entry.dirent.isDirectory() ? "directory" : "file";
2126
2298
  scored.push({
2127
- path: kind === "directory" ? `${display}/` : display,
2128
- kind,
2129
- desc: kind === "directory" ? "directory" : `${entry.stats?.size ?? 0} bytes`,
2299
+ path: isDirectory ? `${display}/` : display,
2300
+ kind: isDirectory ? "directory" : "file",
2301
+ desc: "",
2130
2302
  score
2131
2303
  });
2132
2304
  }
@@ -2138,7 +2310,18 @@ async function getFileMentionCandidates(workspace, query, limit = 50, signal) {
2138
2310
  return a.path.localeCompare(b.path);
2139
2311
  });
2140
2312
  throwIfAborted(signal);
2141
- return scored.slice(0, limit).map(({ score: _score, ...item }) => item);
2313
+ const listed = scored.slice(0, limit);
2314
+ const sizes = await Promise.all(
2315
+ listed.map(
2316
+ (candidate) => candidate.kind === "directory" ? 0 : fileSizeBytes(resolve2(workspace, candidate.path))
2317
+ )
2318
+ );
2319
+ throwIfAborted(signal);
2320
+ return listed.map(({ score: _score, path, kind }, index) => ({
2321
+ path,
2322
+ kind,
2323
+ desc: kind === "directory" ? "directory" : `${sizes[index]} bytes`
2324
+ }));
2142
2325
  }
2143
2326
 
2144
2327
  // src/session/lifecycle.ts
@@ -2154,7 +2337,14 @@ async function runSessionEnd(config, sessionId, reason, options) {
2154
2337
  await runHooks(
2155
2338
  config.settings.hooks.SessionEnd,
2156
2339
  "SessionEnd",
2157
- { workspace: config.workspace, event: "SessionEnd", sessionId, reason },
2340
+ {
2341
+ workspace: config.workspace,
2342
+ event: "SessionEnd",
2343
+ sessionId,
2344
+ reason,
2345
+ status: options?.endOutcome?.status,
2346
+ stopReason: options?.endOutcome?.reason
2347
+ },
2158
2348
  options
2159
2349
  );
2160
2350
  }
@@ -2173,11 +2363,76 @@ function splitTrailingPunctuation(token) {
2173
2363
  while (end > 0 && /[.,;:!?)]/.test(token[end - 1])) end--;
2174
2364
  return { path: token.slice(0, end), trailing: token.slice(end) };
2175
2365
  }
2176
- function findMentionTokens(input) {
2366
+ function codeRanges(input) {
2367
+ const fenced = fencedCodeRanges(input);
2368
+ const inline = [];
2369
+ let cursor = 0;
2370
+ for (const [start, end] of fenced) {
2371
+ collectParagraphInlineCodeSpans(input.slice(cursor, start), cursor, inline);
2372
+ cursor = end;
2373
+ }
2374
+ collectParagraphInlineCodeSpans(input.slice(cursor), cursor, inline);
2375
+ return [...fenced, ...inline];
2376
+ }
2377
+ function collectParagraphInlineCodeSpans(segment, base, ranges) {
2378
+ const blankLine = /\n[ \t]*\r?\n/g;
2379
+ let start = 0;
2380
+ let separator;
2381
+ while ((separator = blankLine.exec(segment)) !== null) {
2382
+ collectInlineCodeSpans(segment.slice(start, separator.index), base + start, ranges);
2383
+ start = separator.index + separator[0].length;
2384
+ }
2385
+ collectInlineCodeSpans(segment.slice(start), base + start, ranges);
2386
+ }
2387
+ function fencedCodeRanges(input) {
2388
+ const ranges = [];
2389
+ let fence = null;
2390
+ let offset = 0;
2391
+ while (offset < input.length) {
2392
+ const newline = input.indexOf("\n", offset);
2393
+ const lineEnd = newline === -1 ? input.length : newline;
2394
+ const rangeEnd = newline === -1 ? input.length : newline + 1;
2395
+ const line = input.slice(offset, lineEnd).replace(/\r$/, "");
2396
+ if (fence) {
2397
+ const close = /^ {0,3}(`{3,}|~{3,})[ \t]*$/.exec(line);
2398
+ if (close && close[1][0] === fence.char && close[1].length >= fence.length) {
2399
+ ranges.push([fence.start, rangeEnd]);
2400
+ fence = null;
2401
+ }
2402
+ } else {
2403
+ const open = /^ {0,3}(?:(`{3,})(?!.*`)|(~{3,}))/.exec(line);
2404
+ if (open) {
2405
+ const run = open[1] ?? open[2];
2406
+ fence = { start: offset, char: run[0], length: run.length };
2407
+ }
2408
+ }
2409
+ offset = rangeEnd;
2410
+ }
2411
+ if (fence) ranges.push([fence.start, input.length]);
2412
+ return ranges;
2413
+ }
2414
+ function collectInlineCodeSpans(text, base, ranges) {
2415
+ const runs = [...text.matchAll(/`+/g)];
2416
+ for (let i = 0; i < runs.length; i++) {
2417
+ const length = runs[i][0].length;
2418
+ for (let j = i + 1; j < runs.length; j++) {
2419
+ if (runs[j][0].length !== length) continue;
2420
+ ranges.push([base + (runs[i].index ?? 0), base + (runs[j].index ?? 0) + length]);
2421
+ i = j;
2422
+ break;
2423
+ }
2424
+ }
2425
+ }
2426
+ function isInsideRanges(ranges, index) {
2427
+ return ranges.some(([start, end]) => index >= start && index < end);
2428
+ }
2429
+ function findMentionTokens(input, excluded = []) {
2430
+ if (!input.includes("@")) return [];
2177
2431
  const tokens = [];
2432
+ const code = codeRanges(input);
2178
2433
  let i = 0;
2179
2434
  while (i < input.length) {
2180
- if (input[i] !== "@" || !isMentionBoundary2(input, i)) {
2435
+ if (input[i] !== "@" || !isMentionBoundary2(input, i) || isInsideRanges(code, i) || isInsideRanges(excluded, i)) {
2181
2436
  i++;
2182
2437
  continue;
2183
2438
  }
@@ -2235,12 +2490,12 @@ function looksBinary(content) {
2235
2490
  }
2236
2491
  function expandMention(filePath, workspace) {
2237
2492
  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");
2493
+ if (!resolved) return null;
2494
+ if (!existsSync2(resolved.filePath)) return null;
2240
2495
  try {
2241
- const stat = statSync2(resolved.filePath);
2242
- if (stat.isDirectory()) return formatMentionError(resolved.relativePath, "path is a directory");
2243
- if (!stat.isFile())
2496
+ const stat2 = statSync2(resolved.filePath);
2497
+ if (stat2.isDirectory()) return null;
2498
+ if (!stat2.isFile())
2244
2499
  return formatMentionError(resolved.relativePath, "path is not a regular file");
2245
2500
  const content = readFileSync2(resolved.filePath, "utf-8");
2246
2501
  if (looksBinary(content))
@@ -2262,24 +2517,14 @@ ${body}${suffix}
2262
2517
  return formatMentionError(filePath, message.slice(0, 200) || "unable to read file");
2263
2518
  }
2264
2519
  }
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;
2520
+ function mentionTokenRanges(input) {
2521
+ return findMentionTokens(input).map((token) => [token.start, token.end]);
2278
2522
  }
2279
- function collectAtMentionObservations(input, workspace, sourceRef) {
2523
+ function collectAtMentionObservations(input, workspace, sourceRef, expandShellInput = false) {
2280
2524
  const workspaceId = workspaceIdentity(workspace);
2281
2525
  const observations = [];
2282
- for (const token of findMentionTokens(input)) {
2526
+ const shellLines = expandShellInput ? findShellLines(input) : [];
2527
+ for (const token of findMentionTokens(input, shellLineRanges(shellLines))) {
2283
2528
  const resolved = resolveWorkspaceMentionPath(workspace, token.path);
2284
2529
  if (!resolved || !existsSync2(resolved.filePath)) continue;
2285
2530
  try {
@@ -2300,17 +2545,38 @@ function collectAtMentionObservations(input, workspace, sourceRef) {
2300
2545
  }
2301
2546
  return observations;
2302
2547
  }
2303
- async function expandShellCommands(input, workspace, signal) {
2304
- const matches = [...input.matchAll(/^!(\S.*)$/gm)];
2305
- if (matches.length === 0) return input;
2548
+ function findShellLines(input) {
2549
+ const fenced = fencedCodeRanges(input);
2550
+ return [...input.matchAll(/^!(\S.*)$/gm)].filter((match) => !isInsideRanges(fenced, match.index ?? 0)).map((match) => {
2551
+ const start = match.index ?? 0;
2552
+ return { start, end: start + match[0].length, command: match[1] };
2553
+ });
2554
+ }
2555
+ function shellLineRanges(lines) {
2556
+ return lines.map((line) => [line.start, line.end]);
2557
+ }
2558
+ async function expandUserInput(input, workspace, options = {}) {
2559
+ const shellLines = options.expandShell ? findShellLines(input) : [];
2560
+ const tokens = findMentionTokens(input, shellLineRanges(shellLines));
2561
+ if (shellLines.length === 0 && tokens.length === 0) return input;
2562
+ const edits = [
2563
+ ...shellLines.map((line) => ({
2564
+ start: line.start,
2565
+ end: line.end,
2566
+ apply: () => executeShellExpansion(line.command, workspace, options.signal)
2567
+ })),
2568
+ ...tokens.map((token) => ({
2569
+ start: token.start,
2570
+ end: token.end,
2571
+ apply: () => expandMention(token.path, workspace) ?? token.raw
2572
+ }))
2573
+ ].sort((left, right) => left.start - right.start);
2306
2574
  let output = "";
2307
2575
  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;
2576
+ for (const edit of edits) {
2577
+ output += input.slice(cursor, edit.start);
2578
+ output += await edit.apply();
2579
+ cursor = edit.end;
2314
2580
  }
2315
2581
  return output + input.slice(cursor);
2316
2582
  }
@@ -2321,6 +2587,9 @@ function executeShellExpansion(command, workspace, signal) {
2321
2587
  {
2322
2588
  cwd: workspace,
2323
2589
  encoding: "utf8",
2590
+ // The `!command` the user typed is theirs, so it must not inherit the NODE_ENV Book
2591
+ // defaulted for its own renderer.
2592
+ env: buildChildEnv(),
2324
2593
  timeout: 1e4,
2325
2594
  maxBuffer: 1024 * 1024,
2326
2595
  signal,
@@ -2411,7 +2680,7 @@ var AgentInteractionController = class {
2411
2680
  this.pendingPermission = null;
2412
2681
  this.pendingPermissionQueue = [];
2413
2682
  this.publish();
2414
- for (const request of pending) request.resolve("deny");
2683
+ for (const request of pending) request.resolve({ result: "deny", reason: "dismissed" });
2415
2684
  log2.event("permission:cancelled-all", { via, count: pending.length });
2416
2685
  return true;
2417
2686
  }
@@ -2594,21 +2863,13 @@ var AgentSessionOperations = class {
2594
2863
  };
2595
2864
 
2596
2865
  // 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
- };
2606
- }
2866
+ var log3 = createDebugLogger("session:usage");
2607
2867
  var AgentSession = class {
2608
2868
  interactions = new AgentInteractionController();
2609
2869
  operations = new AgentSessionOperations();
2610
2870
  runLoop;
2611
2871
  compactRunner;
2872
+ judgeRunner;
2612
2873
  postCompactHooksRunner;
2613
2874
  sessionStartRunner;
2614
2875
  sessionEndRunner;
@@ -2616,12 +2877,27 @@ var AgentSession = class {
2616
2877
  listeners = /* @__PURE__ */ new Set();
2617
2878
  runGeneration = 0;
2618
2879
  lifecycleStartedSessionId;
2880
+ lifecycleStart;
2619
2881
  lifecycleEndedSessionId;
2882
+ lifecycleEnd;
2620
2883
  runtime;
2621
2884
  registryFactory;
2885
+ /**
2886
+ * Every root this session has run, and where its spend is written (#336).
2887
+ *
2888
+ * `onUsage` persists what a run's own responses report, which leaves two
2889
+ * shapes of spend with nowhere to go: whatever a managed child spent after the
2890
+ * root's last response, and a root whose model was never called at all — a
2891
+ * `/review` the host performed. Neither reaches a store on the way out, so the
2892
+ * process ends holding tokens no later process restores, and `--max-budget-usd`
2893
+ * re-authorises them. Registered by `run()` and by hosts that create a root
2894
+ * themselves (`trackRunUsage`), and drained by `dispose`.
2895
+ */
2896
+ usageTargets = /* @__PURE__ */ new Map();
2622
2897
  constructor(dependencies = {}) {
2623
2898
  this.runLoop = dependencies.runLoop ?? runAgentLoop;
2624
2899
  this.compactRunner = dependencies.compactRunner ?? runCompact;
2900
+ this.judgeRunner = dependencies.judgeRunner ?? judgeCompaction;
2625
2901
  this.postCompactHooksRunner = dependencies.postCompactHooksRunner ?? runPostCompactHooks;
2626
2902
  this.sessionStartRunner = dependencies.sessionStartRunner ?? runSessionStart;
2627
2903
  this.sessionEndRunner = dependencies.sessionEndRunner ?? runSessionEnd;
@@ -2766,8 +3042,20 @@ var AgentSession = class {
2766
3042
  getRuntime() {
2767
3043
  return this.runtime;
2768
3044
  }
3045
+ /**
3046
+ * Stop the outgoing runtime, write what it spent, and install its replacement.
3047
+ *
3048
+ * The one place a runtime goes away, so it is where its spend is written: the
3049
+ * children are stopped first, so a managed agent charged during the teardown is
3050
+ * in the figures the flush reads, and its targets are released with it rather
3051
+ * than walked by every later sweep. A `/resume` or `/rewind` that reached the
3052
+ * replacement without this left the outgoing runtime's unpersisted spend behind
3053
+ * with it, and the resumed session restored a carry short of what it had cost.
3054
+ */
2769
3055
  replaceRuntime(options = {}, via = "session_transition") {
2770
- this.runtime.dispose(via);
3056
+ const outgoing = this.runtime;
3057
+ outgoing.dispose(via);
3058
+ this.releaseRunUsage(outgoing);
2771
3059
  this.runtime = new SessionRuntime(options);
2772
3060
  return this.runtime;
2773
3061
  }
@@ -2793,16 +3081,165 @@ var AgentSession = class {
2793
3081
  this.operations.reset({ bookTerminalReason: "session_disposed" });
2794
3082
  this.runGeneration++;
2795
3083
  this.runtime.dispose(via);
3084
+ this.releaseRunUsage(this.runtime);
3085
+ }
3086
+ /**
3087
+ * Record that `rootRunId`'s spend belongs in `target`, so the flush at the end
3088
+ * of the session can persist what no response of that root reported.
3089
+ *
3090
+ * `run()` registers every root it runs. A host that starts a root itself and
3091
+ * never sends a turn — a print-mode `/review`, whose reviewer agents spend
3092
+ * under a root the root model is never asked for — registers it here, or its
3093
+ * spend leaves the process with it. Re-registering a root replaces the target,
3094
+ * so a run under an existing root keeps writing to where that root writes.
3095
+ */
3096
+ trackRunUsage(rootRunId, target) {
3097
+ this.usageTargets.set(rootRunId, { target, runtime: this.runtime });
3098
+ }
3099
+ /**
3100
+ * Register a compaction's root the way `run` registers a turn's.
3101
+ *
3102
+ * A compactor's model calls are charged to a run context, and no turn of that
3103
+ * root may ever run — a manual `/compact` mints a root of its own, and a send
3104
+ * cancelled between the host's auto-compact in `beforePrepare` and `run()`
3105
+ * never reaches `run`'s own registration. Without this, that spend is charged
3106
+ * to a root no flush knows about, and leaves the process unwritten.
3107
+ */
3108
+ registerUsageTarget(request, runtime) {
3109
+ if (!request.runContext || !request.timelineStore || !request.sessionId) return;
3110
+ this.usageTargets.set(request.runContext.rootRunId, {
3111
+ target: { sessionId: request.sessionId, timelineStore: request.timelineStore },
3112
+ runtime
3113
+ });
3114
+ }
3115
+ /**
3116
+ * Write what a root has spent and no `usage` record covers, as one record
3117
+ * named after the response the host is being told about.
3118
+ *
3119
+ * The `onUsage` writer, and the reason it is one function rather than a
3120
+ * closure: every host reaches it the same way, so the watermark it moves and
3121
+ * the ones `flushRunUsage` moves are the same figure, written once. Cost is
3122
+ * not stored — pricing changes between processes, so it is re-derived from the
3123
+ * tokens at bootstrap, deliberately at the most expensive model involved.
3124
+ *
3125
+ * The watermark moves only where a record really lands. A turn that reaches no
3126
+ * store — a session that moved on, a run with nowhere to write — leaves its
3127
+ * spend unpersisted instead, for the next writer under this root to record;
3128
+ * counted as written, no record would ever hold it and no restart would
3129
+ * restore it.
3130
+ */
3131
+ persistReportedUsage(rootRunId, reported, metadata) {
3132
+ const entry = this.usageTargets.get(rootRunId);
3133
+ if (!entry) return;
3134
+ const { target, runtime } = entry;
3135
+ const recordUsage = runtime.runAccounting.peekUnpersistedUsage(rootRunId) ?? reported;
3136
+ if (isSpendlessUsage(recordUsage)) return;
3137
+ const store = target.isCurrent?.() === false ? void 0 : target.timelineStore;
3138
+ if (!store) return;
3139
+ store.append(target.sessionId, {
3140
+ type: "usage",
3141
+ timestamp: Date.now(),
3142
+ data: {
3143
+ version: 1,
3144
+ usage: recordUsage,
3145
+ // The response that triggered this write, which is not the same thing as
3146
+ // the models the delta covers: a dearer child may have finished before it.
3147
+ requestedModel: metadata?.requestedModel,
3148
+ responseModel: metadata?.responseModel,
3149
+ models: runtime.runAccounting.modelsFor(rootRunId)
3150
+ }
3151
+ });
3152
+ runtime.runAccounting.commitPersistedUsage(rootRunId);
3153
+ }
3154
+ /**
3155
+ * Write what a root has spent and no `usage` record covers, if it can be
3156
+ * written at all.
3157
+ *
3158
+ * One record for the whole delta, so what this writes and what it then commits
3159
+ * are the same figure by construction. It is named after the dearest model the
3160
+ * root has spent on rather than after any one of them: `carriedCostUsd`
3161
+ * (`headless.ts`) prices a restored carry at the most expensive model in
3162
+ * `carriedModels` regardless of which record said what, and the records do not
3163
+ * attribute tokens to models, so one name is all the restored pool can be said
3164
+ * to have cost.
3165
+ *
3166
+ * `requireCurrent` is the end of a run, where the target's own currency still
3167
+ * decides whether it is the session to write to. The end-of-session sweep does
3168
+ * not pass it: by then every operation lease has been released, so consulting
3169
+ * one would skip every target the TUI ever registered — which is where a
3170
+ * background agent's late spend goes.
3171
+ *
3172
+ * A store that throws is contained here. This runs from a run's `finally` and
3173
+ * from the runtime swap, so an escaping filesystem error would replace a
3174
+ * completed run's outcome, abort the sweep part-way, and leave the session
3175
+ * installed on a disposed runtime. Nothing was written, so the watermark stays
3176
+ * where it was and the spend remains owed to the next writer that can.
3177
+ */
3178
+ flushRunUsage(rootRunId, requireCurrent) {
3179
+ const entry = this.usageTargets.get(rootRunId);
3180
+ if (!entry) return;
3181
+ const { target, runtime } = entry;
3182
+ const recordUsage = runtime.runAccounting.peekUnpersistedUsage(rootRunId);
3183
+ if (!recordUsage || isSpendlessUsage(recordUsage)) return;
3184
+ const store = requireCurrent && target.isCurrent?.() === false ? void 0 : target.timelineStore;
3185
+ if (!store) return;
3186
+ try {
3187
+ store.append(target.sessionId, {
3188
+ type: "usage",
3189
+ timestamp: Date.now(),
3190
+ data: {
3191
+ version: 1,
3192
+ usage: recordUsage,
3193
+ requestedModel: runtime.runAccounting.dearestModel(rootRunId),
3194
+ models: runtime.runAccounting.modelsFor(rootRunId)
3195
+ }
3196
+ });
3197
+ runtime.runAccounting.commitPersistedUsage(rootRunId);
3198
+ } catch (error) {
3199
+ log3.warn("usage record could not be appended; spend left unpersisted", {
3200
+ rootRunId,
3201
+ sessionId: target.sessionId,
3202
+ error: error instanceof Error ? error.message : String(error)
3203
+ });
3204
+ }
3205
+ }
3206
+ /**
3207
+ * Write what the roots of a runtime that is going away spent, and drop their
3208
+ * targets: nothing can be charged to a root whose runtime is gone, so keeping
3209
+ * its target would only make every later sweep walk it.
3210
+ *
3211
+ * Whatever lease a target carries is ignored — a `send()` lease has been
3212
+ * released by the time any of these runs, so consulting one would skip every
3213
+ * target the TUI ever made.
3214
+ */
3215
+ releaseRunUsage(runtime) {
3216
+ for (const [rootRunId, entry] of [...this.usageTargets.entries()]) {
3217
+ if (entry.runtime !== runtime) continue;
3218
+ this.flushRunUsage(rootRunId, false);
3219
+ this.usageTargets.delete(rootRunId);
3220
+ }
2796
3221
  }
2797
3222
  async startLifecycle(config, sessionId, source, options) {
2798
- if (this.lifecycleStartedSessionId === sessionId) return;
3223
+ if (this.lifecycleStartedSessionId === sessionId) {
3224
+ await this.lifecycleStart?.catch(() => void 0);
3225
+ return;
3226
+ }
2799
3227
  this.lifecycleStartedSessionId = sessionId;
2800
- await this.sessionStartRunner(config, sessionId, source, options);
3228
+ this.lifecycleStart = this.sessionStartRunner(config, sessionId, source, options);
3229
+ await this.lifecycleStart;
2801
3230
  }
3231
+ // A second call for a session that is already ending waits for the SessionEnd in flight
3232
+ // instead of returning at once, which let a second exit or a `/clear` racing an exit move on
3233
+ // while the hooks were still running. The first caller reports a failed SessionEnd; a caller that
3234
+ // only waited for it does not report the same failure a second time.
2802
3235
  async endLifecycle(config, sessionId, reason, options) {
2803
- if (this.lifecycleEndedSessionId === sessionId) return;
3236
+ if (this.lifecycleEndedSessionId === sessionId) {
3237
+ await this.lifecycleEnd?.catch(() => void 0);
3238
+ return;
3239
+ }
2804
3240
  this.lifecycleEndedSessionId = sessionId;
2805
- await this.sessionEndRunner(config, sessionId, reason, options);
3241
+ this.lifecycleEnd = this.sessionEndRunner(config, sessionId, reason, options);
3242
+ await this.lifecycleEnd;
2806
3243
  }
2807
3244
  async clearSession(request) {
2808
3245
  request.onTransitionStart?.();
@@ -2811,9 +3248,19 @@ var AgentSession = class {
2811
3248
  if (request.previousName && request.store) {
2812
3249
  request.store.patchMeta(request.currentSessionId, { name: request.previousName });
2813
3250
  }
2814
- const sessionId = request.store ? request.store.create({ cwd: request.config.workspace }) : request.timelineStore?.create({ cwd: request.config.workspace }) ?? crypto.randomUUID();
3251
+ const sessionId = request.store ? request.store.create({
3252
+ cwd: request.config.workspace,
3253
+ driver: request.config.sessionDriver
3254
+ }) : request.timelineStore?.create({
3255
+ cwd: request.config.workspace,
3256
+ driver: request.config.sessionDriver
3257
+ }) ?? crypto.randomUUID();
2815
3258
  if (request.store && request.timelineStore && request.timelineStore !== request.store) {
2816
- request.timelineStore.create({ id: sessionId, cwd: request.config.workspace });
3259
+ request.timelineStore.create({
3260
+ id: sessionId,
3261
+ cwd: request.config.workspace,
3262
+ driver: request.config.sessionDriver
3263
+ });
2817
3264
  }
2818
3265
  const bootstrap = emptySessionBootstrap(
2819
3266
  sessionId,
@@ -2839,6 +3286,12 @@ var AgentSession = class {
2839
3286
  this.operations.cancel({ bookTerminalReason: "session_replaced" });
2840
3287
  await this.endLifecycle(request.config, request.currentSessionId, "resume");
2841
3288
  request.store.touch(selected.id);
3289
+ recordResumeDriver(
3290
+ request.store,
3291
+ selected.id,
3292
+ loaded.meta.driver,
3293
+ request.config.sessionDriver
3294
+ );
2842
3295
  const bootstrap = {
2843
3296
  sessionId: selected.id,
2844
3297
  sessionName: loaded.meta.name,
@@ -2857,6 +3310,7 @@ var AgentSession = class {
2857
3310
  created: false
2858
3311
  };
2859
3312
  this.reset("session-resume");
3313
+ this.replaceRuntime({ history: bootstrap.transcript ?? bootstrap.history }, "session-resume");
2860
3314
  request.onTransition?.(bootstrap);
2861
3315
  await this.startLifecycle(request.config, selected.id, "resume");
2862
3316
  return { status: "transitioned", bootstrap };
@@ -2903,13 +3357,17 @@ var AgentSession = class {
2903
3357
  return { status: "prepared", contextMessage, rewindTarget, sessionName };
2904
3358
  }
2905
3359
  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);
3360
+ const expandShellInput = request.contextMessage === void 0 && request.expandShellInput !== false;
3361
+ const contextMessage = request.contextMessage !== void 0 ? request.contextMessage : await expandUserInput(request.displayMessage, request.config.workspace, {
3362
+ expandShell: expandShellInput,
3363
+ signal: request.signal
3364
+ });
2908
3365
  request.userMessage.contextContent = contextMessage === request.displayMessage ? void 0 : contextMessage;
2909
3366
  request.userMessage.fileObservations = request.contextMessage === void 0 ? collectAtMentionObservations(
2910
3367
  request.displayMessage,
2911
3368
  request.config.workspace,
2912
- request.userMessage.id
3369
+ request.userMessage.id,
3370
+ expandShellInput
2913
3371
  ) : [];
2914
3372
  const observationLedger = (request.runtime ?? this.runtime).fileObservationLedger;
2915
3373
  for (const observation of request.userMessage.fileObservations) {
@@ -2943,29 +3401,125 @@ var AgentSession = class {
2943
3401
  async compact(request) {
2944
3402
  const runtime = request.runtime ?? this.runtime;
2945
3403
  if (request.runContext) runtime.runAccounting.startRoot(request.runContext);
3404
+ this.registerUsageTarget(request, runtime);
2946
3405
  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
3406
+ ...this.accountedOptions(request.options, request.runContext, runtime, request.onUsage),
3407
+ sessionId: request.sessionId
3408
+ });
3409
+ if (result.status !== "compacted") return { result };
3410
+ if (request.isCurrent?.() === false) return { result };
3411
+ return this.commitCompactResult(result, request);
3412
+ }
3413
+ /**
3414
+ * Deferred compaction (`plans/async-compaction-plan.md`): the reducer half of
3415
+ * `compact`, on a snapshot, without the record, the boundary or the
3416
+ * PostCompact hooks. PreCompact hooks still run -- they are the refusal
3417
+ * point, and the suspect-input scan they see is the snapshot's.
3418
+ */
3419
+ async prepareCompact(request) {
3420
+ const runtime = request.runtime ?? this.runtime;
3421
+ if (request.runContext) runtime.runAccounting.startRoot(request.runContext);
3422
+ this.registerUsageTarget(request, runtime);
3423
+ const result = await this.compactRunner(request.config, request.history, {
3424
+ ...this.accountedOptions(request.options, request.runContext, runtime, request.onUsage),
3425
+ sessionId: request.sessionId
2966
3426
  });
3427
+ if (result.status !== "compacted") return { status: result.status, result };
3428
+ return {
3429
+ status: "prepared",
3430
+ prepared: {
3431
+ snapshot: [...request.history],
3432
+ result,
3433
+ trigger: request.options.trigger,
3434
+ preContextTokens: request.options.preContextTokens
3435
+ }
3436
+ };
3437
+ }
3438
+ /**
3439
+ * The other half: apply a prepared compaction to the history as it stands,
3440
+ * ask the judge whether the checkpoint holds what the steps taken meanwhile
3441
+ * relied on, and only then write the record and run the hooks. A rejected
3442
+ * or inapplicable checkpoint is dropped, and the caller falls back to a
3443
+ * synchronous compaction.
3444
+ */
3445
+ async commitCompact(request) {
3446
+ const runtime = request.runtime ?? this.runtime;
3447
+ const { prepared } = request;
3448
+ const applied = applyCompactResult(prepared.result, prepared.snapshot, request.history, {
3449
+ toolResultMaxTokens: resolveCompactBudgets(request.config).retainedToolResultMaxTokens
3450
+ });
3451
+ if (!applied) {
3452
+ return {
3453
+ result: {
3454
+ status: "skipped",
3455
+ reason: "not-applicable",
3456
+ message: "The history no longer extends the snapshot the reducer read."
3457
+ }
3458
+ };
3459
+ }
3460
+ const snapshotIds = new Set(prepared.snapshot.map((message) => message.id));
3461
+ const delta = request.history.filter((message) => !snapshotIds.has(message.id));
3462
+ if (request.runContext) runtime.runAccounting.startRoot(request.runContext);
3463
+ this.registerUsageTarget(request, runtime);
3464
+ const accounted = this.accountedOptions(
3465
+ { ...request.options, trigger: prepared.trigger },
3466
+ request.runContext,
3467
+ runtime,
3468
+ request.onUsage
3469
+ );
3470
+ const judge = await this.judgeRunner(request.config, applied, delta, {
3471
+ signal: accounted.signal,
3472
+ provider: accounted.provider,
3473
+ beforeModelCall: accounted.beforeModelCall,
3474
+ onUsage: accounted.onUsage,
3475
+ onUsageMissing: accounted.onUsageMissing
3476
+ });
3477
+ if (accounted.signal?.aborted || judge.note === "aborted") {
3478
+ return { result: { status: "failed", reason: "aborted", error: "Compaction aborted." } };
3479
+ }
3480
+ const result = judgedResult(applied, judge);
2967
3481
  if (result.status !== "compacted") return { result };
2968
3482
  if (request.isCurrent?.() === false) return { result };
3483
+ return this.commitCompactResult(result, {
3484
+ config: request.config,
3485
+ history: request.history,
3486
+ sessionId: request.sessionId,
3487
+ transcriptOrdinal: request.transcriptOrdinal,
3488
+ options: { ...request.options, trigger: prepared.trigger },
3489
+ runContext: request.runContext,
3490
+ runtime,
3491
+ timelineStore: request.timelineStore,
3492
+ isCurrent: request.isCurrent,
3493
+ onCommitted: request.onCommitted
3494
+ });
3495
+ }
3496
+ /**
3497
+ * The compactor's model calls charged to the run, the way `compact` has always charged
3498
+ * them, and reported to the host's own session bill when it keeps one (#370).
3499
+ */
3500
+ accountedOptions(options, runContext, runtime, onUsage) {
3501
+ return {
3502
+ ...options,
3503
+ beforeModelCall: runContext ? (model) => {
3504
+ const requestCheck = options.beforeModelCall?.(model);
3505
+ if (requestCheck && !requestCheck.allowed) return requestCheck;
3506
+ return runtime.runAccounting.checkBeforeModelCall(runContext.rootRunId, model);
3507
+ } : options.beforeModelCall,
3508
+ // Left exactly as it was when there is nothing to chain: a compaction with
3509
+ // no run context and no host callback hands the runner its own options back.
3510
+ onUsage: runContext || onUsage ? (usage, metadata) => {
3511
+ if (runContext) runtime.runAccounting.record(runContext, usage, metadata);
3512
+ options.onUsage?.(usage, metadata);
3513
+ onUsage?.(usage);
3514
+ } : options.onUsage,
3515
+ onUsageMissing: runContext ? (metadata) => {
3516
+ runtime.runAccounting.markUsageUnknown(runContext, metadata, "compaction_usage");
3517
+ options.onUsageMissing?.(metadata);
3518
+ } : options.onUsageMissing
3519
+ };
3520
+ }
3521
+ /** The record, the boundary, `onCommitted` and the PostCompact hooks for a compacted result. */
3522
+ async commitCompactResult(result, request) {
2969
3523
  const timestamp = Date.now();
2970
3524
  const boundary = {
2971
3525
  id: result.compactId,
@@ -2977,7 +3531,8 @@ var AgentSession = class {
2977
3531
  postContextTokens: result.postContextTokens,
2978
3532
  generation: result.generation,
2979
3533
  checkpointVersion: 2,
2980
- timestamp
3534
+ timestamp,
3535
+ carriedCount: result.carriedCount
2981
3536
  };
2982
3537
  const data = {
2983
3538
  version: 2,
@@ -2994,10 +3549,12 @@ var AgentSession = class {
2994
3549
  throughEventRef: result.throughEventRef,
2995
3550
  summarizedCount: result.summarizedCount,
2996
3551
  retainedCount: result.retainedCount,
3552
+ carriedCount: result.carriedCount,
2997
3553
  strategy: result.strategy,
2998
3554
  modelCalls: result.modelCalls,
2999
3555
  degraded: result.degraded,
3000
- warning: result.warning
3556
+ warning: result.warning,
3557
+ ...result.judge ? { judge: result.judge } : {}
3001
3558
  };
3002
3559
  if (request.timelineStore && request.sessionId) {
3003
3560
  request.timelineStore.append(request.sessionId, {
@@ -3012,8 +3569,7 @@ var AgentSession = class {
3012
3569
  trigger: result.trigger,
3013
3570
  sessionId: request.sessionId,
3014
3571
  focus: request.options.focus,
3015
- onHookEvent: request.options.onHookEvent,
3016
- signal: request.options.signal
3572
+ onHookEvent: request.options.onHookEvent
3017
3573
  });
3018
3574
  return { result, boundary };
3019
3575
  }
@@ -3049,6 +3605,14 @@ var AgentSession = class {
3049
3605
  allowedTools: request.options?.allowedTools
3050
3606
  })
3051
3607
  );
3608
+ this.usageTargets.set(runContext.rootRunId, {
3609
+ target: {
3610
+ sessionId: request.sessionId,
3611
+ timelineStore: request.timelineStore,
3612
+ isCurrent: request.isCurrent
3613
+ },
3614
+ runtime
3615
+ });
3052
3616
  const finalizeOutcome = (outcome) => {
3053
3617
  if (!terminalOutcome) {
3054
3618
  terminalOutcome = outcome;
@@ -3066,13 +3630,6 @@ var AgentSession = class {
3066
3630
  emit({ type: "session", sessionId: request.sessionId });
3067
3631
  const unsubscribeShellEvents = runtime.shellManager.subscribe((event) => emit(event));
3068
3632
  try {
3069
- let persistedUsage = {
3070
- promptTokens: 0,
3071
- completionTokens: 0,
3072
- totalTokens: 0,
3073
- cacheReadInputTokens: 0,
3074
- cacheCreationInputTokens: 0
3075
- };
3076
3633
  let lastPlanSignature = "";
3077
3634
  const persistPlan = () => {
3078
3635
  if (!request.timelineStore) return;
@@ -3106,6 +3663,7 @@ var AgentSession = class {
3106
3663
  },
3107
3664
  onToolCall: (toolCall) => emit({ type: "tool_use", toolCall }),
3108
3665
  onToolResult: (toolResult) => emit({ type: "tool_result", toolResult }),
3666
+ onNotice: (message) => emit({ type: "notice", message }),
3109
3667
  onError: (error) => {
3110
3668
  emittedError = error;
3111
3669
  emit({ type: "error", error });
@@ -3117,7 +3675,9 @@ var AgentSession = class {
3117
3675
  finalizeOutcome(outcome2);
3118
3676
  },
3119
3677
  onPermissionRequired: (toolCall) => {
3120
- if (request.isCurrent?.() === false) return Promise.resolve("deny");
3678
+ if (request.isCurrent?.() === false) {
3679
+ return Promise.resolve({ result: "deny", reason: "dismissed" });
3680
+ }
3121
3681
  return callbacks.onPermissionRequired ? callbacks.onPermissionRequired(toolCall) : this.interactions.requestPermission(toolCall);
3122
3682
  },
3123
3683
  onPlanApprovalRequired: (plan) => {
@@ -3139,32 +3699,21 @@ var AgentSession = class {
3139
3699
  },
3140
3700
  onUsage: (nextUsage, metadata) => {
3141
3701
  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
- }
3702
+ this.persistReportedUsage(runContext.rootRunId, nextUsage, metadata);
3158
3703
  callbacks.onUsage?.(nextUsage, metadata);
3159
3704
  },
3160
3705
  getMode: callbacks.getMode,
3161
3706
  onModeChange: callbacks.onModeChange,
3707
+ // The root run's only: a managed child and a Task subagent run the loop
3708
+ // with their own callbacks, and neither announces calls to a stream the
3709
+ // host is reading for them.
3710
+ beforeToolExecution: callbacks.beforeToolExecution,
3162
3711
  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.
3712
+ // Passed through as is: the loop gates every compaction site on
3713
+ // `autoCompactEnabled`, the context-overflow recovery included.
3167
3714
  onCompact: callbacks.onCompact,
3715
+ prepareCompact: callbacks.prepareCompact,
3716
+ commitCompact: callbacks.commitCompact,
3168
3717
  onAssistantMessageComplete: (message) => {
3169
3718
  if (request.isCurrent?.() === false) return;
3170
3719
  request.timelineStore?.append(request.sessionId, {
@@ -3180,7 +3729,10 @@ var AgentSession = class {
3180
3729
  kind: message.kind ?? "conversation",
3181
3730
  toolCalls: message.toolCalls,
3182
3731
  toolResults: message.toolResults,
3183
- fileObservations: message.fileObservations
3732
+ fileObservations: message.fileObservations,
3733
+ // Without this a resumed session reads a host notice as the model's
3734
+ // own reply: `finalAnswerText` and the answer walk both key on it.
3735
+ hostNotice: message.hostNotice
3184
3736
  }
3185
3737
  });
3186
3738
  callbacks.onAssistantMessageComplete?.(message);
@@ -3213,7 +3765,10 @@ var AgentSession = class {
3213
3765
  id: message.id,
3214
3766
  content: message.content,
3215
3767
  kind: message.kind ?? "conversation",
3216
- includeInContext: message.includeInContext ?? true
3768
+ includeInContext: message.includeInContext ?? true,
3769
+ // A host-appended prompt stays host-written across a reload: the ledger,
3770
+ // Carried Turns and memory extraction all key on it.
3771
+ derivedContent: message.derivedContent
3217
3772
  }
3218
3773
  });
3219
3774
  },
@@ -3224,21 +3779,26 @@ var AgentSession = class {
3224
3779
  onHookEvent: callbacks.onHookEvent,
3225
3780
  onAgentEvent: (event) => emit(event)
3226
3781
  };
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
- );
3782
+ let messages;
3783
+ try {
3784
+ messages = await this.runLoop(
3785
+ request.config,
3786
+ request.registry,
3787
+ request.prompt,
3788
+ effectiveHistory,
3789
+ baseLoopCallbacks,
3790
+ request.mode,
3791
+ {
3792
+ ...request.options,
3793
+ runtime,
3794
+ runContext,
3795
+ resolveAttachment: request.options?.resolveAttachment ?? (request.timelineStore?.readImageAttachment ? (attachment) => request.timelineStore.readImageAttachment(request.sessionId, attachment) : void 0),
3796
+ signal: request.signal
3797
+ }
3798
+ );
3799
+ } finally {
3800
+ this.flushRunUsage(runContext.rootRunId, true);
3801
+ }
3242
3802
  const partialOutput = streamedAssistantText.length > 0 || messages.slice(effectiveHistory.length).some((message) => message.role === "assistant");
3243
3803
  const outcome = finalizeOutcome(
3244
3804
  terminalOutcome ?? (request.signal?.aborted ? classifyAbortReason(request.signal.reason, partialOutput) : emittedError ? createTerminalOutcome("failed", "provider_error", {
@@ -3481,16 +4041,21 @@ function validateElicitationContent(fields, content) {
3481
4041
  }
3482
4042
 
3483
4043
  export {
4044
+ parseSessionDriver,
4045
+ resolveSessionDriver,
3484
4046
  displaySessionName,
3485
4047
  normalizeWorkspace,
3486
4048
  SessionStore,
3487
4049
  reviewRunnerFor,
3488
4050
  fixRunnerFor,
4051
+ extractJsonObject,
4052
+ parseJsonObject,
3489
4053
  runHostReview,
3490
4054
  reviewReportJson,
3491
4055
  findActiveFileMention,
3492
4056
  replaceActiveFileMention,
3493
4057
  getFileMentionCandidates,
4058
+ mentionTokenRanges,
3494
4059
  createAgentSessionSnapshot,
3495
4060
  reduceAgentSessionSnapshot,
3496
4061
  resolveSessionBootstrap,
@@ -3501,4 +4066,4 @@ export {
3501
4066
  coerceElicitationValue,
3502
4067
  validateElicitationContent
3503
4068
  };
3504
- //# sourceMappingURL=chunk-V4BOYR52.js.map
4069
+ //# sourceMappingURL=chunk-RFJ6Q6LE.js.map