@wrongstack/core 0.273.0 → 0.274.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 (64) hide show
  1. package/dist/{agent-bridge-BZ2enORi.d.ts → agent-bridge-DFo21wmY.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-ehb4xGvd.d.ts → agent-subagent-runner-BwmkIDEd.d.ts} +7 -7
  3. package/dist/{brain-BxN2k2HP.d.ts → brain-gfZX3the.d.ts} +11 -5
  4. package/dist/{provider-model-resolve-DFd3IPpw.d.ts → codex-catalog-CooZ6mOl.d.ts} +47 -4
  5. package/dist/{compactor-72ug-ZRB.d.ts → compactor-riTOds0f.d.ts} +1 -1
  6. package/dist/{config-C8IYxlO8.d.ts → config-BxcrDzri.d.ts} +60 -4
  7. package/dist/{context-Dw55zZ_Q.d.ts → context-DERiLofu.d.ts} +60 -1
  8. package/dist/coordination/index.d.ts +27 -16
  9. package/dist/coordination/index.js +1641 -1398
  10. package/dist/coordination/index.js.map +1 -1
  11. package/dist/defaults/index.d.ts +26 -26
  12. package/dist/defaults/index.js +2154 -1466
  13. package/dist/defaults/index.js.map +1 -1
  14. package/dist/execution/index.d.ts +15 -15
  15. package/dist/execution/index.js +77 -9
  16. package/dist/execution/index.js.map +1 -1
  17. package/dist/execution/prompt-enhancer.d.ts +1 -1
  18. package/dist/extension/index.d.ts +6 -6
  19. package/dist/{global-mailbox-C9dsc9Y_.d.ts → global-mailbox-Cr8TW4t_.d.ts} +1 -1
  20. package/dist/{goal-preamble-NhflDjYb.d.ts → goal-preamble-CEhROp1e.d.ts} +9 -9
  21. package/dist/{goal-store-Cx363x7Z.d.ts → goal-store-xNSVxmpV.d.ts} +1 -1
  22. package/dist/hq/index.d.ts +5 -5
  23. package/dist/{index-B7fHDt0B.d.ts → index-00KPKAlm.d.ts} +5 -5
  24. package/dist/{index-BbVprU-9.d.ts → index-pDBSBE1r.d.ts} +2 -2
  25. package/dist/index.d.ts +72 -45
  26. package/dist/index.js +2840 -1769
  27. package/dist/index.js.map +1 -1
  28. package/dist/infrastructure/index.d.ts +6 -6
  29. package/dist/kernel/index.d.ts +11 -11
  30. package/dist/kernel/index.js.map +1 -1
  31. package/dist/{mcp-servers-B6fSRNC1.d.ts → mcp-servers-BIwRiOxe.d.ts} +3 -3
  32. package/dist/models/index.d.ts +5 -5
  33. package/dist/models/index.js +40 -2
  34. package/dist/models/index.js.map +1 -1
  35. package/dist/{models-registry-4C6Wr91w.d.ts → models-registry-8OorW51H.d.ts} +1 -1
  36. package/dist/{multi-agent-coordinator-q1skFeNP.d.ts → multi-agent-coordinator-CNx48Zoz.d.ts} +1 -1
  37. package/dist/{null-fleet-bus-C9rrgQwc.d.ts → null-fleet-bus-B4ZUJYL6.d.ts} +6 -6
  38. package/dist/observability/index.d.ts +2 -2
  39. package/dist/{parallel-eternal-engine-CtXly2Sf.d.ts → parallel-eternal-engine-rsIclDqO.d.ts} +9 -9
  40. package/dist/{path-resolver-Bim6G5Jz.d.ts → path-resolver-BqU-fwzD.d.ts} +3 -3
  41. package/dist/{permission-CC7XFYWG.d.ts → permission-Dgs3v-Xq.d.ts} +1 -1
  42. package/dist/{permission-policy-cYR4RJmw.d.ts → permission-policy-Dtht2k0e.d.ts} +2 -2
  43. package/dist/{pipeline-CNVKuQDQ.d.ts → pipeline-B-dpCFYS.d.ts} +2 -2
  44. package/dist/{plan-templates-C4wXMmiM.d.ts → plan-templates-B7MxyY2o.d.ts} +58 -5
  45. package/dist/{provider-runner-BpM0mdBE.d.ts → provider-runner-DrmpBE5l.d.ts} +3 -3
  46. package/dist/{retry-policy-BV7nzeAd.d.ts → retry-policy-hYxsm10a.d.ts} +1 -1
  47. package/dist/sdd/index.d.ts +12 -10
  48. package/dist/sdd/index.js +7 -0
  49. package/dist/sdd/index.js.map +1 -1
  50. package/dist/{secret-vault-eMBKfheR.d.ts → secret-vault-DnqIFhPF.d.ts} +1 -1
  51. package/dist/security/index.d.ts +5 -5
  52. package/dist/{selector-C4ORTOid.d.ts → selector-D21RjDIg.d.ts} +1 -1
  53. package/dist/{session-event-bridge-CeNpUL9w.d.ts → session-event-bridge-Dt54CTvq.d.ts} +1 -1
  54. package/dist/{session-reader-BepLSnGL.d.ts → session-reader-CceH13Kq.d.ts} +5 -4
  55. package/dist/storage/index.d.ts +46 -12
  56. package/dist/storage/index.js +2281 -1476
  57. package/dist/storage/index.js.map +1 -1
  58. package/dist/tools/index.d.ts +2 -2
  59. package/dist/types/index.d.ts +19 -19
  60. package/dist/types/index.js +162 -42
  61. package/dist/types/index.js.map +1 -1
  62. package/dist/utils/index.d.ts +2 -2
  63. package/dist/{worktree-manager-BDuXTaWL.d.ts → worktree-manager-DUfBbKzk.d.ts} +1 -1
  64. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import * as crypto2 from 'crypto';
2
2
  import { randomBytes, createCipheriv, createDecipheriv, randomUUID, scryptSync, createHash } from 'crypto';
3
- import * as fsp2 from 'fs/promises';
3
+ import * as fsp3 from 'fs/promises';
4
4
  import { readFile, writeFile, mkdir } from 'fs/promises';
5
5
  import * as path4 from 'path';
6
6
  import { isAbsolute, join, resolve, sep } from 'path';
@@ -37,16 +37,16 @@ __export(atomic_write_exports, {
37
37
  });
38
38
  async function atomicWrite(targetPath, content, opts = {}) {
39
39
  const dir = path4.dirname(targetPath);
40
- await fsp2.mkdir(dir, { recursive: true });
40
+ await fsp3.mkdir(dir, { recursive: true });
41
41
  const tmp = path4.join(dir, `.${path4.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
42
42
  try {
43
43
  if (typeof content === "string") {
44
- await fsp2.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
44
+ await fsp3.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
45
45
  } else {
46
- await fsp2.writeFile(tmp, content, { flag: "wx" });
46
+ await fsp3.writeFile(tmp, content, { flag: "wx" });
47
47
  }
48
48
  try {
49
- const fh = await fsp2.open(tmp, "r+");
49
+ const fh = await fsp3.open(tmp, "r+");
50
50
  try {
51
51
  await fh.sync();
52
52
  } finally {
@@ -56,29 +56,29 @@ async function atomicWrite(targetPath, content, opts = {}) {
56
56
  }
57
57
  let mode;
58
58
  try {
59
- const stat6 = await fsp2.stat(targetPath);
60
- mode = stat6.mode & 511;
59
+ const stat8 = await fsp3.stat(targetPath);
60
+ mode = stat8.mode & 511;
61
61
  } catch {
62
62
  mode = opts.mode;
63
63
  }
64
64
  if (mode !== void 0) {
65
- await fsp2.chmod(tmp, mode);
65
+ await fsp3.chmod(tmp, mode);
66
66
  }
67
67
  await renameWithRetry(tmp, targetPath);
68
68
  } catch (err) {
69
69
  try {
70
- await fsp2.unlink(tmp);
70
+ await fsp3.unlink(tmp);
71
71
  } catch {
72
72
  }
73
73
  throw err;
74
74
  }
75
75
  }
76
76
  async function ensureDir(dir) {
77
- await fsp2.mkdir(dir, { recursive: true });
77
+ await fsp3.mkdir(dir, { recursive: true });
78
78
  }
79
79
  async function withFileLock(targetPath, fn, opts = {}) {
80
80
  const dir = path4.dirname(targetPath);
81
- await fsp2.mkdir(dir, { recursive: true });
81
+ await fsp3.mkdir(dir, { recursive: true });
82
82
  const lockPath = path4.join(dir, `.${path4.basename(targetPath)}.lock`);
83
83
  const timeoutMs = opts.timeoutMs ?? 5e3;
84
84
  const staleMs = opts.staleMs ?? 3e4;
@@ -86,20 +86,20 @@ async function withFileLock(targetPath, fn, opts = {}) {
86
86
  let handle;
87
87
  for (; ; ) {
88
88
  try {
89
- handle = await fsp2.open(lockPath, "wx");
89
+ handle = await fsp3.open(lockPath, "wx");
90
90
  await handle.writeFile(`${process.pid}:${Date.now()}`);
91
91
  break;
92
92
  } catch (err) {
93
93
  const code = err.code;
94
94
  if (code === "ENOENT") {
95
- await fsp2.mkdir(dir, { recursive: true });
95
+ await fsp3.mkdir(dir, { recursive: true });
96
96
  continue;
97
97
  }
98
98
  if (code !== "EEXIST") throw err;
99
99
  try {
100
- const stat6 = await fsp2.stat(lockPath);
101
- if (Date.now() - stat6.mtimeMs > staleMs) {
102
- await fsp2.unlink(lockPath);
100
+ const stat8 = await fsp3.stat(lockPath);
101
+ if (Date.now() - stat8.mtimeMs > staleMs) {
102
+ await fsp3.unlink(lockPath);
103
103
  continue;
104
104
  }
105
105
  } catch {
@@ -119,21 +119,21 @@ async function withFileLock(targetPath, fn, opts = {}) {
119
119
  } catch {
120
120
  }
121
121
  try {
122
- await fsp2.unlink(lockPath);
122
+ await fsp3.unlink(lockPath);
123
123
  } catch {
124
124
  }
125
125
  }
126
126
  }
127
127
  async function renameWithRetry(from, to) {
128
128
  if (process.platform !== "win32") {
129
- await fsp2.rename(from, to);
129
+ await fsp3.rename(from, to);
130
130
  return;
131
131
  }
132
132
  const delays = [10, 25, 60, 120, 250];
133
133
  let lastErr;
134
134
  for (let i = 0; i <= delays.length; i++) {
135
135
  try {
136
- await fsp2.rename(from, to);
136
+ await fsp3.rename(from, to);
137
137
  return;
138
138
  } catch (err) {
139
139
  lastErr = err;
@@ -181,7 +181,7 @@ function envFlag(value) {
181
181
  return !/^(0|false|no|off)$/i.test(value.trim());
182
182
  }
183
183
  var COLOR = isColorTty();
184
- var wrap = (open3, close) => (s) => COLOR ? `\x1B[${open3}m${s}\x1B[${close}m` : s;
184
+ var wrap = (open4, close) => (s) => COLOR ? `\x1B[${open4}m${s}\x1B[${close}m` : s;
185
185
  var color = {
186
186
  reset: wrap("0", "0"),
187
187
  bold: wrap("1", "22"),
@@ -772,7 +772,7 @@ async function expandGlob(pattern) {
772
772
  async function walk3(dir, pat) {
773
773
  let entries;
774
774
  try {
775
- entries = await fsp2.readdir(dir);
775
+ entries = await fsp3.readdir(dir);
776
776
  } catch {
777
777
  return;
778
778
  }
@@ -794,8 +794,8 @@ async function expandGlob(pattern) {
794
794
  for (const e of entries) {
795
795
  const full = `${dir}${SEP}${e}`;
796
796
  try {
797
- const stat6 = await fsp2.stat(full);
798
- if (stat6.isDirectory()) await walk3(full, rest);
797
+ const stat8 = await fsp3.stat(full);
798
+ if (stat8.isDirectory()) await walk3(full, rest);
799
799
  } catch {
800
800
  }
801
801
  }
@@ -812,8 +812,8 @@ async function expandGlob(pattern) {
812
812
  if (entries.includes(seg)) {
813
813
  const full = `${dir}${SEP}${seg}`;
814
814
  try {
815
- const stat6 = await fsp2.stat(full);
816
- if (stat6.isDirectory()) await walk3(full, rest);
815
+ const stat8 = await fsp3.stat(full);
816
+ if (stat8.isDirectory()) await walk3(full, rest);
817
817
  } catch {
818
818
  }
819
819
  }
@@ -1003,11 +1003,11 @@ function validateAgainstSchema(value, schema) {
1003
1003
  walk(value, schema, "", errors);
1004
1004
  return { ok: errors.length === 0, errors };
1005
1005
  }
1006
- function walk(value, schema, path23, errors) {
1006
+ function walk(value, schema, path25, errors) {
1007
1007
  if (schema.enum !== void 0) {
1008
1008
  if (!schema.enum.some((e) => deepEqual(e, value))) {
1009
1009
  errors.push({
1010
- path: path23 || "<root>",
1010
+ path: path25 || "<root>",
1011
1011
  message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
1012
1012
  });
1013
1013
  return;
@@ -1016,7 +1016,7 @@ function walk(value, schema, path23, errors) {
1016
1016
  if (typeof schema.type === "string") {
1017
1017
  if (!checkType(value, schema.type)) {
1018
1018
  errors.push({
1019
- path: path23 || "<root>",
1019
+ path: path25 || "<root>",
1020
1020
  message: `expected ${schema.type}, got ${describeType(value)}`
1021
1021
  });
1022
1022
  return;
@@ -1026,20 +1026,20 @@ function walk(value, schema, path23, errors) {
1026
1026
  const obj = value;
1027
1027
  for (const req of schema.required ?? []) {
1028
1028
  if (!(req in obj)) {
1029
- errors.push({ path: joinPath(path23, req), message: "required property missing" });
1029
+ errors.push({ path: joinPath(path25, req), message: "required property missing" });
1030
1030
  }
1031
1031
  }
1032
1032
  if (schema.properties) {
1033
1033
  for (const [key, subSchema] of Object.entries(schema.properties)) {
1034
1034
  if (key in obj) {
1035
- walk(obj[key], subSchema, joinPath(path23, key), errors);
1035
+ walk(obj[key], subSchema, joinPath(path25, key), errors);
1036
1036
  }
1037
1037
  }
1038
1038
  }
1039
1039
  }
1040
1040
  if (schema.type === "array" && Array.isArray(value) && schema.items) {
1041
1041
  for (let i = 0; i < value.length; i++) {
1042
- walk(value[i], schema.items, `${path23}[${i}]`, errors);
1042
+ walk(value[i], schema.items, `${path25}[${i}]`, errors);
1043
1043
  }
1044
1044
  }
1045
1045
  }
@@ -2205,1456 +2205,1677 @@ function resolveWstackPaths(opts) {
2205
2205
  projectStatus: (projectHash2) => path4.join(globalRoot, "projects", projectHash2, "status.json")
2206
2206
  };
2207
2207
  }
2208
- function sanitizeModel(model) {
2209
- return model.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
2210
- }
2211
- function generateSessionId(startedAt, model) {
2212
- const date = startedAt.slice(0, 10);
2213
- const time = startedAt.slice(11, 19).replace(/:/g, "-");
2214
- const suffix = randomBytes(2).toString("hex");
2215
- const modelPart = model ? `_${sanitizeModel(model)}` : "";
2216
- return `${date}/${time}Z${modelPart}_${suffix}`;
2208
+
2209
+ // src/storage/file-session-writer.ts
2210
+ init_atomic_write();
2211
+
2212
+ // src/storage/session-helpers.ts
2213
+ function userInputTitle(content) {
2214
+ const text = typeof content === "string" ? content : content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
2215
+ return (text || "(non-text input)").slice(0, 60);
2217
2216
  }
2218
2217
 
2219
- // src/storage/session-store.ts
2220
- var DefaultSessionStore = class _DefaultSessionStore {
2221
- dir;
2218
+ // src/storage/file-session-writer.ts
2219
+ var FileSessionWriter = class _FileSessionWriter {
2220
+ constructor(id, handle, startedAt, meta, events, opts = {}, traceId) {
2221
+ this.id = id;
2222
+ this.handle = handle;
2223
+ this.startedAt = startedAt;
2224
+ this.meta = meta;
2225
+ this.events = events;
2226
+ this.resumed = opts.resumed ?? false;
2227
+ this.manifestFile = opts.dir ? path4.join(opts.dir, `${path4.basename(id)}.summary.json`) : "";
2228
+ this.filePath = opts.filePath ?? "";
2229
+ this.secretScrubber = opts.secretScrubber;
2230
+ this.onCloseCb = opts.onClose;
2231
+ this.summary = {
2232
+ id,
2233
+ title: "(empty session)",
2234
+ startedAt,
2235
+ model: meta.model ?? "unknown",
2236
+ provider: meta.provider ?? "unknown",
2237
+ tokenTotal: 0
2238
+ };
2239
+ this.traceId = traceId;
2240
+ }
2241
+ id;
2242
+ handle;
2243
+ startedAt;
2244
+ meta;
2222
2245
  events;
2223
- secretScrubber;
2246
+ closed = false;
2247
+ closePromise = null;
2248
+ manifestFile;
2249
+ summary;
2250
+ tokenIn = 0;
2251
+ tokenOut = 0;
2252
+ filePath;
2253
+ get transcriptPath() {
2254
+ return this.filePath || void 0;
2255
+ }
2224
2256
  /**
2225
- * In-memory cache for load() results, keyed by session ID. The cache is
2226
- * invalidated when the file's mtimeMs or size changes (indicating the
2227
- * file was written to). This eliminates redundant full-file reads and
2228
- * JSON parses when the same session is loaded multiple times within the
2229
- * store's lifetime (e.g., webui session detail views, list() fallbacks).
2230
- *
2231
- * Max size is capped to prevent unbounded memory growth in long-running
2232
- * processes. When the limit is reached, the oldest entry is evicted.
2257
+ * Lazy session_start/session_resumed init, shared by all appenders.
2258
+ * A single promise (not a boolean) so a second append racing the first
2259
+ * can't push its event into the buffer BEFORE the first append's event —
2260
+ * every appender awaits the same init and resumes in FIFO call order.
2233
2261
  */
2234
- _loadCache = /* @__PURE__ */ new Map();
2235
- _indexCache = null;
2236
- static LOAD_CACHE_MAX_ENTRIES = 50;
2237
- static LIST_SCAN_CONCURRENCY = 32;
2238
- constructor(opts) {
2239
- this.dir = opts.dir;
2240
- this.events = opts.events;
2241
- this.secretScrubber = opts.secretScrubber;
2262
+ initPromise = null;
2263
+ ensureInit() {
2264
+ if (!this.initPromise) this.initPromise = this.writeSessionStartLazy();
2265
+ return this.initPromise;
2266
+ }
2267
+ resumed;
2268
+ appendFailCount = 0;
2269
+ lastAppendWarnAt = 0;
2270
+ secretScrubber;
2271
+ onCloseCb;
2272
+ /** Implements SessionWriter.traceId — propagated from ContextInit.traceId. */
2273
+ traceId;
2274
+ // ── Write buffer — batches events to reduce per-event disk I/O ──────────────
2275
+ //
2276
+ // Every append() pushes the scrubbed event into an in-memory buffer instead
2277
+ // of calling handle.appendFile() synchronously. The buffer flushes to disk
2278
+ // when it reaches FLUSH_SIZE events OR after FLUSH_INTERVAL_MS of inactivity.
2279
+ // This cuts the number of disk writes by ~95% without changing the on-disk
2280
+ // format — the JSONL is still one JSON object per line.
2281
+ writeBuffer = [];
2282
+ flushTimer = null;
2283
+ static FLUSH_INTERVAL_MS = 500;
2284
+ static FLUSH_SIZE = 50;
2285
+ // ── Write serialization ─────────────────────────────────────────────────────
2286
+ //
2287
+ // All disk writes are funneled through a FIFO promise chain. Without it,
2288
+ // a timer-driven flush racing an explicit flush()/close() issues two
2289
+ // concurrent appendFile() calls on the shared O_APPEND handle — the kernel
2290
+ // may complete them out of order (chronology breaks) or, for large
2291
+ // batches, interleave partial writes (torn JSONL lines). The chain keeps
2292
+ // exactly one write in flight; failures don't break the chain.
2293
+ writeChain = Promise.resolve();
2294
+ /** Enqueue a write on the FIFO chain. Resolves/rejects with that write. */
2295
+ enqueueWrite(data) {
2296
+ const write = this.writeChain.then(() => this.handle.appendFile(data, "utf8"));
2297
+ this.writeChain = write.then(
2298
+ () => void 0,
2299
+ () => void 0
2300
+ );
2301
+ return write;
2242
2302
  }
2303
+ // ── Enriched summary tracking ───────────────────────────────────────────────
2304
+ iterationCount = 0;
2305
+ toolCallCount = 0;
2306
+ toolErrorCount = 0;
2307
+ toolBreakdown = {};
2308
+ fileChangeCount = 0;
2309
+ compactionCount = 0;
2310
+ outcome = void 0;
2243
2311
  /**
2244
- * Clear the load() cache. Useful for testing or when the caller knows
2245
- * the file has changed externally (e.g., another process wrote to it).
2312
+ * Scrub secrets out of conversation-turn events before they are observed
2313
+ * for the summary, written to the JSONL log, or surfaced on resume. Only
2314
+ * `user_input` / `llm_response` carry free-form user/model text; other event
2315
+ * types either have no secret-bearing content or are already scrubbed
2316
+ * upstream (tool results). Returns the event unchanged when no scrubber is
2317
+ * configured.
2246
2318
  */
2247
- clearLoadCache(sessionId) {
2248
- if (sessionId !== void 0) {
2249
- this._loadCache.delete(sessionId);
2250
- } else {
2251
- this._loadCache.clear();
2319
+ scrubEvent(event) {
2320
+ const s = this.secretScrubber;
2321
+ if (!s) return event;
2322
+ if (event.type === "user_input") {
2323
+ return {
2324
+ ...event,
2325
+ content: typeof event.content === "string" ? s.scrub(event.content) : s.scrubObject(event.content)
2326
+ };
2327
+ }
2328
+ if (event.type === "llm_response") {
2329
+ return { ...event, content: s.scrubObject(event.content) };
2252
2330
  }
2331
+ return event;
2253
2332
  }
2254
- // ── Storage event helpers ───────────────────────────────────────────────────
2255
- emitRead(sessionId, filePath, operation, outcome, durationMs, error) {
2256
- this.events?.emit("storage.read", {
2257
- sessionId,
2258
- store: "session",
2259
- filePath,
2260
- operation,
2261
- outcome,
2262
- durationMs,
2263
- ...error !== void 0 ? { error } : {}
2264
- });
2333
+ pendingFileSnapshots = [];
2334
+ /** Tracks open tool_use IDs during the current run to serialize on close for resume. */
2335
+ openToolUses = /* @__PURE__ */ new Set();
2336
+ recordFileChange(input) {
2337
+ this.pendingFileSnapshots.push(input);
2265
2338
  }
2266
- emitWrite(sessionId, filePath, operation, outcome, durationMs, eventCount, error) {
2267
- this.events?.emit("storage.write", {
2268
- sessionId,
2269
- store: "session",
2270
- filePath,
2271
- operation,
2272
- outcome,
2273
- durationMs,
2274
- ...eventCount !== void 0 ? { eventCount } : {},
2275
- ...error !== void 0 ? { error } : {}
2276
- });
2339
+ get pendingToolUses() {
2340
+ return Array.from(this.openToolUses);
2277
2341
  }
2278
- emitError(sessionId, filePath, operation, error, recoverable) {
2279
- this.events?.emit("storage.error", {
2280
- sessionId,
2281
- store: "session",
2282
- filePath,
2283
- operation,
2284
- error,
2285
- recoverable
2286
- });
2342
+ async writeSessionStartLazy() {
2343
+ const record = `${JSON.stringify({
2344
+ type: this.resumed ? "session_resumed" : "session_start",
2345
+ ts: this.startedAt,
2346
+ id: this.id,
2347
+ model: this.meta.model ?? "unknown",
2348
+ provider: this.meta.provider ?? "unknown"
2349
+ })}
2350
+ `;
2351
+ try {
2352
+ await this.enqueueWrite(record);
2353
+ } catch {
2354
+ }
2287
2355
  }
2288
- /** Absolute path to the session index file. */
2289
- get indexFile() {
2290
- return path4.join(this.dir, "_index.jsonl");
2356
+ async append(event) {
2357
+ if (this.closed) return;
2358
+ await this.ensureInit();
2359
+ const scrubbed = this.scrubEvent(event);
2360
+ this.observeForSummary(scrubbed);
2361
+ this.writeBuffer.push(scrubbed);
2362
+ if (this.writeBuffer.length >= _FileSessionWriter.FLUSH_SIZE) {
2363
+ if (this.flushTimer) {
2364
+ clearTimeout(this.flushTimer);
2365
+ this.flushTimer = null;
2366
+ }
2367
+ await this.flushBuffer();
2368
+ } else {
2369
+ this.scheduleFlush();
2370
+ }
2291
2371
  }
2292
- /** Join session ID to its absolute path within the store directory. */
2293
- sessionPath(id, ext) {
2294
- return path4.join(this.dir, `${id}${ext}`);
2372
+ async appendBatch(events) {
2373
+ if (this.closed || events.length === 0) return;
2374
+ await this.ensureInit();
2375
+ for (const event of events) {
2376
+ const scrubbed = this.scrubEvent(event);
2377
+ this.observeForSummary(scrubbed);
2378
+ this.writeBuffer.push(scrubbed);
2379
+ }
2380
+ if (this.writeBuffer.length >= _FileSessionWriter.FLUSH_SIZE) {
2381
+ if (this.flushTimer) {
2382
+ clearTimeout(this.flushTimer);
2383
+ this.flushTimer = null;
2384
+ }
2385
+ await this.flushBuffer();
2386
+ } else {
2387
+ this.scheduleFlush();
2388
+ }
2295
2389
  }
2296
2390
  /**
2297
- * Ensure the directory implied by the session ID exists. When the ID
2298
- * contains a date prefix like `2026-06-06/...`, this creates the date
2299
- * subdirectory so sessions group naturally by day.
2391
+ * Flush buffered events to disk immediately. Critical events
2392
+ * (user_input, llm_response) call this so they survive SIGKILL/crash
2393
+ * instead of sitting in the in-memory buffer for up to 500ms.
2394
+ *
2395
+ * Idempotent — cancels any pending timer and writes whatever has
2396
+ * accumulated in the buffer. Safe to call even when the buffer
2397
+ * is empty (no-op).
2300
2398
  */
2301
- async ensureShardDir(id) {
2302
- const dirPath = path4.dirname(path4.join(this.dir, id));
2303
- await ensureDir(dirPath);
2304
- return dirPath;
2399
+ async flush() {
2400
+ if (this.flushTimer) {
2401
+ clearTimeout(this.flushTimer);
2402
+ this.flushTimer = null;
2403
+ }
2404
+ await this.flushBuffer();
2305
2405
  }
2306
- async create(meta) {
2307
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2308
- const id = meta.id && meta.id.length > 0 ? meta.id : generateSessionId(startedAt, meta.model ?? meta.provider);
2309
- const shardDir = await this.ensureShardDir(id);
2310
- const file = path4.join(shardDir, `${path4.basename(id)}.jsonl`);
2311
- const t0 = Date.now();
2312
- let handle;
2313
- try {
2314
- handle = await fsp2.open(file, "a", 384);
2315
- } catch (err) {
2316
- this.emitError(id, file, "create", toErrorMessage(err), false);
2317
- throw new Error(
2318
- `Failed to open session file: ${toErrorMessage(err)}`,
2319
- { cause: err }
2320
- );
2321
- }
2322
- try {
2323
- const writer = new FileSessionWriter(id, handle, startedAt, meta, this.events, {
2324
- dir: shardDir,
2325
- filePath: file,
2326
- secretScrubber: this.secretScrubber,
2327
- onClose: (s) => this.appendToIndex(s)
2406
+ /** Schedule a deferred flush. No-op if a timer is already pending. */
2407
+ scheduleFlush() {
2408
+ if (this.flushTimer) return;
2409
+ this.flushTimer = setTimeout(() => {
2410
+ this.flushTimer = null;
2411
+ this.flushBuffer().catch(() => {
2328
2412
  });
2329
- this.emitWrite(id, file, "create", "success", Date.now() - t0);
2330
- return writer;
2331
- } catch (err) {
2332
- await handle.close().catch((e) => console.warn(JSON.stringify({
2333
- level: "warn",
2334
- event: "session_store.handle_close_failed",
2335
- message: e instanceof Error ? e.message : String(e),
2336
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2337
- })));
2338
- this.emitError(id, file, "create", toErrorMessage(err), true);
2339
- throw err;
2340
- }
2341
- }
2342
- async resume(id) {
2343
- const file = this.sessionPath(id, ".jsonl");
2344
- const t0 = Date.now();
2345
- const data = await this.load(id);
2346
- let handle;
2347
- try {
2348
- handle = await fsp2.open(file, "a", 384);
2349
- } catch (err) {
2350
- this.emitError(id, file, "resume", toErrorMessage(err), false);
2351
- throw new Error(
2352
- `Failed to open session "${id}" for append: ${toErrorMessage(err)}`,
2353
- { cause: err }
2354
- );
2355
- }
2356
- try {
2357
- const writer = new FileSessionWriter(
2358
- id,
2359
- handle,
2360
- (/* @__PURE__ */ new Date()).toISOString(),
2361
- {
2362
- id,
2363
- model: data.metadata.model,
2364
- provider: data.metadata.provider
2365
- },
2366
- this.events,
2367
- {
2368
- resumed: true,
2369
- // Shard directory (sessions/<date>/) — must match create() so the
2370
- // .summary.json sidecar lands next to the JSONL instead of the
2371
- // sessions root (where summaryFor() would never find it).
2372
- dir: path4.dirname(file),
2373
- filePath: file,
2374
- secretScrubber: this.secretScrubber,
2375
- onClose: (s) => this.appendToIndex(s)
2376
- }
2377
- );
2378
- this.emitWrite(id, file, "resume", "success", Date.now() - t0);
2379
- return { writer, data };
2380
- } catch (err) {
2381
- await handle.close().catch((e) => console.warn(JSON.stringify({
2382
- level: "warn",
2383
- event: "session_store.handle_close_failed",
2384
- message: e instanceof Error ? e.message : String(e),
2385
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2386
- })));
2387
- this.emitError(id, file, "resume", toErrorMessage(err), true);
2388
- throw err;
2389
- }
2413
+ }, _FileSessionWriter.FLUSH_INTERVAL_MS);
2390
2414
  }
2391
- async load(id) {
2392
- const file = this.sessionPath(id, ".jsonl");
2415
+ /**
2416
+ * Flush all buffered events to disk as a single appendFile call.
2417
+ * Errors use the same throttled-warning pattern the old per-event
2418
+ * append path used — one warning every 5s with a suppressed count.
2419
+ * On failure the buffer is cleared (events are best-effort, same as
2420
+ * the old per-event path where a failed write was silently dropped).
2421
+ */
2422
+ async flushBuffer() {
2423
+ if (this.writeBuffer.length === 0) return;
2424
+ const eventCount = this.writeBuffer.length;
2425
+ const batch = this.writeBuffer.map((e) => JSON.stringify(e)).join("\n") + "\n";
2426
+ this.writeBuffer = [];
2393
2427
  const t0 = Date.now();
2394
2428
  let outcome = "success";
2395
2429
  let errorMsg;
2396
- let cacheHit = false;
2397
2430
  try {
2398
- const s = await fsp2.stat(file);
2399
- const stat6 = { mtimeMs: s.mtimeMs, size: s.size };
2400
- const cached = this._loadCache.get(id);
2401
- if (cached && cached.mtimeMs === stat6.mtimeMs && cached.size === stat6.size) {
2402
- cacheHit = true;
2403
- this._loadCache.delete(id);
2404
- this._loadCache.set(id, cached);
2405
- return cached.data;
2406
- }
2407
- const raw = await fsp2.readFile(file, "utf8");
2408
- const lines = raw.split("\n").filter((l) => l.trim());
2409
- const events = [];
2410
- let sessionStartEvent;
2411
- let sessionEndEvent;
2412
- let sessionModel;
2413
- let sessionProvider;
2414
- let sessionPendingToolUses;
2415
- const messages = [];
2416
- const openToolUses = /* @__PURE__ */ new Set();
2417
- let usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
2418
- for (const line of lines) {
2419
- try {
2420
- const parsed = JSON.parse(line);
2421
- if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
2422
- const ev = parsed;
2423
- events.push(ev);
2424
- if (ev.type === "session_start" && !sessionStartEvent) {
2425
- sessionStartEvent = ev;
2426
- sessionModel = ev.model;
2427
- sessionProvider = ev.provider;
2428
- }
2429
- if (ev.type === "session_end") {
2430
- sessionEndEvent = ev;
2431
- sessionPendingToolUses = ev.pendingToolUses;
2432
- }
2433
- if (ev.type === "user_input") {
2434
- openToolUses.clear();
2435
- messages.push({ role: "user", content: ev.content, ts: ev.ts });
2436
- } else if (ev.type === "llm_response") {
2437
- messages.push({ role: "assistant", content: ev.content, ts: ev.ts });
2438
- for (const b of ev.content) {
2439
- if (b.type === "tool_use") openToolUses.add(b.id);
2440
- }
2441
- usage = {
2442
- input: usage.input + (ev.usage.input ?? 0),
2443
- output: usage.output + (ev.usage.output ?? 0),
2444
- cacheRead: (usage.cacheRead ?? 0) + (ev.usage.cacheRead ?? 0),
2445
- cacheWrite: (usage.cacheWrite ?? 0) + (ev.usage.cacheWrite ?? 0)
2446
- };
2447
- } else if (ev.type === "tool_result") {
2448
- if (!openToolUses.has(ev.id)) {
2449
- this.events?.emit("session.damaged", {
2450
- sessionId: id,
2451
- detail: `Orphan tool_result "${ev.id}" has no matching tool_use`
2452
- });
2453
- continue;
2454
- }
2455
- openToolUses.delete(ev.id);
2456
- const resultBlock = {
2457
- type: "tool_result",
2458
- tool_use_id: ev.id,
2459
- content: typeof ev.content === "string" ? ev.content : JSON.stringify(ev.content),
2460
- is_error: ev.isError
2461
- };
2462
- const last = messages[messages.length - 1];
2463
- const lastIsToolResultUser = last?.role === "user" && Array.isArray(last.content) && last.content.every((b) => b.type === "tool_result");
2464
- if (lastIsToolResultUser && Array.isArray(last.content)) {
2465
- last.content.push(resultBlock);
2466
- } else {
2467
- messages.push({ role: "user", content: [resultBlock], ts: ev.ts });
2468
- }
2469
- }
2470
- }
2471
- } catch {
2472
- }
2473
- }
2474
- if (openToolUses.size > 0) {
2475
- this.events?.emit("session.damaged", {
2476
- sessionId: id,
2477
- detail: `${openToolUses.size} tool_use blocks without matching results - replay repaired`
2478
- });
2479
- }
2480
- const repaired = repairToolUseAdjacency(messages);
2481
- if (repaired.report.changed) {
2482
- this.events?.emit("session.damaged", {
2483
- sessionId: id,
2484
- detail: `Repaired replay adjacency: removed ${repaired.report.removedToolUses.length} tool_use, ${repaired.report.removedToolResults.length} tool_result, ${repaired.report.removedMessages} empty messages`
2485
- });
2486
- }
2487
- const meta = {
2488
- id,
2489
- startedAt: sessionStartEvent?.ts ?? (/* @__PURE__ */ new Date(0)).toISOString(),
2490
- endedAt: sessionEndEvent?.ts,
2491
- model: sessionModel,
2492
- provider: sessionProvider,
2493
- pendingToolUses: sessionPendingToolUses
2494
- };
2495
- const toolCallEnds = extractToolCallEnds(events);
2496
- const data = { metadata: meta, events, messages: repaired.messages, usage, toolCallEnds };
2497
- if (this._loadCache.size >= _DefaultSessionStore.LOAD_CACHE_MAX_ENTRIES) {
2498
- const oldest = this._loadCache.keys().next().value;
2499
- if (oldest !== void 0) {
2500
- this._loadCache.delete(oldest);
2501
- }
2502
- }
2503
- this._loadCache.set(id, { mtimeMs: stat6.mtimeMs, size: stat6.size, data });
2504
- return data;
2431
+ await this.enqueueWrite(batch);
2505
2432
  } catch (err) {
2506
2433
  outcome = "failure";
2507
2434
  errorMsg = toErrorMessage(err);
2508
- throw err;
2509
- } finally {
2510
- this.emitRead(id, file, "load", outcome, Date.now() - t0, errorMsg);
2511
- if (cacheHit) {
2512
- this.events?.emit("storage.cache_hit", {
2513
- sessionId: id,
2514
- store: "session",
2515
- filePath: file,
2516
- operation: "load",
2517
- durationMs: Date.now() - t0
2518
- });
2435
+ this.appendFailCount += eventCount;
2436
+ const now = Date.now();
2437
+ if (now - this.lastAppendWarnAt > 5e3) {
2438
+ const suppressed = this.appendFailCount - 1;
2439
+ const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
2440
+ console.warn(
2441
+ "[session] flush failed:",
2442
+ toErrorMessage(err),
2443
+ tail
2444
+ );
2445
+ this.lastAppendWarnAt = now;
2446
+ this.appendFailCount = 0;
2519
2447
  }
2448
+ } finally {
2449
+ this.events?.emit("storage.write", {
2450
+ sessionId: this.id,
2451
+ store: "session",
2452
+ filePath: this.filePath,
2453
+ operation: "flush",
2454
+ outcome,
2455
+ durationMs: Date.now() - t0,
2456
+ ...errorMsg !== void 0 ? { error: errorMsg } : {},
2457
+ ...eventCount !== void 0 ? { eventCount } : {},
2458
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
2459
+ });
2520
2460
  }
2521
2461
  }
2522
- async list(limit = 20) {
2523
- try {
2524
- await ensureDir(this.dir);
2525
- const indexed = await this.readIndex();
2526
- if (indexed.length > 0) {
2527
- indexed.sort((a, b) => {
2528
- if (a.startedAt < b.startedAt) return 1;
2529
- if (a.startedAt > b.startedAt) return -1;
2530
- return a.id.localeCompare(b.id);
2531
- });
2532
- return indexed.slice(0, limit);
2462
+ observeForSummary(event) {
2463
+ if (event.type === "llm_response") {
2464
+ for (const block of event.content) {
2465
+ if (block.type === "tool_use") this.openToolUses.add(block.id);
2533
2466
  }
2534
- return await this.listFromDirectoryScan(limit);
2535
- } catch {
2536
- return [];
2537
2467
  }
2538
- }
2539
- // ── Session index (_index.jsonl) ─────────────────────────────────────────
2540
- //
2541
- // One JSON line per closed session, appended atomically on close().
2542
- // When a session is deleted, a tombstone {action:"delete",id:"..."} is
2543
- // appended. On read, tombstones filter out matching session entries.
2544
- // This keeps listing O(lines-in-index) instead of O(files-on-disk).
2545
- //
2546
- // The index auto-compacts every N appends to prevent unbounded growth
2547
- // from tombstones and duplicate entries (resume cycles).
2548
- indexAppendCount = 0;
2549
- static COMPACT_EVERY = 30;
2550
- /** Append a session summary to the index. */
2551
- async appendToIndex(summary) {
2552
- try {
2553
- await ensureDir(this.dir);
2554
- const line = JSON.stringify(summary) + "\n";
2555
- await fsp2.appendFile(this.indexFile, line, "utf8");
2556
- this._indexCache = null;
2557
- this.indexAppendCount++;
2558
- if (this.indexAppendCount >= _DefaultSessionStore.COMPACT_EVERY) {
2559
- await this.compactIndex();
2560
- this.indexAppendCount = 0;
2468
+ if (event.type === "tool_use") {
2469
+ this.openToolUses.add(event.id);
2470
+ } else if (event.type === "tool_call_start") {
2471
+ this.toolCallCount++;
2472
+ this.toolBreakdown[event.name] = (this.toolBreakdown[event.name] ?? 0) + 1;
2473
+ } else if (event.type === "tool_result") {
2474
+ this.openToolUses.delete(event.id);
2475
+ if (event.isError) {
2476
+ this.toolErrorCount++;
2477
+ this.outcome = "error";
2561
2478
  }
2562
- } catch {
2479
+ } else if (event.type === "file_snapshot") {
2480
+ this.fileChangeCount += event.files.length;
2481
+ } else if (event.type === "compaction") {
2482
+ this.compactionCount++;
2563
2483
  }
2564
- }
2565
- /** Append a tombstone entry for a deleted session. */
2566
- async writeTombstone(id) {
2567
- try {
2568
- await ensureDir(this.dir);
2569
- const line = JSON.stringify({ action: "delete", id }) + "\n";
2570
- await fsp2.appendFile(this.indexFile, line, "utf8");
2571
- this._indexCache = null;
2572
- this.indexAppendCount++;
2573
- } catch {
2484
+ if (event.type === "error" || event.type === "provider_error") {
2485
+ this.outcome = "error";
2486
+ }
2487
+ if (event.type === "user_input" && this.summary.title === "(empty session)") {
2488
+ this.summary = { ...this.summary, title: userInputTitle(event.content) };
2489
+ } else if (event.type === "llm_response") {
2490
+ this.tokenIn += event.usage.input;
2491
+ this.tokenOut += event.usage.output;
2492
+ this.summary = { ...this.summary, tokenTotal: this.tokenIn + this.tokenOut };
2493
+ } else if (event.type === "session_end") {
2494
+ const total = event.usage.input + event.usage.output;
2495
+ if (total > 0) this.summary = { ...this.summary, tokenTotal: total };
2496
+ } else if (event.type === "in_flight_start") {
2497
+ this.iterationCount++;
2574
2498
  }
2575
2499
  }
2576
- /**
2577
- * Compact the index: read all entries, drop tombstones, deduplicate
2578
- * (keep latest per session), and rewrite. Atomic via temp+rename.
2579
- */
2580
- async compactIndex() {
2581
- const t0 = Date.now();
2582
- let outcome = "success";
2583
- let errorMsg;
2500
+ async close() {
2501
+ if (this.closePromise) return this.closePromise;
2502
+ this.closePromise = this.doClose();
2503
+ return this.closePromise;
2504
+ }
2505
+ async doClose() {
2506
+ this.closed = true;
2507
+ if (this.flushTimer) {
2508
+ clearTimeout(this.flushTimer);
2509
+ this.flushTimer = null;
2510
+ }
2511
+ await this.flushBuffer();
2512
+ await this.writeChain;
2513
+ this.summary = {
2514
+ ...this.summary,
2515
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
2516
+ iterationCount: this.iterationCount,
2517
+ toolCallCount: this.toolCallCount,
2518
+ toolErrorCount: this.toolErrorCount,
2519
+ fileChangeCount: this.fileChangeCount,
2520
+ compactionCount: this.compactionCount > 0 ? this.compactionCount : void 0,
2521
+ toolBreakdown: { ...this.toolBreakdown },
2522
+ outcome: this.outcome ?? "completed"
2523
+ };
2524
+ if (this.manifestFile) {
2525
+ const t0 = Date.now();
2526
+ let outcome = "success";
2527
+ let errorMsg;
2528
+ try {
2529
+ await atomicWrite(this.manifestFile, JSON.stringify(this.summary), { mode: 384 });
2530
+ } catch (err) {
2531
+ outcome = "failure";
2532
+ errorMsg = toErrorMessage(err);
2533
+ } finally {
2534
+ this.events?.emit("storage.write", {
2535
+ sessionId: this.id,
2536
+ store: "session",
2537
+ filePath: this.manifestFile,
2538
+ operation: "close",
2539
+ outcome,
2540
+ durationMs: Date.now() - t0,
2541
+ ...errorMsg !== void 0 ? { error: errorMsg } : {},
2542
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
2543
+ });
2544
+ }
2545
+ }
2546
+ const idxT0 = Date.now();
2547
+ let idxOutcome = "success";
2548
+ let idxError;
2584
2549
  try {
2585
- const entries = await this.readIndex();
2586
- if (entries.length === 0) return;
2587
- const tmp = `${this.indexFile}.compact.tmp`;
2588
- const lines = entries.map((s) => JSON.stringify(s)).join("\n") + "\n";
2589
- await fsp2.writeFile(tmp, lines, "utf8");
2590
- await fsp2.rename(tmp, this.indexFile);
2591
- this._indexCache = null;
2550
+ await this.onCloseCb?.(this.summary);
2592
2551
  } catch (err) {
2593
- outcome = "failure";
2594
- errorMsg = toErrorMessage(err);
2552
+ idxOutcome = "failure";
2553
+ idxError = toErrorMessage(err);
2595
2554
  } finally {
2596
- this.emitWrite("~compact~", this.indexFile, "compact", outcome, Date.now() - t0, void 0, errorMsg);
2555
+ this.events?.emit("storage.write", {
2556
+ sessionId: this.summary.id,
2557
+ store: "session",
2558
+ filePath: this.filePath,
2559
+ operation: "index_append",
2560
+ outcome: idxOutcome,
2561
+ durationMs: Date.now() - idxT0,
2562
+ ...idxError !== void 0 ? { error: idxError } : {},
2563
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
2564
+ });
2597
2565
  }
2598
- }
2599
- /**
2600
- * Read the index file and return deduplicated session summaries.
2601
- * Entries with a matching tombstone are filtered out.
2602
- * Returns empty array when the index doesn't exist or is corrupt.
2603
- */
2604
- async readIndex() {
2605
- let stat6;
2606
2566
  try {
2607
- const s = await fsp2.stat(this.indexFile);
2608
- stat6 = { mtimeMs: s.mtimeMs, size: s.size };
2567
+ await this.handle.close();
2609
2568
  } catch {
2610
- this._indexCache = null;
2611
- return [];
2612
2569
  }
2613
- if (this._indexCache !== null && this._indexCache.mtimeMs === stat6.mtimeMs && this._indexCache.size === stat6.size) {
2614
- return [...this._indexCache.summaries];
2570
+ }
2571
+ async writeCheckpoint(promptIndex, promptPreview) {
2572
+ const fileCount = this.pendingFileSnapshots.length;
2573
+ if (fileCount > 0) {
2574
+ await this.writeFileSnapshot(promptIndex, [...this.pendingFileSnapshots]);
2575
+ this.pendingFileSnapshots = [];
2615
2576
  }
2616
- let raw;
2617
- try {
2618
- raw = await fsp2.readFile(this.indexFile, "utf8");
2619
- } catch {
2620
- this._indexCache = null;
2621
- return [];
2577
+ await this.append({
2578
+ type: "checkpoint",
2579
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2580
+ promptIndex,
2581
+ promptPreview
2582
+ });
2583
+ this.events?.emit("checkpoint.written", {
2584
+ promptIndex,
2585
+ promptPreview,
2586
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2587
+ fileCount
2588
+ });
2589
+ }
2590
+ async writeFileSnapshot(promptIndex, files) {
2591
+ await this.append({
2592
+ type: "file_snapshot",
2593
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2594
+ promptIndex,
2595
+ files
2596
+ });
2597
+ }
2598
+ /**
2599
+ * Truncate the session file to the checkpoint with the given promptIndex,
2600
+ * removing all events that follow it. Uses a single-pass byte-offset scan
2601
+ * so post-checkpoint content is never read or parsed — O(1) memory instead
2602
+ * of O(N) JSON.parse calls over the full file.
2603
+ */
2604
+ async truncateToCheckpoint(targetPromptIndex) {
2605
+ if (!this.filePath) return 0;
2606
+ if (this.flushTimer) {
2607
+ clearTimeout(this.flushTimer);
2608
+ this.flushTimer = null;
2622
2609
  }
2623
- const deleted = /* @__PURE__ */ new Set();
2624
- const seen = /* @__PURE__ */ new Map();
2625
- for (const line of raw.split("\n")) {
2626
- if (!line.trim()) continue;
2627
- try {
2628
- const entry = JSON.parse(line);
2629
- if (entry.action === "delete" && entry.id) {
2630
- deleted.add(entry.id);
2631
- seen.delete(entry.id);
2632
- continue;
2610
+ await this.flushBuffer();
2611
+ await this.writeChain;
2612
+ const CHUNK_SIZE = 65536;
2613
+ let fd;
2614
+ let fileOffset = 0;
2615
+ let lineStartOffset = 0;
2616
+ let checkpointByteOffset = -1;
2617
+ let removedCount = 0;
2618
+ let targetCheckpointSeen = false;
2619
+ try {
2620
+ fd = await fsp3.open(this.filePath, "r", 384);
2621
+ while (true) {
2622
+ const buf = Buffer.alloc(CHUNK_SIZE);
2623
+ const { bytesRead } = await fd.read(buf, 0, CHUNK_SIZE, fileOffset);
2624
+ if (bytesRead === 0) break;
2625
+ let chunkPos = 0;
2626
+ while (chunkPos < bytesRead) {
2627
+ const idx = buf.indexOf("\n", chunkPos);
2628
+ if (idx === -1) {
2629
+ lineStartOffset = fileOffset + chunkPos;
2630
+ break;
2631
+ }
2632
+ if (checkpointByteOffset !== -1) {
2633
+ removedCount++;
2634
+ } else {
2635
+ const lineBytes = buf.subarray(chunkPos, idx);
2636
+ const line = new TextDecoder("utf-8", { fatal: false }).decode(lineBytes);
2637
+ if (line.trim()) {
2638
+ try {
2639
+ const event = JSON.parse(line);
2640
+ if (event.type === "checkpoint") {
2641
+ if (event.promptIndex === targetPromptIndex) {
2642
+ checkpointByteOffset = lineStartOffset;
2643
+ targetCheckpointSeen = true;
2644
+ } else if (event.promptIndex !== void 0 && event.promptIndex > targetPromptIndex) {
2645
+ checkpointByteOffset = lineStartOffset;
2646
+ }
2647
+ } else if (targetCheckpointSeen && event.promptIndex !== void 0 && event.promptIndex > targetPromptIndex) {
2648
+ removedCount++;
2649
+ } else if (targetCheckpointSeen && event.promptIndex === void 0) {
2650
+ removedCount++;
2651
+ } else if (!targetCheckpointSeen && event.promptIndex === void 0) {
2652
+ removedCount++;
2653
+ } else if (!targetCheckpointSeen && event.promptIndex !== void 0 && event.promptIndex > targetPromptIndex) {
2654
+ removedCount++;
2655
+ }
2656
+ } catch {
2657
+ }
2658
+ }
2659
+ }
2660
+ chunkPos = idx + 1;
2661
+ lineStartOffset = fileOffset + chunkPos;
2633
2662
  }
2634
- if (entry.id && !deleted.has(entry.id)) {
2635
- seen.set(entry.id, entry);
2663
+ fileOffset += bytesRead;
2664
+ if (chunkPos >= bytesRead) {
2665
+ lineStartOffset = fileOffset;
2636
2666
  }
2637
- } catch {
2638
2667
  }
2668
+ } finally {
2669
+ await fd?.close();
2639
2670
  }
2640
- const summaries = Array.from(seen.values());
2641
- this._indexCache = { ...stat6, summaries };
2642
- return [...summaries];
2643
- }
2644
- /**
2645
- * Rebuild the index from disk by scanning all sessions and writing a
2646
- * fresh _index.jsonl. Useful after manual cleanup or index corruption.
2647
- */
2648
- async rebuildIndex() {
2649
- const ids = await this.collectSessionIds(this.dir);
2650
- const summaries = await Promise.all(ids.map((id) => this.summaryFor(id).catch(() => null)));
2651
- const valid = summaries.filter((s) => s !== null);
2652
- const tmp = `${this.indexFile}.tmp`;
2653
- const lines = valid.map((s) => JSON.stringify(s)).join("\n") + "\n";
2654
- await fsp2.writeFile(tmp, lines, "utf8");
2655
- await fsp2.rename(tmp, this.indexFile);
2656
- this._indexCache = null;
2657
- return valid.length;
2658
- }
2659
- async listFromDirectoryScan(limit) {
2660
- const refs = await this.collectSessionFiles(this.dir);
2661
- const candidates = await mapWithConcurrency(
2662
- refs,
2663
- _DefaultSessionStore.LIST_SCAN_CONCURRENCY,
2664
- async (ref) => {
2665
- const manifest = await this.readSummaryManifest(ref.id);
2666
- if (manifest) return { summary: manifest, needsBackfill: false };
2667
- const summary = await this.summaryHeaderFor(ref);
2668
- return summary ? { summary, needsBackfill: true } : null;
2669
- }
2670
- );
2671
- const out = candidates.filter((s) => s !== null);
2672
- out.sort((a, b) => compareSessionSummaries(a.summary, b.summary));
2673
- const selected = out.slice(0, limit);
2674
- const summaries = await mapWithConcurrency(
2675
- selected,
2676
- Math.min(_DefaultSessionStore.LIST_SCAN_CONCURRENCY, Math.max(1, limit)),
2677
- async (candidate) => {
2678
- if (!candidate.needsBackfill) return candidate.summary;
2679
- return await this.summaryFor(candidate.summary.id).catch(() => candidate.summary);
2680
- }
2681
- );
2682
- return summaries.filter((s) => s !== null);
2683
- }
2684
- async collectSessionFiles(dir, prefix = "", depth = 0) {
2685
- let entries;
2671
+ if (checkpointByteOffset === -1) return 0;
2672
+ await this.writeChain;
2673
+ await this.handle.close();
2674
+ const tmpPath = `${this.filePath}.rewind.tmp`;
2675
+ const src = await fsp3.open(this.filePath, "r", 384);
2686
2676
  try {
2687
- entries = await fsp2.readdir(dir, { withFileTypes: true });
2688
- } catch {
2689
- return [];
2690
- }
2691
- const dirEntries = [];
2692
- const files = [];
2693
- for (const entry of entries) {
2694
- if (entry.name.startsWith(".") && entry.name !== ".wrongstack") continue;
2695
- if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments")
2696
- continue;
2697
- if (entry.isDirectory()) {
2698
- dirEntries.push(entry);
2699
- } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
2700
- if (entry.name === "_index.jsonl") continue;
2701
- const base = entry.name.replace(/\.jsonl$/, "");
2702
- const id = prefix ? `${prefix}/${base}` : base;
2703
- files.push({ id, filePath: path4.join(dir, entry.name) });
2677
+ const statResult = await src.stat();
2678
+ const totalSize = statResult.size;
2679
+ const prefixBytes = checkpointByteOffset;
2680
+ let newlineAfterCheckpoint = prefixBytes;
2681
+ if (prefixBytes < totalSize) {
2682
+ const probeBuf = Buffer.alloc(Math.min(CHUNK_SIZE, totalSize - prefixBytes));
2683
+ const { bytesRead: probeRead } = await src.read(probeBuf, 0, probeBuf.length, prefixBytes);
2684
+ if (probeRead > 0) {
2685
+ const nl = probeBuf.indexOf("\n");
2686
+ newlineAfterCheckpoint = nl !== -1 ? prefixBytes + nl + 1 : totalSize;
2687
+ }
2688
+ } else {
2689
+ newlineAfterCheckpoint = totalSize;
2704
2690
  }
2705
- }
2706
- const childFileArrays = await Promise.all(
2707
- dirEntries.map((entry) => {
2708
- const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
2709
- return this.collectSessionFiles(path4.join(dir, entry.name), childPrefix, depth + 1);
2710
- })
2711
- );
2712
- return [...childFileArrays.flat(), ...files];
2713
- }
2714
- /** Recursively collect session IDs from date-shard subdirectories.
2715
- * IDs include the date-prefix path (e.g. "2026-06-06/17-46-57Z_…").
2716
- * Skips `.jsonl`/`.summary.json` root files, dot-files, and
2717
- * sub-directories that belong to fleet/subagent sessions. */
2718
- async collectSessionIds(dir, prefix = "", depth = 0) {
2719
- let entries;
2720
- try {
2721
- entries = await fsp2.readdir(dir, { withFileTypes: true });
2722
- } catch {
2723
- return [];
2724
- }
2725
- const dirEntries = [];
2726
- const fileIds = [];
2727
- for (const entry of entries) {
2728
- if (entry.name.startsWith(".") && entry.name !== ".wrongstack") continue;
2729
- if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments")
2730
- continue;
2731
- if (entry.isDirectory()) {
2732
- dirEntries.push(entry);
2733
- } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
2734
- if (entry.name === "_index.jsonl") continue;
2735
- const base = entry.name.replace(/\.jsonl$/, "");
2736
- fileIds.push(prefix ? `${prefix}/${base}` : base);
2691
+ const writeFd = await fsp3.open(tmpPath, "w", 384);
2692
+ try {
2693
+ let readOffset = 0;
2694
+ while (readOffset < newlineAfterCheckpoint) {
2695
+ const toCopy = Math.min(CHUNK_SIZE, newlineAfterCheckpoint - readOffset);
2696
+ const copyBuf = Buffer.alloc(toCopy);
2697
+ const { bytesRead: r } = await src.read(copyBuf, 0, toCopy, readOffset);
2698
+ if (r === 0) break;
2699
+ await writeFd.write(copyBuf, 0, r);
2700
+ readOffset += r;
2701
+ }
2702
+ let tailOffset = newlineAfterCheckpoint;
2703
+ let leftover = "";
2704
+ while (tailOffset < totalSize) {
2705
+ const toRead = Math.min(CHUNK_SIZE, totalSize - tailOffset);
2706
+ const tailBuf = Buffer.alloc(toRead);
2707
+ const { bytesRead: tr } = await src.read(tailBuf, 0, toRead, tailOffset);
2708
+ if (tr === 0) break;
2709
+ const chunk = leftover + tailBuf.subarray(0, tr).toString("utf8");
2710
+ const lastNl = chunk.lastIndexOf("\n");
2711
+ if (lastNl === -1) {
2712
+ leftover = chunk;
2713
+ } else {
2714
+ for (const line of chunk.slice(0, lastNl + 1).split("\n")) {
2715
+ if (!line.trim()) continue;
2716
+ try {
2717
+ JSON.parse(line);
2718
+ } catch {
2719
+ await writeFd.write(`${line}
2720
+ `, void 0, "utf8");
2721
+ }
2722
+ }
2723
+ leftover = chunk.slice(lastNl + 1);
2724
+ }
2725
+ tailOffset += tr;
2726
+ }
2727
+ if (leftover.trim()) {
2728
+ try {
2729
+ JSON.parse(leftover);
2730
+ } catch {
2731
+ await writeFd.write(`${leftover}
2732
+ `, void 0, "utf8");
2733
+ }
2734
+ }
2735
+ } finally {
2736
+ await writeFd.close();
2737
2737
  }
2738
- }
2739
- const childIdArrays = await Promise.all(
2740
- dirEntries.map((entry) => {
2741
- const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
2742
- return this.collectSessionIds(path4.join(dir, entry.name), childPrefix, depth + 1);
2743
- })
2744
- );
2745
- return [...childIdArrays.flat(), ...fileIds];
2746
- }
2747
- async summaryFor(id) {
2748
- const manifest = this.sessionPath(id, ".summary.json");
2749
- const t0 = Date.now();
2750
- let outcome = "success";
2751
- let errorMsg;
2752
- const fromManifest = await this.readSummaryManifest(id, t0);
2753
- if (fromManifest) return fromManifest;
2754
- try {
2755
- const full = this.sessionPath(id, ".jsonl");
2756
- const stat6 = await fsp2.stat(full);
2757
- const summary = await this.summarize(id, stat6.mtime.toISOString());
2758
- await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
2759
- const msg = toErrorMessage(err);
2760
- this.emitError(id, manifest, "summary_fallback", msg, true);
2761
- console.warn(JSON.stringify({
2762
- level: "warn",
2763
- event: "session_store.manifest_write_failed",
2764
- sessionId: id,
2765
- message: msg,
2766
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2767
- }));
2768
- });
2769
- outcome = "failure";
2770
- errorMsg = "summary fallback \xE2\u20AC\u201D manifest rebuilt";
2771
- this.emitRead(id, manifest, "summary", outcome, Date.now() - t0, errorMsg);
2772
- return summary;
2738
+ await src.close();
2739
+ await fsp3.rename(tmpPath, this.filePath);
2740
+ this.handle = await fsp3.open(this.filePath, "a", 384);
2773
2741
  } catch (err) {
2774
- outcome = "failure";
2775
- errorMsg = toErrorMessage(err);
2776
- this.emitRead(id, manifest, "summary", outcome, Date.now() - t0, errorMsg);
2777
- return {
2778
- id,
2779
- title: "(damaged)",
2780
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2781
- model: "unknown",
2782
- provider: "unknown",
2783
- tokenTotal: 0
2784
- };
2742
+ await fsp3.unlink(tmpPath).catch(() => void 0);
2743
+ this.handle = await fsp3.open(this.filePath, "a", 384).catch(() => this.handle);
2744
+ throw err;
2785
2745
  }
2746
+ await this.append({
2747
+ type: "rewound",
2748
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2749
+ toPromptIndex: targetPromptIndex,
2750
+ revertedFiles: []
2751
+ });
2752
+ this.events?.emit("session.rewound", {
2753
+ toPromptIndex: targetPromptIndex,
2754
+ revertedFiles: [],
2755
+ removedEvents: removedCount
2756
+ });
2757
+ return removedCount;
2786
2758
  }
2787
- async readSummaryManifest(id, startTime = Date.now()) {
2788
- const manifest = this.sessionPath(id, ".summary.json");
2789
- try {
2790
- const raw = await fsp2.readFile(manifest, "utf8");
2791
- this.emitRead(id, manifest, "summary", "success", Date.now() - startTime);
2792
- return JSON.parse(raw);
2793
- } catch {
2794
- return null;
2759
+ async clearSession() {
2760
+ if (!this.filePath) return;
2761
+ if (this.flushTimer) {
2762
+ clearTimeout(this.flushTimer);
2763
+ this.flushTimer = null;
2795
2764
  }
2765
+ this.writeBuffer = [];
2766
+ await this.writeChain;
2767
+ const record = `${JSON.stringify({
2768
+ type: "session_start",
2769
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2770
+ id: this.id,
2771
+ model: this.meta.model ?? "unknown",
2772
+ provider: this.meta.provider ?? "unknown"
2773
+ })}
2774
+ `;
2775
+ await fsp3.writeFile(this.filePath, record, "utf8");
2796
2776
  }
2797
- async summaryHeaderFor(ref) {
2798
- let mtime = (/* @__PURE__ */ new Date(0)).toISOString();
2799
- try {
2800
- const stat6 = await fsp2.stat(ref.filePath);
2801
- if (!stat6.isFile()) {
2802
- return {
2803
- id: ref.id,
2804
- title: "(damaged)",
2805
- startedAt: stat6.mtime.toISOString(),
2806
- model: "unknown",
2807
- provider: "unknown",
2808
- tokenTotal: 0
2809
- };
2810
- }
2811
- mtime = stat6.mtime.toISOString();
2812
- } catch {
2813
- return null;
2814
- }
2815
- try {
2816
- for await (const event of this.iterSessionEvents(ref.filePath)) {
2817
- if (event.type === "session_start") {
2818
- return {
2819
- id: ref.id,
2820
- title: "(empty session)",
2821
- startedAt: event.ts,
2822
- model: event.model ?? "unknown",
2823
- provider: event.provider ?? "unknown",
2824
- tokenTotal: 0
2825
- };
2826
- }
2827
- }
2828
- return {
2829
- id: ref.id,
2830
- title: "(empty session)",
2831
- startedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
2832
- model: "unknown",
2833
- provider: "unknown",
2834
- tokenTotal: 0
2835
- };
2836
- } catch {
2837
- return {
2838
- id: ref.id,
2839
- title: "(damaged)",
2840
- startedAt: mtime,
2841
- model: "unknown",
2842
- provider: "unknown",
2843
- tokenTotal: 0
2844
- };
2777
+ /**
2778
+ * Write an in-flight marker. The agent loop should call
2779
+ * this at the start of each long-running operation; a matching
2780
+ * `clearInFlightMarker` follows on clean exit. A stale marker
2781
+ * (no end) is what `SessionRecovery.detectStale` looks for.
2782
+ */
2783
+ async writeInFlightMarker(context) {
2784
+ if (!context || context.length > 500) {
2785
+ throw new Error("In-flight context must be 1..500 chars");
2845
2786
  }
2787
+ await this.append({
2788
+ type: "in_flight_start",
2789
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2790
+ context
2791
+ });
2792
+ this.events?.emit("in_flight.started", { context, ts: (/* @__PURE__ */ new Date()).toISOString() });
2846
2793
  }
2847
2794
  /**
2848
- * Delete a session and all associated files: JSONL, summary, plan/todos
2849
- * sidecars, and the session directory (fleet.json, shared/, subagents/).
2795
+ * Close the in-flight marker. Idempotent in spirit
2796
+ * (you can call it after a successful iteration even if you
2797
+ * didn't open one this round) — but the session log records
2798
+ * every call so postmortem tooling can see "the agent finished
2799
+ * cleanly X times, then died without finishing Y".
2800
+ */
2801
+ async clearInFlightMarker(reason) {
2802
+ await this.append({
2803
+ type: "in_flight_end",
2804
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2805
+ reason
2806
+ });
2807
+ this.events?.emit("in_flight.ended", { reason, ts: (/* @__PURE__ */ new Date()).toISOString() });
2808
+ }
2809
+ };
2810
+ function sanitizeModel(model) {
2811
+ return model.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
2812
+ }
2813
+ function generateSessionId(startedAt, model) {
2814
+ const date = startedAt.slice(0, 10);
2815
+ const time = startedAt.slice(11, 19).replace(/:/g, "-");
2816
+ const suffix = randomBytes(2).toString("hex");
2817
+ const modelPart = model ? `_${sanitizeModel(model)}` : "";
2818
+ return `${date}/${time}Z${modelPart}_${suffix}`;
2819
+ }
2820
+
2821
+ // src/storage/session-store.ts
2822
+ var DefaultSessionStore = class _DefaultSessionStore {
2823
+ dir;
2824
+ events;
2825
+ secretScrubber;
2826
+ /**
2827
+ * In-memory cache for load() results, keyed by session ID. The cache is
2828
+ * invalidated when the file's mtimeMs or size changes (indicating the
2829
+ * file was written to). This eliminates redundant full-file reads and
2830
+ * JSON parses when the same session is loaded multiple times within the
2831
+ * store's lifetime (e.g., webui session detail views, list() fallbacks).
2850
2832
  *
2851
- * Individual file deletions are best-effort (logged as structured warnings),
2852
- * but a tombstone is always written so readIndex() filters this session out.
2853
- * If the session directory itself can't be removed, the error is surfaced
2854
- * to the caller so prune() can report it.
2833
+ * Max size is capped to prevent unbounded memory growth in long-running
2834
+ * processes. When the limit is reached, the oldest entry is evicted.
2855
2835
  */
2856
- async deleteSession(id) {
2857
- const jsonlPath = this.sessionPath(id, ".jsonl");
2858
- const summaryPath = this.sessionPath(id, ".summary.json");
2859
- const shardDir = path4.dirname(path4.join(this.dir, id));
2860
- const base = path4.basename(id);
2861
- const sessDir = path4.join(shardDir, base);
2862
- const deletions = [
2863
- fsp2.unlink(jsonlPath),
2864
- fsp2.unlink(summaryPath),
2865
- fsp2.unlink(path4.join(shardDir, `${base}.plan.json`)),
2866
- fsp2.unlink(path4.join(shardDir, `${base}.todos.json`))
2867
- ];
2868
- const results = await Promise.allSettled(deletions);
2869
- for (const r of results) {
2870
- if (r.status === "rejected") {
2871
- const msg = r.reason instanceof Error ? r.reason.message : String(r.reason);
2872
- if (r.reason?.code !== "ENOENT") {
2873
- console.warn(JSON.stringify({
2874
- level: "warn",
2875
- event: "session_store.delete_failed",
2876
- sessionId: id,
2877
- message: msg,
2878
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2879
- }));
2880
- }
2881
- }
2836
+ _loadCache = /* @__PURE__ */ new Map();
2837
+ _indexCache = null;
2838
+ shardManifestCache = /* @__PURE__ */ new Map();
2839
+ static LOAD_CACHE_MAX_ENTRIES = 50;
2840
+ static LIST_SCAN_CONCURRENCY = 32;
2841
+ constructor(opts) {
2842
+ this.dir = opts.dir;
2843
+ this.events = opts.events;
2844
+ this.secretScrubber = opts.secretScrubber;
2845
+ }
2846
+ /**
2847
+ * Clear the load() cache. Useful for testing or when the caller knows
2848
+ * the file has changed externally (e.g., another process wrote to it).
2849
+ */
2850
+ clearLoadCache(sessionId) {
2851
+ if (sessionId !== void 0) {
2852
+ this._loadCache.delete(sessionId);
2853
+ } else {
2854
+ this._loadCache.clear();
2882
2855
  }
2883
- await fsp2.rm(sessDir, { recursive: true, force: true }).catch((err) => {
2884
- console.warn(JSON.stringify({
2885
- level: "warn",
2886
- event: "session_store.rmdir_failed",
2887
- sessionId: id,
2888
- message: toErrorMessage(err),
2889
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2890
- }));
2856
+ }
2857
+ // ── Storage event helpers ───────────────────────────────────────────────────
2858
+ emitRead(sessionId, filePath, operation, outcome, durationMs, error) {
2859
+ this.events?.emit("storage.read", {
2860
+ sessionId,
2861
+ store: "session",
2862
+ filePath,
2863
+ operation,
2864
+ outcome,
2865
+ durationMs,
2866
+ ...error !== void 0 ? { error } : {}
2867
+ });
2868
+ }
2869
+ emitWrite(sessionId, filePath, operation, outcome, durationMs, eventCount, error) {
2870
+ this.events?.emit("storage.write", {
2871
+ sessionId,
2872
+ store: "session",
2873
+ filePath,
2874
+ operation,
2875
+ outcome,
2876
+ durationMs,
2877
+ ...eventCount !== void 0 ? { eventCount } : {},
2878
+ ...error !== void 0 ? { error } : {}
2891
2879
  });
2892
- await this.writeTombstone(id);
2893
2880
  }
2894
- async delete(id) {
2895
- await this.deleteSession(id);
2881
+ emitError(sessionId, filePath, operation, error, recoverable) {
2882
+ this.events?.emit("storage.error", {
2883
+ sessionId,
2884
+ store: "session",
2885
+ filePath,
2886
+ operation,
2887
+ error,
2888
+ recoverable
2889
+ });
2896
2890
  }
2897
- async prune(maxAgeDays = 30) {
2898
- const cutoff = Date.now() - maxAgeDays * 864e5;
2899
- let deleted = 0;
2900
- let activeSessionId = null;
2891
+ /** Absolute path to the session index file. */
2892
+ get indexFile() {
2893
+ return path4.join(this.dir, "_index.jsonl");
2894
+ }
2895
+ /** Join session ID to its absolute path within the store directory. */
2896
+ sessionPath(id, ext) {
2897
+ return path4.join(this.dir, `${id}${ext}`);
2898
+ }
2899
+ shardManifestPath(shardKey) {
2900
+ return shardKey ? path4.join(this.dir, shardKey, "_manifest.json") : path4.join(this.dir, "_manifest.json");
2901
+ }
2902
+ shardKeyForSessionId(id) {
2903
+ const dirName = path4.dirname(id);
2904
+ return dirName === "." ? "" : dirName;
2905
+ }
2906
+ invalidateShardManifestBySessionId(id) {
2907
+ this.shardManifestCache.delete(this.shardKeyForSessionId(id));
2908
+ }
2909
+ /**
2910
+ * Ensure the directory implied by the session ID exists. When the ID
2911
+ * contains a date prefix like `2026-06-06/...`, this creates the date
2912
+ * subdirectory so sessions group naturally by day.
2913
+ */
2914
+ async ensureShardDir(id) {
2915
+ const dirPath = path4.dirname(path4.join(this.dir, id));
2916
+ await ensureDir(dirPath);
2917
+ return dirPath;
2918
+ }
2919
+ async create(meta) {
2920
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2921
+ const id = meta.id && meta.id.length > 0 ? meta.id : generateSessionId(startedAt, meta.model ?? meta.provider);
2922
+ const shardDir = await this.ensureShardDir(id);
2923
+ const file = path4.join(shardDir, `${path4.basename(id)}.jsonl`);
2924
+ const t0 = Date.now();
2925
+ let handle;
2901
2926
  try {
2902
- const raw = await fsp2.readFile(path4.join(this.dir, "active.json"), "utf8");
2903
- const active = JSON.parse(raw);
2904
- activeSessionId = active.sessionId ?? null;
2905
- } catch {
2927
+ handle = await fsp3.open(file, "a", 384);
2928
+ } catch (err) {
2929
+ this.emitError(id, file, "create", toErrorMessage(err), false);
2930
+ throw new Error(
2931
+ `Failed to open session file: ${toErrorMessage(err)}`,
2932
+ { cause: err }
2933
+ );
2906
2934
  }
2907
- const isPrunableJsonl = (name) => name.endsWith(".jsonl") && name !== "_index.jsonl" && name !== "_mailbox.jsonl" && !name.endsWith(".replay.jsonl") && !name.endsWith(".audit.jsonl");
2908
- const pruneFile = async (dir, name, prefix) => {
2909
- const jsonlPath = path4.join(dir, name);
2910
- try {
2911
- const stat6 = await fsp2.stat(jsonlPath);
2912
- if (stat6.mtimeMs >= cutoff) return;
2913
- } catch {
2914
- return;
2915
- }
2916
- const base = name.replace(/\.jsonl$/, "");
2917
- const id = prefix ? `${prefix}/${base}` : base;
2918
- if (activeSessionId && id === activeSessionId) return;
2919
- await this.deleteSession(id);
2920
- deleted++;
2921
- };
2922
- const entries = await fsp2.readdir(this.dir, { withFileTypes: true }).catch(() => []);
2923
- for (const entry of entries) {
2924
- if (entry.isFile()) {
2925
- if (isPrunableJsonl(entry.name)) await pruneFile(this.dir, entry.name, "");
2926
- continue;
2927
- }
2928
- if (!entry.isDirectory()) continue;
2929
- const dateDir = path4.join(this.dir, entry.name);
2930
- const files = await fsp2.readdir(dateDir, { withFileTypes: true }).catch(() => []);
2931
- for (const file of files) {
2932
- if (!file.isFile() || !isPrunableJsonl(file.name)) continue;
2933
- await pruneFile(dateDir, file.name, entry.name);
2934
- }
2935
+ try {
2936
+ const writer = new FileSessionWriter(id, handle, startedAt, meta, this.events, {
2937
+ dir: shardDir,
2938
+ filePath: file,
2939
+ secretScrubber: this.secretScrubber,
2940
+ onClose: (s) => this.appendToIndex(s)
2941
+ });
2942
+ this.emitWrite(id, file, "create", "success", Date.now() - t0);
2943
+ return writer;
2944
+ } catch (err) {
2945
+ await handle.close().catch((e) => console.warn(JSON.stringify({
2946
+ level: "warn",
2947
+ event: "session_store.handle_close_failed",
2948
+ message: e instanceof Error ? e.message : String(e),
2949
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2950
+ })));
2951
+ this.emitError(id, file, "create", toErrorMessage(err), true);
2952
+ throw err;
2935
2953
  }
2936
- if (deleted > 0) {
2937
- await this.compactIndex().catch(() => void 0);
2954
+ }
2955
+ async resume(id) {
2956
+ const file = this.sessionPath(id, ".jsonl");
2957
+ const t0 = Date.now();
2958
+ const data = await this.load(id);
2959
+ let handle;
2960
+ try {
2961
+ handle = await fsp3.open(file, "a", 384);
2962
+ } catch (err) {
2963
+ this.emitError(id, file, "resume", toErrorMessage(err), false);
2964
+ throw new Error(
2965
+ `Failed to open session "${id}" for append: ${toErrorMessage(err)}`,
2966
+ { cause: err }
2967
+ );
2938
2968
  }
2939
- for (const entry of entries) {
2940
- if (!entry.isDirectory()) continue;
2941
- const dateDir = path4.join(this.dir, entry.name);
2942
- try {
2943
- const remaining = await fsp2.readdir(dateDir);
2944
- if (remaining.length === 0) {
2945
- await fsp2.rmdir(dateDir).catch(() => void 0);
2969
+ try {
2970
+ const writer = new FileSessionWriter(
2971
+ id,
2972
+ handle,
2973
+ (/* @__PURE__ */ new Date()).toISOString(),
2974
+ {
2975
+ id,
2976
+ model: data.metadata.model,
2977
+ provider: data.metadata.provider
2978
+ },
2979
+ this.events,
2980
+ {
2981
+ resumed: true,
2982
+ // Shard directory (sessions/<date>/) — must match create() so the
2983
+ // .summary.json sidecar lands next to the JSONL instead of the
2984
+ // sessions root (where summaryFor() would never find it).
2985
+ dir: path4.dirname(file),
2986
+ filePath: file,
2987
+ secretScrubber: this.secretScrubber,
2988
+ onClose: (s) => this.appendToIndex(s)
2946
2989
  }
2947
- } catch {
2948
- }
2990
+ );
2991
+ this.emitWrite(id, file, "resume", "success", Date.now() - t0);
2992
+ return { writer, data };
2993
+ } catch (err) {
2994
+ await handle.close().catch((e) => console.warn(JSON.stringify({
2995
+ level: "warn",
2996
+ event: "session_store.handle_close_failed",
2997
+ message: e instanceof Error ? e.message : String(e),
2998
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2999
+ })));
3000
+ this.emitError(id, file, "resume", toErrorMessage(err), true);
3001
+ throw err;
2949
3002
  }
2950
- return deleted;
2951
3003
  }
2952
- async clearHistory(id) {
2953
- await this.ensureShardDir(id);
3004
+ async load(id) {
2954
3005
  const file = this.sessionPath(id, ".jsonl");
2955
- const meta = this.sessionPath(id, ".summary.json");
2956
- const record = `${JSON.stringify({
2957
- type: "session_start",
2958
- ts: (/* @__PURE__ */ new Date()).toISOString(),
2959
- id,
2960
- model: "unknown",
2961
- provider: "unknown"
2962
- })}
2963
- `;
2964
- await fsp2.writeFile(file, record, "utf8");
2965
- await fsp2.unlink(meta).catch(() => void 0);
2966
- }
2967
- async summarize(id, mtime) {
3006
+ const t0 = Date.now();
3007
+ let outcome = "success";
3008
+ let errorMsg;
3009
+ let cacheHit = false;
2968
3010
  try {
2969
- const file = this.sessionPath(id, ".jsonl");
2970
- let title = "(empty session)";
2971
- let startedAt = (/* @__PURE__ */ new Date(0)).toISOString();
2972
- let endedAt;
2973
- let model = "unknown";
2974
- let provider = "unknown";
2975
- let tokenIn = 0;
2976
- let tokenOut = 0;
2977
- let iterationCount = 0;
2978
- let toolCallCount = 0;
2979
- let toolErrorCount = 0;
2980
- let fileChangeCount = 0;
2981
- const toolBreakdown = {};
2982
- let outcome;
2983
- let lastEventType;
2984
- let hasError = false;
2985
- let sawStart = false;
2986
- for await (const e of this.iterSessionEvents(file)) {
2987
- lastEventType = e.type;
2988
- if (e.type === "session_start") {
2989
- if (!sawStart) {
2990
- sawStart = true;
2991
- startedAt = e.ts;
2992
- model = e.model ?? "unknown";
2993
- provider = e.provider ?? "unknown";
3011
+ const s = await fsp3.stat(file);
3012
+ const stat8 = { mtimeMs: s.mtimeMs, size: s.size };
3013
+ const cached = this._loadCache.get(id);
3014
+ if (cached && cached.mtimeMs === stat8.mtimeMs && cached.size === stat8.size) {
3015
+ cacheHit = true;
3016
+ this._loadCache.delete(id);
3017
+ this._loadCache.set(id, cached);
3018
+ return cached.data;
3019
+ }
3020
+ const raw = await fsp3.readFile(file, "utf8");
3021
+ const lines = raw.split("\n").filter((l) => l.trim());
3022
+ const events = [];
3023
+ let sessionStartEvent;
3024
+ let sessionEndEvent;
3025
+ let sessionModel;
3026
+ let sessionProvider;
3027
+ let sessionPendingToolUses;
3028
+ const messages = [];
3029
+ const openToolUses = /* @__PURE__ */ new Set();
3030
+ let usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
3031
+ for (const line of lines) {
3032
+ try {
3033
+ const parsed = JSON.parse(line);
3034
+ if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
3035
+ const ev = parsed;
3036
+ events.push(ev);
3037
+ if (ev.type === "session_start" && !sessionStartEvent) {
3038
+ sessionStartEvent = ev;
3039
+ sessionModel = ev.model;
3040
+ sessionProvider = ev.provider;
3041
+ }
3042
+ if (ev.type === "session_end") {
3043
+ sessionEndEvent = ev;
3044
+ sessionPendingToolUses = ev.pendingToolUses;
3045
+ }
3046
+ if (ev.type === "user_input") {
3047
+ openToolUses.clear();
3048
+ messages.push({ role: "user", content: ev.content, ts: ev.ts });
3049
+ } else if (ev.type === "llm_response") {
3050
+ messages.push({ role: "assistant", content: ev.content, ts: ev.ts });
3051
+ for (const b of ev.content) {
3052
+ if (b.type === "tool_use") openToolUses.add(b.id);
3053
+ }
3054
+ usage = {
3055
+ input: usage.input + (ev.usage.input ?? 0),
3056
+ output: usage.output + (ev.usage.output ?? 0),
3057
+ cacheRead: (usage.cacheRead ?? 0) + (ev.usage.cacheRead ?? 0),
3058
+ cacheWrite: (usage.cacheWrite ?? 0) + (ev.usage.cacheWrite ?? 0)
3059
+ };
3060
+ } else if (ev.type === "tool_result") {
3061
+ if (!openToolUses.has(ev.id)) {
3062
+ this.events?.emit("session.damaged", {
3063
+ sessionId: id,
3064
+ detail: `Orphan tool_result "${ev.id}" has no matching tool_use`
3065
+ });
3066
+ continue;
3067
+ }
3068
+ openToolUses.delete(ev.id);
3069
+ const resultBlock = {
3070
+ type: "tool_result",
3071
+ tool_use_id: ev.id,
3072
+ content: typeof ev.content === "string" ? ev.content : JSON.stringify(ev.content),
3073
+ is_error: ev.isError
3074
+ };
3075
+ const last = messages[messages.length - 1];
3076
+ const lastIsToolResultUser = last?.role === "user" && Array.isArray(last.content) && last.content.every((b) => b.type === "tool_result");
3077
+ if (lastIsToolResultUser && Array.isArray(last.content)) {
3078
+ last.content.push(resultBlock);
3079
+ } else {
3080
+ messages.push({ role: "user", content: [resultBlock], ts: ev.ts });
3081
+ }
3082
+ }
2994
3083
  }
2995
- } else if (e.type === "session_end") {
2996
- endedAt = e.ts;
2997
- } else if (e.type === "user_input") {
2998
- if (title === "(empty session)") title = userInputTitle(e.content);
2999
- } else if (e.type === "llm_response") {
3000
- tokenIn += e.usage.input ?? 0;
3001
- tokenOut += e.usage.output ?? 0;
3002
- } else if (e.type === "in_flight_start") iterationCount++;
3003
- else if (e.type === "tool_call_start") {
3004
- toolCallCount++;
3005
- toolBreakdown[e.name] = (toolBreakdown[e.name] ?? 0) + 1;
3006
- } else if (e.type === "tool_result" && e.isError) toolErrorCount++;
3007
- else if (e.type === "file_snapshot") fileChangeCount += e.files.length;
3008
- else if (e.type === "error" || e.type === "provider_error") hasError = true;
3084
+ } catch {
3085
+ }
3009
3086
  }
3010
- if (lastEventType === "session_end") {
3011
- outcome = "completed";
3012
- } else if (lastEventType === "in_flight_start") {
3013
- outcome = "aborted";
3014
- } else if (hasError) {
3015
- outcome = "error";
3087
+ if (openToolUses.size > 0) {
3088
+ this.events?.emit("session.damaged", {
3089
+ sessionId: id,
3090
+ detail: `${openToolUses.size} tool_use blocks without matching results - replay repaired`
3091
+ });
3016
3092
  }
3017
- return {
3018
- id,
3019
- title,
3020
- startedAt,
3021
- endedAt,
3022
- model,
3023
- provider,
3024
- tokenTotal: tokenIn + tokenOut,
3025
- iterationCount: iterationCount > 0 ? iterationCount : void 0,
3026
- toolCallCount: toolCallCount > 0 ? toolCallCount : void 0,
3027
- toolErrorCount: toolErrorCount > 0 ? toolErrorCount : void 0,
3028
- fileChangeCount: fileChangeCount > 0 ? fileChangeCount : void 0,
3029
- toolBreakdown: Object.keys(toolBreakdown).length > 0 ? toolBreakdown : {},
3030
- outcome
3031
- };
3032
- } catch {
3033
- return {
3093
+ const repaired = repairToolUseAdjacency(messages);
3094
+ if (repaired.report.changed) {
3095
+ this.events?.emit("session.damaged", {
3096
+ sessionId: id,
3097
+ detail: `Repaired replay adjacency: removed ${repaired.report.removedToolUses.length} tool_use, ${repaired.report.removedToolResults.length} tool_result, ${repaired.report.removedMessages} empty messages`
3098
+ });
3099
+ }
3100
+ const meta = {
3034
3101
  id,
3035
- title: "(damaged)",
3036
- startedAt: mtime,
3037
- model: "unknown",
3038
- provider: "unknown",
3039
- tokenTotal: 0
3102
+ startedAt: sessionStartEvent?.ts ?? (/* @__PURE__ */ new Date(0)).toISOString(),
3103
+ endedAt: sessionEndEvent?.ts,
3104
+ model: sessionModel,
3105
+ provider: sessionProvider,
3106
+ pendingToolUses: sessionPendingToolUses
3040
3107
  };
3108
+ const toolCallEnds = extractToolCallEnds(events);
3109
+ const data = { metadata: meta, events, messages: repaired.messages, usage, toolCallEnds };
3110
+ if (this._loadCache.size >= _DefaultSessionStore.LOAD_CACHE_MAX_ENTRIES) {
3111
+ const oldest = this._loadCache.keys().next().value;
3112
+ if (oldest !== void 0) {
3113
+ this._loadCache.delete(oldest);
3114
+ }
3115
+ }
3116
+ this._loadCache.set(id, { mtimeMs: stat8.mtimeMs, size: stat8.size, data });
3117
+ return data;
3118
+ } catch (err) {
3119
+ outcome = "failure";
3120
+ errorMsg = toErrorMessage(err);
3121
+ throw err;
3122
+ } finally {
3123
+ this.emitRead(id, file, "load", outcome, Date.now() - t0, errorMsg);
3124
+ if (cacheHit) {
3125
+ this.events?.emit("storage.cache_hit", {
3126
+ sessionId: id,
3127
+ store: "session",
3128
+ filePath: file,
3129
+ operation: "load",
3130
+ durationMs: Date.now() - t0
3131
+ });
3132
+ }
3041
3133
  }
3042
3134
  }
3043
- async *iterSessionEvents(file) {
3044
- const stream = createReadStream(file, { encoding: "utf8" });
3045
- const lines = createInterface({ input: stream, crlfDelay: Infinity });
3135
+ /**
3136
+ * Streaming search over a session's JSONL. Walks the file once, parses
3137
+ * each event lazily, and yields only the events that match `predicate`.
3138
+ * Stops as soon as `opts.limit` matches are collected.
3139
+ *
3140
+ * Why this exists: `load()` parses the entire file into memory and
3141
+ * rebuilds `messages`/`toolCallEnds` for every caller. `search()` only
3142
+ * needs to know which events contain matching text — a per-line
3143
+ * predicate is enough. The full parse work (and the `_loadCache` poll)
3144
+ * is wasted in that case.
3145
+ *
3146
+ * Memory: O(hits) regardless of file size. Disk: one linear scan,
3147
+ * terminated at `limit` if the caller asked for one.
3148
+ *
3149
+ * Errors: missing file yields []. Corrupt lines are skipped (same
3150
+ * policy as `load()`). Aborting via `signal` rejects with `AbortError`.
3151
+ */
3152
+ async searchEvents(id, predicate, opts) {
3153
+ const file = this.sessionPath(id, ".jsonl");
3154
+ const limit = opts?.limit;
3155
+ const signal = opts?.signal;
3156
+ const out = [];
3157
+ let stat8;
3046
3158
  try {
3047
- for await (const line of lines) {
3048
- if (!line.trim()) continue;
3159
+ stat8 = await fsp3.stat(file);
3160
+ } catch (err) {
3161
+ if (err.code === "ENOENT") return [];
3162
+ throw err;
3163
+ }
3164
+ if (stat8.size === 0) return [];
3165
+ let fh;
3166
+ try {
3167
+ fh = await fsp3.open(file, "r");
3168
+ const CHUNK = 64 * 1024;
3169
+ const buf = Buffer.alloc(CHUNK);
3170
+ let leftover = "";
3171
+ let eventIndex = 0;
3172
+ for (let position = 0; ; position += buf.byteLength) {
3173
+ if (signal?.aborted) {
3174
+ const reason = signal.reason ?? new DOMException("Aborted", "AbortError");
3175
+ throw reason;
3176
+ }
3177
+ const { bytesRead } = await fh.read(buf, 0, CHUNK, position);
3178
+ if (bytesRead === 0) break;
3179
+ const text = leftover + buf.subarray(0, bytesRead).toString("utf8");
3180
+ const parts = text.split("\n");
3181
+ leftover = parts.pop() ?? "";
3182
+ for (const line of parts) {
3183
+ if (!line) continue;
3184
+ let ev;
3185
+ try {
3186
+ const parsed = JSON.parse(line);
3187
+ if (parsed === null || typeof parsed !== "object" || typeof parsed.type !== "string" || typeof parsed.ts !== "string") {
3188
+ continue;
3189
+ }
3190
+ ev = parsed;
3191
+ } catch {
3192
+ continue;
3193
+ }
3194
+ if (predicate(ev, eventIndex, ev.ts)) {
3195
+ out.push({ event: ev, eventIndex, ts: ev.ts });
3196
+ if (limit !== void 0 && out.length >= limit) {
3197
+ return out;
3198
+ }
3199
+ }
3200
+ eventIndex++;
3201
+ }
3202
+ }
3203
+ if (leftover.trim()) {
3049
3204
  try {
3050
- const parsed = JSON.parse(line);
3205
+ const parsed = JSON.parse(leftover);
3051
3206
  if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
3052
- yield parsed;
3207
+ const ev = parsed;
3208
+ if (predicate(ev, eventIndex, ev.ts)) {
3209
+ out.push({ event: ev, eventIndex, ts: ev.ts });
3210
+ }
3053
3211
  }
3054
3212
  } catch {
3055
3213
  }
3056
3214
  }
3215
+ return out;
3057
3216
  } finally {
3058
- lines.close();
3059
- stream.destroy();
3217
+ if (fh) await fh.close().catch(() => void 0);
3060
3218
  }
3061
3219
  }
3062
- };
3063
- function extractToolCallEnds(events) {
3064
- const result = [];
3065
- for (const e of events) {
3066
- if (e.type === "tool_call_end") {
3067
- result.push({
3068
- name: e.name,
3069
- id: e.id,
3070
- durationMs: e.durationMs,
3071
- ok: e.ok ?? false,
3072
- outputBytes: e.outputBytes,
3073
- outputTokens: e.outputTokens,
3074
- outputLines: e.outputLines
3220
+ async list(limit = 20) {
3221
+ try {
3222
+ await ensureDir(this.dir);
3223
+ const indexed = await this.readIndex();
3224
+ if (indexed.length > 0) {
3225
+ indexed.sort((a, b) => {
3226
+ if (a.startedAt < b.startedAt) return 1;
3227
+ if (a.startedAt > b.startedAt) return -1;
3228
+ return a.id.localeCompare(b.id);
3229
+ });
3230
+ return indexed.slice(0, limit);
3231
+ }
3232
+ return await this.listFromDirectoryScan(limit);
3233
+ } catch {
3234
+ return [];
3235
+ }
3236
+ }
3237
+ /**
3238
+ * List sessions matching filter criteria, using the cached index.
3239
+ * Filters are applied BEFORE sorting and slicing, so the caller gets
3240
+ * exactly `limit` matching sessions — not a slice of a larger fetch.
3241
+ *
3242
+ * This avoids the DefaultSessionReader pattern of fetching 1000 sessions
3243
+ * then linear-filtering: the index is already in memory (readIndex
3244
+ * caches it), and the filter runs over the cached array without any
3245
+ * additional disk I/O.
3246
+ */
3247
+ async listFiltered(criteria) {
3248
+ const limit = criteria.limit ?? 100;
3249
+ try {
3250
+ await ensureDir(this.dir);
3251
+ const indexed = await this.readIndex();
3252
+ if (indexed.length === 0) {
3253
+ const raw = await this.list(Math.max(limit, 100));
3254
+ return raw.filter((s) => matchesSessionFilter(s, criteria)).slice(0, limit);
3255
+ }
3256
+ const filtered = indexed.filter((s) => matchesSessionFilter(s, criteria));
3257
+ filtered.sort((a, b) => {
3258
+ if (a.startedAt < b.startedAt) return 1;
3259
+ if (a.startedAt > b.startedAt) return -1;
3260
+ return a.id.localeCompare(b.id);
3075
3261
  });
3262
+ return filtered.slice(0, limit);
3263
+ } catch {
3264
+ return [];
3265
+ }
3266
+ }
3267
+ // ── Session index (_index.jsonl) ─────────────────────────────────────────
3268
+ //
3269
+ // One JSON line per closed session, appended atomically on close().
3270
+ // When a session is deleted, a tombstone {action:"delete",id:"..."} is
3271
+ // appended. On read, tombstones filter out matching session entries.
3272
+ // This keeps listing O(lines-in-index) instead of O(files-on-disk).
3273
+ //
3274
+ // The index auto-compacts every N appends to prevent unbounded growth
3275
+ // from tombstones and duplicate entries (resume cycles).
3276
+ indexAppendCount = 0;
3277
+ static COMPACT_EVERY = 30;
3278
+ /** Append a session summary to the index. */
3279
+ async appendToIndex(summary) {
3280
+ try {
3281
+ await ensureDir(this.dir);
3282
+ const line = JSON.stringify(summary) + "\n";
3283
+ await fsp3.appendFile(this.indexFile, line, "utf8");
3284
+ this._indexCache = null;
3285
+ this.invalidateShardManifestBySessionId(summary.id);
3286
+ this.indexAppendCount++;
3287
+ if (this.indexAppendCount >= _DefaultSessionStore.COMPACT_EVERY) {
3288
+ await this.compactIndex();
3289
+ this.indexAppendCount = 0;
3290
+ }
3291
+ } catch {
3292
+ }
3293
+ }
3294
+ /** Append a tombstone entry for a deleted session. */
3295
+ async writeTombstone(id) {
3296
+ try {
3297
+ await ensureDir(this.dir);
3298
+ const line = JSON.stringify({ action: "delete", id }) + "\n";
3299
+ await fsp3.appendFile(this.indexFile, line, "utf8");
3300
+ this._indexCache = null;
3301
+ this.invalidateShardManifestBySessionId(id);
3302
+ this.indexAppendCount++;
3303
+ } catch {
3304
+ }
3305
+ }
3306
+ /**
3307
+ * Compact the index: read all entries, drop tombstones, deduplicate
3308
+ * (keep latest per session), and rewrite. Atomic via temp+rename.
3309
+ */
3310
+ async compactIndex() {
3311
+ const t0 = Date.now();
3312
+ let outcome = "success";
3313
+ let errorMsg;
3314
+ try {
3315
+ const entries = await this.readIndex();
3316
+ if (entries.length === 0) return;
3317
+ const tmp = `${this.indexFile}.compact.tmp`;
3318
+ const lines = entries.map((s) => JSON.stringify(s)).join("\n") + "\n";
3319
+ await fsp3.writeFile(tmp, lines, "utf8");
3320
+ await fsp3.rename(tmp, this.indexFile);
3321
+ this._indexCache = null;
3322
+ } catch (err) {
3323
+ outcome = "failure";
3324
+ errorMsg = toErrorMessage(err);
3325
+ } finally {
3326
+ this.emitWrite("~compact~", this.indexFile, "compact", outcome, Date.now() - t0, void 0, errorMsg);
3327
+ }
3328
+ }
3329
+ /**
3330
+ * Read the index file and return deduplicated session summaries.
3331
+ * Entries with a matching tombstone are filtered out.
3332
+ * Returns empty array when the index doesn't exist or is corrupt.
3333
+ */
3334
+ async readIndex() {
3335
+ let stat8;
3336
+ try {
3337
+ const s = await fsp3.stat(this.indexFile);
3338
+ stat8 = { mtimeMs: s.mtimeMs, size: s.size };
3339
+ } catch {
3340
+ this._indexCache = null;
3341
+ return [];
3342
+ }
3343
+ if (this._indexCache !== null && this._indexCache.mtimeMs === stat8.mtimeMs && this._indexCache.size === stat8.size) {
3344
+ return [...this._indexCache.summaries];
3345
+ }
3346
+ let raw;
3347
+ try {
3348
+ raw = await fsp3.readFile(this.indexFile, "utf8");
3349
+ } catch {
3350
+ this._indexCache = null;
3351
+ return [];
3352
+ }
3353
+ const deleted = /* @__PURE__ */ new Set();
3354
+ const seen = /* @__PURE__ */ new Map();
3355
+ for (const line of raw.split("\n")) {
3356
+ if (!line.trim()) continue;
3357
+ try {
3358
+ const entry = JSON.parse(line);
3359
+ if (entry.action === "delete" && entry.id) {
3360
+ deleted.add(entry.id);
3361
+ seen.delete(entry.id);
3362
+ continue;
3363
+ }
3364
+ if (entry.id && !deleted.has(entry.id)) {
3365
+ seen.set(entry.id, entry);
3366
+ }
3367
+ } catch {
3368
+ }
3076
3369
  }
3077
- }
3078
- return result;
3079
- }
3080
- var FileSessionWriter = class _FileSessionWriter {
3081
- constructor(id, handle, startedAt, meta, events, opts = {}, traceId) {
3082
- this.id = id;
3083
- this.handle = handle;
3084
- this.startedAt = startedAt;
3085
- this.meta = meta;
3086
- this.events = events;
3087
- this.resumed = opts.resumed ?? false;
3088
- this.manifestFile = opts.dir ? path4.join(opts.dir, `${path4.basename(id)}.summary.json`) : "";
3089
- this.filePath = opts.filePath ?? "";
3090
- this.secretScrubber = opts.secretScrubber;
3091
- this.onCloseCb = opts.onClose;
3092
- this.summary = {
3093
- id,
3094
- title: "(empty session)",
3095
- startedAt,
3096
- model: meta.model ?? "unknown",
3097
- provider: meta.provider ?? "unknown",
3098
- tokenTotal: 0
3099
- };
3100
- this.traceId = traceId;
3101
- }
3102
- id;
3103
- handle;
3104
- startedAt;
3105
- meta;
3106
- events;
3107
- closed = false;
3108
- closePromise = null;
3109
- manifestFile;
3110
- summary;
3111
- tokenIn = 0;
3112
- tokenOut = 0;
3113
- filePath;
3114
- get transcriptPath() {
3115
- return this.filePath || void 0;
3370
+ const summaries = Array.from(seen.values());
3371
+ this._indexCache = { ...stat8, summaries };
3372
+ return [...summaries];
3116
3373
  }
3117
3374
  /**
3118
- * Lazy session_start/session_resumed init, shared by all appenders.
3119
- * A single promise (not a boolean) so a second append racing the first
3120
- * can't push its event into the buffer BEFORE the first append's event —
3121
- * every appender awaits the same init and resumes in FIFO call order.
3375
+ * Rebuild the index from disk by scanning all sessions and writing a
3376
+ * fresh _index.jsonl. Useful after manual cleanup or index corruption.
3122
3377
  */
3123
- initPromise = null;
3124
- ensureInit() {
3125
- if (!this.initPromise) this.initPromise = this.writeSessionStartLazy();
3126
- return this.initPromise;
3378
+ async rebuildIndex() {
3379
+ const ids = await this.collectSessionIds(this.dir);
3380
+ const summaries = await Promise.all(ids.map((id) => this.summaryFor(id).catch(() => null)));
3381
+ const valid = summaries.filter((s) => s !== null);
3382
+ const tmp = `${this.indexFile}.tmp`;
3383
+ const lines = valid.map((s) => JSON.stringify(s)).join("\n") + "\n";
3384
+ await fsp3.writeFile(tmp, lines, "utf8");
3385
+ await fsp3.rename(tmp, this.indexFile);
3386
+ this._indexCache = null;
3387
+ return valid.length;
3127
3388
  }
3128
- resumed;
3129
- appendFailCount = 0;
3130
- lastAppendWarnAt = 0;
3131
- secretScrubber;
3132
- onCloseCb;
3133
- /** Implements SessionWriter.traceId — propagated from ContextInit.traceId. */
3134
- traceId;
3135
- // ── Write buffer — batches events to reduce per-event disk I/O ─────────
3136
- //
3137
- // Every append() pushes the scrubbed event into an in-memory buffer instead
3138
- // of calling handle.appendFile() synchronously. The buffer flushes to disk
3139
- // when it reaches FLUSH_SIZE events OR after FLUSH_INTERVAL_MS of inactivity.
3140
- // This cuts the number of disk writes by ~95% without changing the on-disk
3141
- // format — the JSONL is still one JSON object per line.
3142
- writeBuffer = [];
3143
- flushTimer = null;
3144
- static FLUSH_INTERVAL_MS = 500;
3145
- static FLUSH_SIZE = 50;
3146
- // ── Write serialization ─────────────────────────────────────────────────
3147
- //
3148
- // All disk writes are funneled through a FIFO promise chain. Without it,
3149
- // a timer-driven flush racing an explicit flush()/close() issues two
3150
- // concurrent appendFile() calls on the shared O_APPEND handle — the kernel
3151
- // may complete them out of order (chronology breaks) or, for large
3152
- // batches, interleave partial writes (torn JSONL lines). The chain keeps
3153
- // exactly one write in flight; failures don't break the chain.
3154
- writeChain = Promise.resolve();
3155
- /** Enqueue a write on the FIFO chain. Resolves/rejects with that write. */
3156
- enqueueWrite(data) {
3157
- const write = this.writeChain.then(() => this.handle.appendFile(data, "utf8"));
3158
- this.writeChain = write.then(
3159
- () => void 0,
3160
- () => void 0
3389
+ async listFromDirectoryScan(limit) {
3390
+ const shardKeys = await this.collectShardKeys();
3391
+ const shardEntries = await mapWithConcurrency(
3392
+ shardKeys,
3393
+ _DefaultSessionStore.LIST_SCAN_CONCURRENCY,
3394
+ async (shardKey) => await this.readOrBuildShardManifest(shardKey)
3161
3395
  );
3162
- return write;
3396
+ const out = [];
3397
+ for (const entry of shardEntries) {
3398
+ for (const summary of entry.summaries) {
3399
+ out.push({ summary, needsBackfill: false });
3400
+ }
3401
+ }
3402
+ out.sort((a, b) => compareSessionSummaries(a.summary, b.summary));
3403
+ const selected = out.slice(0, limit);
3404
+ const summaries = await mapWithConcurrency(
3405
+ selected,
3406
+ Math.min(_DefaultSessionStore.LIST_SCAN_CONCURRENCY, Math.max(1, limit)),
3407
+ async (candidate) => candidate.summary
3408
+ );
3409
+ return summaries.filter((s) => s !== null);
3163
3410
  }
3164
- // ── Enriched summary tracking ──────────────────────────────────────────
3165
- iterationCount = 0;
3166
- toolCallCount = 0;
3167
- toolErrorCount = 0;
3168
- toolBreakdown = {};
3169
- fileChangeCount = 0;
3170
- compactionCount = 0;
3171
- outcome = void 0;
3172
- /**
3173
- * Scrub secrets out of conversation-turn events before they are observed
3174
- * for the summary, written to the JSONL log, or surfaced on resume. Only
3175
- * `user_input` / `llm_response` carry free-form user/model text; other event
3176
- * types either have no secret-bearing content or are already scrubbed
3177
- * upstream (tool results). Returns the event unchanged when no scrubber is
3178
- * configured.
3179
- */
3180
- scrubEvent(event) {
3181
- const s = this.secretScrubber;
3182
- if (!s) return event;
3183
- if (event.type === "user_input") {
3184
- return {
3185
- ...event,
3186
- content: typeof event.content === "string" ? s.scrub(event.content) : s.scrubObject(event.content)
3187
- };
3411
+ async collectShardKeys() {
3412
+ let entries;
3413
+ try {
3414
+ entries = await fsp3.readdir(this.dir, { withFileTypes: true });
3415
+ } catch {
3416
+ return [""];
3188
3417
  }
3189
- if (event.type === "llm_response") {
3190
- return { ...event, content: s.scrubObject(event.content) };
3418
+ const shardKeys = [""];
3419
+ for (const entry of entries) {
3420
+ if (entry.name.startsWith(".") && entry.name !== ".wrongstack") continue;
3421
+ if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments") continue;
3422
+ if (entry.isDirectory()) shardKeys.push(entry.name);
3191
3423
  }
3192
- return event;
3193
- }
3194
- pendingFileSnapshots = [];
3195
- /** Tracks open tool_use IDs during the current run to serialize on close for resume. */
3196
- openToolUses = /* @__PURE__ */ new Set();
3197
- recordFileChange(input) {
3198
- this.pendingFileSnapshots.push(input);
3199
- }
3200
- get pendingToolUses() {
3201
- return Array.from(this.openToolUses);
3424
+ return shardKeys;
3202
3425
  }
3203
- async writeSessionStartLazy() {
3204
- const record = `${JSON.stringify({
3205
- type: this.resumed ? "session_resumed" : "session_start",
3206
- ts: this.startedAt,
3207
- id: this.id,
3208
- model: this.meta.model ?? "unknown",
3209
- provider: this.meta.provider ?? "unknown"
3210
- })}
3211
- `;
3426
+ async readOrBuildShardManifest(shardKey) {
3427
+ const cached = this.shardManifestCache.get(shardKey);
3428
+ if (cached) return cached;
3429
+ const manifestPath = this.shardManifestPath(shardKey);
3212
3430
  try {
3213
- await this.enqueueWrite(record);
3431
+ const raw = await fsp3.readFile(manifestPath, "utf8");
3432
+ const parsed = JSON.parse(raw);
3433
+ const entry2 = {
3434
+ summaries: Array.isArray(parsed.summaries) ? parsed.summaries : [],
3435
+ ids: Array.isArray(parsed.ids) ? parsed.ids : []
3436
+ };
3437
+ this.shardManifestCache.set(shardKey, entry2);
3438
+ return entry2;
3214
3439
  } catch {
3215
3440
  }
3216
- }
3217
- async append(event) {
3218
- if (this.closed) return;
3219
- await this.ensureInit();
3220
- const scrubbed = this.scrubEvent(event);
3221
- this.observeForSummary(scrubbed);
3222
- this.writeBuffer.push(scrubbed);
3223
- if (this.writeBuffer.length >= _FileSessionWriter.FLUSH_SIZE) {
3224
- if (this.flushTimer) {
3225
- clearTimeout(this.flushTimer);
3226
- this.flushTimer = null;
3441
+ const refs = await this.collectSessionFilesInShard(shardKey);
3442
+ const candidates = await mapWithConcurrency(
3443
+ refs,
3444
+ _DefaultSessionStore.LIST_SCAN_CONCURRENCY,
3445
+ async (ref) => {
3446
+ const manifest = await this.readSummaryManifest(ref.id);
3447
+ if (manifest) return { summary: manifest, needsBackfill: false };
3448
+ const summary = await this.summaryHeaderFor(ref);
3449
+ if (!summary) return null;
3450
+ const hydrated = await this.summaryFor(summary.id).catch(() => summary);
3451
+ return { summary: hydrated, needsBackfill: false };
3227
3452
  }
3228
- await this.flushBuffer();
3229
- } else {
3230
- this.scheduleFlush();
3231
- }
3453
+ );
3454
+ const summaries = candidates.filter((candidate) => candidate !== null).map((candidate) => candidate.summary);
3455
+ summaries.sort(compareSessionSummaries);
3456
+ const entry = { summaries, ids: summaries.map((summary) => summary.id) };
3457
+ this.shardManifestCache.set(shardKey, entry);
3458
+ await atomicWrite(manifestPath, JSON.stringify(entry), { mode: 384 }).catch(() => void 0);
3459
+ return entry;
3460
+ }
3461
+ async collectSessionFilesInShard(shardKey) {
3462
+ const dir = shardKey ? path4.join(this.dir, shardKey) : this.dir;
3463
+ const entries = await this.collectSessionFiles(dir, shardKey);
3464
+ return shardKey ? entries.filter((entry) => entry.id.startsWith(`${shardKey}/`)) : entries.filter((entry) => !entry.id.includes("/"));
3232
3465
  }
3233
- async appendBatch(events) {
3234
- if (this.closed || events.length === 0) return;
3235
- await this.ensureInit();
3236
- for (const event of events) {
3237
- const scrubbed = this.scrubEvent(event);
3238
- this.observeForSummary(scrubbed);
3239
- this.writeBuffer.push(scrubbed);
3466
+ async collectSessionFiles(dir, prefix = "", depth = 0) {
3467
+ let entries;
3468
+ try {
3469
+ entries = await fsp3.readdir(dir, { withFileTypes: true });
3470
+ } catch {
3471
+ return [];
3240
3472
  }
3241
- if (this.writeBuffer.length >= _FileSessionWriter.FLUSH_SIZE) {
3242
- if (this.flushTimer) {
3243
- clearTimeout(this.flushTimer);
3244
- this.flushTimer = null;
3473
+ const dirEntries = [];
3474
+ const files = [];
3475
+ for (const entry of entries) {
3476
+ if (entry.name.startsWith(".") && entry.name !== ".wrongstack") continue;
3477
+ if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments")
3478
+ continue;
3479
+ if (entry.isDirectory()) {
3480
+ dirEntries.push(entry);
3481
+ } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
3482
+ if (entry.name === "_index.jsonl") continue;
3483
+ const base = entry.name.replace(/\.jsonl$/, "");
3484
+ const id = prefix ? `${prefix}/${base}` : base;
3485
+ files.push({ id, filePath: path4.join(dir, entry.name) });
3245
3486
  }
3246
- await this.flushBuffer();
3247
- } else {
3248
- this.scheduleFlush();
3249
3487
  }
3488
+ const childFileArrays = await Promise.all(
3489
+ dirEntries.map((entry) => {
3490
+ const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
3491
+ return this.collectSessionFiles(path4.join(dir, entry.name), childPrefix, depth + 1);
3492
+ })
3493
+ );
3494
+ return [...childFileArrays.flat(), ...files];
3250
3495
  }
3251
- /**
3252
- * Flush buffered events to disk immediately. Critical events
3253
- * (user_input, llm_response) call this so they survive SIGKILL/crash
3254
- * instead of sitting in the in-memory buffer for up to 500ms.
3255
- *
3256
- * Idempotent — cancels any pending timer and writes whatever has
3257
- * accumulated in the buffer. Safe to call even when the buffer
3258
- * is empty (no-op).
3259
- */
3260
- async flush() {
3261
- if (this.flushTimer) {
3262
- clearTimeout(this.flushTimer);
3263
- this.flushTimer = null;
3496
+ /** Recursively collect session IDs from date-shard subdirectories.
3497
+ * IDs include the date-prefix path (e.g. "2026-06-06/17-46-57Z_…").
3498
+ * Skips `.jsonl`/`.summary.json` root files, dot-files, and
3499
+ * sub-directories that belong to fleet/subagent sessions. */
3500
+ async collectSessionIds(dir, prefix = "", depth = 0) {
3501
+ let entries;
3502
+ try {
3503
+ entries = await fsp3.readdir(dir, { withFileTypes: true });
3504
+ } catch {
3505
+ return [];
3264
3506
  }
3265
- await this.flushBuffer();
3266
- }
3267
- /** Schedule a deferred flush. No-op if a timer is already pending. */
3268
- scheduleFlush() {
3269
- if (this.flushTimer) return;
3270
- this.flushTimer = setTimeout(() => {
3271
- this.flushTimer = null;
3272
- this.flushBuffer().catch(() => {
3273
- });
3274
- }, _FileSessionWriter.FLUSH_INTERVAL_MS);
3507
+ const dirEntries = [];
3508
+ const fileIds = [];
3509
+ for (const entry of entries) {
3510
+ if (entry.name.startsWith(".") && entry.name !== ".wrongstack") continue;
3511
+ if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments")
3512
+ continue;
3513
+ if (entry.isDirectory()) {
3514
+ dirEntries.push(entry);
3515
+ } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
3516
+ if (entry.name === "_index.jsonl") continue;
3517
+ const base = entry.name.replace(/\.jsonl$/, "");
3518
+ fileIds.push(prefix ? `${prefix}/${base}` : base);
3519
+ }
3520
+ }
3521
+ const childIdArrays = await Promise.all(
3522
+ dirEntries.map((entry) => {
3523
+ const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
3524
+ return this.collectSessionIds(path4.join(dir, entry.name), childPrefix, depth + 1);
3525
+ })
3526
+ );
3527
+ return [...childIdArrays.flat(), ...fileIds];
3275
3528
  }
3276
- /**
3277
- * Flush all buffered events to disk as a single appendFile call.
3278
- * Errors use the same throttled-warning pattern the old per-event
3279
- * append path used — one warning every 5s with a suppressed count.
3280
- * On failure the buffer is cleared (events are best-effort, same as
3281
- * the old per-event path where a failed write was silently dropped).
3282
- */
3283
- async flushBuffer() {
3284
- if (this.writeBuffer.length === 0) return;
3285
- const eventCount = this.writeBuffer.length;
3286
- const batch = this.writeBuffer.map((e) => JSON.stringify(e)).join("\n") + "\n";
3287
- this.writeBuffer = [];
3529
+ async summaryFor(id) {
3530
+ const manifest = this.sessionPath(id, ".summary.json");
3288
3531
  const t0 = Date.now();
3289
3532
  let outcome = "success";
3290
3533
  let errorMsg;
3534
+ const fromManifest = await this.readSummaryManifest(id, t0);
3535
+ if (fromManifest) return fromManifest;
3291
3536
  try {
3292
- await this.enqueueWrite(batch);
3537
+ const full = this.sessionPath(id, ".jsonl");
3538
+ const stat8 = await fsp3.stat(full);
3539
+ const summary = await this.summarize(id, stat8.mtime.toISOString());
3540
+ await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
3541
+ const msg = toErrorMessage(err);
3542
+ this.emitError(id, manifest, "summary_fallback", msg, true);
3543
+ console.warn(JSON.stringify({
3544
+ level: "warn",
3545
+ event: "session_store.manifest_write_failed",
3546
+ sessionId: id,
3547
+ message: msg,
3548
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3549
+ }));
3550
+ });
3551
+ outcome = "failure";
3552
+ errorMsg = "summary fallback \xE2\u20AC\u201D manifest rebuilt";
3553
+ this.emitRead(id, manifest, "summary", outcome, Date.now() - t0, errorMsg);
3554
+ return summary;
3293
3555
  } catch (err) {
3294
3556
  outcome = "failure";
3295
3557
  errorMsg = toErrorMessage(err);
3296
- this.appendFailCount += eventCount;
3297
- const now = Date.now();
3298
- if (now - this.lastAppendWarnAt > 5e3) {
3299
- const suppressed = this.appendFailCount - 1;
3300
- const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
3301
- console.warn(
3302
- "[session] flush failed:",
3303
- toErrorMessage(err),
3304
- tail
3305
- );
3306
- this.lastAppendWarnAt = now;
3307
- this.appendFailCount = 0;
3308
- }
3309
- } finally {
3310
- this.events?.emit("storage.write", {
3311
- sessionId: this.id,
3312
- store: "session",
3313
- filePath: this.filePath,
3314
- operation: "flush",
3315
- outcome,
3316
- durationMs: Date.now() - t0,
3317
- ...errorMsg !== void 0 ? { error: errorMsg } : {},
3318
- ...eventCount !== void 0 ? { eventCount } : {},
3319
- ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
3320
- });
3558
+ this.emitRead(id, manifest, "summary", outcome, Date.now() - t0, errorMsg);
3559
+ return {
3560
+ id,
3561
+ title: "(damaged)",
3562
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3563
+ model: "unknown",
3564
+ provider: "unknown",
3565
+ tokenTotal: 0
3566
+ };
3321
3567
  }
3322
3568
  }
3323
- observeForSummary(event) {
3324
- if (event.type === "llm_response") {
3325
- for (const block of event.content) {
3326
- if (block.type === "tool_use") this.openToolUses.add(block.id);
3327
- }
3569
+ async readSummaryManifest(id, startTime = Date.now()) {
3570
+ const manifest = this.sessionPath(id, ".summary.json");
3571
+ try {
3572
+ const raw = await fsp3.readFile(manifest, "utf8");
3573
+ this.emitRead(id, manifest, "summary", "success", Date.now() - startTime);
3574
+ return JSON.parse(raw);
3575
+ } catch {
3576
+ return null;
3328
3577
  }
3329
- if (event.type === "tool_use") {
3330
- this.openToolUses.add(event.id);
3331
- } else if (event.type === "tool_call_start") {
3332
- this.toolCallCount++;
3333
- this.toolBreakdown[event.name] = (this.toolBreakdown[event.name] ?? 0) + 1;
3334
- } else if (event.type === "tool_result") {
3335
- this.openToolUses.delete(event.id);
3336
- if (event.isError) {
3337
- this.toolErrorCount++;
3338
- this.outcome = "error";
3578
+ }
3579
+ async summaryHeaderFor(ref) {
3580
+ let mtime = (/* @__PURE__ */ new Date(0)).toISOString();
3581
+ try {
3582
+ const stat8 = await fsp3.stat(ref.filePath);
3583
+ if (!stat8.isFile()) {
3584
+ return {
3585
+ id: ref.id,
3586
+ title: "(damaged)",
3587
+ startedAt: stat8.mtime.toISOString(),
3588
+ model: "unknown",
3589
+ provider: "unknown",
3590
+ tokenTotal: 0
3591
+ };
3339
3592
  }
3340
- } else if (event.type === "file_snapshot") {
3341
- this.fileChangeCount += event.files.length;
3342
- } else if (event.type === "compaction") {
3343
- this.compactionCount++;
3344
- }
3345
- if (event.type === "error" || event.type === "provider_error") {
3346
- this.outcome = "error";
3593
+ mtime = stat8.mtime.toISOString();
3594
+ } catch {
3595
+ return null;
3347
3596
  }
3348
- if (event.type === "user_input" && this.summary.title === "(empty session)") {
3349
- this.summary = { ...this.summary, title: userInputTitle(event.content) };
3350
- } else if (event.type === "llm_response") {
3351
- this.tokenIn += event.usage.input;
3352
- this.tokenOut += event.usage.output;
3353
- this.summary = { ...this.summary, tokenTotal: this.tokenIn + this.tokenOut };
3354
- } else if (event.type === "session_end") {
3355
- const total = event.usage.input + event.usage.output;
3356
- if (total > 0) this.summary = { ...this.summary, tokenTotal: total };
3357
- } else if (event.type === "in_flight_start") {
3358
- this.iterationCount++;
3597
+ try {
3598
+ for await (const event of this.iterSessionEvents(ref.filePath)) {
3599
+ if (event.type === "session_start") {
3600
+ return {
3601
+ id: ref.id,
3602
+ title: "(empty session)",
3603
+ startedAt: event.ts,
3604
+ model: event.model ?? "unknown",
3605
+ provider: event.provider ?? "unknown",
3606
+ tokenTotal: 0
3607
+ };
3608
+ }
3609
+ }
3610
+ return {
3611
+ id: ref.id,
3612
+ title: "(empty session)",
3613
+ startedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
3614
+ model: "unknown",
3615
+ provider: "unknown",
3616
+ tokenTotal: 0
3617
+ };
3618
+ } catch {
3619
+ return {
3620
+ id: ref.id,
3621
+ title: "(damaged)",
3622
+ startedAt: mtime,
3623
+ model: "unknown",
3624
+ provider: "unknown",
3625
+ tokenTotal: 0
3626
+ };
3359
3627
  }
3360
3628
  }
3361
- async close() {
3362
- if (this.closePromise) return this.closePromise;
3363
- this.closePromise = this.doClose();
3364
- return this.closePromise;
3365
- }
3366
- async doClose() {
3367
- this.closed = true;
3368
- if (this.flushTimer) {
3369
- clearTimeout(this.flushTimer);
3370
- this.flushTimer = null;
3371
- }
3372
- await this.flushBuffer();
3373
- await this.writeChain;
3374
- this.summary = {
3375
- ...this.summary,
3376
- endedAt: (/* @__PURE__ */ new Date()).toISOString(),
3377
- iterationCount: this.iterationCount,
3378
- toolCallCount: this.toolCallCount,
3379
- toolErrorCount: this.toolErrorCount,
3380
- fileChangeCount: this.fileChangeCount,
3381
- compactionCount: this.compactionCount > 0 ? this.compactionCount : void 0,
3382
- toolBreakdown: { ...this.toolBreakdown },
3383
- outcome: this.outcome ?? "completed"
3384
- };
3385
- if (this.manifestFile) {
3386
- const t0 = Date.now();
3387
- let outcome = "success";
3388
- let errorMsg;
3389
- try {
3390
- await atomicWrite(this.manifestFile, JSON.stringify(this.summary), { mode: 384 });
3391
- } catch (err) {
3392
- outcome = "failure";
3393
- errorMsg = toErrorMessage(err);
3394
- } finally {
3395
- this.events?.emit("storage.write", {
3396
- sessionId: this.id,
3397
- store: "session",
3398
- filePath: this.manifestFile,
3399
- operation: "close",
3400
- outcome,
3401
- durationMs: Date.now() - t0,
3402
- ...errorMsg !== void 0 ? { error: errorMsg } : {},
3403
- ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
3404
- });
3629
+ /**
3630
+ * Delete a session and all associated files: JSONL, summary, plan/todos
3631
+ * sidecars, and the session directory (fleet.json, shared/, subagents/).
3632
+ *
3633
+ * Individual file deletions are best-effort (logged as structured warnings),
3634
+ * but a tombstone is always written so readIndex() filters this session out.
3635
+ * If the session directory itself can't be removed, the error is surfaced
3636
+ * to the caller so prune() can report it.
3637
+ */
3638
+ async deleteSession(id) {
3639
+ const jsonlPath = this.sessionPath(id, ".jsonl");
3640
+ const summaryPath = this.sessionPath(id, ".summary.json");
3641
+ const shardDir = path4.dirname(path4.join(this.dir, id));
3642
+ const base = path4.basename(id);
3643
+ const sessDir = path4.join(shardDir, base);
3644
+ const deletions = [
3645
+ fsp3.unlink(jsonlPath),
3646
+ fsp3.unlink(summaryPath),
3647
+ fsp3.unlink(path4.join(shardDir, `${base}.plan.json`)),
3648
+ fsp3.unlink(path4.join(shardDir, `${base}.todos.json`))
3649
+ ];
3650
+ const results = await Promise.allSettled(deletions);
3651
+ for (const r of results) {
3652
+ if (r.status === "rejected") {
3653
+ const msg = r.reason instanceof Error ? r.reason.message : String(r.reason);
3654
+ if (r.reason?.code !== "ENOENT") {
3655
+ console.warn(JSON.stringify({
3656
+ level: "warn",
3657
+ event: "session_store.delete_failed",
3658
+ sessionId: id,
3659
+ message: msg,
3660
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3661
+ }));
3662
+ }
3405
3663
  }
3406
3664
  }
3407
- const idxT0 = Date.now();
3408
- let idxOutcome = "success";
3409
- let idxError;
3410
- try {
3411
- await this.onCloseCb?.(this.summary);
3412
- } catch (err) {
3413
- idxOutcome = "failure";
3414
- idxError = toErrorMessage(err);
3415
- } finally {
3416
- this.events?.emit("storage.write", {
3417
- sessionId: this.summary.id,
3418
- store: "session",
3419
- filePath: this.filePath,
3420
- operation: "index_append",
3421
- outcome: idxOutcome,
3422
- durationMs: Date.now() - idxT0,
3423
- ...idxError !== void 0 ? { error: idxError } : {},
3424
- ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
3425
- });
3426
- }
3665
+ await fsp3.rm(sessDir, { recursive: true, force: true }).catch((err) => {
3666
+ console.warn(JSON.stringify({
3667
+ level: "warn",
3668
+ event: "session_store.rmdir_failed",
3669
+ sessionId: id,
3670
+ message: toErrorMessage(err),
3671
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3672
+ }));
3673
+ });
3674
+ await this.writeTombstone(id);
3675
+ }
3676
+ async delete(id) {
3677
+ await this.deleteSession(id);
3678
+ }
3679
+ async prune(maxAgeDays = 30) {
3680
+ const cutoff = Date.now() - maxAgeDays * 864e5;
3681
+ let deleted = 0;
3682
+ let activeSessionId = null;
3427
3683
  try {
3428
- await this.handle.close();
3684
+ const raw = await fsp3.readFile(path4.join(this.dir, "active.json"), "utf8");
3685
+ const active = JSON.parse(raw);
3686
+ activeSessionId = active.sessionId ?? null;
3429
3687
  } catch {
3430
3688
  }
3431
- }
3432
- async writeCheckpoint(promptIndex, promptPreview) {
3433
- const fileCount = this.pendingFileSnapshots.length;
3434
- if (fileCount > 0) {
3435
- await this.writeFileSnapshot(promptIndex, [...this.pendingFileSnapshots]);
3436
- this.pendingFileSnapshots = [];
3689
+ const isPrunableJsonl = (name) => name.endsWith(".jsonl") && name !== "_index.jsonl" && name !== "_mailbox.jsonl" && !name.endsWith(".replay.jsonl") && !name.endsWith(".audit.jsonl");
3690
+ const pruneFile = async (dir, name, prefix) => {
3691
+ const jsonlPath = path4.join(dir, name);
3692
+ try {
3693
+ const stat8 = await fsp3.stat(jsonlPath);
3694
+ if (stat8.mtimeMs >= cutoff) return;
3695
+ } catch {
3696
+ return;
3697
+ }
3698
+ const base = name.replace(/\.jsonl$/, "");
3699
+ const id = prefix ? `${prefix}/${base}` : base;
3700
+ if (activeSessionId && id === activeSessionId) return;
3701
+ await this.deleteSession(id);
3702
+ deleted++;
3703
+ };
3704
+ const entries = await fsp3.readdir(this.dir, { withFileTypes: true }).catch(() => []);
3705
+ for (const entry of entries) {
3706
+ if (entry.isFile()) {
3707
+ if (isPrunableJsonl(entry.name)) await pruneFile(this.dir, entry.name, "");
3708
+ continue;
3709
+ }
3710
+ if (!entry.isDirectory()) continue;
3711
+ const dateDir = path4.join(this.dir, entry.name);
3712
+ const files = await fsp3.readdir(dateDir, { withFileTypes: true }).catch(() => []);
3713
+ for (const file of files) {
3714
+ if (!file.isFile() || !isPrunableJsonl(file.name)) continue;
3715
+ await pruneFile(dateDir, file.name, entry.name);
3716
+ }
3437
3717
  }
3438
- await this.append({
3439
- type: "checkpoint",
3440
- ts: (/* @__PURE__ */ new Date()).toISOString(),
3441
- promptIndex,
3442
- promptPreview
3443
- });
3444
- this.events?.emit("checkpoint.written", {
3445
- promptIndex,
3446
- promptPreview,
3447
- ts: (/* @__PURE__ */ new Date()).toISOString(),
3448
- fileCount
3449
- });
3718
+ if (deleted > 0) {
3719
+ await this.compactIndex().catch(() => void 0);
3720
+ }
3721
+ for (const entry of entries) {
3722
+ if (!entry.isDirectory()) continue;
3723
+ const dateDir = path4.join(this.dir, entry.name);
3724
+ try {
3725
+ const remaining = await fsp3.readdir(dateDir);
3726
+ if (remaining.length === 0) {
3727
+ await fsp3.rmdir(dateDir).catch(() => void 0);
3728
+ }
3729
+ } catch {
3730
+ }
3731
+ }
3732
+ return deleted;
3450
3733
  }
3451
- async writeFileSnapshot(promptIndex, files) {
3452
- await this.append({
3453
- type: "file_snapshot",
3734
+ async clearHistory(id) {
3735
+ await this.ensureShardDir(id);
3736
+ const file = this.sessionPath(id, ".jsonl");
3737
+ const meta = this.sessionPath(id, ".summary.json");
3738
+ const record = `${JSON.stringify({
3739
+ type: "session_start",
3454
3740
  ts: (/* @__PURE__ */ new Date()).toISOString(),
3455
- promptIndex,
3456
- files
3457
- });
3741
+ id,
3742
+ model: "unknown",
3743
+ provider: "unknown"
3744
+ })}
3745
+ `;
3746
+ await fsp3.writeFile(file, record, "utf8");
3747
+ await fsp3.unlink(meta).catch(() => void 0);
3458
3748
  }
3459
- /**
3460
- * Truncate the session file to the checkpoint with the given promptIndex,
3461
- * removing all events that follow it. Uses a single-pass byte-offset scan
3462
- * so post-checkpoint content is never read or parsed — O(1) memory instead
3463
- * of O(N) JSON.parse calls over the full file.
3464
- */
3465
- async truncateToCheckpoint(targetPromptIndex) {
3466
- if (!this.filePath) return 0;
3467
- if (this.flushTimer) {
3468
- clearTimeout(this.flushTimer);
3469
- this.flushTimer = null;
3470
- }
3471
- await this.flushBuffer();
3472
- await this.writeChain;
3473
- const CHUNK_SIZE = 65536;
3474
- let fd;
3475
- let fileOffset = 0;
3476
- let lineStartOffset = 0;
3477
- let checkpointByteOffset = -1;
3478
- let removedCount = 0;
3479
- let targetCheckpointSeen = false;
3749
+ async summarize(id, mtime) {
3480
3750
  try {
3481
- fd = await fsp2.open(this.filePath, "r", 384);
3482
- while (true) {
3483
- const buf = Buffer.alloc(CHUNK_SIZE);
3484
- const { bytesRead } = await fd.read(buf, 0, CHUNK_SIZE, fileOffset);
3485
- if (bytesRead === 0) break;
3486
- let chunkPos = 0;
3487
- while (chunkPos < bytesRead) {
3488
- const idx = buf.indexOf("\n", chunkPos);
3489
- if (idx === -1) {
3490
- lineStartOffset = fileOffset + chunkPos;
3491
- break;
3492
- }
3493
- if (checkpointByteOffset !== -1) {
3494
- removedCount++;
3495
- } else {
3496
- const lineBytes = buf.subarray(chunkPos, idx);
3497
- const line = new TextDecoder("utf-8", { fatal: false }).decode(lineBytes);
3498
- if (line.trim()) {
3499
- try {
3500
- const event = JSON.parse(line);
3501
- if (event.type === "checkpoint") {
3502
- if (event.promptIndex === targetPromptIndex) {
3503
- checkpointByteOffset = lineStartOffset;
3504
- targetCheckpointSeen = true;
3505
- } else if (event.promptIndex !== void 0 && event.promptIndex > targetPromptIndex) {
3506
- checkpointByteOffset = lineStartOffset;
3507
- }
3508
- } else if (targetCheckpointSeen && event.promptIndex !== void 0 && event.promptIndex > targetPromptIndex) {
3509
- removedCount++;
3510
- } else if (targetCheckpointSeen && event.promptIndex === void 0) {
3511
- removedCount++;
3512
- } else if (!targetCheckpointSeen && event.promptIndex === void 0) {
3513
- removedCount++;
3514
- } else if (!targetCheckpointSeen && event.promptIndex !== void 0 && event.promptIndex > targetPromptIndex) {
3515
- removedCount++;
3516
- }
3517
- } catch {
3518
- }
3519
- }
3751
+ const file = this.sessionPath(id, ".jsonl");
3752
+ let title = "(empty session)";
3753
+ let startedAt = (/* @__PURE__ */ new Date(0)).toISOString();
3754
+ let endedAt;
3755
+ let model = "unknown";
3756
+ let provider = "unknown";
3757
+ let tokenIn = 0;
3758
+ let tokenOut = 0;
3759
+ let iterationCount = 0;
3760
+ let toolCallCount = 0;
3761
+ let toolErrorCount = 0;
3762
+ let fileChangeCount = 0;
3763
+ const toolBreakdown = {};
3764
+ let outcome;
3765
+ let lastEventType;
3766
+ let hasError = false;
3767
+ let sawStart = false;
3768
+ for await (const e of this.iterSessionEvents(file)) {
3769
+ lastEventType = e.type;
3770
+ if (e.type === "session_start") {
3771
+ if (!sawStart) {
3772
+ sawStart = true;
3773
+ startedAt = e.ts;
3774
+ model = e.model ?? "unknown";
3775
+ provider = e.provider ?? "unknown";
3520
3776
  }
3521
- chunkPos = idx + 1;
3522
- lineStartOffset = fileOffset + chunkPos;
3523
- }
3524
- fileOffset += bytesRead;
3525
- if (chunkPos >= bytesRead) {
3526
- lineStartOffset = fileOffset;
3527
- }
3777
+ } else if (e.type === "session_end") {
3778
+ endedAt = e.ts;
3779
+ } else if (e.type === "user_input") {
3780
+ if (title === "(empty session)") title = userInputTitle(e.content);
3781
+ } else if (e.type === "llm_response") {
3782
+ tokenIn += e.usage.input ?? 0;
3783
+ tokenOut += e.usage.output ?? 0;
3784
+ } else if (e.type === "in_flight_start") iterationCount++;
3785
+ else if (e.type === "tool_call_start") {
3786
+ toolCallCount++;
3787
+ toolBreakdown[e.name] = (toolBreakdown[e.name] ?? 0) + 1;
3788
+ } else if (e.type === "tool_result" && e.isError) toolErrorCount++;
3789
+ else if (e.type === "file_snapshot") fileChangeCount += e.files.length;
3790
+ else if (e.type === "error" || e.type === "provider_error") hasError = true;
3528
3791
  }
3529
- } finally {
3530
- await fd?.close();
3531
- }
3532
- if (checkpointByteOffset === -1) return 0;
3533
- await this.writeChain;
3534
- await this.handle.close();
3535
- const tmpPath = `${this.filePath}.rewind.tmp`;
3536
- const src = await fsp2.open(this.filePath, "r", 384);
3537
- try {
3538
- const statResult = await src.stat();
3539
- const totalSize = statResult.size;
3540
- const prefixBytes = checkpointByteOffset;
3541
- let newlineAfterCheckpoint = prefixBytes;
3542
- if (prefixBytes < totalSize) {
3543
- const probeBuf = Buffer.alloc(Math.min(CHUNK_SIZE, totalSize - prefixBytes));
3544
- const { bytesRead: probeRead } = await src.read(probeBuf, 0, probeBuf.length, prefixBytes);
3545
- if (probeRead > 0) {
3546
- const nl = probeBuf.indexOf("\n");
3547
- newlineAfterCheckpoint = nl !== -1 ? prefixBytes + nl + 1 : totalSize;
3548
- }
3549
- } else {
3550
- newlineAfterCheckpoint = totalSize;
3792
+ if (lastEventType === "session_end") {
3793
+ outcome = "completed";
3794
+ } else if (lastEventType === "in_flight_start") {
3795
+ outcome = "aborted";
3796
+ } else if (hasError) {
3797
+ outcome = "error";
3551
3798
  }
3552
- const writeFd = await fsp2.open(tmpPath, "w", 384);
3553
- try {
3554
- let readOffset = 0;
3555
- while (readOffset < newlineAfterCheckpoint) {
3556
- const toCopy = Math.min(CHUNK_SIZE, newlineAfterCheckpoint - readOffset);
3557
- const copyBuf = Buffer.alloc(toCopy);
3558
- const { bytesRead: r } = await src.read(copyBuf, 0, toCopy, readOffset);
3559
- if (r === 0) break;
3560
- await writeFd.write(copyBuf, 0, r);
3561
- readOffset += r;
3562
- }
3563
- const raw = await fsp2.readFile(this.filePath);
3564
- const tail = raw.subarray(newlineAfterCheckpoint).toString("utf8");
3565
- for (const line of tail.split("\n")) {
3566
- if (!line.trim()) continue;
3567
- try {
3568
- JSON.parse(line);
3569
- } catch {
3570
- await writeFd.write(`${line}
3571
- `, void 0, "utf8");
3799
+ return {
3800
+ id,
3801
+ title,
3802
+ startedAt,
3803
+ endedAt,
3804
+ model,
3805
+ provider,
3806
+ tokenTotal: tokenIn + tokenOut,
3807
+ iterationCount: iterationCount > 0 ? iterationCount : void 0,
3808
+ toolCallCount: toolCallCount > 0 ? toolCallCount : void 0,
3809
+ toolErrorCount: toolErrorCount > 0 ? toolErrorCount : void 0,
3810
+ fileChangeCount: fileChangeCount > 0 ? fileChangeCount : void 0,
3811
+ toolBreakdown: Object.keys(toolBreakdown).length > 0 ? toolBreakdown : {},
3812
+ outcome
3813
+ };
3814
+ } catch {
3815
+ return {
3816
+ id,
3817
+ title: "(damaged)",
3818
+ startedAt: mtime,
3819
+ model: "unknown",
3820
+ provider: "unknown",
3821
+ tokenTotal: 0
3822
+ };
3823
+ }
3824
+ }
3825
+ async *iterSessionEvents(file) {
3826
+ const stream = createReadStream(file, { encoding: "utf8" });
3827
+ const lines = createInterface({ input: stream, crlfDelay: Infinity });
3828
+ try {
3829
+ for await (const line of lines) {
3830
+ if (!line.trim()) continue;
3831
+ try {
3832
+ const parsed = JSON.parse(line);
3833
+ if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
3834
+ yield parsed;
3572
3835
  }
3836
+ } catch {
3573
3837
  }
3574
- } finally {
3575
- await writeFd.close();
3576
3838
  }
3577
- await src.close();
3578
- await fsp2.rename(tmpPath, this.filePath);
3579
- this.handle = await fsp2.open(this.filePath, "a", 384);
3580
- } catch (err) {
3581
- await fsp2.unlink(tmpPath).catch(() => void 0);
3582
- this.handle = await fsp2.open(this.filePath, "a", 384).catch(() => this.handle);
3583
- throw err;
3584
- }
3585
- await this.append({
3586
- type: "rewound",
3587
- ts: (/* @__PURE__ */ new Date()).toISOString(),
3588
- toPromptIndex: targetPromptIndex,
3589
- revertedFiles: []
3590
- });
3591
- this.events?.emit("session.rewound", {
3592
- toPromptIndex: targetPromptIndex,
3593
- revertedFiles: [],
3594
- removedEvents: removedCount
3595
- });
3596
- return removedCount;
3597
- }
3598
- async clearSession() {
3599
- if (!this.filePath) return;
3600
- if (this.flushTimer) {
3601
- clearTimeout(this.flushTimer);
3602
- this.flushTimer = null;
3839
+ } finally {
3840
+ lines.close();
3841
+ stream.destroy();
3603
3842
  }
3604
- this.writeBuffer = [];
3605
- await this.writeChain;
3606
- const record = `${JSON.stringify({
3607
- type: "session_start",
3608
- ts: (/* @__PURE__ */ new Date()).toISOString(),
3609
- id: this.id,
3610
- model: this.meta.model ?? "unknown",
3611
- provider: this.meta.provider ?? "unknown"
3612
- })}
3613
- `;
3614
- await fsp2.writeFile(this.filePath, record, "utf8");
3615
3843
  }
3616
- /**
3617
- * Idea #1 — write an in-flight marker. The agent loop should call
3618
- * this at the start of each long-running operation; a matching
3619
- * `clearInFlightMarker` follows on clean exit. A stale marker
3620
- * (no end) is what `SessionRecovery.detectStale` looks for.
3621
- */
3622
- async writeInFlightMarker(context) {
3623
- if (!context || context.length > 500) {
3624
- throw new Error("In-flight context must be 1..500 chars");
3844
+ };
3845
+ function extractToolCallEnds(events) {
3846
+ const result = [];
3847
+ for (const e of events) {
3848
+ if (e.type === "tool_call_end") {
3849
+ result.push({
3850
+ name: e.name,
3851
+ id: e.id,
3852
+ durationMs: e.durationMs,
3853
+ ok: e.ok ?? false,
3854
+ outputBytes: e.outputBytes,
3855
+ outputTokens: e.outputTokens,
3856
+ outputLines: e.outputLines
3857
+ });
3625
3858
  }
3626
- await this.append({
3627
- type: "in_flight_start",
3628
- ts: (/* @__PURE__ */ new Date()).toISOString(),
3629
- context
3630
- });
3631
- this.events?.emit("in_flight.started", { context, ts: (/* @__PURE__ */ new Date()).toISOString() });
3632
- }
3633
- /**
3634
- * Idea #1 — close the in-flight marker. Idempotent in spirit
3635
- * (you can call it after a successful iteration even if you
3636
- * didn't open one this round) — but the session log records
3637
- * every call so postmortem tooling can see "the agent finished
3638
- * cleanly X times, then died without finishing Y".
3639
- */
3640
- async clearInFlightMarker(reason) {
3641
- await this.append({
3642
- type: "in_flight_end",
3643
- ts: (/* @__PURE__ */ new Date()).toISOString(),
3644
- reason
3645
- });
3646
- this.events?.emit("in_flight.ended", { reason, ts: (/* @__PURE__ */ new Date()).toISOString() });
3647
3859
  }
3648
- };
3649
- function userInputTitle(content) {
3650
- const text = typeof content === "string" ? content : content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
3651
- return (text || "(non-text input)").slice(0, 60);
3860
+ return result;
3652
3861
  }
3653
3862
  function compareSessionSummaries(a, b) {
3654
3863
  if (a.startedAt < b.startedAt) return 1;
3655
3864
  if (a.startedAt > b.startedAt) return -1;
3656
3865
  return a.id.localeCompare(b.id);
3657
3866
  }
3867
+ function matchesSessionFilter(s, criteria) {
3868
+ if (criteria.since && s.startedAt < criteria.since) return false;
3869
+ if (criteria.until && s.startedAt > criteria.until) return false;
3870
+ if (criteria.provider && s.provider !== criteria.provider) return false;
3871
+ if (criteria.model && s.model !== criteria.model) return false;
3872
+ if (criteria.minTokens !== void 0 && s.tokenTotal < criteria.minTokens) return false;
3873
+ if (criteria.titleContains) {
3874
+ const needle = criteria.titleContains.toLowerCase();
3875
+ if (!s.title.toLowerCase().includes(needle)) return false;
3876
+ }
3877
+ return true;
3878
+ }
3658
3879
  async function mapWithConcurrency(items, concurrency, fn) {
3659
3880
  if (items.length === 0) return [];
3660
3881
  const out = new Array(items.length);
@@ -3726,7 +3947,7 @@ var QueueStore = class {
3726
3947
  const t0 = Date.now();
3727
3948
  let raw;
3728
3949
  try {
3729
- raw = await fsp2.readFile(this.file, "utf8");
3950
+ raw = await fsp3.readFile(this.file, "utf8");
3730
3951
  } catch (err) {
3731
3952
  const code = err.code;
3732
3953
  if (code === "ENOENT") {
@@ -3807,7 +4028,7 @@ var QueueStore = class {
3807
4028
  async clear() {
3808
4029
  const t0 = Date.now();
3809
4030
  try {
3810
- await fsp2.unlink(this.file);
4031
+ await fsp3.unlink(this.file);
3811
4032
  this.events?.emit("storage.write", {
3812
4033
  sessionId: this.traceId ?? "~boot~",
3813
4034
  store: "queue",
@@ -3867,7 +4088,7 @@ var DefaultAttachmentStore = class {
3867
4088
  let spooledPath;
3868
4089
  let data = input.data;
3869
4090
  if (this.spoolDir && bytes >= this.spoolThreshold) {
3870
- await fsp2.mkdir(this.spoolDir, { recursive: true });
4091
+ await fsp3.mkdir(this.spoolDir, { recursive: true });
3871
4092
  spooledPath = path4.join(this.spoolDir, `${id}.bin`);
3872
4093
  await atomicWrite(spooledPath, input.data, {
3873
4094
  encoding: input.kind === "image" ? "base64" : "utf8"
@@ -3930,7 +4151,7 @@ var DefaultAttachmentStore = class {
3930
4151
  for (const att of this.items.values()) {
3931
4152
  if (att.path) toDelete.push(att.path);
3932
4153
  }
3933
- await Promise.all(toDelete.map((p) => fsp2.unlink(p).catch(() => void 0)));
4154
+ await Promise.all(toDelete.map((p) => fsp3.unlink(p).catch(() => void 0)));
3934
4155
  }
3935
4156
  this.items.clear();
3936
4157
  this.refs.length = 0;
@@ -3938,7 +4159,7 @@ var DefaultAttachmentStore = class {
3938
4159
  }
3939
4160
  async toBlock(att) {
3940
4161
  if (att.kind === "image") {
3941
- const data = att.data ?? (att.path ? await fsp2.readFile(att.path, { encoding: "base64" }) : "");
4162
+ const data = att.data ?? (att.path ? await fsp3.readFile(att.path, { encoding: "base64" }) : "");
3942
4163
  return {
3943
4164
  type: "image",
3944
4165
  source: {
@@ -3948,7 +4169,7 @@ var DefaultAttachmentStore = class {
3948
4169
  }
3949
4170
  };
3950
4171
  }
3951
- const raw = att.data ?? (att.path ? await fsp2.readFile(att.path, "utf8") : "");
4172
+ const raw = att.data ?? (att.path ? await fsp3.readFile(att.path, "utf8") : "");
3952
4173
  const label = att.meta.filename ? `<file path="${att.meta.filename}">` : "<pasted>";
3953
4174
  const close = att.meta.filename ? "</file>" : "</pasted>";
3954
4175
  return { type: "text", text: `${label}
@@ -4084,7 +4305,7 @@ var FileMemoryBackend = class {
4084
4305
  await ensureDir(path4.dirname(file));
4085
4306
  let existing = "";
4086
4307
  try {
4087
- existing = await fsp2.readFile(file, "utf8");
4308
+ existing = await fsp3.readFile(file, "utf8");
4088
4309
  } catch {
4089
4310
  }
4090
4311
  const id = `mem_${Date.now()}_${randomUUID().slice(0, 8)}`;
@@ -4101,7 +4322,7 @@ ${line}`;
4101
4322
  return withFileLock(file, async () => {
4102
4323
  let existing;
4103
4324
  try {
4104
- existing = await fsp2.readFile(file, "utf8");
4325
+ existing = await fsp3.readFile(file, "utf8");
4105
4326
  } catch {
4106
4327
  return 0;
4107
4328
  }
@@ -4137,7 +4358,7 @@ ${line}`;
4137
4358
  async readAll(scope, filePath) {
4138
4359
  const file = this.resolveFile(filePath, scope);
4139
4360
  try {
4140
- return await fsp2.readFile(file, "utf8");
4361
+ return await fsp3.readFile(file, "utf8");
4141
4362
  } catch {
4142
4363
  return "";
4143
4364
  }
@@ -4172,7 +4393,7 @@ ${line}`;
4172
4393
  const file = this.resolveFile(filePath, scope);
4173
4394
  let existing;
4174
4395
  try {
4175
- existing = await fsp2.readFile(file, "utf8");
4396
+ existing = await fsp3.readFile(file, "utf8");
4176
4397
  } catch {
4177
4398
  return 0;
4178
4399
  }
@@ -4192,7 +4413,7 @@ ${line}`;
4192
4413
  const next = lines.join("\n");
4193
4414
  const backup = `${file}.bak.${Date.now()}`;
4194
4415
  try {
4195
- await fsp2.copyFile(file, backup);
4416
+ await fsp3.copyFile(file, backup);
4196
4417
  await pruneConsolidateBackups(file);
4197
4418
  } catch {
4198
4419
  }
@@ -4208,11 +4429,11 @@ async function pruneConsolidateBackups(file) {
4208
4429
  const dir = path4.dirname(file);
4209
4430
  const base = path4.basename(file);
4210
4431
  const prefix = `${base}.bak.`;
4211
- const backups = (await fsp2.readdir(dir)).filter((name) => name.startsWith(prefix)).sort().reverse();
4432
+ const backups = (await fsp3.readdir(dir)).filter((name) => name.startsWith(prefix)).sort().reverse();
4212
4433
  await Promise.all(
4213
4434
  backups.slice(MAX_MEMORY_CONSOLIDATE_BACKUPS).map(async (name) => {
4214
4435
  try {
4215
- await fsp2.unlink(path4.join(dir, name));
4436
+ await fsp3.unlink(path4.join(dir, name));
4216
4437
  } catch {
4217
4438
  }
4218
4439
  })
@@ -4767,9 +4988,9 @@ ${body.trim()}`);
4767
4988
  if (!this.persistBackup || scope === "project-agents") return;
4768
4989
  try {
4769
4990
  const content = await this.backend.readAll(scope, this.files[scope]);
4770
- const { writeFile: writeFile7, mkdir: mkdir8 } = await import('fs/promises');
4991
+ const { writeFile: writeFile8, mkdir: mkdir8 } = await import('fs/promises');
4771
4992
  await mkdir8(this.backupDir, { recursive: true });
4772
- await writeFile7(`${this.backupDir}/${scope}.md`, content, "utf8");
4993
+ await writeFile8(`${this.backupDir}/${scope}.md`, content, "utf8");
4773
4994
  } catch {
4774
4995
  }
4775
4996
  }
@@ -5103,8 +5324,8 @@ function unwrapDataKey(buf, keyFile) {
5103
5324
  function checkKeyFilePermissions(keyFile) {
5104
5325
  if (process.platform === "win32") return;
5105
5326
  try {
5106
- const stat6 = fs4.statSync(keyFile);
5107
- const actualMode = stat6.mode & 511;
5327
+ const stat8 = fs4.statSync(keyFile);
5328
+ const actualMode = stat8.mode & 511;
5108
5329
  if (actualMode !== KEY_FILE_MODE) {
5109
5330
  console.warn(JSON.stringify({
5110
5331
  level: "warn",
@@ -5369,20 +5590,20 @@ function isSecretField(name) {
5369
5590
  async function rewriteConfigEncrypted(configPath, vault, patch) {
5370
5591
  let current = {};
5371
5592
  try {
5372
- const raw = await fsp2.readFile(configPath, "utf8");
5593
+ const raw = await fsp3.readFile(configPath, "utf8");
5373
5594
  current = JSON.parse(raw);
5374
5595
  } catch {
5375
5596
  }
5376
5597
  const merged = deepMerge(current, patch ?? {});
5377
5598
  const encrypted = encryptConfigSecrets(merged, vault);
5378
- await fsp2.mkdir(path4.dirname(configPath), { recursive: true });
5599
+ await fsp3.mkdir(path4.dirname(configPath), { recursive: true });
5379
5600
  await atomicWrite(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
5380
5601
  await restrictFilePermissions(configPath);
5381
5602
  }
5382
5603
  async function migratePlaintextSecrets(configPath, vault, logger) {
5383
5604
  let raw;
5384
5605
  try {
5385
- raw = await fsp2.readFile(configPath, "utf8");
5606
+ raw = await fsp3.readFile(configPath, "utf8");
5386
5607
  } catch {
5387
5608
  return { migrated: 0, file: configPath };
5388
5609
  }
@@ -5424,7 +5645,7 @@ async function restrictFilePermissions(filePath, opts) {
5424
5645
  }
5425
5646
  } else {
5426
5647
  try {
5427
- await fsp2.chmod(filePath, 384);
5648
+ await fsp3.chmod(filePath, 384);
5428
5649
  } catch {
5429
5650
  }
5430
5651
  }
@@ -5510,6 +5731,9 @@ function isContextWindowModeId(id) {
5510
5731
  return CONTEXT_WINDOW_MODES.some((m) => m.id === id);
5511
5732
  }
5512
5733
 
5734
+ // src/types/config.ts
5735
+ var DEFAULT_TUI_THINKING_WORD = "thinking";
5736
+
5513
5737
  // src/types/default-config.ts
5514
5738
  var DEFAULT_TOOLS_CONFIG = Object.freeze({
5515
5739
  defaultExecutionStrategy: "smart",
@@ -5528,6 +5752,10 @@ var DEFAULT_CONTEXT_CONFIG = Object.freeze({
5528
5752
  var DEFAULT_AUTONOMY_CONFIG = Object.freeze({
5529
5753
  autoProceedDelayMs: 45e3
5530
5754
  });
5755
+ var DEFAULT_CIRCUIT_BREAKER_CONFIG = Object.freeze({
5756
+ enabled: false,
5757
+ autoKillResetMs: 6e4
5758
+ });
5531
5759
  var DEFAULT_SESSION_LOGGING_CONFIG = Object.freeze({
5532
5760
  auditLevel: "standard",
5533
5761
  sampling: {
@@ -5554,7 +5782,8 @@ var BEHAVIOR_DEFAULTS = {
5554
5782
  hardThreshold: 0.9,
5555
5783
  autoCompact: true,
5556
5784
  preserveK: DEFAULT_CONTEXT_CONFIG.preserveK,
5557
- eliseThreshold: DEFAULT_CONTEXT_CONFIG.eliseThreshold
5785
+ eliseThreshold: DEFAULT_CONTEXT_CONFIG.eliseThreshold,
5786
+ strategy: "hybrid"
5558
5787
  },
5559
5788
  tools: {
5560
5789
  defaultExecutionStrategy: DEFAULT_TOOLS_CONFIG.defaultExecutionStrategy,
@@ -5577,6 +5806,13 @@ var BEHAVIOR_DEFAULTS = {
5577
5806
  allowOutsideProjectRoot: true
5578
5807
  },
5579
5808
  mcpServers: {},
5809
+ fallbackAuto: true,
5810
+ maxConcurrent: 4,
5811
+ yolo: false,
5812
+ nextPrediction: false,
5813
+ hints: true,
5814
+ debugStream: false,
5815
+ configScope: "global",
5580
5816
  indexing: {
5581
5817
  onSessionStart: true,
5582
5818
  onEdit: true,
@@ -5584,8 +5820,60 @@ var BEHAVIOR_DEFAULTS = {
5584
5820
  debounceMs: 400
5585
5821
  },
5586
5822
  session: { ...DEFAULT_SESSION_LOGGING_CONFIG },
5587
- autonomy: { autoProceedDelayMs: DEFAULT_AUTONOMY_CONFIG.autoProceedDelayMs }
5823
+ autonomy: {
5824
+ defaultMode: "off",
5825
+ autoProceedDelayMs: DEFAULT_AUTONOMY_CONFIG.autoProceedDelayMs,
5826
+ autoProceedMaxIterations: 50,
5827
+ autonomyNextPrompt: "auto {{suggestion}}",
5828
+ terminalTitleAnimation: true,
5829
+ yolo: false,
5830
+ streamFleet: true,
5831
+ chime: false,
5832
+ confirmExit: true,
5833
+ mouseMode: false,
5834
+ enhance: true,
5835
+ enhanceDelayMs: 6e4,
5836
+ enhanceLanguage: "original",
5837
+ statuslineMode: "detailed",
5838
+ thinkingWord: DEFAULT_TUI_THINKING_WORD
5839
+ },
5840
+ circuitBreaker: { ...DEFAULT_CIRCUIT_BREAKER_CONFIG },
5841
+ modelRuntime: {
5842
+ // `effort` is intentionally undefined by default. Leaving it unset lets
5843
+ // each model use its provider-recommended reasoning effort (or none at
5844
+ // all) instead of forcing an opinionated value that may be unsupported,
5845
+ // silently omitted, and surfaced as a per-request warning. Users who
5846
+ // want a specific effort can opt in via `/settings` or the WebUI panel.
5847
+ reasoning: { mode: "auto" },
5848
+ cache: {}
5849
+ }
5588
5850
  };
5851
+ function isPlainRecord(value) {
5852
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5853
+ }
5854
+ function cloneJsonValue(value) {
5855
+ return structuredClone(value);
5856
+ }
5857
+ function fillMissingDefaults(target, defaults) {
5858
+ const value = cloneJsonValue(target);
5859
+ const changed = fillMissingDefaultsInPlace(value, defaults);
5860
+ return { value, changed };
5861
+ }
5862
+ function fillMissingDefaultsInPlace(target, defaults) {
5863
+ let changed = false;
5864
+ for (const [key, defaultValue] of Object.entries(defaults)) {
5865
+ if (!Object.prototype.hasOwnProperty.call(target, key)) {
5866
+ target[key] = cloneJsonValue(defaultValue);
5867
+ changed = true;
5868
+ continue;
5869
+ }
5870
+ const current = target[key];
5871
+ if (isPlainRecord(current) && isPlainRecord(defaultValue)) {
5872
+ changed = fillMissingDefaultsInPlace(current, defaultValue) || changed;
5873
+ }
5874
+ }
5875
+ return changed;
5876
+ }
5589
5877
  function envBool(v) {
5590
5878
  return !/^(0|false|no|off)$/i.test(v.trim());
5591
5879
  }
@@ -5637,27 +5925,139 @@ var defaultIndexing = {
5637
5925
  watchExternal: true,
5638
5926
  debounceMs: 400
5639
5927
  };
5640
- var IN_PROJECT_FORBIDDEN_KEYS = /* @__PURE__ */ new Set([
5928
+ var IN_PROJECT_ALLOWED_KEYS = /* @__PURE__ */ new Set([
5929
+ "version",
5930
+ "model",
5931
+ "cwd",
5932
+ "context",
5933
+ "tools",
5934
+ "features",
5935
+ "autonomy",
5936
+ "indexing",
5937
+ "session",
5938
+ "log",
5939
+ "launch",
5940
+ "nextPrediction",
5941
+ "hints",
5942
+ "debugStream",
5943
+ "configScope",
5944
+ "maxConcurrent",
5945
+ "fallbackModels",
5946
+ "fallbackAuto",
5947
+ "models",
5948
+ "modelMatrix",
5949
+ "circuitBreaker",
5950
+ "adaptiveConcurrency",
5951
+ "modelRuntime"
5952
+ ]);
5953
+ var KNOWN_DENIED_IN_PROJECT = [
5954
+ { key: "provider", reason: "Provider id override; can intercept prompts/responses." },
5955
+ { key: "apiKey", reason: "Overrides user API key; exfiltrates prompts." },
5956
+ { key: "baseUrl", reason: "Redirects provider endpoint; leaks real API key." },
5957
+ { key: "providers", reason: "Per-provider apiKey/baseUrl/oauthConfig; same redirect/exfil." },
5958
+ { key: "mcpServers", reason: "Arbitrary command/args/env spawned at boot (RCE)." },
5959
+ { key: "hooks", reason: "Shell command arrays on lifecycle events (RCE)." },
5960
+ { key: "plugins", reason: "Dynamic npm package load at boot (RCE)." },
5961
+ { key: "sync", reason: "Carries githubToken credential and target repo." },
5962
+ { key: "yolo", reason: "Disables all permission confirmation prompts." },
5963
+ { key: "extensions", reason: "Per-plugin config can carry command/credential fields." },
5964
+ { key: "hq", reason: "Carries HQ client token credential and endpoint URL." }
5965
+ ];
5966
+ var KNOWN_CONFIG_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
5967
+ "version",
5641
5968
  "provider",
5969
+ "model",
5642
5970
  "apiKey",
5643
5971
  "baseUrl",
5972
+ "maxConcurrent",
5644
5973
  "providers",
5974
+ "models",
5975
+ "modelMatrix",
5976
+ "context",
5977
+ "tools",
5645
5978
  "mcpServers",
5979
+ "fallbackModels",
5980
+ "fallbackAuto",
5646
5981
  "hooks",
5647
5982
  "plugins",
5648
- "sync",
5983
+ "log",
5984
+ "features",
5649
5985
  "yolo",
5986
+ "nextPrediction",
5987
+ "cwd",
5988
+ "autonomy",
5989
+ "hints",
5990
+ "debugStream",
5991
+ "configScope",
5992
+ "indexing",
5993
+ "circuitBreaker",
5994
+ "adaptiveConcurrency",
5995
+ "launch",
5996
+ "session",
5997
+ "modelRuntime",
5998
+ "hq",
5999
+ "sync",
5650
6000
  "extensions"
5651
6001
  ]);
6002
+ function assertInProjectAllowListComplete() {
6003
+ const missingFromBoth = [];
6004
+ for (const key of KNOWN_CONFIG_TOP_LEVEL_KEYS) {
6005
+ if (IN_PROJECT_ALLOWED_KEYS.has(key)) continue;
6006
+ const denied = KNOWN_DENIED_IN_PROJECT.find((d) => d.key === key);
6007
+ if (!denied) missingFromBoth.push(key);
6008
+ }
6009
+ const staleDenials = KNOWN_DENIED_IN_PROJECT.filter((d) => !KNOWN_CONFIG_TOP_LEVEL_KEYS.has(d.key)).map((d) => d.key);
6010
+ const duplicate = KNOWN_DENIED_IN_PROJECT.filter((d) => IN_PROJECT_ALLOWED_KEYS.has(d.key)).map((d) => d.key);
6011
+ const problems = [];
6012
+ if (missingFromBoth.length > 0) {
6013
+ problems.push(
6014
+ `new Config field(s) not classified as allowed or denied for in-project config: ` + missingFromBoth.join(", ") + ". Add each to IN_PROJECT_ALLOWED_KEYS (if safe) or KNOWN_DENIED_IN_PROJECT (with a reason)."
6015
+ );
6016
+ }
6017
+ if (staleDenials.length > 0) {
6018
+ problems.push(
6019
+ `KNOWN_DENIED_IN_PROJECT references keys that no longer exist on Config: ` + staleDenials.join(", ") + ". Remove them or restore the field on Config."
6020
+ );
6021
+ }
6022
+ if (duplicate.length > 0) {
6023
+ problems.push(
6024
+ `field(s) appear in BOTH IN_PROJECT_ALLOWED_KEYS and KNOWN_DENIED_IN_PROJECT: ` + duplicate.join(", ") + ". The allow-list wins at runtime; remove from one of the two."
6025
+ );
6026
+ }
6027
+ if (problems.length > 0) {
6028
+ throw new Error(
6029
+ `stripUnsafeInProjectFields drift check failed:
6030
+ - ${problems.join("\n - ")}`
6031
+ );
6032
+ }
6033
+ }
6034
+ var driftChecked = false;
5652
6035
  function stripUnsafeInProjectFields(inProject, sourcePath, warn = (msg) => console.warn(msg)) {
6036
+ if (!driftChecked) {
6037
+ assertInProjectAllowListComplete();
6038
+ driftChecked = true;
6039
+ }
5653
6040
  const stripped = [];
5654
6041
  const out = {};
5655
6042
  for (const [k, v] of Object.entries(inProject)) {
5656
- if (IN_PROJECT_FORBIDDEN_KEYS.has(k)) {
5657
- stripped.push(k);
6043
+ if (IN_PROJECT_ALLOWED_KEYS.has(k)) {
6044
+ out[k] = v;
5658
6045
  continue;
5659
6046
  }
5660
- out[k] = v;
6047
+ stripped.push(k);
6048
+ }
6049
+ const outTools = out["tools"];
6050
+ if (outTools && typeof outTools === "object") {
6051
+ const execCfg = outTools["exec"];
6052
+ if (execCfg && typeof execCfg === "object" && "allow" in execCfg) {
6053
+ const clonedExec = { ...execCfg };
6054
+ delete clonedExec["allow"];
6055
+ out["tools"] = {
6056
+ ...outTools,
6057
+ exec: clonedExec
6058
+ };
6059
+ stripped.push("tools.exec.allow");
6060
+ }
5661
6061
  }
5662
6062
  if (stripped.length > 0) {
5663
6063
  warn(
@@ -5666,7 +6066,7 @@ function stripUnsafeInProjectFields(inProject, sourcePath, warn = (msg) => conso
5666
6066
  event: "config.in_project_unsafe_fields_ignored",
5667
6067
  path: sourcePath,
5668
6068
  ignoredKeys: stripped,
5669
- message: `Ignored ${stripped.length} unsafe field(s) from the repo-committed config "${sourcePath}": ${stripped.join(", ")}. These can only be set in your personal ~/.wrongstack/config.json, not in a project-committed file.`,
6069
+ message: `Ignored ${stripped.length} field(s) from the repo-committed config "${sourcePath}": ${stripped.join(", ")}. Only a small allow-list of benign preferences (model, context, tools limits, features, \u2026) may be set by <project>/.wrongstack/config.json. Everything else must live in your personal ~/.wrongstack/config.json.`,
5670
6070
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5671
6071
  })
5672
6072
  );
@@ -5700,6 +6100,7 @@ var DefaultConfigLoader = class {
5700
6100
  extraSources;
5701
6101
  events;
5702
6102
  traceId;
6103
+ jsonCache = /* @__PURE__ */ new Map();
5703
6104
  constructor(opts) {
5704
6105
  this.paths = opts.paths;
5705
6106
  this.strict = opts.strict ?? false;
@@ -5710,6 +6111,7 @@ var DefaultConfigLoader = class {
5710
6111
  }
5711
6112
  async load(opts = {}) {
5712
6113
  let cfg = { ...BEHAVIOR_DEFAULTS };
6114
+ await this.ensureGlobalDefaults();
5713
6115
  const inProjectCollides = samePath(this.paths.inProjectConfig, this.paths.globalConfig) || samePath(this.paths.inProjectConfig, this.paths.projectLocalConfig);
5714
6116
  const [global, local, inProject] = await Promise.all([
5715
6117
  this.readJson(this.paths.globalConfig),
@@ -5774,6 +6176,80 @@ var DefaultConfigLoader = class {
5774
6176
  }
5775
6177
  return Object.freeze(cfg);
5776
6178
  }
6179
+ async ensureGlobalDefaults() {
6180
+ const fp = this.paths.globalConfig;
6181
+ const t0 = Date.now();
6182
+ try {
6183
+ await withFileLock(fp, async () => {
6184
+ let parsed;
6185
+ try {
6186
+ const raw = await fsp3.readFile(fp, "utf8");
6187
+ const result = safeParse(raw);
6188
+ if (!result.ok || !isPlainRecord(result.value)) {
6189
+ return;
6190
+ }
6191
+ parsed = result.value;
6192
+ } catch (err) {
6193
+ if (err.code !== "ENOENT") {
6194
+ this.events?.emit("storage.error", {
6195
+ sessionId: "~config~",
6196
+ store: "config",
6197
+ filePath: fp,
6198
+ operation: "ensure_defaults",
6199
+ outcome: "failure",
6200
+ error: storageErrorString(err),
6201
+ recoverable: false,
6202
+ durationMs: Date.now() - t0,
6203
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
6204
+ });
6205
+ console.warn(JSON.stringify({
6206
+ level: "warn",
6207
+ event: "config.defaults_read_failed",
6208
+ path: fp,
6209
+ message: toErrorMessage(err),
6210
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6211
+ }));
6212
+ return;
6213
+ }
6214
+ parsed = {};
6215
+ }
6216
+ const { value, changed } = fillMissingDefaults(
6217
+ parsed,
6218
+ BEHAVIOR_DEFAULTS
6219
+ );
6220
+ if (!changed) return;
6221
+ await atomicWrite(fp, JSON.stringify(value, null, 2), { mode: 384 });
6222
+ this.events?.emit("storage.write", {
6223
+ sessionId: "~config~",
6224
+ store: "config",
6225
+ filePath: fp,
6226
+ operation: "ensure_defaults",
6227
+ outcome: "success",
6228
+ durationMs: Date.now() - t0,
6229
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
6230
+ });
6231
+ });
6232
+ } catch (err) {
6233
+ this.events?.emit("storage.error", {
6234
+ sessionId: "~config~",
6235
+ store: "config",
6236
+ filePath: fp,
6237
+ operation: "ensure_defaults",
6238
+ outcome: "failure",
6239
+ error: storageErrorString(err),
6240
+ recoverable: false,
6241
+ durationMs: Date.now() - t0,
6242
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
6243
+ });
6244
+ console.warn(JSON.stringify({
6245
+ level: "warn",
6246
+ event: "config.defaults_write_failed",
6247
+ path: fp,
6248
+ message: toErrorMessage(err),
6249
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6250
+ }));
6251
+ }
6252
+ }
5777
6253
  /**
5778
6254
  * Persist a sync config to ~/.wrongstack/sync.json, with the token encrypted
5779
6255
  * by the vault (if provided). The file is isolated from the main config
@@ -5822,7 +6298,7 @@ var DefaultConfigLoader = class {
5822
6298
  const fp = this.paths.syncConfig;
5823
6299
  const t0 = Date.now();
5824
6300
  try {
5825
- const raw = await fsp2.readFile(fp, "utf8");
6301
+ const raw = await fsp3.readFile(fp, "utf8");
5826
6302
  const parsed = safeParse(raw);
5827
6303
  if (!parsed.ok || !parsed.value) {
5828
6304
  this.events?.emit("storage.read", {
@@ -5883,10 +6359,42 @@ var DefaultConfigLoader = class {
5883
6359
  }
5884
6360
  }
5885
6361
  async readJson(file) {
5886
- let raw;
5887
6362
  const t0 = Date.now();
6363
+ let mtimeMs = null;
6364
+ try {
6365
+ const stat8 = await fsp3.stat(file);
6366
+ mtimeMs = stat8.mtimeMs;
6367
+ const cached = this.jsonCache.get(file);
6368
+ if (cached && cached.mtimeMs === mtimeMs) {
6369
+ return structuredClone(cached.value);
6370
+ }
6371
+ } catch (err) {
6372
+ if (err.code === "ENOENT") {
6373
+ this.jsonCache.set(file, { mtimeMs: null, value: {} });
6374
+ return {};
6375
+ }
6376
+ this.events?.emit("storage.read", {
6377
+ sessionId: "~config~",
6378
+ store: "config",
6379
+ filePath: file,
6380
+ operation: "read_json",
6381
+ outcome: "failure",
6382
+ durationMs: Date.now() - t0,
6383
+ error: storageErrorString(err),
6384
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
6385
+ });
6386
+ console.warn(JSON.stringify({
6387
+ level: "warn",
6388
+ event: "config.read_failed",
6389
+ path: file,
6390
+ message: toErrorMessage(err),
6391
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6392
+ }));
6393
+ return {};
6394
+ }
6395
+ let raw;
5888
6396
  try {
5889
- raw = await fsp2.readFile(file, "utf8");
6397
+ raw = await fsp3.readFile(file, "utf8");
5890
6398
  } catch (err) {
5891
6399
  if (err.code !== "ENOENT") {
5892
6400
  this.events?.emit("storage.read", {
@@ -5907,6 +6415,7 @@ var DefaultConfigLoader = class {
5907
6415
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5908
6416
  }));
5909
6417
  }
6418
+ this.jsonCache.set(file, { mtimeMs: null, value: {} });
5910
6419
  return {};
5911
6420
  }
5912
6421
  const parsed = safeParse(raw);
@@ -5930,6 +6439,7 @@ var DefaultConfigLoader = class {
5930
6439
  }));
5931
6440
  return {};
5932
6441
  }
6442
+ this.jsonCache.set(file, { mtimeMs, value: structuredClone(parsed.value) });
5933
6443
  return parsed.value;
5934
6444
  }
5935
6445
  validateBehavior(cfg) {
@@ -6127,7 +6637,7 @@ var RecoveryLock = class {
6127
6637
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
6128
6638
  };
6129
6639
  try {
6130
- await fsp2.writeFile(this.file, JSON.stringify(lock), { flag: "wx", mode: 384 });
6640
+ await fsp3.writeFile(this.file, JSON.stringify(lock), { flag: "wx", mode: 384 });
6131
6641
  } catch (err) {
6132
6642
  const code = err.code;
6133
6643
  if (code === "EEXIST") {
@@ -6143,7 +6653,7 @@ var RecoveryLock = class {
6143
6653
  */
6144
6654
  async clear() {
6145
6655
  try {
6146
- await fsp2.unlink(this.file);
6656
+ await fsp3.unlink(this.file);
6147
6657
  } catch (err) {
6148
6658
  const code = err.code;
6149
6659
  if (code === "ENOENT") return;
@@ -6153,7 +6663,7 @@ var RecoveryLock = class {
6153
6663
  async readLock() {
6154
6664
  let raw;
6155
6665
  try {
6156
- raw = await fsp2.readFile(this.file, "utf8");
6666
+ raw = await fsp3.readFile(this.file, "utf8");
6157
6667
  } catch (err) {
6158
6668
  const code = err.code;
6159
6669
  if (code === "ENOENT") return null;
@@ -6184,26 +6694,82 @@ function defaultIsPidAlive(pid) {
6184
6694
  return false;
6185
6695
  }
6186
6696
  }
6187
-
6188
- // src/storage/session-reader.ts
6189
- var DefaultSessionReader = class {
6697
+ var DefaultSessionReader = class _DefaultSessionReader {
6190
6698
  store;
6699
+ eventCache = /* @__PURE__ */ new Map();
6700
+ eventCacheMtimes = /* @__PURE__ */ new Map();
6701
+ static EVENT_CACHE_MAX_ENTRIES = 32;
6191
6702
  constructor(opts) {
6192
6703
  this.store = opts.store;
6193
6704
  }
6705
+ async loadCachedSessionData(sessionId) {
6706
+ const storeWithPath = this.store;
6707
+ const rootDir = storeWithPath.dir;
6708
+ if (!rootDir) {
6709
+ return await this.store.load(sessionId);
6710
+ }
6711
+ const sessionPath = path4.join(rootDir, `${sessionId}.jsonl`);
6712
+ let mtimeMs = null;
6713
+ try {
6714
+ const stat8 = await fsp3.stat(sessionPath);
6715
+ mtimeMs = stat8.mtimeMs;
6716
+ } catch {
6717
+ this.eventCache.delete(sessionId);
6718
+ this.eventCacheMtimes.delete(sessionId);
6719
+ return await this.store.load(sessionId);
6720
+ }
6721
+ const cachedMtime = this.eventCacheMtimes.get(sessionId);
6722
+ const cachedData = this.eventCache.get(sessionId);
6723
+ if (cachedData && cachedMtime === mtimeMs) {
6724
+ this.eventCache.delete(sessionId);
6725
+ this.eventCacheMtimes.delete(sessionId);
6726
+ this.eventCache.set(sessionId, cachedData);
6727
+ this.eventCacheMtimes.set(sessionId, mtimeMs);
6728
+ return cachedData;
6729
+ }
6730
+ const data = await this.store.load(sessionId);
6731
+ this.eventCache.delete(sessionId);
6732
+ this.eventCacheMtimes.delete(sessionId);
6733
+ this.eventCache.set(sessionId, data);
6734
+ this.eventCacheMtimes.set(sessionId, mtimeMs);
6735
+ while (this.eventCache.size > _DefaultSessionReader.EVENT_CACHE_MAX_ENTRIES) {
6736
+ const oldest = this.eventCache.keys().next().value;
6737
+ if (oldest === void 0) break;
6738
+ this.eventCache.delete(oldest);
6739
+ this.eventCacheMtimes.delete(oldest);
6740
+ }
6741
+ if (data.metadata.endedAt) {
6742
+ storeWithPath.clearLoadCache?.(sessionId);
6743
+ }
6744
+ return data;
6745
+ }
6194
6746
  async query(q = {}) {
6195
- const raw = await this.store.list(q.limit ? Math.max(q.limit, 100) : 1e3);
6196
- const titleNeedle = q.titleContains?.toLowerCase();
6197
- const filtered = raw.filter((s) => {
6198
- if (q.since && s.startedAt < q.since) return false;
6199
- if (q.until && s.startedAt > q.until) return false;
6200
- if (q.provider && s.provider !== q.provider) return false;
6201
- if (q.model && s.model !== q.model) return false;
6202
- if (q.minTokens !== void 0 && s.tokenTotal < q.minTokens) return false;
6203
- if (titleNeedle && !s.title.toLowerCase().includes(titleNeedle)) return false;
6204
- return true;
6205
- });
6206
- const out = filtered.map((s) => ({
6747
+ const storeWithFilter = this.store;
6748
+ let raw;
6749
+ if (typeof storeWithFilter.listFiltered === "function") {
6750
+ raw = await storeWithFilter.listFiltered({
6751
+ since: q.since,
6752
+ until: q.until,
6753
+ provider: q.provider,
6754
+ model: q.model,
6755
+ minTokens: q.minTokens,
6756
+ titleContains: q.titleContains,
6757
+ limit: q.limit
6758
+ });
6759
+ } else {
6760
+ const fetched = await this.store.list(q.limit ? Math.max(q.limit, 100) : 1e3);
6761
+ const titleNeedle = q.titleContains?.toLowerCase();
6762
+ raw = fetched.filter((s) => {
6763
+ if (q.since && s.startedAt < q.since) return false;
6764
+ if (q.until && s.startedAt > q.until) return false;
6765
+ if (q.provider && s.provider !== q.provider) return false;
6766
+ if (q.model && s.model !== q.model) return false;
6767
+ if (q.minTokens !== void 0 && s.tokenTotal < q.minTokens) return false;
6768
+ if (titleNeedle && !s.title.toLowerCase().includes(titleNeedle)) return false;
6769
+ return true;
6770
+ });
6771
+ }
6772
+ const out = raw.map((s) => ({
6207
6773
  id: s.id,
6208
6774
  title: s.title,
6209
6775
  startedAt: s.startedAt,
@@ -6214,7 +6780,7 @@ var DefaultSessionReader = class {
6214
6780
  return q.limit ? out.slice(0, q.limit) : out;
6215
6781
  }
6216
6782
  async *replay(sessionId) {
6217
- const data = await this.store.load(sessionId);
6783
+ const data = await this.loadCachedSessionData(sessionId);
6218
6784
  for (const e of data.events) yield e;
6219
6785
  }
6220
6786
  async search(q, sessionId, sessionQuery) {
@@ -6225,24 +6791,66 @@ var DefaultSessionReader = class {
6225
6791
  if (sessionId) {
6226
6792
  ids = [sessionId];
6227
6793
  } else {
6228
- const sessions = await this.store.list(1e3);
6229
- const titleNeedle = sessionQuery?.titleContains?.toLowerCase();
6230
- const filtered = sessions.filter((s) => {
6231
- if (sessionQuery?.since && s.startedAt < sessionQuery.since) return false;
6232
- if (sessionQuery?.until && s.startedAt > sessionQuery.until) return false;
6233
- if (sessionQuery?.provider && s.provider !== sessionQuery.provider) return false;
6234
- if (sessionQuery?.model && s.model !== sessionQuery.model) return false;
6235
- if (sessionQuery?.minTokens !== void 0 && s.tokenTotal < sessionQuery.minTokens) return false;
6236
- if (titleNeedle && !s.title.toLowerCase().includes(titleNeedle)) return false;
6237
- return true;
6238
- });
6239
- ids = filtered.map((s) => s.id);
6794
+ const storeWithFilter = this.store;
6795
+ let sessions;
6796
+ if (typeof storeWithFilter.listFiltered === "function") {
6797
+ sessions = await storeWithFilter.listFiltered({
6798
+ since: sessionQuery?.since,
6799
+ until: sessionQuery?.until,
6800
+ provider: sessionQuery?.provider,
6801
+ model: sessionQuery?.model,
6802
+ minTokens: sessionQuery?.minTokens,
6803
+ titleContains: sessionQuery?.titleContains,
6804
+ limit: 1e3
6805
+ });
6806
+ } else {
6807
+ sessions = await this.store.list(1e3);
6808
+ const titleNeedle = sessionQuery?.titleContains?.toLowerCase();
6809
+ sessions = sessions.filter((s) => {
6810
+ if (sessionQuery?.since && s.startedAt < sessionQuery.since) return false;
6811
+ if (sessionQuery?.until && s.startedAt > sessionQuery.until) return false;
6812
+ if (sessionQuery?.provider && s.provider !== sessionQuery.provider) return false;
6813
+ if (sessionQuery?.model && s.model !== sessionQuery.model) return false;
6814
+ if (sessionQuery?.minTokens !== void 0 && s.tokenTotal < sessionQuery.minTokens) return false;
6815
+ if (titleNeedle && !s.title.toLowerCase().includes(titleNeedle)) return false;
6816
+ return true;
6817
+ });
6818
+ }
6819
+ ids = sessions.map((s) => s.id);
6240
6820
  }
6241
6821
  const hits = [];
6822
+ const streaming = this.store.searchEvents?.bind(this.store);
6823
+ if (streaming) {
6824
+ for (const id of ids) {
6825
+ const matched = await streaming(
6826
+ id,
6827
+ (ev) => {
6828
+ if (allowedTypes && !allowedTypes.has(ev.type)) return false;
6829
+ const text = eventText(ev);
6830
+ if (text === null) return false;
6831
+ return matcher(text) !== null;
6832
+ },
6833
+ { limit: limit - hits.length }
6834
+ );
6835
+ for (const m of matched) {
6836
+ const text = expectDefined(eventText(m.event));
6837
+ const hit = expectDefined(matcher(text));
6838
+ hits.push({
6839
+ sessionId: id,
6840
+ eventIndex: m.eventIndex,
6841
+ ts: m.ts,
6842
+ type: m.event.type,
6843
+ snippet: snippetOf(text, hit.start, hit.end)
6844
+ });
6845
+ if (hits.length >= limit) return hits;
6846
+ }
6847
+ }
6848
+ return hits;
6849
+ }
6242
6850
  for (const id of ids) {
6243
6851
  let data;
6244
6852
  try {
6245
- data = await this.store.load(id);
6853
+ data = await this.loadCachedSessionData(id);
6246
6854
  } catch {
6247
6855
  continue;
6248
6856
  }
@@ -6266,7 +6874,7 @@ var DefaultSessionReader = class {
6266
6874
  return hits;
6267
6875
  }
6268
6876
  async export(sessionId, opts) {
6269
- const data = await this.store.load(sessionId);
6877
+ const data = await this.loadCachedSessionData(sessionId);
6270
6878
  const includeTools = opts.includeTools ?? true;
6271
6879
  const includeDiagnostics = opts.includeDiagnostics ?? true;
6272
6880
  const filtered = data.events.filter((e) => {
@@ -6287,7 +6895,7 @@ var DefaultSessionReader = class {
6287
6895
  return renderMarkdown(data.metadata, filtered);
6288
6896
  }
6289
6897
  async metadata(sessionId) {
6290
- const data = await this.store.load(sessionId);
6898
+ const data = await this.loadCachedSessionData(sessionId);
6291
6899
  return data.metadata;
6292
6900
  }
6293
6901
  };
@@ -6681,7 +7289,7 @@ async function loadTodosCheckpoint(filePath, events, traceId) {
6681
7289
  const t0 = Date.now();
6682
7290
  let raw;
6683
7291
  try {
6684
- raw = await fsp2.readFile(filePath, "utf8");
7292
+ raw = await fsp3.readFile(filePath, "utf8");
6685
7293
  } catch (err) {
6686
7294
  events?.emit("storage.error", {
6687
7295
  sessionId: traceId ?? "~boot~",
@@ -6823,7 +7431,7 @@ async function loadPlan(filePath, events) {
6823
7431
  const t0 = Date.now();
6824
7432
  let raw;
6825
7433
  try {
6826
- raw = await fsp2.readFile(filePath, "utf8");
7434
+ raw = await fsp3.readFile(filePath, "utf8");
6827
7435
  } catch (err) {
6828
7436
  events?.emit("storage.error", {
6829
7437
  sessionId: "~boot~",
@@ -7117,7 +7725,7 @@ init_atomic_write();
7117
7725
  async function loadDirectorState(filePath) {
7118
7726
  let raw;
7119
7727
  try {
7120
- raw = await fsp2.readFile(filePath, "utf8");
7728
+ raw = await fsp3.readFile(filePath, "utf8");
7121
7729
  } catch {
7122
7730
  return null;
7123
7731
  }
@@ -7132,7 +7740,7 @@ async function loadDirectorState(filePath) {
7132
7740
  async function acquireDirectorStateLock(lockPath, processId = process.pid) {
7133
7741
  let existing;
7134
7742
  try {
7135
- existing = await fsp2.readFile(lockPath, "utf8");
7743
+ existing = await fsp3.readFile(lockPath, "utf8");
7136
7744
  } catch {
7137
7745
  }
7138
7746
  if (existing) {
@@ -7156,7 +7764,7 @@ async function acquireDirectorStateLock(lockPath, processId = process.pid) {
7156
7764
  }
7157
7765
  async function releaseDirectorStateLock(lockPath) {
7158
7766
  try {
7159
- await fsp2.unlink(lockPath);
7767
+ await fsp3.unlink(lockPath);
7160
7768
  } catch {
7161
7769
  }
7162
7770
  }
@@ -7701,7 +8309,7 @@ var DefaultPermissionPolicy = class {
7701
8309
  }
7702
8310
  async reload() {
7703
8311
  try {
7704
- const raw = await fsp2.readFile(this.trustFile, "utf8");
8312
+ const raw = await fsp3.readFile(this.trustFile, "utf8");
7705
8313
  const parsed = safeParse(raw);
7706
8314
  if (parsed.ok && parsed.value) this.policy = parsed.value;
7707
8315
  } catch {
@@ -8187,12 +8795,12 @@ var DefaultSkillLoader = class {
8187
8795
  const seen = /* @__PURE__ */ new Set();
8188
8796
  for (const { dir, source } of this.dirs) {
8189
8797
  try {
8190
- const entries = await fsp2.readdir(dir, { withFileTypes: true });
8798
+ const entries = await fsp3.readdir(dir, { withFileTypes: true });
8191
8799
  for (const e of entries) {
8192
8800
  if (!e.isDirectory()) continue;
8193
8801
  const skillFile = path4.join(dir, e.name, "SKILL.md");
8194
8802
  try {
8195
- const raw = await fsp2.readFile(skillFile, "utf8");
8803
+ const raw = await fsp3.readFile(skillFile, "utf8");
8196
8804
  const meta = parseFrontmatter(raw);
8197
8805
  if (!meta.name || !meta.description) continue;
8198
8806
  if (seen.has(meta.name)) continue;
@@ -8251,7 +8859,7 @@ var DefaultSkillLoader = class {
8251
8859
  if (cached !== void 0) return cached;
8252
8860
  const m = await this.find(name);
8253
8861
  if (!m) throw new Error(`Skill "${name}" not found`);
8254
- const body = await fsp2.readFile(m.path, "utf8");
8862
+ const body = await fsp3.readFile(m.path, "utf8");
8255
8863
  this.bodyCache.set(key, body);
8256
8864
  return body;
8257
8865
  }
@@ -8264,9 +8872,9 @@ var DefaultSkillLoader = class {
8264
8872
  const savePath = path4.join(path4.dirname(m.path), "SKILL.save.md");
8265
8873
  let result;
8266
8874
  try {
8267
- result = await fsp2.readFile(savePath, "utf8");
8875
+ result = await fsp3.readFile(savePath, "utf8");
8268
8876
  } catch {
8269
- const full = await fsp2.readFile(m.path, "utf8");
8877
+ const full = await fsp3.readFile(m.path, "utf8");
8270
8878
  const body = stripFrontmatter(full);
8271
8879
  const compact = compactSkillBody(body);
8272
8880
  if (compact) {
@@ -10052,6 +10660,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
10052
10660
  level,
10053
10661
  tokens,
10054
10662
  load,
10663
+ hardThreshold: adaptiveThresholds.hard,
10055
10664
  budget,
10056
10665
  signals: { repeatedReadCount: repetition }
10057
10666
  });
@@ -10099,6 +10708,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
10099
10708
  }
10100
10709
  }
10101
10710
  async compact(ctx, aggressive, pressure) {
10711
+ let postCompactionOverflow = null;
10102
10712
  try {
10103
10713
  const report = await this.compactor.compact(ctx, { aggressive });
10104
10714
  this.recordAttempt(pressure.level, pressure.tokens, report);
@@ -10128,6 +10738,38 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
10128
10738
  ...report.collapsedDigest ? { digest: truncateDigest(report.collapsedDigest) } : {}
10129
10739
  });
10130
10740
  ctx.clearFileTracking();
10741
+ const afterTokens = report.fullRequestTokensAfter ?? report.after;
10742
+ const afterLoad = this._maxContext > 0 ? afterTokens / this._maxContext : 0;
10743
+ const stillHard = afterLoad >= pressure.hardThreshold;
10744
+ const fatal = stillHard && (this.failureMode === "throw" || this.failureMode === "throw_on_hard" && pressure.level === "hard");
10745
+ if (stillHard) {
10746
+ const error = new Error(
10747
+ `Auto-compaction left context above the hard threshold after ${pressure.level} compaction`
10748
+ );
10749
+ this.events?.emit("compaction.failed", {
10750
+ err: error,
10751
+ aggressive,
10752
+ level: pressure.level,
10753
+ tokens: afterTokens,
10754
+ maxContext: this._maxContext,
10755
+ budget: computeContextWindowBudget(ctx, afterTokens, this._maxContext),
10756
+ signals: pressure.signals,
10757
+ load: afterLoad,
10758
+ fatal
10759
+ });
10760
+ if (fatal) {
10761
+ postCompactionOverflow = new AgentError({
10762
+ message: `Auto-compaction did not reduce context below hard threshold`,
10763
+ code: ERROR_CODES.AGENT_CONTEXT_OVERFLOW,
10764
+ recoverable: true,
10765
+ context: {
10766
+ level: pressure.level,
10767
+ tokens: afterTokens,
10768
+ maxContext: this._maxContext
10769
+ }
10770
+ });
10771
+ }
10772
+ }
10131
10773
  } catch (err) {
10132
10774
  const error = err instanceof Error ? err : new Error(String(err));
10133
10775
  const fatal = this.failureMode === "throw" || this.failureMode === "throw_on_hard" && pressure.level === "hard";
@@ -10156,6 +10798,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
10156
10798
  });
10157
10799
  }
10158
10800
  }
10801
+ if (postCompactionOverflow) throw postCompactionOverflow;
10159
10802
  }
10160
10803
  };
10161
10804
  function computeContextWindowBudget(ctx, inputTokens, maxContext) {
@@ -10324,7 +10967,7 @@ ${errorDetails}`,
10324
10967
  "tool.name": tool.name,
10325
10968
  "tool.mutating": tool.mutating,
10326
10969
  "tool.permission": tool.permission,
10327
- "tool.capabilities": JSON.stringify(tool.capabilities ?? []),
10970
+ "tool.capabilities": toolCapsForAudit.length > 0 ? JSON.stringify(tool.capabilities ?? []) : "[]",
10328
10971
  "tool.has_dangerous_capabilities": toolCapsForAudit.length > 0
10329
10972
  });
10330
10973
  try {
@@ -10680,11 +11323,11 @@ async function maybePersistLargeToolOutput(toolName, content, budget) {
10680
11323
  }
10681
11324
  try {
10682
11325
  const dir = path4.join(wstackGlobalRoot(), "tool-output");
10683
- await fsp2.mkdir(dir, { recursive: true });
11326
+ await fsp3.mkdir(dir, { recursive: true });
10684
11327
  const safeTool = toolName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 40) || "tool";
10685
11328
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
10686
11329
  const filePath = path4.join(dir, `${stamp}-${safeTool}-${randomUUID()}.log`);
10687
- await fsp2.writeFile(filePath, content, "utf8");
11330
+ await fsp3.writeFile(filePath, content, "utf8");
10688
11331
  return content + `
10689
11332
  [full tool output: ${bytes} bytes at ${filePath}; read/grep that file selectively instead of re-running or requesting more output]`;
10690
11333
  } catch {
@@ -10879,7 +11522,7 @@ async function loadGoal(filePath, events) {
10879
11522
  const t0 = Date.now();
10880
11523
  let raw;
10881
11524
  try {
10882
- raw = await fsp2.readFile(filePath, "utf8");
11525
+ raw = await fsp3.readFile(filePath, "utf8");
10883
11526
  } catch (err) {
10884
11527
  const code = err.code;
10885
11528
  if (code === "ENOENT") {
@@ -11629,8 +12272,8 @@ ${recentJournal}` : "No prior iterations.",
11629
12272
  await saveGoal(this.goalPath, abandoned, this.opts.events);
11630
12273
  }
11631
12274
  try {
11632
- const { unlink: unlink13 } = await import('fs/promises');
11633
- await unlink13(this.goalPath);
12275
+ const { unlink: unlink14 } = await import('fs/promises');
12276
+ await unlink14(this.goalPath);
11634
12277
  } catch {
11635
12278
  }
11636
12279
  this.opts.onEternalStop?.();
@@ -17292,9 +17935,9 @@ var CollabSession = class extends EventEmitter {
17292
17935
  }
17293
17936
  for (const filePath of allFiles) {
17294
17937
  try {
17295
- const [content, stat6] = await Promise.all([
17296
- fsp2.readFile(filePath, "utf8"),
17297
- fsp2.stat(filePath)
17938
+ const [content, stat8] = await Promise.all([
17939
+ fsp3.readFile(filePath, "utf8"),
17940
+ fsp3.stat(filePath)
17298
17941
  ]);
17299
17942
  const ext = filePath.split(".").pop() ?? "";
17300
17943
  const language = ext === "ts" || ext === "tsx" ? "typescript" : ext === "js" || ext === "jsx" ? "javascript" : ext === "md" ? "markdown" : ext === "json" ? "json" : void 0;
@@ -17302,8 +17945,8 @@ var CollabSession = class extends EventEmitter {
17302
17945
  path: filePath,
17303
17946
  content,
17304
17947
  language,
17305
- snapshotMtimeMs: stat6.mtimeMs,
17306
- snapshotSizeBytes: stat6.size
17948
+ snapshotMtimeMs: stat8.mtimeMs,
17949
+ snapshotSizeBytes: stat8.size
17307
17950
  });
17308
17951
  } catch {
17309
17952
  this.snapshot.files.push({ path: filePath, content: "", language: void 0 });
@@ -17716,9 +18359,9 @@ Emit each evaluation immediately. Do not wait until you have read all reports.`;
17716
18359
  for (const file of this.snapshot.files) {
17717
18360
  if (file.snapshotMtimeMs === void 0 && file.snapshotSizeBytes === void 0) continue;
17718
18361
  try {
17719
- const stat6 = await fsp2.stat(file.path);
17720
- const mtimeChanged = file.snapshotMtimeMs !== void 0 && stat6.mtimeMs > file.snapshotMtimeMs + 1;
17721
- const sizeChanged = file.snapshotSizeBytes !== void 0 && stat6.size !== file.snapshotSizeBytes;
18362
+ const stat8 = await fsp3.stat(file.path);
18363
+ const mtimeChanged = file.snapshotMtimeMs !== void 0 && stat8.mtimeMs > file.snapshotMtimeMs + 1;
18364
+ const sizeChanged = file.snapshotSizeBytes !== void 0 && stat8.size !== file.snapshotSizeBytes;
17722
18365
  if (mtimeChanged || sizeChanged) {
17723
18366
  warnings.push(`${file.path} changed after the collab snapshot was captured.`);
17724
18367
  }
@@ -18920,7 +19563,7 @@ var Director = class _Director {
18920
19563
  this.fleetManager = opts.fleetManager;
18921
19564
  this.logger = opts.logger;
18922
19565
  if (this.sharedScratchpadPath) {
18923
- void fsp2.mkdir(this.sharedScratchpadPath, { recursive: true }).catch((err) => this.logShutdownError("shared_scratchpad_mkdir", err));
19566
+ void fsp3.mkdir(this.sharedScratchpadPath, { recursive: true }).catch((err) => this.logShutdownError("shared_scratchpad_mkdir", err));
18924
19567
  }
18925
19568
  this.transport = new InMemoryBridgeTransport();
18926
19569
  this.bridge = new InMemoryAgentBridge(
@@ -19520,7 +20163,7 @@ var Director = class _Director {
19520
20163
  })),
19521
20164
  usage: this.usage.snapshot()
19522
20165
  };
19523
- await fsp2.mkdir(path4.dirname(this.manifestPath), { recursive: true });
20166
+ await fsp3.mkdir(path4.dirname(this.manifestPath), { recursive: true });
19524
20167
  await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
19525
20168
  return this.manifestPath;
19526
20169
  }
@@ -19754,7 +20397,7 @@ var Director = class _Director {
19754
20397
  const filePath = path4.join(this.sessionsRoot, this.directorRunId, `${subagentId}.jsonl`);
19755
20398
  let raw;
19756
20399
  try {
19757
- raw = await fsp2.readFile(filePath, "utf8");
20400
+ raw = await fsp3.readFile(filePath, "utf8");
19758
20401
  } catch {
19759
20402
  return null;
19760
20403
  }
@@ -20284,7 +20927,7 @@ async function readSubagentPartial(opts, subagentId) {
20284
20927
  candidates.push(path4.join(opts.sessionsRoot, opts.directorRunId, `${subagentId}.jsonl`));
20285
20928
  } else {
20286
20929
  try {
20287
- const entries = await fsp2.readdir(opts.sessionsRoot, { withFileTypes: true });
20930
+ const entries = await fsp3.readdir(opts.sessionsRoot, { withFileTypes: true });
20288
20931
  for (const entry of entries) {
20289
20932
  if (entry.isDirectory()) {
20290
20933
  candidates.push(path4.join(opts.sessionsRoot, entry.name, `${subagentId}.jsonl`));
@@ -20297,7 +20940,7 @@ async function readSubagentPartial(opts, subagentId) {
20297
20940
  for (const file of candidates) {
20298
20941
  let raw;
20299
20942
  try {
20300
- raw = await fsp2.readFile(file, "utf8");
20943
+ raw = await fsp3.readFile(file, "utf8");
20301
20944
  } catch {
20302
20945
  continue;
20303
20946
  }
@@ -20627,7 +21270,7 @@ var DefaultModelsRegistry = class {
20627
21270
  async readOverlayFile() {
20628
21271
  if (!this.overlayFile) return void 0;
20629
21272
  try {
20630
- const raw = await fsp2.readFile(this.overlayFile, "utf8");
21273
+ const raw = await fsp3.readFile(this.overlayFile, "utf8");
20631
21274
  return JSON.parse(raw);
20632
21275
  } catch {
20633
21276
  return void 0;
@@ -20706,7 +21349,7 @@ var DefaultModelsRegistry = class {
20706
21349
  }
20707
21350
  async readCacheAt(file) {
20708
21351
  try {
20709
- const raw = await fsp2.readFile(file, "utf8");
21352
+ const raw = await fsp3.readFile(file, "utf8");
20710
21353
  return JSON.parse(raw);
20711
21354
  } catch {
20712
21355
  return void 0;
@@ -21092,7 +21735,7 @@ var DefaultModeStore = class {
21092
21735
  async loadActiveMode() {
21093
21736
  try {
21094
21737
  const configPath = path4.join(this.configDir, "mode.json");
21095
- const content = await fsp2.readFile(configPath, "utf8");
21738
+ const content = await fsp3.readFile(configPath, "utf8");
21096
21739
  const data = JSON.parse(content);
21097
21740
  this.activeModeId = data.activeMode ?? null;
21098
21741
  } catch {
@@ -21101,7 +21744,7 @@ var DefaultModeStore = class {
21101
21744
  }
21102
21745
  async saveActiveMode() {
21103
21746
  try {
21104
- await fsp2.mkdir(this.configDir, { recursive: true });
21747
+ await fsp3.mkdir(this.configDir, { recursive: true });
21105
21748
  const configPath = path4.join(this.configDir, "mode.json");
21106
21749
  await atomicWrite(
21107
21750
  configPath,
@@ -21114,13 +21757,13 @@ var DefaultModeStore = class {
21114
21757
  async function loadProjectModes(modesDir) {
21115
21758
  const modes = [];
21116
21759
  try {
21117
- const entries = await fsp2.readdir(modesDir);
21760
+ const entries = await fsp3.readdir(modesDir);
21118
21761
  for (const entry of entries) {
21119
21762
  if (!entry.endsWith(".md") && !entry.endsWith(".txt")) continue;
21120
21763
  const filePath = path4.join(modesDir, entry);
21121
- const stat6 = await fsp2.stat(filePath);
21122
- if (!stat6.isFile()) continue;
21123
- const content = await fsp2.readFile(filePath, "utf8");
21764
+ const stat8 = await fsp3.stat(filePath);
21765
+ if (!stat8.isFile()) continue;
21766
+ const content = await fsp3.readFile(filePath, "utf8");
21124
21767
  const id = path4.basename(entry, path4.extname(entry));
21125
21768
  modes.push({
21126
21769
  id,
@@ -21138,7 +21781,7 @@ async function loadUserModes(modesDir) {
21138
21781
  const modes = [];
21139
21782
  try {
21140
21783
  const manifestPath = path4.join(modesDir, "modes.json");
21141
- const content = await fsp2.readFile(manifestPath, "utf8");
21784
+ const content = await fsp3.readFile(manifestPath, "utf8");
21142
21785
  const manifest = JSON.parse(content);
21143
21786
  for (const mode of manifest.modes) {
21144
21787
  modes.push(mode);
@@ -21148,11 +21791,41 @@ async function loadUserModes(modesDir) {
21148
21791
  return modes;
21149
21792
  }
21150
21793
 
21794
+ // src/models/codex-catalog.ts
21795
+ var CODEX_MODELS = [
21796
+ {
21797
+ id: "gpt-5.5",
21798
+ name: "GPT-5.5",
21799
+ description: "Frontier model for complex coding, research, and real-world work.",
21800
+ current: true
21801
+ },
21802
+ {
21803
+ id: "gpt-5.4",
21804
+ name: "GPT-5.4",
21805
+ description: "Strong model for everyday coding."
21806
+ },
21807
+ {
21808
+ id: "gpt-5.4-mini",
21809
+ name: "GPT-5.4 Mini",
21810
+ description: "Small, fast, and cost-efficient model for simpler coding tasks."
21811
+ },
21812
+ {
21813
+ id: "gpt-5.3-codex-spark",
21814
+ name: "GPT-5.3 Codex Spark",
21815
+ description: "Ultra-fast coding model."
21816
+ }
21817
+ ];
21818
+ var BY_ID = new Map(CODEX_MODELS.map((m) => [m.id, m]));
21819
+ function codexModelMeta(id) {
21820
+ return BY_ID.get(id);
21821
+ }
21822
+
21151
21823
  // src/models/provider-model-resolve.ts
21152
21824
  function describeCatalogModel(m) {
21153
21825
  return {
21154
21826
  id: m.id,
21155
21827
  name: m.name,
21828
+ ...m.description !== void 0 ? { description: m.description } : {},
21156
21829
  releaseDate: m.release_date,
21157
21830
  contextWindow: m.limit?.context,
21158
21831
  inputCost: m.cost?.input,
@@ -21169,8 +21842,16 @@ function resolveProviderModelList(savedModels, catalog) {
21169
21842
  if (savedModels && savedModels.length > 0) {
21170
21843
  const byId = new Map((catalog?.models ?? []).map((m) => [m.id, m]));
21171
21844
  return savedModels.map((id) => {
21845
+ const codex = codexModelMeta(id);
21172
21846
  const hit = byId.get(id);
21173
- return hit ? describeCatalogModel(hit) : { id, name: id, capabilities: [] };
21847
+ if (hit) {
21848
+ const described = describeCatalogModel(hit);
21849
+ return codex ? { ...described, description: codex.description } : described;
21850
+ }
21851
+ if (codex) {
21852
+ return { id, name: codex.name, description: codex.description, capabilities: [] };
21853
+ }
21854
+ return { id, name: id, capabilities: [] };
21174
21855
  });
21175
21856
  }
21176
21857
  if (catalog) return catalog.models.map(describeCatalogModel);
@@ -22241,7 +22922,7 @@ var SpecStore = class {
22241
22922
  }
22242
22923
  async load(id) {
22243
22924
  try {
22244
- const raw = await fsp2.readFile(this.filePath(id), "utf8");
22925
+ const raw = await fsp3.readFile(this.filePath(id), "utf8");
22245
22926
  return JSON.parse(raw);
22246
22927
  } catch {
22247
22928
  return null;
@@ -22253,7 +22934,7 @@ var SpecStore = class {
22253
22934
  }
22254
22935
  async delete(id) {
22255
22936
  try {
22256
- await fsp2.unlink(this.filePath(id));
22937
+ await fsp3.unlink(this.filePath(id));
22257
22938
  await this.removeFromIndex(id);
22258
22939
  return true;
22259
22940
  } catch {
@@ -22262,7 +22943,7 @@ var SpecStore = class {
22262
22943
  }
22263
22944
  async exists(id) {
22264
22945
  try {
22265
- await fsp2.access(this.filePath(id));
22946
+ await fsp3.access(this.filePath(id));
22266
22947
  return true;
22267
22948
  } catch {
22268
22949
  return false;
@@ -22304,7 +22985,7 @@ var SpecStore = class {
22304
22985
  }
22305
22986
  async readIndex() {
22306
22987
  try {
22307
- const raw = await fsp2.readFile(this.indexPath, "utf8");
22988
+ const raw = await fsp3.readFile(this.indexPath, "utf8");
22308
22989
  const parsed = JSON.parse(raw);
22309
22990
  if (parsed?.version === 1) return parsed;
22310
22991
  } catch {
@@ -22367,7 +23048,7 @@ var TaskGraphStore = class {
22367
23048
  }
22368
23049
  async load(id) {
22369
23050
  try {
22370
- const raw = await fsp2.readFile(this.filePath(id), "utf8");
23051
+ const raw = await fsp3.readFile(this.filePath(id), "utf8");
22371
23052
  return graphFromJSON(raw);
22372
23053
  } catch {
22373
23054
  return null;
@@ -22379,7 +23060,7 @@ var TaskGraphStore = class {
22379
23060
  }
22380
23061
  async delete(id) {
22381
23062
  try {
22382
- await fsp2.unlink(this.filePath(id));
23063
+ await fsp3.unlink(this.filePath(id));
22383
23064
  await this.removeFromIndex(id);
22384
23065
  return true;
22385
23066
  } catch {
@@ -22388,7 +23069,7 @@ var TaskGraphStore = class {
22388
23069
  }
22389
23070
  async exists(id) {
22390
23071
  try {
22391
- await fsp2.access(this.filePath(id));
23072
+ await fsp3.access(this.filePath(id));
22392
23073
  return true;
22393
23074
  } catch {
22394
23075
  return false;
@@ -22399,7 +23080,7 @@ var TaskGraphStore = class {
22399
23080
  }
22400
23081
  async readIndex() {
22401
23082
  try {
22402
- const raw = await fsp2.readFile(this.indexPath, "utf8");
23083
+ const raw = await fsp3.readFile(this.indexPath, "utf8");
22403
23084
  const parsed = JSON.parse(raw);
22404
23085
  if (parsed?.version === 1) return parsed;
22405
23086
  } catch {
@@ -22471,6 +23152,9 @@ function buildQuestioningPrompt(session, min, max) {
22471
23152
  "- Adapt based on previous answers",
22472
23153
  "- Cover: scope, constraints, edge cases, integrations, security, performance as relevant",
22473
23154
  "- When you have enough info, respond with the full specification in JSON format",
23155
+ "- This is a planning interview: respond with TEXT ONLY (a question, or the spec JSON).",
23156
+ " Do NOT write or edit files, and do NOT run shell/terminal commands \u2014 the code is",
23157
+ " written later, after the plan is approved.",
22474
23158
  "",
22475
23159
  `**Question budget:** ${budget}/${max} remaining`,
22476
23160
  `**Minimum required:** ${remaining > 0 ? remaining : "met"}`
@@ -22538,6 +23222,9 @@ function buildImplementationPrompt(session) {
22538
23222
  "",
22539
23223
  "**Instructions for AI:**",
22540
23224
  "Generate a detailed implementation plan for this specification.",
23225
+ "This is a PLANNING step \u2014 describe the plan and emit the task JSON as TEXT. Do NOT",
23226
+ "create or edit files and do NOT run shell/terminal commands here; the tasks you list",
23227
+ "are executed later, one by one, after you approve them.",
22541
23228
  "Include:",
22542
23229
  "1. Architecture decisions",
22543
23230
  "2. File structure changes",
@@ -22649,10 +23336,10 @@ var AISpecBuilder = class {
22649
23336
  async saveSession() {
22650
23337
  if (!this.sessionPath) return;
22651
23338
  try {
22652
- const fsp18 = await import('fs/promises');
22653
- const path23 = await import('path');
23339
+ const fsp19 = await import('fs/promises');
23340
+ const path25 = await import('path');
22654
23341
  const { atomicWrite: atomicWrite2 } = await Promise.resolve().then(() => (init_atomic_write(), atomic_write_exports));
22655
- await fsp18.mkdir(path23.dirname(this.sessionPath), { recursive: true });
23342
+ await fsp19.mkdir(path25.dirname(this.sessionPath), { recursive: true });
22656
23343
  await atomicWrite2(this.sessionPath, JSON.stringify(this.session, null, 2));
22657
23344
  } catch {
22658
23345
  }
@@ -22661,8 +23348,8 @@ var AISpecBuilder = class {
22661
23348
  async loadSession() {
22662
23349
  if (!this.sessionPath) return false;
22663
23350
  try {
22664
- const fsp18 = await import('fs/promises');
22665
- const raw = await fsp18.readFile(this.sessionPath, "utf8");
23351
+ const fsp19 = await import('fs/promises');
23352
+ const raw = await fsp19.readFile(this.sessionPath, "utf8");
22666
23353
  const loaded = JSON.parse(raw);
22667
23354
  if (loaded?.id && loaded?.phase && loaded?.title) {
22668
23355
  this.session = loaded;
@@ -22676,8 +23363,8 @@ var AISpecBuilder = class {
22676
23363
  async deleteSession() {
22677
23364
  if (!this.sessionPath) return;
22678
23365
  try {
22679
- const fsp18 = await import('fs/promises');
22680
- await fsp18.unlink(this.sessionPath);
23366
+ const fsp19 = await import('fs/promises');
23367
+ await fsp19.unlink(this.sessionPath);
22681
23368
  } catch {
22682
23369
  }
22683
23370
  }
@@ -23379,15 +24066,15 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
23379
24066
  maxId = id;
23380
24067
  }
23381
24068
  }
23382
- const path23 = [];
24069
+ const path25 = [];
23383
24070
  let current = maxId;
23384
24071
  const visited = /* @__PURE__ */ new Set();
23385
24072
  while (current && !visited.has(current)) {
23386
24073
  visited.add(current);
23387
- path23.unshift(current);
24074
+ path25.unshift(current);
23388
24075
  current = prev.get(current) ?? null;
23389
24076
  }
23390
- return path23;
24077
+ return path25;
23391
24078
  }
23392
24079
  function computeParallelGroups(graph, blockedByMap) {
23393
24080
  const groups = [];
@@ -25154,7 +25841,7 @@ var SddBoardStore = class {
25154
25841
  }
25155
25842
  async load(runId) {
25156
25843
  try {
25157
- const raw = await fsp2.readFile(this.snapshotPath(runId), "utf8");
25844
+ const raw = await fsp3.readFile(this.snapshotPath(runId), "utf8");
25158
25845
  return JSON.parse(raw);
25159
25846
  } catch {
25160
25847
  return null;
@@ -25172,7 +25859,7 @@ var SddBoardStore = class {
25172
25859
  async appendEvent(runId, event) {
25173
25860
  try {
25174
25861
  await ensureDir(this.baseDir);
25175
- await fsp2.appendFile(this.eventsPath(runId), `${JSON.stringify(event)}
25862
+ await fsp3.appendFile(this.eventsPath(runId), `${JSON.stringify(event)}
25176
25863
  `, { mode: 384 });
25177
25864
  } catch {
25178
25865
  }
@@ -25180,7 +25867,7 @@ var SddBoardStore = class {
25180
25867
  /** Append a control command (used by readers to steer a CLI-owned run). */
25181
25868
  async appendControl(runId, command) {
25182
25869
  await ensureDir(this.baseDir);
25183
- await fsp2.appendFile(this.controlPath(runId), `${JSON.stringify(command)}
25870
+ await fsp3.appendFile(this.controlPath(runId), `${JSON.stringify(command)}
25184
25871
  `, { mode: 384 });
25185
25872
  }
25186
25873
  /** Read + truncate the control queue (the run drains it). Returns parsed commands. */
@@ -25188,12 +25875,12 @@ var SddBoardStore = class {
25188
25875
  const p = this.controlPath(runId);
25189
25876
  let raw;
25190
25877
  try {
25191
- raw = await fsp2.readFile(p, "utf8");
25878
+ raw = await fsp3.readFile(p, "utf8");
25192
25879
  } catch {
25193
25880
  return [];
25194
25881
  }
25195
25882
  try {
25196
- await fsp2.writeFile(p, "", { mode: 384 });
25883
+ await fsp3.writeFile(p, "", { mode: 384 });
25197
25884
  } catch {
25198
25885
  }
25199
25886
  return raw.split("\n").filter((l) => l.trim()).map((l) => {
@@ -25206,9 +25893,9 @@ var SddBoardStore = class {
25206
25893
  }
25207
25894
  async delete(runId) {
25208
25895
  await Promise.allSettled([
25209
- fsp2.unlink(this.snapshotPath(runId)),
25210
- fsp2.unlink(this.eventsPath(runId)),
25211
- fsp2.unlink(this.controlPath(runId))
25896
+ fsp3.unlink(this.snapshotPath(runId)),
25897
+ fsp3.unlink(this.eventsPath(runId)),
25898
+ fsp3.unlink(this.controlPath(runId))
25212
25899
  ]);
25213
25900
  await this.removeFromIndex(runId);
25214
25901
  }
@@ -25218,7 +25905,7 @@ var SddBoardStore = class {
25218
25905
  }
25219
25906
  async readIndex() {
25220
25907
  try {
25221
- const raw = await fsp2.readFile(this.indexPath, "utf8");
25908
+ const raw = await fsp3.readFile(this.indexPath, "utf8");
25222
25909
  const parsed = JSON.parse(raw);
25223
25910
  if (parsed?.version === 1) return parsed;
25224
25911
  } catch {
@@ -25654,6 +26341,7 @@ var SddInterviewDriver = class {
25654
26341
  sessionId: s.id,
25655
26342
  phase: s.phase,
25656
26343
  title: s.title,
26344
+ goal: s.userIntent || s.title,
25657
26345
  questionCount: s.questionCount,
25658
26346
  minQuestions: this.minQuestions,
25659
26347
  maxQuestions: this.maxQuestions,
@@ -26348,14 +27036,14 @@ async function destroySddProject(opts) {
26348
27036
  const deleted = [];
26349
27037
  const rmDir = async (dir, label) => {
26350
27038
  try {
26351
- await fsp2.rm(dir, { recursive: true, force: true });
27039
+ await fsp3.rm(dir, { recursive: true, force: true });
26352
27040
  deleted.push(label);
26353
27041
  } catch {
26354
27042
  }
26355
27043
  };
26356
27044
  const rmFile = async (file, label) => {
26357
27045
  try {
26358
- await fsp2.unlink(file);
27046
+ await fsp3.unlink(file);
26359
27047
  deleted.push(label);
26360
27048
  } catch {
26361
27049
  }
@@ -26711,7 +27399,7 @@ async function startMetricsServer(opts) {
26711
27399
  const tls = opts.tls;
26712
27400
  const useHttps = !!(tls?.cert && tls?.key);
26713
27401
  const host = opts.host ?? "127.0.0.1";
26714
- const path23 = opts.path ?? "/metrics";
27402
+ const path25 = opts.path ?? "/metrics";
26715
27403
  const healthPath = opts.healthPath ?? "/healthz";
26716
27404
  const healthRegistry = opts.healthRegistry;
26717
27405
  const listener = (req, res) => {
@@ -26721,7 +27409,7 @@ async function startMetricsServer(opts) {
26721
27409
  return;
26722
27410
  }
26723
27411
  const url = req.url.split("?")[0];
26724
- if (url === path23) {
27412
+ if (url === path25) {
26725
27413
  let body;
26726
27414
  try {
26727
27415
  body = renderPrometheus(opts.sink.snapshot());
@@ -26785,7 +27473,7 @@ async function startMetricsServer(opts) {
26785
27473
  const protocol = useHttps ? "https" : "http";
26786
27474
  return {
26787
27475
  port: boundPort,
26788
- url: `${protocol}://${host}:${boundPort}${path23}`,
27476
+ url: `${protocol}://${host}:${boundPort}${path25}`,
26789
27477
  close: () => new Promise((resolve8, reject) => {
26790
27478
  server.close((err) => err ? reject(err) : resolve8());
26791
27479
  })
@@ -27459,6 +28147,6 @@ var allServers = () => ({
27459
28147
  ssh: { ...sshManagerServer(), enabled: false }
27460
28148
  });
27461
28149
 
27462
- export { AGENTS_BY_PHASE, AGENT_CATALOG, AISpecBuilder, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, ConfigMigrationError, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_CONFIG, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_SUBAGENT_BASELINE, DEFAULT_TOOLS_CONFIG, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPermissionPolicy, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultTaskStore, Director, DirectorStateCheckpoint, DoneConditionChecker, EternalAutonomyEngine, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FleetBus, FleetSpawnBudgetError, FleetUsageAggregator, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, IntelligentCompactor, LLMSelector, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, ParallelEternalEngine, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, SddBoardProjector, SddBoardStore, SddInterviewDriver, SddParallelRun, SddRunRegistry, SddSupervisor, SddTaskDecomposer, SelectiveCompactor, SessionAnalyzer, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, ToolExecutor, addPlanItem, allServers, analyzeCriticalPath, applyRosterBudget, attachAutoExtend, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildBoardSnapshot, buildBoardTasks, buildGoalPreamble, buildOtlpMetricsRequest, buildOtlpTracesRequest, classifyFamily, cleanupSddWorktrees, clearPlan, composeDirectorPrompt, composeSubagentPrompt, context7Server, contextManagerTool, createAutoExecutor, createContextManagerTool, createDelegateTool, createMessage, createSessionEventBridge, createStrategyCompactor, decryptConfigSecrets, deriveTodosFromPlanItem, describeCatalogModel, destroySddProject, dispatchAgent, emptyPlan, encryptConfigSecrets, everArtServer, extractVerificationCommand, filesystemServer, formatPlan, formatPlanTemplates, getAgentDefinition, getPlanTemplate, getTemplate, githubServer, googleMapsServer, hasConflictMarkers, isExplanatoryText, listPlanTemplates, listTemplates, loadDirectorState, loadPlan, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeAskTool, makeAssignTool, makeAutonomyPromptContributor, makeAwaitTasksTool, makeCollabDebugTool, makeCommandVerifier, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeLlmConflictResolver, makeLlmSubtaskGenerator, makePreferSideConflictResolver, makeRollUpTool, makeSpawnTool, makeTerminateTool, migratePlaintextSecrets, miniMaxVisionServer, playwrightServer, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, resolveAuditLevel, resolveConflictText, resolveProviderModelList, resolveSessionLoggingConfig, rewriteConfigEncrypted, rollbackSddRunFromDisk, rosterSummaryFromConfigs, runConfigMigrations, savePlan, saveTodosCheckpoint, scoreAgents, sentinelServer, setPlanItemStatus, shortIdMap, slackServer, sshManagerServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, startSddRun, templateToMarkdown, wireMetricsToEvents, zaiVisionServer };
28150
+ export { AGENTS_BY_PHASE, AGENT_CATALOG, AISpecBuilder, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, CODEX_MODELS, ConfigMigrationError, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_CONFIG, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_SUBAGENT_BASELINE, DEFAULT_TOOLS_CONFIG, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPermissionPolicy, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultTaskStore, Director, DirectorStateCheckpoint, DoneConditionChecker, EternalAutonomyEngine, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FleetBus, FleetSpawnBudgetError, FleetUsageAggregator, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, IntelligentCompactor, LLMSelector, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, ParallelEternalEngine, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, SddBoardProjector, SddBoardStore, SddInterviewDriver, SddParallelRun, SddRunRegistry, SddSupervisor, SddTaskDecomposer, SelectiveCompactor, SessionAnalyzer, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, ToolExecutor, addPlanItem, allServers, analyzeCriticalPath, applyRosterBudget, attachAutoExtend, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildBoardSnapshot, buildBoardTasks, buildGoalPreamble, buildOtlpMetricsRequest, buildOtlpTracesRequest, classifyFamily, cleanupSddWorktrees, clearPlan, codexModelMeta, composeDirectorPrompt, composeSubagentPrompt, context7Server, contextManagerTool, createAutoExecutor, createContextManagerTool, createDelegateTool, createMessage, createSessionEventBridge, createStrategyCompactor, decryptConfigSecrets, deriveTodosFromPlanItem, describeCatalogModel, destroySddProject, dispatchAgent, emptyPlan, encryptConfigSecrets, everArtServer, extractVerificationCommand, filesystemServer, formatPlan, formatPlanTemplates, getAgentDefinition, getPlanTemplate, getTemplate, githubServer, googleMapsServer, hasConflictMarkers, isExplanatoryText, listPlanTemplates, listTemplates, loadDirectorState, loadPlan, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeAskTool, makeAssignTool, makeAutonomyPromptContributor, makeAwaitTasksTool, makeCollabDebugTool, makeCommandVerifier, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeLlmConflictResolver, makeLlmSubtaskGenerator, makePreferSideConflictResolver, makeRollUpTool, makeSpawnTool, makeTerminateTool, migratePlaintextSecrets, miniMaxVisionServer, playwrightServer, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, resolveAuditLevel, resolveConflictText, resolveProviderModelList, resolveSessionLoggingConfig, rewriteConfigEncrypted, rollbackSddRunFromDisk, rosterSummaryFromConfigs, runConfigMigrations, savePlan, saveTodosCheckpoint, scoreAgents, sentinelServer, setPlanItemStatus, shortIdMap, slackServer, sshManagerServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, startSddRun, templateToMarkdown, wireMetricsToEvents, zaiVisionServer };
27463
28151
  //# sourceMappingURL=index.js.map
27464
28152
  //# sourceMappingURL=index.js.map