@letrquan/book 0.3.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 (148) hide show
  1. package/CHANGELOG.md +1024 -0
  2. package/README.md +6 -3
  3. package/dist/{additional-roots-H7UNAEHS.js → additional-roots-VAWRQ625.js} +8 -7
  4. package/dist/{app-GE4OHJOC.js → app-J7BVPSUH.js} +5397 -2953
  5. package/dist/app-J7BVPSUH.js.map +1 -0
  6. package/dist/chunk-32WBCUAN.js +660 -0
  7. package/dist/chunk-32WBCUAN.js.map +1 -0
  8. package/dist/{chunk-525CAZPY.js → chunk-4DKHLZRI.js} +6 -6
  9. package/dist/{chunk-4AS7LKKQ.js → chunk-62EEAUID.js} +2 -2
  10. package/dist/{chunk-AQEQX4FF.js → chunk-AHC7OU75.js} +385 -55
  11. package/dist/chunk-AHC7OU75.js.map +1 -0
  12. package/dist/{chunk-EFJ2NH6E.js → chunk-AJZWLKLQ.js} +63 -14
  13. package/dist/chunk-AJZWLKLQ.js.map +1 -0
  14. package/dist/chunk-B5FC3MYZ.js +395 -0
  15. package/dist/chunk-B5FC3MYZ.js.map +1 -0
  16. package/dist/{chunk-67UZEWWF.js → chunk-DNFQTSMJ.js} +53 -11
  17. package/dist/chunk-DNFQTSMJ.js.map +1 -0
  18. package/dist/{chunk-TLX3H2CL.js → chunk-E5WLQSKA.js} +231 -40
  19. package/dist/chunk-E5WLQSKA.js.map +1 -0
  20. package/dist/chunk-ECLC4CIJ.js +1181 -0
  21. package/dist/chunk-ECLC4CIJ.js.map +1 -0
  22. package/dist/chunk-EEX6QNXV.js +127 -0
  23. package/dist/chunk-EEX6QNXV.js.map +1 -0
  24. package/dist/chunk-EFXVSQ5J.js +931 -0
  25. package/dist/chunk-EFXVSQ5J.js.map +1 -0
  26. package/dist/{chunk-RFJ6Q6LE.js → chunk-ENLB7NYV.js} +577 -164
  27. package/dist/chunk-ENLB7NYV.js.map +1 -0
  28. package/dist/{chunk-IME7R2UI.js → chunk-FPR7SKIE.js} +1268 -883
  29. package/dist/chunk-FPR7SKIE.js.map +1 -0
  30. package/dist/{chunk-VPNLCMVR.js → chunk-HFB74CLM.js} +13 -5
  31. package/dist/chunk-HFB74CLM.js.map +1 -0
  32. package/dist/{chunk-5RDRAO4B.js → chunk-HOZXKOE3.js} +37 -1
  33. package/dist/chunk-HOZXKOE3.js.map +1 -0
  34. package/dist/chunk-IDYWN37T.js +36 -0
  35. package/dist/chunk-IDYWN37T.js.map +1 -0
  36. package/dist/{chunk-YC6W4EJH.js → chunk-JGF5HLSI.js} +193 -75
  37. package/dist/chunk-JGF5HLSI.js.map +1 -0
  38. package/dist/{chunk-5YHTKWJN.js → chunk-KDNWMKXV.js} +2447 -907
  39. package/dist/chunk-KDNWMKXV.js.map +1 -0
  40. package/dist/{chunk-Y2VHN7TC.js → chunk-MEOB2DZA.js} +11 -1
  41. package/dist/chunk-MEOB2DZA.js.map +1 -0
  42. package/dist/{chunk-KBQS35OV.js → chunk-MV5PQNIA.js} +2 -2
  43. package/dist/chunk-N2G4Q7ME.js +2672 -0
  44. package/dist/chunk-N2G4Q7ME.js.map +1 -0
  45. package/dist/{chunk-62QL5QUP.js → chunk-OJBMDM5H.js} +17 -1
  46. package/dist/chunk-OJBMDM5H.js.map +1 -0
  47. package/dist/{chunk-56SIEMDX.js → chunk-PTDC7ZUP.js} +2 -2
  48. package/dist/{chunk-X4NJT436.js → chunk-REASC2QG.js} +67 -28
  49. package/dist/chunk-REASC2QG.js.map +1 -0
  50. package/dist/chunk-SBTU5BEC.js +114 -0
  51. package/dist/chunk-SBTU5BEC.js.map +1 -0
  52. package/dist/{chunk-HQI7NYGX.js → chunk-SWPDKLNM.js} +2 -2
  53. package/dist/{chunk-W3XFHEF4.js → chunk-UCFJ43OS.js} +140 -28
  54. package/dist/chunk-UCFJ43OS.js.map +1 -0
  55. package/dist/{chunk-QYBD22H5.js → chunk-W7SLYS3M.js} +80 -16
  56. package/dist/chunk-W7SLYS3M.js.map +1 -0
  57. package/dist/{chunk-SOW4WRQP.js → chunk-WV3O5PGX.js} +3 -3
  58. package/dist/{chunk-JTPOI3KU.js → chunk-XJRJVXJR.js} +262 -38
  59. package/dist/chunk-XJRJVXJR.js.map +1 -0
  60. package/dist/{chunk-224FJZLH.js → chunk-YIL6HTJX.js} +228 -5
  61. package/dist/chunk-YIL6HTJX.js.map +1 -0
  62. package/dist/{command-approvals-VBIU6BLM.js → command-approvals-ZRY66LCW.js} +5 -4
  63. package/dist/context-C3RAP6K5.js +41 -0
  64. package/dist/{hook-approvals-ZVT4V7IM.js → hook-approvals-DPQD5H65.js} +5 -4
  65. package/dist/index.js +657 -754
  66. package/dist/index.js.map +1 -1
  67. package/dist/{interactive-assets-T4I4SEHU.js → interactive-assets-73AQO5WP.js} +11 -6
  68. package/dist/{interactive-assets-T4I4SEHU.js.map → interactive-assets-73AQO5WP.js.map} +1 -1
  69. package/dist/job-runner.js +10 -3
  70. package/dist/job-runner.js.map +1 -1
  71. package/dist/job-supervisor.js +13 -4
  72. package/dist/job-supervisor.js.map +1 -1
  73. package/dist/loader-DQNPDKOX.js +27 -0
  74. package/dist/{mcp-approvals-HJ2VDYMC.js → mcp-approvals-27LJYADQ.js} +5 -4
  75. package/dist/{mcp-config-RNB5LEVE.js → mcp-config-GXSODNCM.js} +3 -2
  76. package/dist/memory-store-I6Z54VUN.js +51 -0
  77. package/dist/{model-window-store-MTSQ5JDJ.js → model-window-store-6VKXXTPJ.js} +4 -3
  78. package/dist/{openai-compatible-2OB54YST.js → openai-compatible-IPUNBJ53.js} +5 -3
  79. package/dist/{permission-approvals-YLV6UC5W.js → permission-approvals-Q5IMFEZI.js} +5 -4
  80. package/dist/{permissions-DHOKF3W2.js → permissions-7Y35X6HZ.js} +26 -9
  81. package/dist/provider-approvals-5AGH3RVT.js +76 -0
  82. package/dist/{sandbox-WRYTZH3D.js → sandbox-N2RY72GN.js} +6 -5
  83. package/dist/{scrollback-35ME2GFX.js → scrollback-GD76GNM4.js} +36 -28
  84. package/dist/scrollback-GD76GNM4.js.map +1 -0
  85. package/dist/sdk.d.ts +876 -64
  86. package/dist/sdk.js +57 -37
  87. package/dist/sdk.js.map +1 -1
  88. package/dist/settings-loader-KDBH45AQ.js +43 -0
  89. package/dist/{settings-removed-JWWPT4WA.js → settings-removed-3ATWJEF3.js} +2 -2
  90. package/dist/shell-selection-Z5WORZU4.js +31 -0
  91. package/dist/shell-selection-Z5WORZU4.js.map +1 -0
  92. package/package.json +5 -5
  93. package/dist/app-GE4OHJOC.js.map +0 -1
  94. package/dist/chunk-224FJZLH.js.map +0 -1
  95. package/dist/chunk-5RDRAO4B.js.map +0 -1
  96. package/dist/chunk-5YHTKWJN.js.map +0 -1
  97. package/dist/chunk-62QL5QUP.js.map +0 -1
  98. package/dist/chunk-67UZEWWF.js.map +0 -1
  99. package/dist/chunk-6OQQOMZR.js +0 -785
  100. package/dist/chunk-6OQQOMZR.js.map +0 -1
  101. package/dist/chunk-AQEQX4FF.js.map +0 -1
  102. package/dist/chunk-C7ADUNPR.js +0 -273
  103. package/dist/chunk-C7ADUNPR.js.map +0 -1
  104. package/dist/chunk-EFJ2NH6E.js.map +0 -1
  105. package/dist/chunk-IME7R2UI.js.map +0 -1
  106. package/dist/chunk-J62VZYOB.js +0 -1339
  107. package/dist/chunk-J62VZYOB.js.map +0 -1
  108. package/dist/chunk-JTPOI3KU.js.map +0 -1
  109. package/dist/chunk-QYBD22H5.js.map +0 -1
  110. package/dist/chunk-RFJ6Q6LE.js.map +0 -1
  111. package/dist/chunk-S4JMID4G.js +0 -259
  112. package/dist/chunk-S4JMID4G.js.map +0 -1
  113. package/dist/chunk-TLX3H2CL.js.map +0 -1
  114. package/dist/chunk-VPNLCMVR.js.map +0 -1
  115. package/dist/chunk-W3XFHEF4.js.map +0 -1
  116. package/dist/chunk-X4NJT436.js.map +0 -1
  117. package/dist/chunk-X66I2NQO.js +0 -137
  118. package/dist/chunk-X66I2NQO.js.map +0 -1
  119. package/dist/chunk-Y2VHN7TC.js.map +0 -1
  120. package/dist/chunk-YC6W4EJH.js.map +0 -1
  121. package/dist/context-CBOBDY5Q.js +0 -38
  122. package/dist/loader-S77OVTHH.js +0 -18
  123. package/dist/memory-store-DZUT5HUH.js +0 -75
  124. package/dist/scrollback-35ME2GFX.js.map +0 -1
  125. package/dist/settings-loader-JUNPGAAM.js +0 -35
  126. package/dist/shell-selection-CDHGJ4GC.js +0 -17
  127. /package/dist/{additional-roots-H7UNAEHS.js.map → additional-roots-VAWRQ625.js.map} +0 -0
  128. /package/dist/{chunk-525CAZPY.js.map → chunk-4DKHLZRI.js.map} +0 -0
  129. /package/dist/{chunk-4AS7LKKQ.js.map → chunk-62EEAUID.js.map} +0 -0
  130. /package/dist/{chunk-KBQS35OV.js.map → chunk-MV5PQNIA.js.map} +0 -0
  131. /package/dist/{chunk-56SIEMDX.js.map → chunk-PTDC7ZUP.js.map} +0 -0
  132. /package/dist/{chunk-HQI7NYGX.js.map → chunk-SWPDKLNM.js.map} +0 -0
  133. /package/dist/{chunk-SOW4WRQP.js.map → chunk-WV3O5PGX.js.map} +0 -0
  134. /package/dist/{command-approvals-VBIU6BLM.js.map → command-approvals-ZRY66LCW.js.map} +0 -0
  135. /package/dist/{context-CBOBDY5Q.js.map → context-C3RAP6K5.js.map} +0 -0
  136. /package/dist/{hook-approvals-ZVT4V7IM.js.map → hook-approvals-DPQD5H65.js.map} +0 -0
  137. /package/dist/{loader-S77OVTHH.js.map → loader-DQNPDKOX.js.map} +0 -0
  138. /package/dist/{mcp-approvals-HJ2VDYMC.js.map → mcp-approvals-27LJYADQ.js.map} +0 -0
  139. /package/dist/{mcp-config-RNB5LEVE.js.map → mcp-config-GXSODNCM.js.map} +0 -0
  140. /package/dist/{memory-store-DZUT5HUH.js.map → memory-store-I6Z54VUN.js.map} +0 -0
  141. /package/dist/{model-window-store-MTSQ5JDJ.js.map → model-window-store-6VKXXTPJ.js.map} +0 -0
  142. /package/dist/{openai-compatible-2OB54YST.js.map → openai-compatible-IPUNBJ53.js.map} +0 -0
  143. /package/dist/{permission-approvals-YLV6UC5W.js.map → permission-approvals-Q5IMFEZI.js.map} +0 -0
  144. /package/dist/{permissions-DHOKF3W2.js.map → permissions-7Y35X6HZ.js.map} +0 -0
  145. /package/dist/{sandbox-WRYTZH3D.js.map → provider-approvals-5AGH3RVT.js.map} +0 -0
  146. /package/dist/{settings-loader-JUNPGAAM.js.map → sandbox-N2RY72GN.js.map} +0 -0
  147. /package/dist/{settings-removed-JWWPT4WA.js.map → settings-loader-KDBH45AQ.js.map} +0 -0
  148. /package/dist/{shell-selection-CDHGJ4GC.js.map → settings-removed-3ATWJEF3.js.map} +0 -0
@@ -8,8 +8,9 @@ import {
8
8
  isSpendlessUsage,
9
9
  isTerminalStatus,
10
10
  loadGitignore,
11
- runAgentLoop
12
- } from "./chunk-5YHTKWJN.js";
11
+ runAgentLoop,
12
+ seedShellIds
13
+ } from "./chunk-KDNWMKXV.js";
13
14
  import {
14
15
  DEFAULT_CONFIDENCE_THRESHOLD,
15
16
  REVIEW_LENSES,
@@ -18,6 +19,7 @@ import {
18
19
  buildReviewerPrompt,
19
20
  buildSingleReviewPrompt,
20
21
  clampConfidence,
22
+ createGoal,
21
23
  isReviewCategory,
22
24
  isReviewSeverity,
23
25
  judgeCompaction,
@@ -27,30 +29,56 @@ import {
27
29
  renderReviewTarget,
28
30
  resolveCompactBudgets,
29
31
  resolveReviewTarget,
32
+ restoreGoal,
30
33
  runCompact,
31
34
  runHooks,
32
35
  runPostCompactHooks
33
- } from "./chunk-IME7R2UI.js";
36
+ } from "./chunk-FPR7SKIE.js";
34
37
  import {
35
38
  observationKey,
39
+ observationPathFor,
36
40
  onAbort,
37
41
  throwIfAborted,
38
42
  workspaceIdentity,
39
43
  yieldToEventLoop
40
- } from "./chunk-J62VZYOB.js";
44
+ } from "./chunk-N2G4Q7ME.js";
41
45
  import {
42
46
  globWalkIgnore
43
- } from "./chunk-JTPOI3KU.js";
47
+ } from "./chunk-XJRJVXJR.js";
48
+ import {
49
+ defaultGitRunner,
50
+ describeMissingObjects,
51
+ hardenedGitArgs,
52
+ noFetchGitEnv,
53
+ readRepositoryGitConfig
54
+ } from "./chunk-32WBCUAN.js";
44
55
  import {
45
56
  buildChildEnv
46
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";
47
65
  import {
48
66
  createDebugLogger,
49
67
  createDebugLoggerWithCounter
50
- } from "./chunk-62QL5QUP.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";
51
79
  import {
52
- normalizeToolResult
53
- } from "./chunk-224FJZLH.js";
80
+ renderUntrustedText
81
+ } from "./chunk-WV3O5PGX.js";
54
82
 
55
83
  // src/session-driver.ts
56
84
  var SESSION_DRIVERS = ["human", "agent"];
@@ -77,7 +105,7 @@ function resolveSessionDriver(input) {
77
105
  var UNTITLED_SESSION_NAME = "Untitled session";
78
106
  var MAX_SESSION_NAME_LENGTH = 56;
79
107
  function deriveSessionName(prompt) {
80
- 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();
81
109
  if (!normalized) return UNTITLED_SESSION_NAME;
82
110
  const titled = normalized.replace(/^[a-z]/, (letter) => letter.toUpperCase());
83
111
  if (Array.from(titled).length <= MAX_SESSION_NAME_LENGTH) return titled;
@@ -118,6 +146,7 @@ var READ_REF_LIMIT = 8;
118
146
  var READ_TOTAL_CHARS = 16e3;
119
147
  var TOOL_RESULT_PREVIEW_CHARS = 4e3;
120
148
  var SESSION_INDEX_FILE = "session-index.json";
149
+ var HEADER_PROBE_BYTES = 16 * 1024;
121
150
  var JSONL_READ_BUFFER_BYTES = 64 * 1024;
122
151
  var IMAGE_STORAGE_KEY = /^[a-f0-9]{64}\.(?:png|jpg|gif|webp)$/;
123
152
  function imageSessionDirectory(root, sessionId) {
@@ -206,30 +235,37 @@ var SessionStore = class {
206
235
  }
207
236
  persistIndex(deletedIds = [], changedIds) {
208
237
  if (this.buildingIndex) return;
238
+ this.withIndexLock(() => this.writeIndexLocked(deletedIds, changedIds));
239
+ }
240
+ withIndexLock(action) {
209
241
  const lock = this.acquireIndexLock();
210
242
  try {
211
- const sessions = this.readPersistedIndexEntries();
212
- const ids = changedIds ?? Array.from(this.metadataIndex.keys());
213
- for (const id of ids) {
214
- const entry = this.metadataIndex.get(id);
215
- if (entry) sessions[id] = entry;
216
- }
217
- for (const id of deletedIds) delete sessions[id];
218
- const target = this.indexPath();
219
- const temporary = `${target}.${process.pid}.${crypto.randomUUID()}.tmp`;
220
- try {
221
- writeFileSync(temporary, JSON.stringify({ version: 1, sessions }), "utf-8");
222
- renameSync(temporary, target);
223
- this.indexMtimeMs = statSync(target).mtimeMs;
224
- this.metadataIndex.clear();
225
- for (const [id, entry] of Object.entries(sessions)) this.metadataIndex.set(id, entry);
226
- } finally {
227
- if (existsSync(temporary)) unlinkSync(temporary);
228
- }
243
+ return action();
229
244
  } finally {
230
245
  this.releaseIndexLock(lock);
231
246
  }
232
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
+ }
233
269
  acquireIndexLock() {
234
270
  const path = `${this.indexPath()}.lock`;
235
271
  for (let attempt = 0; attempt < 50; attempt++) {
@@ -339,7 +375,7 @@ var SessionStore = class {
339
375
  if (cached && cached.size === stat2.size && cached.mtimeMs === stat2.mtimeMs) {
340
376
  return cached.records;
341
377
  }
342
- const records = [];
378
+ const records2 = [];
343
379
  const descriptor = openSync(p, "r");
344
380
  const buffer = Buffer.allocUnsafe(JSONL_READ_BUFFER_BYTES);
345
381
  const decoder = new StringDecoder("utf8");
@@ -349,7 +385,7 @@ var SessionStore = class {
349
385
  lineNumber++;
350
386
  if (!line) return;
351
387
  try {
352
- records.push(JSON.parse(line));
388
+ records2.push(JSON.parse(line));
353
389
  } catch {
354
390
  log.warn("ignoring corrupt session record", { id, line: lineNumber });
355
391
  }
@@ -373,9 +409,9 @@ var SessionStore = class {
373
409
  this.parsedSessions.set(id, {
374
410
  size: stat2.size,
375
411
  mtimeMs: stat2.mtimeMs,
376
- records
412
+ records: records2
377
413
  });
378
- return records;
414
+ return records2;
379
415
  }
380
416
  updateIndexAfterAppend(id, record) {
381
417
  const entry = this.metadataIndex.get(id);
@@ -396,7 +432,9 @@ var SessionStore = class {
396
432
  } else if (record.type === "user") {
397
433
  entry.meta.messageCount++;
398
434
  entry.lastMessageRole = "user";
399
- 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
+ }
400
438
  } else if (record.type === "local") {
401
439
  entry.meta.messageCount++;
402
440
  entry.lastMessageRole = "assistant";
@@ -430,10 +468,10 @@ var SessionStore = class {
430
468
  });
431
469
  }
432
470
  load(id) {
433
- const records = this.readRecords(id);
471
+ const records2 = this.readRecords(id);
434
472
  const parsedCache = this.parsedSessions.get(id);
435
473
  if (parsedCache?.loaded) return cloneLoadedSession(parsedCache.loaded);
436
- const metaRec = records.find(
474
+ const metaRec = records2.find(
437
475
  (record) => record.data?.kind === "session_meta"
438
476
  );
439
477
  const storedMeta = metaRec?.data;
@@ -456,11 +494,12 @@ var SessionStore = class {
456
494
  const checkpointByUserEventId = /* @__PURE__ */ new Map();
457
495
  const snapshotIds = /* @__PURE__ */ new Set();
458
496
  let latestPlan;
497
+ let latestGoal;
459
498
  let carriedUsage;
460
499
  const carriedModels = /* @__PURE__ */ new Set();
461
500
  let count = 0;
462
- for (let lineIndex = 0; lineIndex < records.length; lineIndex++) {
463
- const record = records[lineIndex];
501
+ for (let lineIndex = 0; lineIndex < records2.length; lineIndex++) {
502
+ const record = records2[lineIndex];
464
503
  const eventId = record.eventId ?? `legacy-line-${lineIndex + 1}`;
465
504
  const data = record.data;
466
505
  if (data.kind === "session_meta_patch") {
@@ -485,6 +524,11 @@ var SessionStore = class {
485
524
  if (plan?.version === 1) latestPlan = plan;
486
525
  continue;
487
526
  }
527
+ if (record.type === "goal") {
528
+ const goal = record.data;
529
+ if (goal?.version === 1) latestGoal = goal;
530
+ continue;
531
+ }
488
532
  if (record.type === "turn_checkpoint") {
489
533
  const checkpoint = record.data;
490
534
  if (checkpoint?.version !== 1 || !checkpoint.checkpointId || !checkpoint.userEventId || typeof checkpoint.prompt !== "string") {
@@ -495,6 +539,7 @@ var SessionStore = class {
495
539
  id: checkpoint.checkpointId,
496
540
  userEventId: checkpoint.userEventId,
497
541
  prompt: checkpoint.prompt,
542
+ ...checkpoint.promptTyped === true ? { promptTyped: true } : {},
498
543
  attachments: checkpoint.attachments,
499
544
  timestamp: record.timestamp,
500
545
  ...checkpoint.checkpoint,
@@ -675,11 +720,11 @@ var SessionStore = class {
675
720
  }
676
721
  }
677
722
  }
678
- if (records.length) meta.updatedAt = records[records.length - 1].timestamp;
723
+ if (records2.length) meta.updatedAt = records2[records2.length - 1].timestamp;
679
724
  meta.messageCount = count;
680
725
  if (!meta.name) {
681
726
  const firstUserPrompt = transcript.find(
682
- (message) => message.role === "user" && message.kind !== "local"
727
+ (message) => message.role === "user" && message.kind !== "local" && message.kind !== "agent-notification"
683
728
  )?.content;
684
729
  if (firstUserPrompt) meta.name = deriveSessionName(firstUserPrompt);
685
730
  }
@@ -694,6 +739,7 @@ var SessionStore = class {
694
739
  activeEventIds,
695
740
  meta,
696
741
  plan: latestPlan,
742
+ goal: latestGoal,
697
743
  carriedUsage,
698
744
  carriedModels: [...carriedModels],
699
745
  history: contextHistory
@@ -816,9 +862,9 @@ var SessionStore = class {
816
862
  activeRecordMap(id) {
817
863
  const cache = this.parsedSessions.get(id);
818
864
  if (cache?.recordsByActiveId) return cache.recordsByActiveId;
819
- const records = this.activeRecordIndex(id);
865
+ const records2 = this.activeRecordIndex(id);
820
866
  const refreshed = this.parsedSessions.get(id);
821
- const byId = new Map(records.map((record) => [record.eventId, record]));
867
+ const byId = new Map(records2.map((record) => [record.eventId, record]));
822
868
  if (refreshed) refreshed.recordsByActiveId = byId;
823
869
  return byId;
824
870
  }
@@ -841,9 +887,15 @@ var SessionStore = class {
841
887
  }
842
888
  return references;
843
889
  }
844
- 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) {
845
897
  const normalized = normalizeWorkspace(cwd);
846
- return this.list().find((meta) => meta.cwd === normalized);
898
+ return this.list().find((meta) => meta.cwd === normalized && accept(meta));
847
899
  }
848
900
  findByName(name) {
849
901
  return this.list().find((meta) => meta.name === name);
@@ -860,19 +912,13 @@ var SessionStore = class {
860
912
  if (preserveIds.has(id)) continue;
861
913
  try {
862
914
  if (entry.meta.updatedAt < cutoff) {
863
- unlinkSync(this.path(id));
864
- rmSync(imageSessionDirectory(this.root, id), { recursive: true, force: true });
865
- this.metadataIndex.delete(id);
866
- this.parsedSessions.delete(id);
915
+ this.discardSession(id);
867
916
  removedIds.push(id);
868
917
  removed++;
869
918
  }
870
919
  } catch {
871
920
  if (existsSync(this.path(id)) && statSync(this.path(id)).mtimeMs < cutoff) {
872
- unlinkSync(this.path(id));
873
- rmSync(imageSessionDirectory(this.root, id), { recursive: true, force: true });
874
- this.metadataIndex.delete(id);
875
- this.parsedSessions.delete(id);
921
+ this.discardSession(id);
876
922
  removedIds.push(id);
877
923
  removed++;
878
924
  }
@@ -881,7 +927,85 @@ var SessionStore = class {
881
927
  if (removedIds.length > 0) this.persistIndex(removedIds, []);
882
928
  return removed;
883
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
+ }
884
1004
  };
1005
+ function isErrno(error, codes) {
1006
+ const code = error?.code;
1007
+ return code !== void 0 && codes.includes(code);
1008
+ }
885
1009
  function cloneLoadedSession(session) {
886
1010
  const contextHistory = [...session.contextHistory];
887
1011
  return {
@@ -896,6 +1020,7 @@ function cloneLoadedSession(session) {
896
1020
  todos: session.plan.todos?.map((todo) => ({ ...todo })),
897
1021
  tasks: session.plan.tasks?.map((task) => ({ ...task }))
898
1022
  } : void 0,
1023
+ goal: session.goal ? { version: 1, goal: session.goal.goal ? { ...session.goal.goal } : null } : void 0,
899
1024
  carriedUsage: session.carriedUsage ? { ...session.carriedUsage } : void 0,
900
1025
  carriedModels: session.carriedModels ? [...session.carriedModels] : void 0,
901
1026
  history: contextHistory
@@ -1019,8 +1144,8 @@ function fixRunnerFor(manager, attribution = {}) {
1019
1144
  const evidence = (await manager.listEvidence({ ids: [evidenceId] }))[0];
1020
1145
  return evidence ? evidenceProjection(evidence) : void 0;
1021
1146
  },
1022
- async apply(agentId, evidenceId) {
1023
- const result = await manager.apply(agentId, evidenceId);
1147
+ async apply(agentId, evidenceId, options) {
1148
+ const result = await manager.apply(agentId, evidenceId, options);
1024
1149
  return { status: result.status, commit: result.commit, error: result.error };
1025
1150
  }
1026
1151
  };
@@ -1815,7 +1940,15 @@ async function applyReviewFixes(runner, confirmed, options = {}) {
1815
1940
  );
1816
1941
  continue;
1817
1942
  }
1818
- 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
+ }
1819
1952
  if (applied.status === "applied") {
1820
1953
  result.applied++;
1821
1954
  result.messages.push(
@@ -1989,6 +2122,7 @@ function reduceAgentSessionSnapshot(snapshot, event) {
1989
2122
  case "agent_persistence":
1990
2123
  case "skill_lifecycle":
1991
2124
  case "notice":
2125
+ case "goal_status":
1992
2126
  case "background_job_start":
1993
2127
  case "background_job_update":
1994
2128
  case "background_job_output":
@@ -2037,6 +2171,7 @@ function reduceAgentSessionSnapshot(snapshot, event) {
2037
2171
  }
2038
2172
 
2039
2173
  // src/session/resolve.ts
2174
+ var log2 = createDebugLogger("session:resolve");
2040
2175
  function recordResumeDriver(store, sessionId, recorded, driver) {
2041
2176
  if (driver === "agent" && recorded !== "agent") store.patchMeta(sessionId, { driver: "agent" });
2042
2177
  }
@@ -2080,6 +2215,25 @@ function persistHistory(store, sessionId, history) {
2080
2215
  });
2081
2216
  }
2082
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
+ }
2083
2237
  function resolveSessionBootstrap(store, options) {
2084
2238
  if (!store) {
2085
2239
  if (options.resume || options.continue) {
@@ -2146,6 +2300,7 @@ function resolveSessionBootstrap(store, options) {
2146
2300
  rewindTargets: loaded.rewindTargets,
2147
2301
  activeEventIds: loaded.activeEventIds,
2148
2302
  plan: loaded.plan,
2303
+ goal: loaded.goal,
2149
2304
  source: "resume",
2150
2305
  persisted: true,
2151
2306
  created: true
@@ -2163,6 +2318,7 @@ function resolveSessionBootstrap(store, options) {
2163
2318
  rewindTargets: loaded.rewindTargets,
2164
2319
  activeEventIds: loaded.activeEventIds,
2165
2320
  plan: loaded.plan,
2321
+ goal: loaded.goal,
2166
2322
  carriedUsage: loaded.carriedUsage,
2167
2323
  carriedModels: loaded.carriedModels,
2168
2324
  source: "resume",
@@ -2214,9 +2370,8 @@ function resolveWorkspaceMentionPath(workspace, mentionPath) {
2214
2370
  const normalized = mentionPath.replace(/\\/g, "/");
2215
2371
  const candidate = isAbsolute(normalized) ? normalized : resolve2(root, normalized);
2216
2372
  const filePath = resolve2(candidate);
2217
- const rel = relative(root, filePath);
2218
- if (rel.startsWith("..") || isAbsolute(rel)) return null;
2219
- return { filePath, relativePath: normalizeMentionPath(rel) };
2373
+ if (isOutside(root, filePath)) return null;
2374
+ return { filePath, relativePath: normalizeMentionPath(relative(root, filePath)) };
2220
2375
  }
2221
2376
  function findActiveFileMention(input) {
2222
2377
  for (let i = input.length - 1; i >= 0; i--) {
@@ -2260,6 +2415,8 @@ function withMissingAncestors(entries) {
2260
2415
  return missing.length > 0 ? [...entries, ...missing] : entries;
2261
2416
  }
2262
2417
  async function getFileMentionCandidates(workspace, query, limit = 50, signal) {
2418
+ const reachesUnc = createUncGuard();
2419
+ if (reachesUnc(workspace)) return [];
2263
2420
  const normalizedQuery = normalizeMentionPath(query).toLowerCase();
2264
2421
  const gitignore = loadGitignore(workspace).patterns;
2265
2422
  const ignore = globWalkIgnore([...DEFAULT_IGNORE, ...gitignore]);
@@ -2274,6 +2431,9 @@ async function getFileMentionCandidates(workspace, query, limit = 50, signal) {
2274
2431
  // A pattern naming a directory returns that directory, not everything inside it — which is
2275
2432
  // the whole point of walking for mentions.
2276
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),
2277
2437
  signal
2278
2438
  })
2279
2439
  );
@@ -2350,9 +2510,83 @@ async function runSessionEnd(config, sessionId, reason, options) {
2350
2510
  }
2351
2511
 
2352
2512
  // src/input/input-expansion.ts
2353
- import { existsSync as existsSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
2513
+ import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
2354
2514
  import { exec } from "child_process";
2355
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
2356
2590
  var AT_MENTION_CHAR_LIMIT = 2e4;
2357
2591
  function isMentionBoundary2(input, index) {
2358
2592
  if (index === 0) return true;
@@ -2488,63 +2722,134 @@ function formatMentionError(filePath, reason) {
2488
2722
  function looksBinary(content) {
2489
2723
  return content.includes("\0");
2490
2724
  }
2491
- function expandMention(filePath, workspace) {
2492
- const resolved = resolveWorkspaceMentionPath(workspace, filePath);
2493
- if (!resolved) return null;
2494
- if (!existsSync2(resolved.filePath)) return null;
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;
2495
2752
  try {
2496
- const stat2 = statSync2(resolved.filePath);
2753
+ const stat2 = statSync2(realPath);
2497
2754
  if (stat2.isDirectory()) return null;
2498
- if (!stat2.isFile())
2499
- return formatMentionError(resolved.relativePath, "path is not a regular file");
2500
- const content = readFileSync2(resolved.filePath, "utf-8");
2501
- if (looksBinary(content))
2502
- return formatMentionError(resolved.relativePath, "file appears to be binary");
2503
- const truncated = content.length > AT_MENTION_CHAR_LIMIT;
2504
- const body = truncated ? content.slice(0, AT_MENTION_CHAR_LIMIT) : content;
2505
- const suffix = truncated ? `
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;
2810
+ try {
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 ? `
2506
2837
 
2507
2838
  [File truncated at ${AT_MENTION_CHAR_LIMIT} characters; use the Read tool for more.]` : "";
2508
- return `
2509
- Contents of ${resolved.relativePath}:
2839
+ return {
2840
+ text: `
2841
+ Contents of ${decision.relativePath}:
2510
2842
 
2511
2843
  \`\`\`
2512
2844
  ${body}${suffix}
2513
2845
  \`\`\`
2514
- `;
2515
- } catch (error) {
2516
- const message = error instanceof Error ? error.message : String(error);
2517
- return formatMentionError(filePath, message.slice(0, 200) || "unable to read file");
2518
- }
2846
+ `,
2847
+ observation
2848
+ };
2519
2849
  }
2520
2850
  function mentionTokenRanges(input) {
2521
2851
  return findMentionTokens(input).map((token) => [token.start, token.end]);
2522
2852
  }
2523
- function collectAtMentionObservations(input, workspace, sourceRef, expandShellInput = false) {
2524
- const workspaceId = workspaceIdentity(workspace);
2525
- const observations = [];
2526
- const shellLines = expandShellInput ? findShellLines(input) : [];
2527
- for (const token of findMentionTokens(input, shellLineRanges(shellLines))) {
2528
- const resolved = resolveWorkspaceMentionPath(workspace, token.path);
2529
- if (!resolved || !existsSync2(resolved.filePath)) continue;
2530
- try {
2531
- const info = statSync2(resolved.filePath);
2532
- if (!info.isFile()) continue;
2533
- const bytes = readFileSync2(resolved.filePath);
2534
- observations.push({
2535
- path: resolved.relativePath,
2536
- workspaceId,
2537
- sha256: createHash2("sha256").update(bytes).digest("hex"),
2538
- byteSize: bytes.byteLength,
2539
- operation: "mention",
2540
- sourceRef,
2541
- timestamp: Date.now()
2542
- });
2543
- } catch {
2544
- }
2545
- }
2546
- return observations;
2547
- }
2548
2853
  function findShellLines(input) {
2549
2854
  const fenced = fencedCodeRanges(input);
2550
2855
  return [...input.matchAll(/^!(\S.*)$/gm)].filter((match) => !isInsideRanges(fenced, match.index ?? 0)).map((match) => {
@@ -2555,20 +2860,39 @@ function findShellLines(input) {
2555
2860
  function shellLineRanges(lines) {
2556
2861
  return lines.map((line) => [line.start, line.end]);
2557
2862
  }
2558
- async function expandUserInput(input, workspace, options = {}) {
2559
- const shellLines = options.expandShell ? findShellLines(input) : [];
2863
+ async function expandUserInputDetailed(input, workspace, options = {}) {
2864
+ const shellLines = options.expandShell && !options.projectCommand ? findShellLines(input) : [];
2560
2865
  const tokens = findMentionTokens(input, shellLineRanges(shellLines));
2561
- if (shellLines.length === 0 && tokens.length === 0) return input;
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 = [];
2562
2879
  const edits = [
2563
2880
  ...shellLines.map((line) => ({
2564
2881
  start: line.start,
2565
2882
  end: line.end,
2566
2883
  apply: () => executeShellExpansion(line.command, workspace, options.signal)
2567
2884
  })),
2568
- ...tokens.map((token) => ({
2885
+ ...tokens.map((token, index) => ({
2569
2886
  start: token.start,
2570
2887
  end: token.end,
2571
- apply: () => expandMention(token.path, workspace) ?? token.raw
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
+ }
2572
2896
  }))
2573
2897
  ].sort((left, right) => left.start - right.start);
2574
2898
  let output = "";
@@ -2578,10 +2902,10 @@ async function expandUserInput(input, workspace, options = {}) {
2578
2902
  output += await edit.apply();
2579
2903
  cursor = edit.end;
2580
2904
  }
2581
- return output + input.slice(cursor);
2905
+ return { text: output + input.slice(cursor), observations, refused };
2582
2906
  }
2583
2907
  function executeShellExpansion(command, workspace, signal) {
2584
- return new Promise((resolve3) => {
2908
+ return new Promise((resolve4) => {
2585
2909
  exec(
2586
2910
  command,
2587
2911
  {
@@ -2598,18 +2922,18 @@ function executeShellExpansion(command, workspace, signal) {
2598
2922
  (error, stdout) => {
2599
2923
  if (error) {
2600
2924
  const message = error.message.slice(0, 200) || "unknown error";
2601
- resolve3(`(command '${command}' failed: ${message})`);
2925
+ resolve4(`(command '${command}' failed: ${message})`);
2602
2926
  return;
2603
2927
  }
2604
2928
  const value = stdout.trim();
2605
- resolve3(value || `(command '${command}' produced no output)`);
2929
+ resolve4(value || `(command '${command}' produced no output)`);
2606
2930
  }
2607
2931
  );
2608
2932
  });
2609
2933
  }
2610
2934
 
2611
2935
  // src/session/agent-interactions.ts
2612
- var log2 = createDebugLoggerWithCounter("session:interactions");
2936
+ var log3 = createDebugLoggerWithCounter("session:interactions");
2613
2937
  function snapshotOf(permission, planApproval, userQuestions, elicitations) {
2614
2938
  return Object.freeze({
2615
2939
  pendingPermission: permission ? Object.freeze({ toolCall: permission.toolCall }) : null,
@@ -2638,11 +2962,11 @@ var AgentInteractionController = class {
2638
2962
  return () => this.listeners.delete(listener);
2639
2963
  }
2640
2964
  requestPermission(toolCall) {
2641
- return new Promise((resolve3) => {
2642
- const request = { toolCall, resolve: resolve3 };
2965
+ return new Promise((resolve4) => {
2966
+ const request = { toolCall, resolve: resolve4 };
2643
2967
  if (this.pendingPermission) {
2644
2968
  this.pendingPermissionQueue.push(request);
2645
- log2.event("permission:queued", {
2969
+ log3.event("permission:queued", {
2646
2970
  tool: toolCall.name,
2647
2971
  id: toolCall.id,
2648
2972
  queueLength: this.pendingPermissionQueue.length
@@ -2650,19 +2974,19 @@ var AgentInteractionController = class {
2650
2974
  } else {
2651
2975
  this.pendingPermission = request;
2652
2976
  this.publish();
2653
- log2.event("permission:pending", { tool: toolCall.name, id: toolCall.id });
2977
+ log3.event("permission:pending", { tool: toolCall.name, id: toolCall.id });
2654
2978
  }
2655
2979
  });
2656
2980
  }
2657
2981
  settlePermission(result, via) {
2658
2982
  const pending = this.pendingPermission;
2659
2983
  if (!pending) {
2660
- log2.event("permission:settled:noop", { reason: "no-pending", result, via });
2984
+ log3.event("permission:settled:noop", { reason: "no-pending", result, via });
2661
2985
  return false;
2662
2986
  }
2663
2987
  this.pendingPermission = this.pendingPermissionQueue.shift() ?? null;
2664
2988
  this.publish();
2665
- log2.event("permission:settled", {
2989
+ log3.event("permission:settled", {
2666
2990
  tool: pending.toolCall.name,
2667
2991
  id: pending.toolCall.id,
2668
2992
  result,
@@ -2681,26 +3005,26 @@ var AgentInteractionController = class {
2681
3005
  this.pendingPermissionQueue = [];
2682
3006
  this.publish();
2683
3007
  for (const request of pending) request.resolve({ result: "deny", reason: "dismissed" });
2684
- log2.event("permission:cancelled-all", { via, count: pending.length });
3008
+ log3.event("permission:cancelled-all", { via, count: pending.length });
2685
3009
  return true;
2686
3010
  }
2687
3011
  requestPlanApproval(plan) {
2688
3012
  if (this.pendingPlanApproval) this.settlePlanApproval("reject", "superseded");
2689
- return new Promise((resolve3) => {
2690
- this.pendingPlanApproval = { plan, resolve: resolve3 };
3013
+ return new Promise((resolve4) => {
3014
+ this.pendingPlanApproval = { plan, resolve: resolve4 };
2691
3015
  this.publish();
2692
- log2.event("plan-approval:pending", { len: plan.length });
3016
+ log3.event("plan-approval:pending", { len: plan.length });
2693
3017
  });
2694
3018
  }
2695
3019
  settlePlanApproval(result, via) {
2696
3020
  const pending = this.pendingPlanApproval;
2697
3021
  if (!pending) {
2698
- log2.event("plan-approval:settled:noop", { reason: "no-pending", result, via });
3022
+ log3.event("plan-approval:settled:noop", { reason: "no-pending", result, via });
2699
3023
  return false;
2700
3024
  }
2701
3025
  this.pendingPlanApproval = null;
2702
3026
  this.publish();
2703
- log2.event("plan-approval:settled", { result, via, len: pending.plan.length });
3027
+ log3.event("plan-approval:settled", { result, via, len: pending.plan.length });
2704
3028
  pending.resolve(result);
2705
3029
  return true;
2706
3030
  }
@@ -2711,10 +3035,10 @@ var AgentInteractionController = class {
2711
3035
  message: `Duplicate user question request: ${request.id}`
2712
3036
  });
2713
3037
  }
2714
- return new Promise((resolve3) => {
2715
- this.pendingUserQuestions = [...this.pendingUserQuestions, { request, resolve: resolve3 }];
3038
+ return new Promise((resolve4) => {
3039
+ this.pendingUserQuestions = [...this.pendingUserQuestions, { request, resolve: resolve4 }];
2716
3040
  this.publish();
2717
- log2.event("user-question:pending", {
3041
+ log3.event("user-question:pending", {
2718
3042
  id: request.id,
2719
3043
  count: request.questions.length,
2720
3044
  queueLength: this.pendingUserQuestions.length,
@@ -2725,14 +3049,14 @@ var AgentInteractionController = class {
2725
3049
  settleUserQuestion(result, via, requestId) {
2726
3050
  const index = requestId ? this.pendingUserQuestions.findIndex((entry) => entry.request.id === requestId) : 0;
2727
3051
  if (index < 0 || this.pendingUserQuestions.length === 0) {
2728
- log2.event("user-question:settled:noop", { reason: "no-pending", via, requestId });
3052
+ log3.event("user-question:settled:noop", { reason: "no-pending", via, requestId });
2729
3053
  return false;
2730
3054
  }
2731
3055
  const next = [...this.pendingUserQuestions];
2732
3056
  const [pending] = next.splice(index, 1);
2733
3057
  this.pendingUserQuestions = next;
2734
3058
  this.publish();
2735
- log2.event("user-question:settled", {
3059
+ log3.event("user-question:settled", {
2736
3060
  id: pending.request.id,
2737
3061
  action: result.action,
2738
3062
  via,
@@ -2749,7 +3073,7 @@ var AgentInteractionController = class {
2749
3073
  for (const entry of pending) {
2750
3074
  entry.resolve({ action: "cancel", message: `Question cancelled via ${via}.` });
2751
3075
  }
2752
- log2.event("user-question:cancelled-all", { via, count: pending.length });
3076
+ log3.event("user-question:cancelled-all", { via, count: pending.length });
2753
3077
  return pending.length;
2754
3078
  }
2755
3079
  /**
@@ -2760,10 +3084,10 @@ var AgentInteractionController = class {
2760
3084
  if (this.pendingElicitations.some((entry) => entry.request.id === request.id)) {
2761
3085
  return Promise.resolve({ action: "cancel" });
2762
3086
  }
2763
- return new Promise((resolve3) => {
2764
- this.pendingElicitations = [...this.pendingElicitations, { request, resolve: resolve3 }];
3087
+ return new Promise((resolve4) => {
3088
+ this.pendingElicitations = [...this.pendingElicitations, { request, resolve: resolve4 }];
2765
3089
  this.publish();
2766
- log2.event("elicitation:pending", {
3090
+ log3.event("elicitation:pending", {
2767
3091
  id: request.id,
2768
3092
  server: request.server,
2769
3093
  fields: request.fields.length,
@@ -2774,14 +3098,14 @@ var AgentInteractionController = class {
2774
3098
  settleElicitation(result, via, requestId) {
2775
3099
  const index = requestId ? this.pendingElicitations.findIndex((entry) => entry.request.id === requestId) : 0;
2776
3100
  if (index < 0 || this.pendingElicitations.length === 0) {
2777
- log2.event("elicitation:settled:noop", { reason: "no-pending", via, requestId });
3101
+ log3.event("elicitation:settled:noop", { reason: "no-pending", via, requestId });
2778
3102
  return false;
2779
3103
  }
2780
3104
  const next = [...this.pendingElicitations];
2781
3105
  const [pending] = next.splice(index, 1);
2782
3106
  this.pendingElicitations = next;
2783
3107
  this.publish();
2784
- log2.event("elicitation:settled", {
3108
+ log3.event("elicitation:settled", {
2785
3109
  id: pending.request.id,
2786
3110
  action: result.action,
2787
3111
  via,
@@ -2796,7 +3120,7 @@ var AgentInteractionController = class {
2796
3120
  this.pendingElicitations = [];
2797
3121
  this.publish();
2798
3122
  for (const entry of pending) entry.resolve({ action: "cancel" });
2799
- log2.event("elicitation:cancelled-all", { via, count: pending.length });
3123
+ log3.event("elicitation:cancelled-all", { via, count: pending.length });
2800
3124
  return pending.length;
2801
3125
  }
2802
3126
  cancelAll(via) {
@@ -2863,7 +3187,16 @@ var AgentSessionOperations = class {
2863
3187
  };
2864
3188
 
2865
3189
  // src/session/agent-session.ts
2866
- var log3 = createDebugLogger("session:usage");
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
+ });
3199
+ }
2867
3200
  var AgentSession = class {
2868
3201
  interactions = new AgentInteractionController();
2869
3202
  operations = new AgentSessionOperations();
@@ -2950,6 +3283,11 @@ var AgentSession = class {
2950
3283
  contextMessage: request.contextMessage,
2951
3284
  userMessage,
2952
3285
  sessionName: request.sessionName,
3286
+ sessionNameSource: request.sessionNameSource,
3287
+ sessionNameExact: request.sessionNameExact,
3288
+ expandShellInput: request.expandShellInput,
3289
+ projectCommand: request.projectCommand,
3290
+ typedText: request.typedText,
2953
3291
  snapshotStore: request.snapshotStore,
2954
3292
  timelineStore: request.timelineStore,
2955
3293
  signal: control.signal,
@@ -2995,6 +3333,7 @@ var AgentSession = class {
2995
3333
  ...request.options,
2996
3334
  runtime,
2997
3335
  displayMessage: request.displayMessage,
3336
+ typedText: request.typedText,
2998
3337
  userMessageId: userMessage.id,
2999
3338
  userMessageTimestamp: userMessage.timestamp,
3000
3339
  userFileObservations: userMessage.fileObservations,
@@ -3042,6 +3381,41 @@ var AgentSession = class {
3042
3381
  getRuntime() {
3043
3382
  return this.runtime;
3044
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
+ }
3045
3419
  /**
3046
3420
  * Stop the outgoing runtime, write what it spent, and install its replacement.
3047
3421
  *
@@ -3054,9 +3428,14 @@ var AgentSession = class {
3054
3428
  */
3055
3429
  replaceRuntime(options = {}, via = "session_transition") {
3056
3430
  const outgoing = this.runtime;
3431
+ outgoing.agentManager?.handOffLateApplications(() => this.runtime.agentManager);
3057
3432
  outgoing.dispose(via);
3058
3433
  this.releaseRunUsage(outgoing);
3059
- this.runtime = new SessionRuntime(options);
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
+ });
3060
3439
  return this.runtime;
3061
3440
  }
3062
3441
  subscribe(listener) {
@@ -3196,7 +3575,7 @@ var AgentSession = class {
3196
3575
  });
3197
3576
  runtime.runAccounting.commitPersistedUsage(rootRunId);
3198
3577
  } catch (error) {
3199
- log3.warn("usage record could not be appended; spend left unpersisted", {
3578
+ log4.warn("usage record could not be appended; spend left unpersisted", {
3200
3579
  rootRunId,
3201
3580
  sessionId: target.sessionId,
3202
3581
  error: error instanceof Error ? error.message : String(error)
@@ -3255,6 +3634,7 @@ var AgentSession = class {
3255
3634
  cwd: request.config.workspace,
3256
3635
  driver: request.config.sessionDriver
3257
3636
  }) ?? crypto.randomUUID();
3637
+ if (request.store) request.createdSessions?.add(sessionId);
3258
3638
  if (request.store && request.timelineStore && request.timelineStore !== request.store) {
3259
3639
  request.timelineStore.create({
3260
3640
  id: sessionId,
@@ -3305,12 +3685,14 @@ var AgentSession = class {
3305
3685
  // user who wrote a plan, switched away and came back resumed a half-finished
3306
3686
  // objective with an empty task list and no notice that it had been dropped.
3307
3687
  plan: loaded.plan,
3688
+ goal: loaded.goal,
3308
3689
  source: "resume",
3309
3690
  persisted: true,
3310
3691
  created: false
3311
3692
  };
3312
3693
  this.reset("session-resume");
3313
- this.replaceRuntime({ history: bootstrap.transcript ?? bootstrap.history }, "session-resume");
3694
+ const resumed = this.replaceRuntime({}, "session-resume");
3695
+ seedShellIds(resumed.backgroundShells, loaded.transcript, loaded.contextHistory);
3314
3696
  request.onTransition?.(bootstrap);
3315
3697
  await this.startLifecycle(request.config, selected.id, "resume");
3316
3698
  return { status: "transitioned", bootstrap };
@@ -3331,10 +3713,13 @@ var AgentSession = class {
3331
3713
  gitHead: capture.gitHead,
3332
3714
  codeUnavailableReason: capture.reason
3333
3715
  };
3716
+ const rewindPrompt = request.typedText ?? request.displayMessage;
3717
+ const promptTyped = request.typedText !== void 0;
3334
3718
  const rewindTarget = {
3335
3719
  id: checkpointId,
3336
3720
  userEventId: request.userMessage.id,
3337
- prompt: request.displayMessage,
3721
+ prompt: rewindPrompt,
3722
+ ...promptTyped ? { promptTyped } : {},
3338
3723
  attachments: request.userMessage.attachments,
3339
3724
  timestamp: checkpointTimestamp,
3340
3725
  ...checkpoint,
@@ -3348,27 +3733,32 @@ var AgentSession = class {
3348
3733
  version: 1,
3349
3734
  checkpointId,
3350
3735
  userEventId: request.userMessage.id,
3351
- prompt: request.displayMessage,
3736
+ prompt: rewindPrompt,
3737
+ ...promptTyped ? { promptTyped } : {},
3352
3738
  attachments: request.userMessage.attachments,
3353
3739
  checkpoint
3354
3740
  }
3355
3741
  });
3356
- const { contextMessage, sessionName } = await this.recordUserMessage(request);
3357
- return { status: "prepared", contextMessage, rewindTarget, sessionName };
3742
+ const { contextMessage, sessionName, mentionNotices } = await this.recordUserMessage(request);
3743
+ return { status: "prepared", contextMessage, rewindTarget, sessionName, mentionNotices };
3358
3744
  }
3359
3745
  async recordUserMessage(request) {
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
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
3364
3757
  });
3758
+ const contextMessage = expansion?.text ?? request.contextMessage ?? request.displayMessage;
3759
+ const mentionNotices = (expansion?.refused ?? []).map(formatRefusedMentionNotice);
3365
3760
  request.userMessage.contextContent = contextMessage === request.displayMessage ? void 0 : contextMessage;
3366
- request.userMessage.fileObservations = request.contextMessage === void 0 ? collectAtMentionObservations(
3367
- request.displayMessage,
3368
- request.config.workspace,
3369
- request.userMessage.id,
3370
- expandShellInput
3371
- ) : [];
3761
+ request.userMessage.fileObservations = expansion?.observations ?? [];
3372
3762
  const observationLedger = (request.runtime ?? this.runtime).fileObservationLedger;
3373
3763
  for (const observation of request.userMessage.fileObservations) {
3374
3764
  observationLedger.set(observationKey(observation.workspaceId, observation.path), observation);
@@ -3392,11 +3782,14 @@ var AgentSession = class {
3392
3782
  }
3393
3783
  });
3394
3784
  if (request.timelineStore) request.onUserMessagePersisted?.();
3395
- 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);
3396
3789
  if (!request.sessionName?.trim()) {
3397
3790
  request.timelineStore?.patchMeta?.(request.sessionId, { name: sessionName });
3398
3791
  }
3399
- return { contextMessage, sessionName };
3792
+ return { contextMessage, sessionName, mentionNotices };
3400
3793
  }
3401
3794
  async compact(request) {
3402
3795
  const runtime = request.runtime ?? this.runtime;
@@ -3521,7 +3914,7 @@ var AgentSession = class {
3521
3914
  /** The record, the boundary, `onCommitted` and the PostCompact hooks for a compacted result. */
3522
3915
  async commitCompactResult(result, request) {
3523
3916
  const timestamp = Date.now();
3524
- const boundary = {
3917
+ const recorded = {
3525
3918
  id: result.compactId,
3526
3919
  trigger: result.trigger,
3527
3920
  transcriptOrdinal: request.transcriptOrdinal,
@@ -3534,6 +3927,11 @@ var AgentSession = class {
3534
3927
  timestamp,
3535
3928
  carriedCount: result.carriedCount
3536
3929
  };
3930
+ const boundary = {
3931
+ ...recorded,
3932
+ ...result.degraded ? { degraded: true } : {},
3933
+ ...result.warning ? { warning: result.warning } : {}
3934
+ };
3537
3935
  const data = {
3538
3936
  version: 2,
3539
3937
  compactId: result.compactId,
@@ -3545,7 +3943,7 @@ var AgentSession = class {
3545
3943
  preContextTokens: result.preContextTokens,
3546
3944
  postContextTokens: result.postContextTokens,
3547
3945
  replacementHistory: result.replacementHistory,
3548
- boundary,
3946
+ boundary: recorded,
3549
3947
  throughEventRef: result.throughEventRef,
3550
3948
  summarizedCount: result.summarizedCount,
3551
3949
  retainedCount: result.retainedCount,
@@ -3651,6 +4049,15 @@ var AgentSession = class {
3651
4049
  data
3652
4050
  });
3653
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
+ };
3654
4061
  const baseLoopCallbacks = {
3655
4062
  onText: (content) => {
3656
4063
  streamedAssistantText += content;
@@ -3664,6 +4071,11 @@ var AgentSession = class {
3664
4071
  onToolCall: (toolCall) => emit({ type: "tool_use", toolCall }),
3665
4072
  onToolResult: (toolResult) => emit({ type: "tool_result", toolResult }),
3666
4073
  onNotice: (message) => emit({ type: "notice", message }),
4074
+ onGoalStatus: (event) => {
4075
+ persistRunGoal();
4076
+ emit({ type: "goal_status", event });
4077
+ },
4078
+ onAuxiliaryUsage: callbacks.onAuxiliaryUsage,
3667
4079
  onError: (error) => {
3668
4080
  emittedError = error;
3669
4081
  emit({ type: "error", error });
@@ -3767,7 +4179,7 @@ var AgentSession = class {
3767
4179
  kind: message.kind ?? "conversation",
3768
4180
  includeInContext: message.includeInContext ?? true,
3769
4181
  // A host-appended prompt stays host-written across a reload: the ledger,
3770
- // Carried Turns and memory extraction all key on it.
4182
+ // Carried Turns key on it.
3771
4183
  derivedContent: message.derivedContent
3772
4184
  }
3773
4185
  });
@@ -3798,6 +4210,7 @@ var AgentSession = class {
3798
4210
  );
3799
4211
  } finally {
3800
4212
  this.flushRunUsage(runContext.rootRunId, true);
4213
+ if (runtime.activeGoal) persistRunGoal();
3801
4214
  }
3802
4215
  const partialOutput = streamedAssistantText.length > 0 || messages.slice(effectiveHistory.length).some((message) => message.role === "assistant");
3803
4216
  const outcome = finalizeOutcome(
@@ -3847,10 +4260,10 @@ async function captureSnapshot(snapshotStore) {
3847
4260
  return { ok: false, reason: "Filesystem checkpoint storage is unavailable." };
3848
4261
  }
3849
4262
  if (snapshotStore.captureAsync) return snapshotStore.captureAsync();
3850
- return new Promise((resolve3, reject) => {
4263
+ return new Promise((resolve4, reject) => {
3851
4264
  setTimeout(() => {
3852
4265
  try {
3853
- resolve3(snapshotStore.capture());
4266
+ resolve4(snapshotStore.capture());
3854
4267
  } catch (error) {
3855
4268
  reject(error);
3856
4269
  }
@@ -4048,8 +4461,6 @@ export {
4048
4461
  SessionStore,
4049
4462
  reviewRunnerFor,
4050
4463
  fixRunnerFor,
4051
- extractJsonObject,
4052
- parseJsonObject,
4053
4464
  runHostReview,
4054
4465
  reviewReportJson,
4055
4466
  findActiveFileMention,
@@ -4058,6 +4469,8 @@ export {
4058
4469
  mentionTokenRanges,
4059
4470
  createAgentSessionSnapshot,
4060
4471
  reduceAgentSessionSnapshot,
4472
+ discardUntouchedSession,
4473
+ discardUntouchedSessions,
4061
4474
  resolveSessionBootstrap,
4062
4475
  AgentSession,
4063
4476
  toElicitationFields,
@@ -4066,4 +4479,4 @@ export {
4066
4479
  coerceElicitationValue,
4067
4480
  validateElicitationContent
4068
4481
  };
4069
- //# sourceMappingURL=chunk-RFJ6Q6LE.js.map
4482
+ //# sourceMappingURL=chunk-ENLB7NYV.js.map