@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.
- package/dist/{agent-bridge-BZ2enORi.d.ts → agent-bridge-DFo21wmY.d.ts} +1 -1
- package/dist/{agent-subagent-runner-ehb4xGvd.d.ts → agent-subagent-runner-BwmkIDEd.d.ts} +7 -7
- package/dist/{brain-BxN2k2HP.d.ts → brain-gfZX3the.d.ts} +11 -5
- package/dist/{provider-model-resolve-DFd3IPpw.d.ts → codex-catalog-CooZ6mOl.d.ts} +47 -4
- package/dist/{compactor-72ug-ZRB.d.ts → compactor-riTOds0f.d.ts} +1 -1
- package/dist/{config-C8IYxlO8.d.ts → config-BxcrDzri.d.ts} +60 -4
- package/dist/{context-Dw55zZ_Q.d.ts → context-DERiLofu.d.ts} +60 -1
- package/dist/coordination/index.d.ts +27 -16
- package/dist/coordination/index.js +1641 -1398
- package/dist/coordination/index.js.map +1 -1
- package/dist/defaults/index.d.ts +26 -26
- package/dist/defaults/index.js +2154 -1466
- package/dist/defaults/index.js.map +1 -1
- package/dist/execution/index.d.ts +15 -15
- package/dist/execution/index.js +77 -9
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/prompt-enhancer.d.ts +1 -1
- package/dist/extension/index.d.ts +6 -6
- package/dist/{global-mailbox-C9dsc9Y_.d.ts → global-mailbox-Cr8TW4t_.d.ts} +1 -1
- package/dist/{goal-preamble-NhflDjYb.d.ts → goal-preamble-CEhROp1e.d.ts} +9 -9
- package/dist/{goal-store-Cx363x7Z.d.ts → goal-store-xNSVxmpV.d.ts} +1 -1
- package/dist/hq/index.d.ts +5 -5
- package/dist/{index-B7fHDt0B.d.ts → index-00KPKAlm.d.ts} +5 -5
- package/dist/{index-BbVprU-9.d.ts → index-pDBSBE1r.d.ts} +2 -2
- package/dist/index.d.ts +72 -45
- package/dist/index.js +2840 -1769
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/index.d.ts +6 -6
- package/dist/kernel/index.d.ts +11 -11
- package/dist/kernel/index.js.map +1 -1
- package/dist/{mcp-servers-B6fSRNC1.d.ts → mcp-servers-BIwRiOxe.d.ts} +3 -3
- package/dist/models/index.d.ts +5 -5
- package/dist/models/index.js +40 -2
- package/dist/models/index.js.map +1 -1
- package/dist/{models-registry-4C6Wr91w.d.ts → models-registry-8OorW51H.d.ts} +1 -1
- package/dist/{multi-agent-coordinator-q1skFeNP.d.ts → multi-agent-coordinator-CNx48Zoz.d.ts} +1 -1
- package/dist/{null-fleet-bus-C9rrgQwc.d.ts → null-fleet-bus-B4ZUJYL6.d.ts} +6 -6
- package/dist/observability/index.d.ts +2 -2
- package/dist/{parallel-eternal-engine-CtXly2Sf.d.ts → parallel-eternal-engine-rsIclDqO.d.ts} +9 -9
- package/dist/{path-resolver-Bim6G5Jz.d.ts → path-resolver-BqU-fwzD.d.ts} +3 -3
- package/dist/{permission-CC7XFYWG.d.ts → permission-Dgs3v-Xq.d.ts} +1 -1
- package/dist/{permission-policy-cYR4RJmw.d.ts → permission-policy-Dtht2k0e.d.ts} +2 -2
- package/dist/{pipeline-CNVKuQDQ.d.ts → pipeline-B-dpCFYS.d.ts} +2 -2
- package/dist/{plan-templates-C4wXMmiM.d.ts → plan-templates-B7MxyY2o.d.ts} +58 -5
- package/dist/{provider-runner-BpM0mdBE.d.ts → provider-runner-DrmpBE5l.d.ts} +3 -3
- package/dist/{retry-policy-BV7nzeAd.d.ts → retry-policy-hYxsm10a.d.ts} +1 -1
- package/dist/sdd/index.d.ts +12 -10
- package/dist/sdd/index.js +7 -0
- package/dist/sdd/index.js.map +1 -1
- package/dist/{secret-vault-eMBKfheR.d.ts → secret-vault-DnqIFhPF.d.ts} +1 -1
- package/dist/security/index.d.ts +5 -5
- package/dist/{selector-C4ORTOid.d.ts → selector-D21RjDIg.d.ts} +1 -1
- package/dist/{session-event-bridge-CeNpUL9w.d.ts → session-event-bridge-Dt54CTvq.d.ts} +1 -1
- package/dist/{session-reader-BepLSnGL.d.ts → session-reader-CceH13Kq.d.ts} +5 -4
- package/dist/storage/index.d.ts +46 -12
- package/dist/storage/index.js +2281 -1476
- package/dist/storage/index.js.map +1 -1
- package/dist/tools/index.d.ts +2 -2
- package/dist/types/index.d.ts +19 -19
- package/dist/types/index.js +162 -42
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.d.ts +2 -2
- package/dist/{worktree-manager-BDuXTaWL.d.ts → worktree-manager-DUfBbKzk.d.ts} +1 -1
- package/package.json +1 -1
package/dist/defaults/index.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
44
|
+
await fsp3.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
|
|
45
45
|
} else {
|
|
46
|
-
await
|
|
46
|
+
await fsp3.writeFile(tmp, content, { flag: "wx" });
|
|
47
47
|
}
|
|
48
48
|
try {
|
|
49
|
-
const fh = await
|
|
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
|
|
60
|
-
mode =
|
|
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
|
|
65
|
+
await fsp3.chmod(tmp, mode);
|
|
66
66
|
}
|
|
67
67
|
await renameWithRetry(tmp, targetPath);
|
|
68
68
|
} catch (err) {
|
|
69
69
|
try {
|
|
70
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
95
|
+
await fsp3.mkdir(dir, { recursive: true });
|
|
96
96
|
continue;
|
|
97
97
|
}
|
|
98
98
|
if (code !== "EEXIST") throw err;
|
|
99
99
|
try {
|
|
100
|
-
const
|
|
101
|
-
if (Date.now() -
|
|
102
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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 = (
|
|
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
|
|
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
|
|
798
|
-
if (
|
|
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
|
|
816
|
-
if (
|
|
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,
|
|
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:
|
|
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:
|
|
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(
|
|
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(
|
|
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, `${
|
|
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
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
const
|
|
2215
|
-
|
|
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-
|
|
2220
|
-
var
|
|
2221
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
2226
|
-
*
|
|
2227
|
-
*
|
|
2228
|
-
*
|
|
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
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
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
|
-
*
|
|
2245
|
-
* the
|
|
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
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
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
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
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
|
-
|
|
2267
|
-
|
|
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
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
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
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
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
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
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
|
-
*
|
|
2298
|
-
*
|
|
2299
|
-
*
|
|
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
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2399
|
+
async flush() {
|
|
2400
|
+
if (this.flushTimer) {
|
|
2401
|
+
clearTimeout(this.flushTimer);
|
|
2402
|
+
this.flushTimer = null;
|
|
2403
|
+
}
|
|
2404
|
+
await this.flushBuffer();
|
|
2305
2405
|
}
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2392
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
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
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
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
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
this.
|
|
2572
|
-
this.
|
|
2573
|
-
}
|
|
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
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2594
|
-
|
|
2552
|
+
idxOutcome = "failure";
|
|
2553
|
+
idxError = toErrorMessage(err);
|
|
2595
2554
|
} finally {
|
|
2596
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
2614
|
-
|
|
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
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
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
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
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
|
-
|
|
2635
|
-
|
|
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
|
-
|
|
2641
|
-
this.
|
|
2642
|
-
|
|
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
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
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
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
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
|
-
|
|
2740
|
-
|
|
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
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
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
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
this.
|
|
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
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
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
|
-
*
|
|
2849
|
-
*
|
|
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
|
-
*
|
|
2852
|
-
*
|
|
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
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
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
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
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
|
-
|
|
2895
|
-
|
|
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
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
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
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
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
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
}
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
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
|
-
|
|
2937
|
-
|
|
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
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
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
|
-
|
|
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
|
|
2953
|
-
await this.ensureShardDir(id);
|
|
3004
|
+
async load(id) {
|
|
2954
3005
|
const file = this.sessionPath(id, ".jsonl");
|
|
2955
|
-
const
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
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
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
let
|
|
2983
|
-
let
|
|
2984
|
-
let
|
|
2985
|
-
let
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
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
|
-
}
|
|
2996
|
-
|
|
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 (
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
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
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
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
|
-
|
|
3036
|
-
|
|
3037
|
-
model:
|
|
3038
|
-
provider:
|
|
3039
|
-
|
|
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
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
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
|
-
|
|
3048
|
-
|
|
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(
|
|
3205
|
+
const parsed = JSON.parse(leftover);
|
|
3051
3206
|
if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
|
|
3052
|
-
|
|
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
|
-
|
|
3059
|
-
stream.destroy();
|
|
3217
|
+
if (fh) await fh.close().catch(() => void 0);
|
|
3060
3218
|
}
|
|
3061
3219
|
}
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
3119
|
-
*
|
|
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
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
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
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
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
|
-
|
|
3190
|
-
|
|
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
|
|
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
|
|
3204
|
-
const
|
|
3205
|
-
|
|
3206
|
-
|
|
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
|
|
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
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
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
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
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
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
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
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
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
|
-
*
|
|
3253
|
-
*
|
|
3254
|
-
*
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
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
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
})
|
|
3274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
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
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
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
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
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
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
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
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
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
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
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
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
}
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
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.
|
|
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
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
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
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
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
|
|
3452
|
-
await this.
|
|
3453
|
-
|
|
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
|
-
|
|
3456
|
-
|
|
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
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
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
|
-
|
|
3522
|
-
|
|
3523
|
-
}
|
|
3524
|
-
|
|
3525
|
-
if (
|
|
3526
|
-
|
|
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
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
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
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
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
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
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
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
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
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
4991
|
+
const { writeFile: writeFile8, mkdir: mkdir8 } = await import('fs/promises');
|
|
4771
4992
|
await mkdir8(this.backupDir, { recursive: true });
|
|
4772
|
-
await
|
|
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
|
|
5107
|
-
const actualMode =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: {
|
|
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
|
|
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
|
-
"
|
|
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 (
|
|
5657
|
-
|
|
6043
|
+
if (IN_PROJECT_ALLOWED_KEYS.has(k)) {
|
|
6044
|
+
out[k] = v;
|
|
5658
6045
|
continue;
|
|
5659
6046
|
}
|
|
5660
|
-
|
|
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}
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
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.
|
|
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
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
8875
|
+
result = await fsp3.readFile(savePath, "utf8");
|
|
8268
8876
|
} catch {
|
|
8269
|
-
const full = await
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
11633
|
-
await
|
|
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,
|
|
17296
|
-
|
|
17297
|
-
|
|
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:
|
|
17306
|
-
snapshotSizeBytes:
|
|
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
|
|
17720
|
-
const mtimeChanged = file.snapshotMtimeMs !== void 0 &&
|
|
17721
|
-
const sizeChanged = file.snapshotSizeBytes !== void 0 &&
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
21122
|
-
if (!
|
|
21123
|
-
const content = await
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
22653
|
-
const
|
|
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
|
|
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
|
|
22665
|
-
const raw = await
|
|
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
|
|
22680
|
-
await
|
|
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
|
|
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
|
-
|
|
24074
|
+
path25.unshift(current);
|
|
23388
24075
|
current = prev.get(current) ?? null;
|
|
23389
24076
|
}
|
|
23390
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
25878
|
+
raw = await fsp3.readFile(p, "utf8");
|
|
25192
25879
|
} catch {
|
|
25193
25880
|
return [];
|
|
25194
25881
|
}
|
|
25195
25882
|
try {
|
|
25196
|
-
await
|
|
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
|
-
|
|
25210
|
-
|
|
25211
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 ===
|
|
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}${
|
|
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
|