@wrongstack/core 0.273.1 → 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-DpKIxHhE.d.ts → agent-bridge-DFo21wmY.d.ts} +1 -1
- package/dist/{agent-subagent-runner-Dx7fZ1bE.d.ts → agent-subagent-runner-BwmkIDEd.d.ts} +7 -7
- package/dist/{brain-BDcQaku-.d.ts → brain-gfZX3the.d.ts} +1 -1
- package/dist/{provider-model-resolve-Cz6OlIOp.d.ts → codex-catalog-CooZ6mOl.d.ts} +47 -4
- package/dist/{compactor-BuSdj3fq.d.ts → compactor-riTOds0f.d.ts} +1 -1
- package/dist/{config-CR2yoG8c.d.ts → config-BxcrDzri.d.ts} +7 -1
- package/dist/{context-DulAr8Zo.d.ts → context-DERiLofu.d.ts} +36 -1
- package/dist/coordination/index.d.ts +20 -16
- package/dist/coordination/index.js +1622 -1479
- package/dist/coordination/index.js.map +1 -1
- package/dist/defaults/index.d.ts +26 -26
- package/dist/defaults/index.js +1832 -1541
- package/dist/defaults/index.js.map +1 -1
- package/dist/execution/index.d.ts +15 -15
- package/dist/execution/index.js +2 -8
- 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-CwcubDkA.d.ts → global-mailbox-Cr8TW4t_.d.ts} +1 -1
- package/dist/{goal-preamble-Bu0a2uCG.d.ts → goal-preamble-CEhROp1e.d.ts} +9 -9
- package/dist/{goal-store-CTmFuZ8J.d.ts → goal-store-xNSVxmpV.d.ts} +1 -1
- package/dist/hq/index.d.ts +5 -5
- package/dist/{index-CTq5wU3m.d.ts → index-00KPKAlm.d.ts} +5 -5
- package/dist/{index-CxP-HBhX.d.ts → index-pDBSBE1r.d.ts} +2 -2
- package/dist/index.d.ts +49 -43
- package/dist/index.js +1973 -1444
- 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/{mcp-servers-BQaOE71z.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-BEcny4kP.d.ts → models-registry-8OorW51H.d.ts} +1 -1
- package/dist/{multi-agent-coordinator-Bx8EFkv2.d.ts → multi-agent-coordinator-CNx48Zoz.d.ts} +1 -1
- package/dist/{null-fleet-bus-BC5ZXCQw.d.ts → null-fleet-bus-B4ZUJYL6.d.ts} +6 -6
- package/dist/observability/index.d.ts +2 -2
- package/dist/{parallel-eternal-engine-C345TI3n.d.ts → parallel-eternal-engine-rsIclDqO.d.ts} +9 -9
- package/dist/{path-resolver-C-W_wzkF.d.ts → path-resolver-BqU-fwzD.d.ts} +3 -3
- package/dist/{permission-CsBGZkxp.d.ts → permission-Dgs3v-Xq.d.ts} +1 -1
- package/dist/{permission-policy-g3Sg0GdZ.d.ts → permission-policy-Dtht2k0e.d.ts} +2 -2
- package/dist/{pipeline-xnw_24Z8.d.ts → pipeline-B-dpCFYS.d.ts} +2 -2
- package/dist/{plan-templates-DGaiYEcS.d.ts → plan-templates-B7MxyY2o.d.ts} +32 -5
- package/dist/{provider-runner-7J0HqF6B.d.ts → provider-runner-DrmpBE5l.d.ts} +3 -3
- package/dist/{retry-policy-kqXJOVkX.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-CMQUr-eB.d.ts → secret-vault-DnqIFhPF.d.ts} +1 -1
- package/dist/security/index.d.ts +5 -5
- package/dist/{selector-B4r34PWR.d.ts → selector-D21RjDIg.d.ts} +1 -1
- package/dist/{session-event-bridge-BD3LoyLC.d.ts → session-event-bridge-Dt54CTvq.d.ts} +1 -1
- package/dist/{session-reader-DjrKGD9c.d.ts → session-reader-CceH13Kq.d.ts} +5 -4
- package/dist/storage/index.d.ts +46 -12
- package/dist/storage/index.js +1987 -1548
- 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 +134 -42
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.d.ts +2 -2
- package/dist/{worktree-manager-DHdrWQ_7.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,1541 +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
|
-
*
|
|
2527
|
-
* Why this exists: `load()` parses the entire file into memory and
|
|
2528
|
-
* rebuilds `messages`/`toolCallEnds` for every caller. `search()` only
|
|
2529
|
-
* needs to know which events contain matching text — a per-line
|
|
2530
|
-
* predicate is enough. The full parse work (and the `_loadCache` poll)
|
|
2531
|
-
* is wasted in that case.
|
|
2532
|
-
*
|
|
2533
|
-
* Memory: O(hits) regardless of file size. Disk: one linear scan,
|
|
2534
|
-
* terminated at `limit` if the caller asked for one.
|
|
2535
|
-
*
|
|
2536
|
-
* Errors: missing file yields []. Corrupt lines are skipped (same
|
|
2537
|
-
* policy as `load()`). Aborting via `signal` rejects with `AbortError`.
|
|
2538
|
-
*/
|
|
2539
|
-
async searchEvents(id, predicate, opts) {
|
|
2540
|
-
const file = this.sessionPath(id, ".jsonl");
|
|
2541
|
-
const limit = opts?.limit;
|
|
2542
|
-
const signal = opts?.signal;
|
|
2543
|
-
const out = [];
|
|
2544
|
-
let stat6;
|
|
2545
|
-
try {
|
|
2546
|
-
stat6 = await fsp2.stat(file);
|
|
2547
|
-
} catch (err) {
|
|
2548
|
-
if (err.code === "ENOENT") return [];
|
|
2549
|
-
throw err;
|
|
2550
|
-
}
|
|
2551
|
-
if (stat6.size === 0) return [];
|
|
2552
|
-
let fh;
|
|
2553
|
-
try {
|
|
2554
|
-
fh = await fsp2.open(file, "r");
|
|
2555
|
-
const CHUNK = 64 * 1024;
|
|
2556
|
-
const buf = Buffer.alloc(CHUNK);
|
|
2557
|
-
let leftover = "";
|
|
2558
|
-
let eventIndex = 0;
|
|
2559
|
-
for (let position = 0; ; position += buf.byteLength) {
|
|
2560
|
-
if (signal?.aborted) {
|
|
2561
|
-
const reason = signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
2562
|
-
throw reason;
|
|
2563
|
-
}
|
|
2564
|
-
const { bytesRead } = await fh.read(buf, 0, CHUNK, position);
|
|
2565
|
-
if (bytesRead === 0) break;
|
|
2566
|
-
const text = leftover + buf.subarray(0, bytesRead).toString("utf8");
|
|
2567
|
-
const parts = text.split("\n");
|
|
2568
|
-
leftover = parts.pop() ?? "";
|
|
2569
|
-
for (const line of parts) {
|
|
2570
|
-
if (!line) continue;
|
|
2571
|
-
let ev;
|
|
2572
|
-
try {
|
|
2573
|
-
const parsed = JSON.parse(line);
|
|
2574
|
-
if (parsed === null || typeof parsed !== "object" || typeof parsed.type !== "string" || typeof parsed.ts !== "string") {
|
|
2575
|
-
continue;
|
|
2576
|
-
}
|
|
2577
|
-
ev = parsed;
|
|
2578
|
-
} catch {
|
|
2579
|
-
continue;
|
|
2580
|
-
}
|
|
2581
|
-
if (predicate(ev, eventIndex, ev.ts)) {
|
|
2582
|
-
out.push({ event: ev, eventIndex, ts: ev.ts });
|
|
2583
|
-
if (limit !== void 0 && out.length >= limit) {
|
|
2584
|
-
return out;
|
|
2585
|
-
}
|
|
2586
|
-
}
|
|
2587
|
-
eventIndex++;
|
|
2588
|
-
}
|
|
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);
|
|
2589
2466
|
}
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2467
|
+
}
|
|
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";
|
|
2601
2478
|
}
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2479
|
+
} else if (event.type === "file_snapshot") {
|
|
2480
|
+
this.fileChangeCount += event.files.length;
|
|
2481
|
+
} else if (event.type === "compaction") {
|
|
2482
|
+
this.compactionCount++;
|
|
2483
|
+
}
|
|
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++;
|
|
2605
2498
|
}
|
|
2606
2499
|
}
|
|
2607
|
-
async
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
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 } : {}
|
|
2616
2543
|
});
|
|
2617
|
-
return indexed.slice(0, limit);
|
|
2618
2544
|
}
|
|
2619
|
-
return await this.listFromDirectoryScan(limit);
|
|
2620
|
-
} catch {
|
|
2621
|
-
return [];
|
|
2622
2545
|
}
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
// One JSON line per closed session, appended atomically on close().
|
|
2627
|
-
// When a session is deleted, a tombstone {action:"delete",id:"..."} is
|
|
2628
|
-
// appended. On read, tombstones filter out matching session entries.
|
|
2629
|
-
// This keeps listing O(lines-in-index) instead of O(files-on-disk).
|
|
2630
|
-
//
|
|
2631
|
-
// The index auto-compacts every N appends to prevent unbounded growth
|
|
2632
|
-
// from tombstones and duplicate entries (resume cycles).
|
|
2633
|
-
indexAppendCount = 0;
|
|
2634
|
-
static COMPACT_EVERY = 30;
|
|
2635
|
-
/** Append a session summary to the index. */
|
|
2636
|
-
async appendToIndex(summary) {
|
|
2546
|
+
const idxT0 = Date.now();
|
|
2547
|
+
let idxOutcome = "success";
|
|
2548
|
+
let idxError;
|
|
2637
2549
|
try {
|
|
2638
|
-
await
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2550
|
+
await this.onCloseCb?.(this.summary);
|
|
2551
|
+
} catch (err) {
|
|
2552
|
+
idxOutcome = "failure";
|
|
2553
|
+
idxError = toErrorMessage(err);
|
|
2554
|
+
} finally {
|
|
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
|
+
});
|
|
2648
2565
|
}
|
|
2649
|
-
}
|
|
2650
|
-
/** Append a tombstone entry for a deleted session. */
|
|
2651
|
-
async writeTombstone(id) {
|
|
2652
2566
|
try {
|
|
2653
|
-
await
|
|
2654
|
-
const line = JSON.stringify({ action: "delete", id }) + "\n";
|
|
2655
|
-
await fsp2.appendFile(this.indexFile, line, "utf8");
|
|
2656
|
-
this._indexCache = null;
|
|
2657
|
-
this.indexAppendCount++;
|
|
2567
|
+
await this.handle.close();
|
|
2658
2568
|
} catch {
|
|
2659
2569
|
}
|
|
2660
2570
|
}
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
const t0 = Date.now();
|
|
2667
|
-
let outcome = "success";
|
|
2668
|
-
let errorMsg;
|
|
2669
|
-
try {
|
|
2670
|
-
const entries = await this.readIndex();
|
|
2671
|
-
if (entries.length === 0) return;
|
|
2672
|
-
const tmp = `${this.indexFile}.compact.tmp`;
|
|
2673
|
-
const lines = entries.map((s) => JSON.stringify(s)).join("\n") + "\n";
|
|
2674
|
-
await fsp2.writeFile(tmp, lines, "utf8");
|
|
2675
|
-
await fsp2.rename(tmp, this.indexFile);
|
|
2676
|
-
this._indexCache = null;
|
|
2677
|
-
} catch (err) {
|
|
2678
|
-
outcome = "failure";
|
|
2679
|
-
errorMsg = toErrorMessage(err);
|
|
2680
|
-
} finally {
|
|
2681
|
-
this.emitWrite("~compact~", this.indexFile, "compact", outcome, Date.now() - t0, void 0, errorMsg);
|
|
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 = [];
|
|
2682
2576
|
}
|
|
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
|
+
});
|
|
2683
2597
|
}
|
|
2684
2598
|
/**
|
|
2685
|
-
*
|
|
2686
|
-
*
|
|
2687
|
-
*
|
|
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.
|
|
2688
2603
|
*/
|
|
2689
|
-
async
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
} catch {
|
|
2695
|
-
this._indexCache = null;
|
|
2696
|
-
return [];
|
|
2697
|
-
}
|
|
2698
|
-
if (this._indexCache !== null && this._indexCache.mtimeMs === stat6.mtimeMs && this._indexCache.size === stat6.size) {
|
|
2699
|
-
return [...this._indexCache.summaries];
|
|
2604
|
+
async truncateToCheckpoint(targetPromptIndex) {
|
|
2605
|
+
if (!this.filePath) return 0;
|
|
2606
|
+
if (this.flushTimer) {
|
|
2607
|
+
clearTimeout(this.flushTimer);
|
|
2608
|
+
this.flushTimer = null;
|
|
2700
2609
|
}
|
|
2701
|
-
|
|
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;
|
|
2702
2619
|
try {
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
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;
|
|
2662
|
+
}
|
|
2663
|
+
fileOffset += bytesRead;
|
|
2664
|
+
if (chunkPos >= bytesRead) {
|
|
2665
|
+
lineStartOffset = fileOffset;
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
} finally {
|
|
2669
|
+
await fd?.close();
|
|
2707
2670
|
}
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
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);
|
|
2676
|
+
try {
|
|
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;
|
|
2690
|
+
}
|
|
2691
|
+
const writeFd = await fsp3.open(tmpPath, "w", 384);
|
|
2712
2692
|
try {
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
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;
|
|
2718
2701
|
}
|
|
2719
|
-
|
|
2720
|
-
|
|
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;
|
|
2721
2726
|
}
|
|
2722
|
-
|
|
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();
|
|
2723
2737
|
}
|
|
2738
|
+
await src.close();
|
|
2739
|
+
await fsp3.rename(tmpPath, this.filePath);
|
|
2740
|
+
this.handle = await fsp3.open(this.filePath, "a", 384);
|
|
2741
|
+
} catch (err) {
|
|
2742
|
+
await fsp3.unlink(tmpPath).catch(() => void 0);
|
|
2743
|
+
this.handle = await fsp3.open(this.filePath, "a", 384).catch(() => this.handle);
|
|
2744
|
+
throw err;
|
|
2724
2745
|
}
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
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;
|
|
2758
|
+
}
|
|
2759
|
+
async clearSession() {
|
|
2760
|
+
if (!this.filePath) return;
|
|
2761
|
+
if (this.flushTimer) {
|
|
2762
|
+
clearTimeout(this.flushTimer);
|
|
2763
|
+
this.flushTimer = null;
|
|
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");
|
|
2728
2776
|
}
|
|
2729
2777
|
/**
|
|
2730
|
-
*
|
|
2731
|
-
*
|
|
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.
|
|
2732
2782
|
*/
|
|
2733
|
-
async
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2783
|
+
async writeInFlightMarker(context) {
|
|
2784
|
+
if (!context || context.length > 500) {
|
|
2785
|
+
throw new Error("In-flight context must be 1..500 chars");
|
|
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() });
|
|
2743
2793
|
}
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
const selected = out.slice(0, limit);
|
|
2759
|
-
const summaries = await mapWithConcurrency(
|
|
2760
|
-
selected,
|
|
2761
|
-
Math.min(_DefaultSessionStore.LIST_SCAN_CONCURRENCY, Math.max(1, limit)),
|
|
2762
|
-
async (candidate) => {
|
|
2763
|
-
if (!candidate.needsBackfill) return candidate.summary;
|
|
2764
|
-
return await this.summaryFor(candidate.summary.id).catch(() => candidate.summary);
|
|
2765
|
-
}
|
|
2766
|
-
);
|
|
2767
|
-
return summaries.filter((s) => s !== null);
|
|
2794
|
+
/**
|
|
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() });
|
|
2768
2808
|
}
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
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).
|
|
2832
|
+
*
|
|
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.
|
|
2835
|
+
*/
|
|
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();
|
|
2775
2855
|
}
|
|
2776
|
-
const dirEntries = [];
|
|
2777
|
-
const files = [];
|
|
2778
|
-
for (const entry of entries) {
|
|
2779
|
-
if (entry.name.startsWith(".") && entry.name !== ".wrongstack") continue;
|
|
2780
|
-
if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments")
|
|
2781
|
-
continue;
|
|
2782
|
-
if (entry.isDirectory()) {
|
|
2783
|
-
dirEntries.push(entry);
|
|
2784
|
-
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
2785
|
-
if (entry.name === "_index.jsonl") continue;
|
|
2786
|
-
const base = entry.name.replace(/\.jsonl$/, "");
|
|
2787
|
-
const id = prefix ? `${prefix}/${base}` : base;
|
|
2788
|
-
files.push({ id, filePath: path4.join(dir, entry.name) });
|
|
2789
|
-
}
|
|
2790
|
-
}
|
|
2791
|
-
const childFileArrays = await Promise.all(
|
|
2792
|
-
dirEntries.map((entry) => {
|
|
2793
|
-
const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
|
|
2794
|
-
return this.collectSessionFiles(path4.join(dir, entry.name), childPrefix, depth + 1);
|
|
2795
|
-
})
|
|
2796
|
-
);
|
|
2797
|
-
return [...childFileArrays.flat(), ...files];
|
|
2798
2856
|
}
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
}
|
|
2810
|
-
const dirEntries = [];
|
|
2811
|
-
const fileIds = [];
|
|
2812
|
-
for (const entry of entries) {
|
|
2813
|
-
if (entry.name.startsWith(".") && entry.name !== ".wrongstack") continue;
|
|
2814
|
-
if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments")
|
|
2815
|
-
continue;
|
|
2816
|
-
if (entry.isDirectory()) {
|
|
2817
|
-
dirEntries.push(entry);
|
|
2818
|
-
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
2819
|
-
if (entry.name === "_index.jsonl") continue;
|
|
2820
|
-
const base = entry.name.replace(/\.jsonl$/, "");
|
|
2821
|
-
fileIds.push(prefix ? `${prefix}/${base}` : base);
|
|
2822
|
-
}
|
|
2823
|
-
}
|
|
2824
|
-
const childIdArrays = await Promise.all(
|
|
2825
|
-
dirEntries.map((entry) => {
|
|
2826
|
-
const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
|
|
2827
|
-
return this.collectSessionIds(path4.join(dir, entry.name), childPrefix, depth + 1);
|
|
2828
|
-
})
|
|
2829
|
-
);
|
|
2830
|
-
return [...childIdArrays.flat(), ...fileIds];
|
|
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
|
+
});
|
|
2831
2868
|
}
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
|
|
2844
|
-
const msg = toErrorMessage(err);
|
|
2845
|
-
this.emitError(id, manifest, "summary_fallback", msg, true);
|
|
2846
|
-
console.warn(JSON.stringify({
|
|
2847
|
-
level: "warn",
|
|
2848
|
-
event: "session_store.manifest_write_failed",
|
|
2849
|
-
sessionId: id,
|
|
2850
|
-
message: msg,
|
|
2851
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2852
|
-
}));
|
|
2853
|
-
});
|
|
2854
|
-
outcome = "failure";
|
|
2855
|
-
errorMsg = "summary fallback \xE2\u20AC\u201D manifest rebuilt";
|
|
2856
|
-
this.emitRead(id, manifest, "summary", outcome, Date.now() - t0, errorMsg);
|
|
2857
|
-
return summary;
|
|
2858
|
-
} catch (err) {
|
|
2859
|
-
outcome = "failure";
|
|
2860
|
-
errorMsg = toErrorMessage(err);
|
|
2861
|
-
this.emitRead(id, manifest, "summary", outcome, Date.now() - t0, errorMsg);
|
|
2862
|
-
return {
|
|
2863
|
-
id,
|
|
2864
|
-
title: "(damaged)",
|
|
2865
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2866
|
-
model: "unknown",
|
|
2867
|
-
provider: "unknown",
|
|
2868
|
-
tokenTotal: 0
|
|
2869
|
-
};
|
|
2870
|
-
}
|
|
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 } : {}
|
|
2879
|
+
});
|
|
2871
2880
|
}
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
}
|
|
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
|
+
});
|
|
2881
2890
|
}
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
}
|
|
2900
|
-
try {
|
|
2901
|
-
for await (const event of this.iterSessionEvents(ref.filePath)) {
|
|
2902
|
-
if (event.type === "session_start") {
|
|
2903
|
-
return {
|
|
2904
|
-
id: ref.id,
|
|
2905
|
-
title: "(empty session)",
|
|
2906
|
-
startedAt: event.ts,
|
|
2907
|
-
model: event.model ?? "unknown",
|
|
2908
|
-
provider: event.provider ?? "unknown",
|
|
2909
|
-
tokenTotal: 0
|
|
2910
|
-
};
|
|
2911
|
-
}
|
|
2912
|
-
}
|
|
2913
|
-
return {
|
|
2914
|
-
id: ref.id,
|
|
2915
|
-
title: "(empty session)",
|
|
2916
|
-
startedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
2917
|
-
model: "unknown",
|
|
2918
|
-
provider: "unknown",
|
|
2919
|
-
tokenTotal: 0
|
|
2920
|
-
};
|
|
2921
|
-
} catch {
|
|
2922
|
-
return {
|
|
2923
|
-
id: ref.id,
|
|
2924
|
-
title: "(damaged)",
|
|
2925
|
-
startedAt: mtime,
|
|
2926
|
-
model: "unknown",
|
|
2927
|
-
provider: "unknown",
|
|
2928
|
-
tokenTotal: 0
|
|
2929
|
-
};
|
|
2930
|
-
}
|
|
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));
|
|
2931
2908
|
}
|
|
2932
2909
|
/**
|
|
2933
|
-
*
|
|
2934
|
-
*
|
|
2935
|
-
*
|
|
2936
|
-
* Individual file deletions are best-effort (logged as structured warnings),
|
|
2937
|
-
* but a tombstone is always written so readIndex() filters this session out.
|
|
2938
|
-
* If the session directory itself can't be removed, the error is surfaced
|
|
2939
|
-
* to the caller so prune() can report it.
|
|
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.
|
|
2940
2913
|
*/
|
|
2941
|
-
async
|
|
2942
|
-
const
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
const base = path4.basename(id);
|
|
2946
|
-
const sessDir = path4.join(shardDir, base);
|
|
2947
|
-
const deletions = [
|
|
2948
|
-
fsp2.unlink(jsonlPath),
|
|
2949
|
-
fsp2.unlink(summaryPath),
|
|
2950
|
-
fsp2.unlink(path4.join(shardDir, `${base}.plan.json`)),
|
|
2951
|
-
fsp2.unlink(path4.join(shardDir, `${base}.todos.json`))
|
|
2952
|
-
];
|
|
2953
|
-
const results = await Promise.allSettled(deletions);
|
|
2954
|
-
for (const r of results) {
|
|
2955
|
-
if (r.status === "rejected") {
|
|
2956
|
-
const msg = r.reason instanceof Error ? r.reason.message : String(r.reason);
|
|
2957
|
-
if (r.reason?.code !== "ENOENT") {
|
|
2958
|
-
console.warn(JSON.stringify({
|
|
2959
|
-
level: "warn",
|
|
2960
|
-
event: "session_store.delete_failed",
|
|
2961
|
-
sessionId: id,
|
|
2962
|
-
message: msg,
|
|
2963
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2964
|
-
}));
|
|
2965
|
-
}
|
|
2966
|
-
}
|
|
2967
|
-
}
|
|
2968
|
-
await fsp2.rm(sessDir, { recursive: true, force: true }).catch((err) => {
|
|
2969
|
-
console.warn(JSON.stringify({
|
|
2970
|
-
level: "warn",
|
|
2971
|
-
event: "session_store.rmdir_failed",
|
|
2972
|
-
sessionId: id,
|
|
2973
|
-
message: toErrorMessage(err),
|
|
2974
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2975
|
-
}));
|
|
2976
|
-
});
|
|
2977
|
-
await this.writeTombstone(id);
|
|
2978
|
-
}
|
|
2979
|
-
async delete(id) {
|
|
2980
|
-
await this.deleteSession(id);
|
|
2914
|
+
async ensureShardDir(id) {
|
|
2915
|
+
const dirPath = path4.dirname(path4.join(this.dir, id));
|
|
2916
|
+
await ensureDir(dirPath);
|
|
2917
|
+
return dirPath;
|
|
2981
2918
|
}
|
|
2982
|
-
async
|
|
2983
|
-
const
|
|
2984
|
-
|
|
2985
|
-
|
|
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;
|
|
2986
2926
|
try {
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
const jsonlPath = path4.join(dir, name);
|
|
2995
|
-
try {
|
|
2996
|
-
const stat6 = await fsp2.stat(jsonlPath);
|
|
2997
|
-
if (stat6.mtimeMs >= cutoff) return;
|
|
2998
|
-
} catch {
|
|
2999
|
-
return;
|
|
3000
|
-
}
|
|
3001
|
-
const base = name.replace(/\.jsonl$/, "");
|
|
3002
|
-
const id = prefix ? `${prefix}/${base}` : base;
|
|
3003
|
-
if (activeSessionId && id === activeSessionId) return;
|
|
3004
|
-
await this.deleteSession(id);
|
|
3005
|
-
deleted++;
|
|
3006
|
-
};
|
|
3007
|
-
const entries = await fsp2.readdir(this.dir, { withFileTypes: true }).catch(() => []);
|
|
3008
|
-
for (const entry of entries) {
|
|
3009
|
-
if (entry.isFile()) {
|
|
3010
|
-
if (isPrunableJsonl(entry.name)) await pruneFile(this.dir, entry.name, "");
|
|
3011
|
-
continue;
|
|
3012
|
-
}
|
|
3013
|
-
if (!entry.isDirectory()) continue;
|
|
3014
|
-
const dateDir = path4.join(this.dir, entry.name);
|
|
3015
|
-
const files = await fsp2.readdir(dateDir, { withFileTypes: true }).catch(() => []);
|
|
3016
|
-
for (const file of files) {
|
|
3017
|
-
if (!file.isFile() || !isPrunableJsonl(file.name)) continue;
|
|
3018
|
-
await pruneFile(dateDir, file.name, entry.name);
|
|
3019
|
-
}
|
|
3020
|
-
}
|
|
3021
|
-
if (deleted > 0) {
|
|
3022
|
-
await this.compactIndex().catch(() => void 0);
|
|
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
|
+
);
|
|
3023
2934
|
}
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
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;
|
|
3034
2953
|
}
|
|
3035
|
-
return deleted;
|
|
3036
2954
|
}
|
|
3037
|
-
async
|
|
3038
|
-
await this.ensureShardDir(id);
|
|
2955
|
+
async resume(id) {
|
|
3039
2956
|
const file = this.sessionPath(id, ".jsonl");
|
|
3040
|
-
const
|
|
3041
|
-
const
|
|
3042
|
-
|
|
3043
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3044
|
-
id,
|
|
3045
|
-
model: "unknown",
|
|
3046
|
-
provider: "unknown"
|
|
3047
|
-
})}
|
|
3048
|
-
`;
|
|
3049
|
-
await fsp2.writeFile(file, record, "utf8");
|
|
3050
|
-
await fsp2.unlink(meta).catch(() => void 0);
|
|
3051
|
-
}
|
|
3052
|
-
async summarize(id, mtime) {
|
|
2957
|
+
const t0 = Date.now();
|
|
2958
|
+
const data = await this.load(id);
|
|
2959
|
+
let handle;
|
|
3053
2960
|
try {
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
let toolErrorCount = 0;
|
|
3065
|
-
let fileChangeCount = 0;
|
|
3066
|
-
const toolBreakdown = {};
|
|
3067
|
-
let outcome;
|
|
3068
|
-
let lastEventType;
|
|
3069
|
-
let hasError = false;
|
|
3070
|
-
let sawStart = false;
|
|
3071
|
-
for await (const e of this.iterSessionEvents(file)) {
|
|
3072
|
-
lastEventType = e.type;
|
|
3073
|
-
if (e.type === "session_start") {
|
|
3074
|
-
if (!sawStart) {
|
|
3075
|
-
sawStart = true;
|
|
3076
|
-
startedAt = e.ts;
|
|
3077
|
-
model = e.model ?? "unknown";
|
|
3078
|
-
provider = e.provider ?? "unknown";
|
|
3079
|
-
}
|
|
3080
|
-
} else if (e.type === "session_end") {
|
|
3081
|
-
endedAt = e.ts;
|
|
3082
|
-
} else if (e.type === "user_input") {
|
|
3083
|
-
if (title === "(empty session)") title = userInputTitle(e.content);
|
|
3084
|
-
} else if (e.type === "llm_response") {
|
|
3085
|
-
tokenIn += e.usage.input ?? 0;
|
|
3086
|
-
tokenOut += e.usage.output ?? 0;
|
|
3087
|
-
} else if (e.type === "in_flight_start") iterationCount++;
|
|
3088
|
-
else if (e.type === "tool_call_start") {
|
|
3089
|
-
toolCallCount++;
|
|
3090
|
-
toolBreakdown[e.name] = (toolBreakdown[e.name] ?? 0) + 1;
|
|
3091
|
-
} else if (e.type === "tool_result" && e.isError) toolErrorCount++;
|
|
3092
|
-
else if (e.type === "file_snapshot") fileChangeCount += e.files.length;
|
|
3093
|
-
else if (e.type === "error" || e.type === "provider_error") hasError = true;
|
|
3094
|
-
}
|
|
3095
|
-
if (lastEventType === "session_end") {
|
|
3096
|
-
outcome = "completed";
|
|
3097
|
-
} else if (lastEventType === "in_flight_start") {
|
|
3098
|
-
outcome = "aborted";
|
|
3099
|
-
} else if (hasError) {
|
|
3100
|
-
outcome = "error";
|
|
3101
|
-
}
|
|
3102
|
-
return {
|
|
3103
|
-
id,
|
|
3104
|
-
title,
|
|
3105
|
-
startedAt,
|
|
3106
|
-
endedAt,
|
|
3107
|
-
model,
|
|
3108
|
-
provider,
|
|
3109
|
-
tokenTotal: tokenIn + tokenOut,
|
|
3110
|
-
iterationCount: iterationCount > 0 ? iterationCount : void 0,
|
|
3111
|
-
toolCallCount: toolCallCount > 0 ? toolCallCount : void 0,
|
|
3112
|
-
toolErrorCount: toolErrorCount > 0 ? toolErrorCount : void 0,
|
|
3113
|
-
fileChangeCount: fileChangeCount > 0 ? fileChangeCount : void 0,
|
|
3114
|
-
toolBreakdown: Object.keys(toolBreakdown).length > 0 ? toolBreakdown : {},
|
|
3115
|
-
outcome
|
|
3116
|
-
};
|
|
3117
|
-
} catch {
|
|
3118
|
-
return {
|
|
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
|
+
);
|
|
2968
|
+
}
|
|
2969
|
+
try {
|
|
2970
|
+
const writer = new FileSessionWriter(
|
|
3119
2971
|
id,
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
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)
|
|
2989
|
+
}
|
|
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;
|
|
3126
3002
|
}
|
|
3127
3003
|
}
|
|
3128
|
-
async
|
|
3129
|
-
const
|
|
3130
|
-
const
|
|
3004
|
+
async load(id) {
|
|
3005
|
+
const file = this.sessionPath(id, ".jsonl");
|
|
3006
|
+
const t0 = Date.now();
|
|
3007
|
+
let outcome = "success";
|
|
3008
|
+
let errorMsg;
|
|
3009
|
+
let cacheHit = false;
|
|
3131
3010
|
try {
|
|
3132
|
-
|
|
3133
|
-
|
|
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) {
|
|
3134
3032
|
try {
|
|
3135
3033
|
const parsed = JSON.parse(line);
|
|
3136
3034
|
if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
|
|
3137
|
-
|
|
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
|
+
}
|
|
3138
3083
|
}
|
|
3139
3084
|
} catch {
|
|
3140
3085
|
}
|
|
3141
3086
|
}
|
|
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
|
+
});
|
|
3092
|
+
}
|
|
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 = {
|
|
3101
|
+
id,
|
|
3102
|
+
startedAt: sessionStartEvent?.ts ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
3103
|
+
endedAt: sessionEndEvent?.ts,
|
|
3104
|
+
model: sessionModel,
|
|
3105
|
+
provider: sessionProvider,
|
|
3106
|
+
pendingToolUses: sessionPendingToolUses
|
|
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;
|
|
3142
3122
|
} finally {
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
name: e.name,
|
|
3154
|
-
id: e.id,
|
|
3155
|
-
durationMs: e.durationMs,
|
|
3156
|
-
ok: e.ok ?? false,
|
|
3157
|
-
outputBytes: e.outputBytes,
|
|
3158
|
-
outputTokens: e.outputTokens,
|
|
3159
|
-
outputLines: e.outputLines
|
|
3160
|
-
});
|
|
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
|
+
}
|
|
3161
3133
|
}
|
|
3162
3134
|
}
|
|
3163
|
-
return result;
|
|
3164
|
-
}
|
|
3165
|
-
var FileSessionWriter = class _FileSessionWriter {
|
|
3166
|
-
constructor(id, handle, startedAt, meta, events, opts = {}, traceId) {
|
|
3167
|
-
this.id = id;
|
|
3168
|
-
this.handle = handle;
|
|
3169
|
-
this.startedAt = startedAt;
|
|
3170
|
-
this.meta = meta;
|
|
3171
|
-
this.events = events;
|
|
3172
|
-
this.resumed = opts.resumed ?? false;
|
|
3173
|
-
this.manifestFile = opts.dir ? path4.join(opts.dir, `${path4.basename(id)}.summary.json`) : "";
|
|
3174
|
-
this.filePath = opts.filePath ?? "";
|
|
3175
|
-
this.secretScrubber = opts.secretScrubber;
|
|
3176
|
-
this.onCloseCb = opts.onClose;
|
|
3177
|
-
this.summary = {
|
|
3178
|
-
id,
|
|
3179
|
-
title: "(empty session)",
|
|
3180
|
-
startedAt,
|
|
3181
|
-
model: meta.model ?? "unknown",
|
|
3182
|
-
provider: meta.provider ?? "unknown",
|
|
3183
|
-
tokenTotal: 0
|
|
3184
|
-
};
|
|
3185
|
-
this.traceId = traceId;
|
|
3186
|
-
}
|
|
3187
|
-
id;
|
|
3188
|
-
handle;
|
|
3189
|
-
startedAt;
|
|
3190
|
-
meta;
|
|
3191
|
-
events;
|
|
3192
|
-
closed = false;
|
|
3193
|
-
closePromise = null;
|
|
3194
|
-
manifestFile;
|
|
3195
|
-
summary;
|
|
3196
|
-
tokenIn = 0;
|
|
3197
|
-
tokenOut = 0;
|
|
3198
|
-
filePath;
|
|
3199
|
-
get transcriptPath() {
|
|
3200
|
-
return this.filePath || void 0;
|
|
3201
|
-
}
|
|
3202
|
-
/**
|
|
3203
|
-
* Lazy session_start/session_resumed init, shared by all appenders.
|
|
3204
|
-
* A single promise (not a boolean) so a second append racing the first
|
|
3205
|
-
* can't push its event into the buffer BEFORE the first append's event —
|
|
3206
|
-
* every appender awaits the same init and resumes in FIFO call order.
|
|
3207
|
-
*/
|
|
3208
|
-
initPromise = null;
|
|
3209
|
-
ensureInit() {
|
|
3210
|
-
if (!this.initPromise) this.initPromise = this.writeSessionStartLazy();
|
|
3211
|
-
return this.initPromise;
|
|
3212
|
-
}
|
|
3213
|
-
resumed;
|
|
3214
|
-
appendFailCount = 0;
|
|
3215
|
-
lastAppendWarnAt = 0;
|
|
3216
|
-
secretScrubber;
|
|
3217
|
-
onCloseCb;
|
|
3218
|
-
/** Implements SessionWriter.traceId — propagated from ContextInit.traceId. */
|
|
3219
|
-
traceId;
|
|
3220
|
-
// ── Write buffer — batches events to reduce per-event disk I/O ─────────
|
|
3221
|
-
//
|
|
3222
|
-
// Every append() pushes the scrubbed event into an in-memory buffer instead
|
|
3223
|
-
// of calling handle.appendFile() synchronously. The buffer flushes to disk
|
|
3224
|
-
// when it reaches FLUSH_SIZE events OR after FLUSH_INTERVAL_MS of inactivity.
|
|
3225
|
-
// This cuts the number of disk writes by ~95% without changing the on-disk
|
|
3226
|
-
// format — the JSONL is still one JSON object per line.
|
|
3227
|
-
writeBuffer = [];
|
|
3228
|
-
flushTimer = null;
|
|
3229
|
-
static FLUSH_INTERVAL_MS = 500;
|
|
3230
|
-
static FLUSH_SIZE = 50;
|
|
3231
|
-
// ── Write serialization ─────────────────────────────────────────────────
|
|
3232
|
-
//
|
|
3233
|
-
// All disk writes are funneled through a FIFO promise chain. Without it,
|
|
3234
|
-
// a timer-driven flush racing an explicit flush()/close() issues two
|
|
3235
|
-
// concurrent appendFile() calls on the shared O_APPEND handle — the kernel
|
|
3236
|
-
// may complete them out of order (chronology breaks) or, for large
|
|
3237
|
-
// batches, interleave partial writes (torn JSONL lines). The chain keeps
|
|
3238
|
-
// exactly one write in flight; failures don't break the chain.
|
|
3239
|
-
writeChain = Promise.resolve();
|
|
3240
|
-
/** Enqueue a write on the FIFO chain. Resolves/rejects with that write. */
|
|
3241
|
-
enqueueWrite(data) {
|
|
3242
|
-
const write = this.writeChain.then(() => this.handle.appendFile(data, "utf8"));
|
|
3243
|
-
this.writeChain = write.then(
|
|
3244
|
-
() => void 0,
|
|
3245
|
-
() => void 0
|
|
3246
|
-
);
|
|
3247
|
-
return write;
|
|
3248
|
-
}
|
|
3249
|
-
// ── Enriched summary tracking ──────────────────────────────────────────
|
|
3250
|
-
iterationCount = 0;
|
|
3251
|
-
toolCallCount = 0;
|
|
3252
|
-
toolErrorCount = 0;
|
|
3253
|
-
toolBreakdown = {};
|
|
3254
|
-
fileChangeCount = 0;
|
|
3255
|
-
compactionCount = 0;
|
|
3256
|
-
outcome = void 0;
|
|
3257
3135
|
/**
|
|
3258
|
-
*
|
|
3259
|
-
*
|
|
3260
|
-
*
|
|
3261
|
-
*
|
|
3262
|
-
*
|
|
3263
|
-
*
|
|
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`.
|
|
3264
3151
|
*/
|
|
3265
|
-
|
|
3266
|
-
const
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
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;
|
|
3158
|
+
try {
|
|
3159
|
+
stat8 = await fsp3.stat(file);
|
|
3160
|
+
} catch (err) {
|
|
3161
|
+
if (err.code === "ENOENT") return [];
|
|
3162
|
+
throw err;
|
|
3273
3163
|
}
|
|
3274
|
-
if (
|
|
3275
|
-
|
|
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()) {
|
|
3204
|
+
try {
|
|
3205
|
+
const parsed = JSON.parse(leftover);
|
|
3206
|
+
if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
|
|
3207
|
+
const ev = parsed;
|
|
3208
|
+
if (predicate(ev, eventIndex, ev.ts)) {
|
|
3209
|
+
out.push({ event: ev, eventIndex, ts: ev.ts });
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
} catch {
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
return out;
|
|
3216
|
+
} finally {
|
|
3217
|
+
if (fh) await fh.close().catch(() => void 0);
|
|
3276
3218
|
}
|
|
3277
|
-
return event;
|
|
3278
|
-
}
|
|
3279
|
-
pendingFileSnapshots = [];
|
|
3280
|
-
/** Tracks open tool_use IDs during the current run to serialize on close for resume. */
|
|
3281
|
-
openToolUses = /* @__PURE__ */ new Set();
|
|
3282
|
-
recordFileChange(input) {
|
|
3283
|
-
this.pendingFileSnapshots.push(input);
|
|
3284
3219
|
}
|
|
3285
|
-
|
|
3286
|
-
|
|
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
|
+
}
|
|
3287
3236
|
}
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
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;
|
|
3297
3249
|
try {
|
|
3298
|
-
await this.
|
|
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);
|
|
3261
|
+
});
|
|
3262
|
+
return filtered.slice(0, limit);
|
|
3299
3263
|
} catch {
|
|
3264
|
+
return [];
|
|
3300
3265
|
}
|
|
3301
3266
|
}
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
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;
|
|
3312
3290
|
}
|
|
3313
|
-
|
|
3314
|
-
} else {
|
|
3315
|
-
this.scheduleFlush();
|
|
3291
|
+
} catch {
|
|
3316
3292
|
}
|
|
3317
3293
|
}
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
const
|
|
3323
|
-
this.
|
|
3324
|
-
this.
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
clearTimeout(this.flushTimer);
|
|
3329
|
-
this.flushTimer = null;
|
|
3330
|
-
}
|
|
3331
|
-
await this.flushBuffer();
|
|
3332
|
-
} else {
|
|
3333
|
-
this.scheduleFlush();
|
|
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 {
|
|
3334
3304
|
}
|
|
3335
3305
|
}
|
|
3336
3306
|
/**
|
|
3337
|
-
*
|
|
3338
|
-
* (
|
|
3339
|
-
* instead of sitting in the in-memory buffer for up to 500ms.
|
|
3340
|
-
*
|
|
3341
|
-
* Idempotent — cancels any pending timer and writes whatever has
|
|
3342
|
-
* accumulated in the buffer. Safe to call even when the buffer
|
|
3343
|
-
* is empty (no-op).
|
|
3307
|
+
* Compact the index: read all entries, drop tombstones, deduplicate
|
|
3308
|
+
* (keep latest per session), and rewrite. Atomic via temp+rename.
|
|
3344
3309
|
*/
|
|
3345
|
-
async
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
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);
|
|
3349
3327
|
}
|
|
3350
|
-
await this.flushBuffer();
|
|
3351
3328
|
}
|
|
3352
|
-
/**
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
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
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
const summaries = Array.from(seen.values());
|
|
3371
|
+
this._indexCache = { ...stat8, summaries };
|
|
3372
|
+
return [...summaries];
|
|
3360
3373
|
}
|
|
3361
3374
|
/**
|
|
3362
|
-
*
|
|
3363
|
-
*
|
|
3364
|
-
* append path used — one warning every 5s with a suppressed count.
|
|
3365
|
-
* On failure the buffer is cleared (events are best-effort, same as
|
|
3366
|
-
* the old per-event path where a failed write was silently dropped).
|
|
3375
|
+
* Rebuild the index from disk by scanning all sessions and writing a
|
|
3376
|
+
* fresh _index.jsonl. Useful after manual cleanup or index corruption.
|
|
3367
3377
|
*/
|
|
3368
|
-
async
|
|
3369
|
-
|
|
3370
|
-
const
|
|
3371
|
-
const
|
|
3372
|
-
|
|
3373
|
-
const
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
} catch (err) {
|
|
3379
|
-
outcome = "failure";
|
|
3380
|
-
errorMsg = toErrorMessage(err);
|
|
3381
|
-
this.appendFailCount += eventCount;
|
|
3382
|
-
const now = Date.now();
|
|
3383
|
-
if (now - this.lastAppendWarnAt > 5e3) {
|
|
3384
|
-
const suppressed = this.appendFailCount - 1;
|
|
3385
|
-
const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
|
|
3386
|
-
console.warn(
|
|
3387
|
-
"[session] flush failed:",
|
|
3388
|
-
toErrorMessage(err),
|
|
3389
|
-
tail
|
|
3390
|
-
);
|
|
3391
|
-
this.lastAppendWarnAt = now;
|
|
3392
|
-
this.appendFailCount = 0;
|
|
3393
|
-
}
|
|
3394
|
-
} finally {
|
|
3395
|
-
this.events?.emit("storage.write", {
|
|
3396
|
-
sessionId: this.id,
|
|
3397
|
-
store: "session",
|
|
3398
|
-
filePath: this.filePath,
|
|
3399
|
-
operation: "flush",
|
|
3400
|
-
outcome,
|
|
3401
|
-
durationMs: Date.now() - t0,
|
|
3402
|
-
...errorMsg !== void 0 ? { error: errorMsg } : {},
|
|
3403
|
-
...eventCount !== void 0 ? { eventCount } : {},
|
|
3404
|
-
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
3405
|
-
});
|
|
3406
|
-
}
|
|
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;
|
|
3407
3388
|
}
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
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)
|
|
3395
|
+
);
|
|
3396
|
+
const out = [];
|
|
3397
|
+
for (const entry of shardEntries) {
|
|
3398
|
+
for (const summary of entry.summaries) {
|
|
3399
|
+
out.push({ summary, needsBackfill: false });
|
|
3412
3400
|
}
|
|
3413
3401
|
}
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
this.
|
|
3427
|
-
}
|
|
3428
|
-
|
|
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);
|
|
3410
|
+
}
|
|
3411
|
+
async collectShardKeys() {
|
|
3412
|
+
let entries;
|
|
3413
|
+
try {
|
|
3414
|
+
entries = await fsp3.readdir(this.dir, { withFileTypes: true });
|
|
3415
|
+
} catch {
|
|
3416
|
+
return [""];
|
|
3429
3417
|
}
|
|
3430
|
-
|
|
3431
|
-
|
|
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);
|
|
3432
3423
|
}
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
const
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3424
|
+
return shardKeys;
|
|
3425
|
+
}
|
|
3426
|
+
async readOrBuildShardManifest(shardKey) {
|
|
3427
|
+
const cached = this.shardManifestCache.get(shardKey);
|
|
3428
|
+
if (cached) return cached;
|
|
3429
|
+
const manifestPath = this.shardManifestPath(shardKey);
|
|
3430
|
+
try {
|
|
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;
|
|
3439
|
+
} catch {
|
|
3444
3440
|
}
|
|
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 };
|
|
3452
|
+
}
|
|
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("/"));
|
|
3445
3465
|
}
|
|
3446
|
-
async
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3466
|
+
async collectSessionFiles(dir, prefix = "", depth = 0) {
|
|
3467
|
+
let entries;
|
|
3468
|
+
try {
|
|
3469
|
+
entries = await fsp3.readdir(dir, { withFileTypes: true });
|
|
3470
|
+
} catch {
|
|
3471
|
+
return [];
|
|
3472
|
+
}
|
|
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) });
|
|
3486
|
+
}
|
|
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];
|
|
3450
3495
|
}
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
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 [];
|
|
3456
3506
|
}
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
};
|
|
3470
|
-
if (this.manifestFile) {
|
|
3471
|
-
const t0 = Date.now();
|
|
3472
|
-
let outcome = "success";
|
|
3473
|
-
let errorMsg;
|
|
3474
|
-
try {
|
|
3475
|
-
await atomicWrite(this.manifestFile, JSON.stringify(this.summary), { mode: 384 });
|
|
3476
|
-
} catch (err) {
|
|
3477
|
-
outcome = "failure";
|
|
3478
|
-
errorMsg = toErrorMessage(err);
|
|
3479
|
-
} finally {
|
|
3480
|
-
this.events?.emit("storage.write", {
|
|
3481
|
-
sessionId: this.id,
|
|
3482
|
-
store: "session",
|
|
3483
|
-
filePath: this.manifestFile,
|
|
3484
|
-
operation: "close",
|
|
3485
|
-
outcome,
|
|
3486
|
-
durationMs: Date.now() - t0,
|
|
3487
|
-
...errorMsg !== void 0 ? { error: errorMsg } : {},
|
|
3488
|
-
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
3489
|
-
});
|
|
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);
|
|
3490
3519
|
}
|
|
3491
3520
|
}
|
|
3492
|
-
const
|
|
3493
|
-
|
|
3494
|
-
|
|
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];
|
|
3528
|
+
}
|
|
3529
|
+
async summaryFor(id) {
|
|
3530
|
+
const manifest = this.sessionPath(id, ".summary.json");
|
|
3531
|
+
const t0 = Date.now();
|
|
3532
|
+
let outcome = "success";
|
|
3533
|
+
let errorMsg;
|
|
3534
|
+
const fromManifest = await this.readSummaryManifest(id, t0);
|
|
3535
|
+
if (fromManifest) return fromManifest;
|
|
3495
3536
|
try {
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
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
|
+
}));
|
|
3510
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;
|
|
3555
|
+
} catch (err) {
|
|
3556
|
+
outcome = "failure";
|
|
3557
|
+
errorMsg = toErrorMessage(err);
|
|
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
|
+
};
|
|
3511
3567
|
}
|
|
3568
|
+
}
|
|
3569
|
+
async readSummaryManifest(id, startTime = Date.now()) {
|
|
3570
|
+
const manifest = this.sessionPath(id, ".summary.json");
|
|
3512
3571
|
try {
|
|
3513
|
-
await
|
|
3572
|
+
const raw = await fsp3.readFile(manifest, "utf8");
|
|
3573
|
+
this.emitRead(id, manifest, "summary", "success", Date.now() - startTime);
|
|
3574
|
+
return JSON.parse(raw);
|
|
3514
3575
|
} catch {
|
|
3576
|
+
return null;
|
|
3515
3577
|
}
|
|
3516
3578
|
}
|
|
3517
|
-
async
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
await
|
|
3521
|
-
|
|
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
|
+
};
|
|
3592
|
+
}
|
|
3593
|
+
mtime = stat8.mtime.toISOString();
|
|
3594
|
+
} catch {
|
|
3595
|
+
return null;
|
|
3596
|
+
}
|
|
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
|
+
};
|
|
3522
3627
|
}
|
|
3523
|
-
await this.append({
|
|
3524
|
-
type: "checkpoint",
|
|
3525
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3526
|
-
promptIndex,
|
|
3527
|
-
promptPreview
|
|
3528
|
-
});
|
|
3529
|
-
this.events?.emit("checkpoint.written", {
|
|
3530
|
-
promptIndex,
|
|
3531
|
-
promptPreview,
|
|
3532
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3533
|
-
fileCount
|
|
3534
|
-
});
|
|
3535
|
-
}
|
|
3536
|
-
async writeFileSnapshot(promptIndex, files) {
|
|
3537
|
-
await this.append({
|
|
3538
|
-
type: "file_snapshot",
|
|
3539
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3540
|
-
promptIndex,
|
|
3541
|
-
files
|
|
3542
|
-
});
|
|
3543
3628
|
}
|
|
3544
3629
|
/**
|
|
3545
|
-
*
|
|
3546
|
-
*
|
|
3547
|
-
*
|
|
3548
|
-
*
|
|
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.
|
|
3549
3637
|
*/
|
|
3550
|
-
async
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
if (idx === -1) {
|
|
3575
|
-
lineStartOffset = fileOffset + chunkPos;
|
|
3576
|
-
break;
|
|
3577
|
-
}
|
|
3578
|
-
if (checkpointByteOffset !== -1) {
|
|
3579
|
-
removedCount++;
|
|
3580
|
-
} else {
|
|
3581
|
-
const lineBytes = buf.subarray(chunkPos, idx);
|
|
3582
|
-
const line = new TextDecoder("utf-8", { fatal: false }).decode(lineBytes);
|
|
3583
|
-
if (line.trim()) {
|
|
3584
|
-
try {
|
|
3585
|
-
const event = JSON.parse(line);
|
|
3586
|
-
if (event.type === "checkpoint") {
|
|
3587
|
-
if (event.promptIndex === targetPromptIndex) {
|
|
3588
|
-
checkpointByteOffset = lineStartOffset;
|
|
3589
|
-
targetCheckpointSeen = true;
|
|
3590
|
-
} else if (event.promptIndex !== void 0 && event.promptIndex > targetPromptIndex) {
|
|
3591
|
-
checkpointByteOffset = lineStartOffset;
|
|
3592
|
-
}
|
|
3593
|
-
} else if (targetCheckpointSeen && event.promptIndex !== void 0 && event.promptIndex > targetPromptIndex) {
|
|
3594
|
-
removedCount++;
|
|
3595
|
-
} else if (targetCheckpointSeen && event.promptIndex === void 0) {
|
|
3596
|
-
removedCount++;
|
|
3597
|
-
} else if (!targetCheckpointSeen && event.promptIndex === void 0) {
|
|
3598
|
-
removedCount++;
|
|
3599
|
-
} else if (!targetCheckpointSeen && event.promptIndex !== void 0 && event.promptIndex > targetPromptIndex) {
|
|
3600
|
-
removedCount++;
|
|
3601
|
-
}
|
|
3602
|
-
} catch {
|
|
3603
|
-
}
|
|
3604
|
-
}
|
|
3605
|
-
}
|
|
3606
|
-
chunkPos = idx + 1;
|
|
3607
|
-
lineStartOffset = fileOffset + chunkPos;
|
|
3608
|
-
}
|
|
3609
|
-
fileOffset += bytesRead;
|
|
3610
|
-
if (chunkPos >= bytesRead) {
|
|
3611
|
-
lineStartOffset = fileOffset;
|
|
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
|
+
}));
|
|
3612
3662
|
}
|
|
3613
3663
|
}
|
|
3614
|
-
} finally {
|
|
3615
|
-
await fd?.close();
|
|
3616
3664
|
}
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
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;
|
|
3622
3683
|
try {
|
|
3623
|
-
const
|
|
3624
|
-
const
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
const nl = probeBuf.indexOf("\n");
|
|
3632
|
-
newlineAfterCheckpoint = nl !== -1 ? prefixBytes + nl + 1 : totalSize;
|
|
3633
|
-
}
|
|
3634
|
-
} else {
|
|
3635
|
-
newlineAfterCheckpoint = totalSize;
|
|
3636
|
-
}
|
|
3637
|
-
const writeFd = await fsp2.open(tmpPath, "w", 384);
|
|
3684
|
+
const raw = await fsp3.readFile(path4.join(this.dir, "active.json"), "utf8");
|
|
3685
|
+
const active = JSON.parse(raw);
|
|
3686
|
+
activeSessionId = active.sessionId ?? null;
|
|
3687
|
+
} catch {
|
|
3688
|
+
}
|
|
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);
|
|
3638
3692
|
try {
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
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
|
+
}
|
|
3717
|
+
}
|
|
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);
|
|
3658
3728
|
}
|
|
3659
|
-
}
|
|
3660
|
-
await writeFd.close();
|
|
3729
|
+
} catch {
|
|
3661
3730
|
}
|
|
3662
|
-
await src.close();
|
|
3663
|
-
await fsp2.rename(tmpPath, this.filePath);
|
|
3664
|
-
this.handle = await fsp2.open(this.filePath, "a", 384);
|
|
3665
|
-
} catch (err) {
|
|
3666
|
-
await fsp2.unlink(tmpPath).catch(() => void 0);
|
|
3667
|
-
this.handle = await fsp2.open(this.filePath, "a", 384).catch(() => this.handle);
|
|
3668
|
-
throw err;
|
|
3669
3731
|
}
|
|
3670
|
-
|
|
3671
|
-
type: "rewound",
|
|
3672
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3673
|
-
toPromptIndex: targetPromptIndex,
|
|
3674
|
-
revertedFiles: []
|
|
3675
|
-
});
|
|
3676
|
-
this.events?.emit("session.rewound", {
|
|
3677
|
-
toPromptIndex: targetPromptIndex,
|
|
3678
|
-
revertedFiles: [],
|
|
3679
|
-
removedEvents: removedCount
|
|
3680
|
-
});
|
|
3681
|
-
return removedCount;
|
|
3732
|
+
return deleted;
|
|
3682
3733
|
}
|
|
3683
|
-
async
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
this.flushTimer = null;
|
|
3688
|
-
}
|
|
3689
|
-
this.writeBuffer = [];
|
|
3690
|
-
await this.writeChain;
|
|
3734
|
+
async clearHistory(id) {
|
|
3735
|
+
await this.ensureShardDir(id);
|
|
3736
|
+
const file = this.sessionPath(id, ".jsonl");
|
|
3737
|
+
const meta = this.sessionPath(id, ".summary.json");
|
|
3691
3738
|
const record = `${JSON.stringify({
|
|
3692
3739
|
type: "session_start",
|
|
3693
3740
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3694
|
-
id
|
|
3695
|
-
model:
|
|
3696
|
-
provider:
|
|
3741
|
+
id,
|
|
3742
|
+
model: "unknown",
|
|
3743
|
+
provider: "unknown"
|
|
3697
3744
|
})}
|
|
3698
3745
|
`;
|
|
3699
|
-
await
|
|
3746
|
+
await fsp3.writeFile(file, record, "utf8");
|
|
3747
|
+
await fsp3.unlink(meta).catch(() => void 0);
|
|
3700
3748
|
}
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3749
|
+
async summarize(id, mtime) {
|
|
3750
|
+
try {
|
|
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";
|
|
3776
|
+
}
|
|
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;
|
|
3791
|
+
}
|
|
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";
|
|
3798
|
+
}
|
|
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
|
+
};
|
|
3710
3823
|
}
|
|
3711
|
-
await this.append({
|
|
3712
|
-
type: "in_flight_start",
|
|
3713
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3714
|
-
context
|
|
3715
|
-
});
|
|
3716
|
-
this.events?.emit("in_flight.started", { context, ts: (/* @__PURE__ */ new Date()).toISOString() });
|
|
3717
3824
|
}
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
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;
|
|
3835
|
+
}
|
|
3836
|
+
} catch {
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
} finally {
|
|
3840
|
+
lines.close();
|
|
3841
|
+
stream.destroy();
|
|
3842
|
+
}
|
|
3732
3843
|
}
|
|
3733
3844
|
};
|
|
3734
|
-
function
|
|
3735
|
-
const
|
|
3736
|
-
|
|
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
|
+
});
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
return result;
|
|
3737
3861
|
}
|
|
3738
3862
|
function compareSessionSummaries(a, b) {
|
|
3739
3863
|
if (a.startedAt < b.startedAt) return 1;
|
|
3740
3864
|
if (a.startedAt > b.startedAt) return -1;
|
|
3741
3865
|
return a.id.localeCompare(b.id);
|
|
3742
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
|
+
}
|
|
3743
3879
|
async function mapWithConcurrency(items, concurrency, fn) {
|
|
3744
3880
|
if (items.length === 0) return [];
|
|
3745
3881
|
const out = new Array(items.length);
|
|
@@ -3811,7 +3947,7 @@ var QueueStore = class {
|
|
|
3811
3947
|
const t0 = Date.now();
|
|
3812
3948
|
let raw;
|
|
3813
3949
|
try {
|
|
3814
|
-
raw = await
|
|
3950
|
+
raw = await fsp3.readFile(this.file, "utf8");
|
|
3815
3951
|
} catch (err) {
|
|
3816
3952
|
const code = err.code;
|
|
3817
3953
|
if (code === "ENOENT") {
|
|
@@ -3892,7 +4028,7 @@ var QueueStore = class {
|
|
|
3892
4028
|
async clear() {
|
|
3893
4029
|
const t0 = Date.now();
|
|
3894
4030
|
try {
|
|
3895
|
-
await
|
|
4031
|
+
await fsp3.unlink(this.file);
|
|
3896
4032
|
this.events?.emit("storage.write", {
|
|
3897
4033
|
sessionId: this.traceId ?? "~boot~",
|
|
3898
4034
|
store: "queue",
|
|
@@ -3952,7 +4088,7 @@ var DefaultAttachmentStore = class {
|
|
|
3952
4088
|
let spooledPath;
|
|
3953
4089
|
let data = input.data;
|
|
3954
4090
|
if (this.spoolDir && bytes >= this.spoolThreshold) {
|
|
3955
|
-
await
|
|
4091
|
+
await fsp3.mkdir(this.spoolDir, { recursive: true });
|
|
3956
4092
|
spooledPath = path4.join(this.spoolDir, `${id}.bin`);
|
|
3957
4093
|
await atomicWrite(spooledPath, input.data, {
|
|
3958
4094
|
encoding: input.kind === "image" ? "base64" : "utf8"
|
|
@@ -4015,7 +4151,7 @@ var DefaultAttachmentStore = class {
|
|
|
4015
4151
|
for (const att of this.items.values()) {
|
|
4016
4152
|
if (att.path) toDelete.push(att.path);
|
|
4017
4153
|
}
|
|
4018
|
-
await Promise.all(toDelete.map((p) =>
|
|
4154
|
+
await Promise.all(toDelete.map((p) => fsp3.unlink(p).catch(() => void 0)));
|
|
4019
4155
|
}
|
|
4020
4156
|
this.items.clear();
|
|
4021
4157
|
this.refs.length = 0;
|
|
@@ -4023,7 +4159,7 @@ var DefaultAttachmentStore = class {
|
|
|
4023
4159
|
}
|
|
4024
4160
|
async toBlock(att) {
|
|
4025
4161
|
if (att.kind === "image") {
|
|
4026
|
-
const data = att.data ?? (att.path ? await
|
|
4162
|
+
const data = att.data ?? (att.path ? await fsp3.readFile(att.path, { encoding: "base64" }) : "");
|
|
4027
4163
|
return {
|
|
4028
4164
|
type: "image",
|
|
4029
4165
|
source: {
|
|
@@ -4033,7 +4169,7 @@ var DefaultAttachmentStore = class {
|
|
|
4033
4169
|
}
|
|
4034
4170
|
};
|
|
4035
4171
|
}
|
|
4036
|
-
const raw = att.data ?? (att.path ? await
|
|
4172
|
+
const raw = att.data ?? (att.path ? await fsp3.readFile(att.path, "utf8") : "");
|
|
4037
4173
|
const label = att.meta.filename ? `<file path="${att.meta.filename}">` : "<pasted>";
|
|
4038
4174
|
const close = att.meta.filename ? "</file>" : "</pasted>";
|
|
4039
4175
|
return { type: "text", text: `${label}
|
|
@@ -4169,7 +4305,7 @@ var FileMemoryBackend = class {
|
|
|
4169
4305
|
await ensureDir(path4.dirname(file));
|
|
4170
4306
|
let existing = "";
|
|
4171
4307
|
try {
|
|
4172
|
-
existing = await
|
|
4308
|
+
existing = await fsp3.readFile(file, "utf8");
|
|
4173
4309
|
} catch {
|
|
4174
4310
|
}
|
|
4175
4311
|
const id = `mem_${Date.now()}_${randomUUID().slice(0, 8)}`;
|
|
@@ -4186,7 +4322,7 @@ ${line}`;
|
|
|
4186
4322
|
return withFileLock(file, async () => {
|
|
4187
4323
|
let existing;
|
|
4188
4324
|
try {
|
|
4189
|
-
existing = await
|
|
4325
|
+
existing = await fsp3.readFile(file, "utf8");
|
|
4190
4326
|
} catch {
|
|
4191
4327
|
return 0;
|
|
4192
4328
|
}
|
|
@@ -4222,7 +4358,7 @@ ${line}`;
|
|
|
4222
4358
|
async readAll(scope, filePath) {
|
|
4223
4359
|
const file = this.resolveFile(filePath, scope);
|
|
4224
4360
|
try {
|
|
4225
|
-
return await
|
|
4361
|
+
return await fsp3.readFile(file, "utf8");
|
|
4226
4362
|
} catch {
|
|
4227
4363
|
return "";
|
|
4228
4364
|
}
|
|
@@ -4257,7 +4393,7 @@ ${line}`;
|
|
|
4257
4393
|
const file = this.resolveFile(filePath, scope);
|
|
4258
4394
|
let existing;
|
|
4259
4395
|
try {
|
|
4260
|
-
existing = await
|
|
4396
|
+
existing = await fsp3.readFile(file, "utf8");
|
|
4261
4397
|
} catch {
|
|
4262
4398
|
return 0;
|
|
4263
4399
|
}
|
|
@@ -4277,7 +4413,7 @@ ${line}`;
|
|
|
4277
4413
|
const next = lines.join("\n");
|
|
4278
4414
|
const backup = `${file}.bak.${Date.now()}`;
|
|
4279
4415
|
try {
|
|
4280
|
-
await
|
|
4416
|
+
await fsp3.copyFile(file, backup);
|
|
4281
4417
|
await pruneConsolidateBackups(file);
|
|
4282
4418
|
} catch {
|
|
4283
4419
|
}
|
|
@@ -4293,11 +4429,11 @@ async function pruneConsolidateBackups(file) {
|
|
|
4293
4429
|
const dir = path4.dirname(file);
|
|
4294
4430
|
const base = path4.basename(file);
|
|
4295
4431
|
const prefix = `${base}.bak.`;
|
|
4296
|
-
const backups = (await
|
|
4432
|
+
const backups = (await fsp3.readdir(dir)).filter((name) => name.startsWith(prefix)).sort().reverse();
|
|
4297
4433
|
await Promise.all(
|
|
4298
4434
|
backups.slice(MAX_MEMORY_CONSOLIDATE_BACKUPS).map(async (name) => {
|
|
4299
4435
|
try {
|
|
4300
|
-
await
|
|
4436
|
+
await fsp3.unlink(path4.join(dir, name));
|
|
4301
4437
|
} catch {
|
|
4302
4438
|
}
|
|
4303
4439
|
})
|
|
@@ -4852,9 +4988,9 @@ ${body.trim()}`);
|
|
|
4852
4988
|
if (!this.persistBackup || scope === "project-agents") return;
|
|
4853
4989
|
try {
|
|
4854
4990
|
const content = await this.backend.readAll(scope, this.files[scope]);
|
|
4855
|
-
const { writeFile:
|
|
4991
|
+
const { writeFile: writeFile8, mkdir: mkdir8 } = await import('fs/promises');
|
|
4856
4992
|
await mkdir8(this.backupDir, { recursive: true });
|
|
4857
|
-
await
|
|
4993
|
+
await writeFile8(`${this.backupDir}/${scope}.md`, content, "utf8");
|
|
4858
4994
|
} catch {
|
|
4859
4995
|
}
|
|
4860
4996
|
}
|
|
@@ -5188,8 +5324,8 @@ function unwrapDataKey(buf, keyFile) {
|
|
|
5188
5324
|
function checkKeyFilePermissions(keyFile) {
|
|
5189
5325
|
if (process.platform === "win32") return;
|
|
5190
5326
|
try {
|
|
5191
|
-
const
|
|
5192
|
-
const actualMode =
|
|
5327
|
+
const stat8 = fs4.statSync(keyFile);
|
|
5328
|
+
const actualMode = stat8.mode & 511;
|
|
5193
5329
|
if (actualMode !== KEY_FILE_MODE) {
|
|
5194
5330
|
console.warn(JSON.stringify({
|
|
5195
5331
|
level: "warn",
|
|
@@ -5454,20 +5590,20 @@ function isSecretField(name) {
|
|
|
5454
5590
|
async function rewriteConfigEncrypted(configPath, vault, patch) {
|
|
5455
5591
|
let current = {};
|
|
5456
5592
|
try {
|
|
5457
|
-
const raw = await
|
|
5593
|
+
const raw = await fsp3.readFile(configPath, "utf8");
|
|
5458
5594
|
current = JSON.parse(raw);
|
|
5459
5595
|
} catch {
|
|
5460
5596
|
}
|
|
5461
5597
|
const merged = deepMerge(current, patch ?? {});
|
|
5462
5598
|
const encrypted = encryptConfigSecrets(merged, vault);
|
|
5463
|
-
await
|
|
5599
|
+
await fsp3.mkdir(path4.dirname(configPath), { recursive: true });
|
|
5464
5600
|
await atomicWrite(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
5465
5601
|
await restrictFilePermissions(configPath);
|
|
5466
5602
|
}
|
|
5467
5603
|
async function migratePlaintextSecrets(configPath, vault, logger) {
|
|
5468
5604
|
let raw;
|
|
5469
5605
|
try {
|
|
5470
|
-
raw = await
|
|
5606
|
+
raw = await fsp3.readFile(configPath, "utf8");
|
|
5471
5607
|
} catch {
|
|
5472
5608
|
return { migrated: 0, file: configPath };
|
|
5473
5609
|
}
|
|
@@ -5509,7 +5645,7 @@ async function restrictFilePermissions(filePath, opts) {
|
|
|
5509
5645
|
}
|
|
5510
5646
|
} else {
|
|
5511
5647
|
try {
|
|
5512
|
-
await
|
|
5648
|
+
await fsp3.chmod(filePath, 384);
|
|
5513
5649
|
} catch {
|
|
5514
5650
|
}
|
|
5515
5651
|
}
|
|
@@ -5703,7 +5839,12 @@ var BEHAVIOR_DEFAULTS = {
|
|
|
5703
5839
|
},
|
|
5704
5840
|
circuitBreaker: { ...DEFAULT_CIRCUIT_BREAKER_CONFIG },
|
|
5705
5841
|
modelRuntime: {
|
|
5706
|
-
|
|
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" },
|
|
5707
5848
|
cache: {}
|
|
5708
5849
|
}
|
|
5709
5850
|
};
|
|
@@ -5959,6 +6100,7 @@ var DefaultConfigLoader = class {
|
|
|
5959
6100
|
extraSources;
|
|
5960
6101
|
events;
|
|
5961
6102
|
traceId;
|
|
6103
|
+
jsonCache = /* @__PURE__ */ new Map();
|
|
5962
6104
|
constructor(opts) {
|
|
5963
6105
|
this.paths = opts.paths;
|
|
5964
6106
|
this.strict = opts.strict ?? false;
|
|
@@ -6041,7 +6183,7 @@ var DefaultConfigLoader = class {
|
|
|
6041
6183
|
await withFileLock(fp, async () => {
|
|
6042
6184
|
let parsed;
|
|
6043
6185
|
try {
|
|
6044
|
-
const raw = await
|
|
6186
|
+
const raw = await fsp3.readFile(fp, "utf8");
|
|
6045
6187
|
const result = safeParse(raw);
|
|
6046
6188
|
if (!result.ok || !isPlainRecord(result.value)) {
|
|
6047
6189
|
return;
|
|
@@ -6156,7 +6298,7 @@ var DefaultConfigLoader = class {
|
|
|
6156
6298
|
const fp = this.paths.syncConfig;
|
|
6157
6299
|
const t0 = Date.now();
|
|
6158
6300
|
try {
|
|
6159
|
-
const raw = await
|
|
6301
|
+
const raw = await fsp3.readFile(fp, "utf8");
|
|
6160
6302
|
const parsed = safeParse(raw);
|
|
6161
6303
|
if (!parsed.ok || !parsed.value) {
|
|
6162
6304
|
this.events?.emit("storage.read", {
|
|
@@ -6217,10 +6359,42 @@ var DefaultConfigLoader = class {
|
|
|
6217
6359
|
}
|
|
6218
6360
|
}
|
|
6219
6361
|
async readJson(file) {
|
|
6220
|
-
let raw;
|
|
6221
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;
|
|
6222
6396
|
try {
|
|
6223
|
-
raw = await
|
|
6397
|
+
raw = await fsp3.readFile(file, "utf8");
|
|
6224
6398
|
} catch (err) {
|
|
6225
6399
|
if (err.code !== "ENOENT") {
|
|
6226
6400
|
this.events?.emit("storage.read", {
|
|
@@ -6241,6 +6415,7 @@ var DefaultConfigLoader = class {
|
|
|
6241
6415
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6242
6416
|
}));
|
|
6243
6417
|
}
|
|
6418
|
+
this.jsonCache.set(file, { mtimeMs: null, value: {} });
|
|
6244
6419
|
return {};
|
|
6245
6420
|
}
|
|
6246
6421
|
const parsed = safeParse(raw);
|
|
@@ -6264,6 +6439,7 @@ var DefaultConfigLoader = class {
|
|
|
6264
6439
|
}));
|
|
6265
6440
|
return {};
|
|
6266
6441
|
}
|
|
6442
|
+
this.jsonCache.set(file, { mtimeMs, value: structuredClone(parsed.value) });
|
|
6267
6443
|
return parsed.value;
|
|
6268
6444
|
}
|
|
6269
6445
|
validateBehavior(cfg) {
|
|
@@ -6461,7 +6637,7 @@ var RecoveryLock = class {
|
|
|
6461
6637
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6462
6638
|
};
|
|
6463
6639
|
try {
|
|
6464
|
-
await
|
|
6640
|
+
await fsp3.writeFile(this.file, JSON.stringify(lock), { flag: "wx", mode: 384 });
|
|
6465
6641
|
} catch (err) {
|
|
6466
6642
|
const code = err.code;
|
|
6467
6643
|
if (code === "EEXIST") {
|
|
@@ -6477,7 +6653,7 @@ var RecoveryLock = class {
|
|
|
6477
6653
|
*/
|
|
6478
6654
|
async clear() {
|
|
6479
6655
|
try {
|
|
6480
|
-
await
|
|
6656
|
+
await fsp3.unlink(this.file);
|
|
6481
6657
|
} catch (err) {
|
|
6482
6658
|
const code = err.code;
|
|
6483
6659
|
if (code === "ENOENT") return;
|
|
@@ -6487,7 +6663,7 @@ var RecoveryLock = class {
|
|
|
6487
6663
|
async readLock() {
|
|
6488
6664
|
let raw;
|
|
6489
6665
|
try {
|
|
6490
|
-
raw = await
|
|
6666
|
+
raw = await fsp3.readFile(this.file, "utf8");
|
|
6491
6667
|
} catch (err) {
|
|
6492
6668
|
const code = err.code;
|
|
6493
6669
|
if (code === "ENOENT") return null;
|
|
@@ -6518,26 +6694,82 @@ function defaultIsPidAlive(pid) {
|
|
|
6518
6694
|
return false;
|
|
6519
6695
|
}
|
|
6520
6696
|
}
|
|
6521
|
-
|
|
6522
|
-
// src/storage/session-reader.ts
|
|
6523
|
-
var DefaultSessionReader = class {
|
|
6697
|
+
var DefaultSessionReader = class _DefaultSessionReader {
|
|
6524
6698
|
store;
|
|
6699
|
+
eventCache = /* @__PURE__ */ new Map();
|
|
6700
|
+
eventCacheMtimes = /* @__PURE__ */ new Map();
|
|
6701
|
+
static EVENT_CACHE_MAX_ENTRIES = 32;
|
|
6525
6702
|
constructor(opts) {
|
|
6526
6703
|
this.store = opts.store;
|
|
6527
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
|
+
}
|
|
6528
6746
|
async query(q = {}) {
|
|
6529
|
-
const
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
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) => ({
|
|
6541
6773
|
id: s.id,
|
|
6542
6774
|
title: s.title,
|
|
6543
6775
|
startedAt: s.startedAt,
|
|
@@ -6548,7 +6780,7 @@ var DefaultSessionReader = class {
|
|
|
6548
6780
|
return q.limit ? out.slice(0, q.limit) : out;
|
|
6549
6781
|
}
|
|
6550
6782
|
async *replay(sessionId) {
|
|
6551
|
-
const data = await this.
|
|
6783
|
+
const data = await this.loadCachedSessionData(sessionId);
|
|
6552
6784
|
for (const e of data.events) yield e;
|
|
6553
6785
|
}
|
|
6554
6786
|
async search(q, sessionId, sessionQuery) {
|
|
@@ -6559,18 +6791,32 @@ var DefaultSessionReader = class {
|
|
|
6559
6791
|
if (sessionId) {
|
|
6560
6792
|
ids = [sessionId];
|
|
6561
6793
|
} else {
|
|
6562
|
-
const
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
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);
|
|
6574
6820
|
}
|
|
6575
6821
|
const hits = [];
|
|
6576
6822
|
const streaming = this.store.searchEvents?.bind(this.store);
|
|
@@ -6604,7 +6850,7 @@ var DefaultSessionReader = class {
|
|
|
6604
6850
|
for (const id of ids) {
|
|
6605
6851
|
let data;
|
|
6606
6852
|
try {
|
|
6607
|
-
data = await this.
|
|
6853
|
+
data = await this.loadCachedSessionData(id);
|
|
6608
6854
|
} catch {
|
|
6609
6855
|
continue;
|
|
6610
6856
|
}
|
|
@@ -6628,7 +6874,7 @@ var DefaultSessionReader = class {
|
|
|
6628
6874
|
return hits;
|
|
6629
6875
|
}
|
|
6630
6876
|
async export(sessionId, opts) {
|
|
6631
|
-
const data = await this.
|
|
6877
|
+
const data = await this.loadCachedSessionData(sessionId);
|
|
6632
6878
|
const includeTools = opts.includeTools ?? true;
|
|
6633
6879
|
const includeDiagnostics = opts.includeDiagnostics ?? true;
|
|
6634
6880
|
const filtered = data.events.filter((e) => {
|
|
@@ -6649,7 +6895,7 @@ var DefaultSessionReader = class {
|
|
|
6649
6895
|
return renderMarkdown(data.metadata, filtered);
|
|
6650
6896
|
}
|
|
6651
6897
|
async metadata(sessionId) {
|
|
6652
|
-
const data = await this.
|
|
6898
|
+
const data = await this.loadCachedSessionData(sessionId);
|
|
6653
6899
|
return data.metadata;
|
|
6654
6900
|
}
|
|
6655
6901
|
};
|
|
@@ -7043,7 +7289,7 @@ async function loadTodosCheckpoint(filePath, events, traceId) {
|
|
|
7043
7289
|
const t0 = Date.now();
|
|
7044
7290
|
let raw;
|
|
7045
7291
|
try {
|
|
7046
|
-
raw = await
|
|
7292
|
+
raw = await fsp3.readFile(filePath, "utf8");
|
|
7047
7293
|
} catch (err) {
|
|
7048
7294
|
events?.emit("storage.error", {
|
|
7049
7295
|
sessionId: traceId ?? "~boot~",
|
|
@@ -7185,7 +7431,7 @@ async function loadPlan(filePath, events) {
|
|
|
7185
7431
|
const t0 = Date.now();
|
|
7186
7432
|
let raw;
|
|
7187
7433
|
try {
|
|
7188
|
-
raw = await
|
|
7434
|
+
raw = await fsp3.readFile(filePath, "utf8");
|
|
7189
7435
|
} catch (err) {
|
|
7190
7436
|
events?.emit("storage.error", {
|
|
7191
7437
|
sessionId: "~boot~",
|
|
@@ -7479,7 +7725,7 @@ init_atomic_write();
|
|
|
7479
7725
|
async function loadDirectorState(filePath) {
|
|
7480
7726
|
let raw;
|
|
7481
7727
|
try {
|
|
7482
|
-
raw = await
|
|
7728
|
+
raw = await fsp3.readFile(filePath, "utf8");
|
|
7483
7729
|
} catch {
|
|
7484
7730
|
return null;
|
|
7485
7731
|
}
|
|
@@ -7494,7 +7740,7 @@ async function loadDirectorState(filePath) {
|
|
|
7494
7740
|
async function acquireDirectorStateLock(lockPath, processId = process.pid) {
|
|
7495
7741
|
let existing;
|
|
7496
7742
|
try {
|
|
7497
|
-
existing = await
|
|
7743
|
+
existing = await fsp3.readFile(lockPath, "utf8");
|
|
7498
7744
|
} catch {
|
|
7499
7745
|
}
|
|
7500
7746
|
if (existing) {
|
|
@@ -7518,7 +7764,7 @@ async function acquireDirectorStateLock(lockPath, processId = process.pid) {
|
|
|
7518
7764
|
}
|
|
7519
7765
|
async function releaseDirectorStateLock(lockPath) {
|
|
7520
7766
|
try {
|
|
7521
|
-
await
|
|
7767
|
+
await fsp3.unlink(lockPath);
|
|
7522
7768
|
} catch {
|
|
7523
7769
|
}
|
|
7524
7770
|
}
|
|
@@ -8063,7 +8309,7 @@ var DefaultPermissionPolicy = class {
|
|
|
8063
8309
|
}
|
|
8064
8310
|
async reload() {
|
|
8065
8311
|
try {
|
|
8066
|
-
const raw = await
|
|
8312
|
+
const raw = await fsp3.readFile(this.trustFile, "utf8");
|
|
8067
8313
|
const parsed = safeParse(raw);
|
|
8068
8314
|
if (parsed.ok && parsed.value) this.policy = parsed.value;
|
|
8069
8315
|
} catch {
|
|
@@ -8549,12 +8795,12 @@ var DefaultSkillLoader = class {
|
|
|
8549
8795
|
const seen = /* @__PURE__ */ new Set();
|
|
8550
8796
|
for (const { dir, source } of this.dirs) {
|
|
8551
8797
|
try {
|
|
8552
|
-
const entries = await
|
|
8798
|
+
const entries = await fsp3.readdir(dir, { withFileTypes: true });
|
|
8553
8799
|
for (const e of entries) {
|
|
8554
8800
|
if (!e.isDirectory()) continue;
|
|
8555
8801
|
const skillFile = path4.join(dir, e.name, "SKILL.md");
|
|
8556
8802
|
try {
|
|
8557
|
-
const raw = await
|
|
8803
|
+
const raw = await fsp3.readFile(skillFile, "utf8");
|
|
8558
8804
|
const meta = parseFrontmatter(raw);
|
|
8559
8805
|
if (!meta.name || !meta.description) continue;
|
|
8560
8806
|
if (seen.has(meta.name)) continue;
|
|
@@ -8613,7 +8859,7 @@ var DefaultSkillLoader = class {
|
|
|
8613
8859
|
if (cached !== void 0) return cached;
|
|
8614
8860
|
const m = await this.find(name);
|
|
8615
8861
|
if (!m) throw new Error(`Skill "${name}" not found`);
|
|
8616
|
-
const body = await
|
|
8862
|
+
const body = await fsp3.readFile(m.path, "utf8");
|
|
8617
8863
|
this.bodyCache.set(key, body);
|
|
8618
8864
|
return body;
|
|
8619
8865
|
}
|
|
@@ -8626,9 +8872,9 @@ var DefaultSkillLoader = class {
|
|
|
8626
8872
|
const savePath = path4.join(path4.dirname(m.path), "SKILL.save.md");
|
|
8627
8873
|
let result;
|
|
8628
8874
|
try {
|
|
8629
|
-
result = await
|
|
8875
|
+
result = await fsp3.readFile(savePath, "utf8");
|
|
8630
8876
|
} catch {
|
|
8631
|
-
const full = await
|
|
8877
|
+
const full = await fsp3.readFile(m.path, "utf8");
|
|
8632
8878
|
const body = stripFrontmatter(full);
|
|
8633
8879
|
const compact = compactSkillBody(body);
|
|
8634
8880
|
if (compact) {
|
|
@@ -10721,7 +10967,7 @@ ${errorDetails}`,
|
|
|
10721
10967
|
"tool.name": tool.name,
|
|
10722
10968
|
"tool.mutating": tool.mutating,
|
|
10723
10969
|
"tool.permission": tool.permission,
|
|
10724
|
-
"tool.capabilities": JSON.stringify(tool.capabilities ?? []),
|
|
10970
|
+
"tool.capabilities": toolCapsForAudit.length > 0 ? JSON.stringify(tool.capabilities ?? []) : "[]",
|
|
10725
10971
|
"tool.has_dangerous_capabilities": toolCapsForAudit.length > 0
|
|
10726
10972
|
});
|
|
10727
10973
|
try {
|
|
@@ -11077,11 +11323,11 @@ async function maybePersistLargeToolOutput(toolName, content, budget) {
|
|
|
11077
11323
|
}
|
|
11078
11324
|
try {
|
|
11079
11325
|
const dir = path4.join(wstackGlobalRoot(), "tool-output");
|
|
11080
|
-
await
|
|
11326
|
+
await fsp3.mkdir(dir, { recursive: true });
|
|
11081
11327
|
const safeTool = toolName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 40) || "tool";
|
|
11082
11328
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
11083
11329
|
const filePath = path4.join(dir, `${stamp}-${safeTool}-${randomUUID()}.log`);
|
|
11084
|
-
await
|
|
11330
|
+
await fsp3.writeFile(filePath, content, "utf8");
|
|
11085
11331
|
return content + `
|
|
11086
11332
|
[full tool output: ${bytes} bytes at ${filePath}; read/grep that file selectively instead of re-running or requesting more output]`;
|
|
11087
11333
|
} catch {
|
|
@@ -11276,7 +11522,7 @@ async function loadGoal(filePath, events) {
|
|
|
11276
11522
|
const t0 = Date.now();
|
|
11277
11523
|
let raw;
|
|
11278
11524
|
try {
|
|
11279
|
-
raw = await
|
|
11525
|
+
raw = await fsp3.readFile(filePath, "utf8");
|
|
11280
11526
|
} catch (err) {
|
|
11281
11527
|
const code = err.code;
|
|
11282
11528
|
if (code === "ENOENT") {
|
|
@@ -12026,8 +12272,8 @@ ${recentJournal}` : "No prior iterations.",
|
|
|
12026
12272
|
await saveGoal(this.goalPath, abandoned, this.opts.events);
|
|
12027
12273
|
}
|
|
12028
12274
|
try {
|
|
12029
|
-
const { unlink:
|
|
12030
|
-
await
|
|
12275
|
+
const { unlink: unlink14 } = await import('fs/promises');
|
|
12276
|
+
await unlink14(this.goalPath);
|
|
12031
12277
|
} catch {
|
|
12032
12278
|
}
|
|
12033
12279
|
this.opts.onEternalStop?.();
|
|
@@ -17689,9 +17935,9 @@ var CollabSession = class extends EventEmitter {
|
|
|
17689
17935
|
}
|
|
17690
17936
|
for (const filePath of allFiles) {
|
|
17691
17937
|
try {
|
|
17692
|
-
const [content,
|
|
17693
|
-
|
|
17694
|
-
|
|
17938
|
+
const [content, stat8] = await Promise.all([
|
|
17939
|
+
fsp3.readFile(filePath, "utf8"),
|
|
17940
|
+
fsp3.stat(filePath)
|
|
17695
17941
|
]);
|
|
17696
17942
|
const ext = filePath.split(".").pop() ?? "";
|
|
17697
17943
|
const language = ext === "ts" || ext === "tsx" ? "typescript" : ext === "js" || ext === "jsx" ? "javascript" : ext === "md" ? "markdown" : ext === "json" ? "json" : void 0;
|
|
@@ -17699,8 +17945,8 @@ var CollabSession = class extends EventEmitter {
|
|
|
17699
17945
|
path: filePath,
|
|
17700
17946
|
content,
|
|
17701
17947
|
language,
|
|
17702
|
-
snapshotMtimeMs:
|
|
17703
|
-
snapshotSizeBytes:
|
|
17948
|
+
snapshotMtimeMs: stat8.mtimeMs,
|
|
17949
|
+
snapshotSizeBytes: stat8.size
|
|
17704
17950
|
});
|
|
17705
17951
|
} catch {
|
|
17706
17952
|
this.snapshot.files.push({ path: filePath, content: "", language: void 0 });
|
|
@@ -18113,9 +18359,9 @@ Emit each evaluation immediately. Do not wait until you have read all reports.`;
|
|
|
18113
18359
|
for (const file of this.snapshot.files) {
|
|
18114
18360
|
if (file.snapshotMtimeMs === void 0 && file.snapshotSizeBytes === void 0) continue;
|
|
18115
18361
|
try {
|
|
18116
|
-
const
|
|
18117
|
-
const mtimeChanged = file.snapshotMtimeMs !== void 0 &&
|
|
18118
|
-
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;
|
|
18119
18365
|
if (mtimeChanged || sizeChanged) {
|
|
18120
18366
|
warnings.push(`${file.path} changed after the collab snapshot was captured.`);
|
|
18121
18367
|
}
|
|
@@ -19317,7 +19563,7 @@ var Director = class _Director {
|
|
|
19317
19563
|
this.fleetManager = opts.fleetManager;
|
|
19318
19564
|
this.logger = opts.logger;
|
|
19319
19565
|
if (this.sharedScratchpadPath) {
|
|
19320
|
-
void
|
|
19566
|
+
void fsp3.mkdir(this.sharedScratchpadPath, { recursive: true }).catch((err) => this.logShutdownError("shared_scratchpad_mkdir", err));
|
|
19321
19567
|
}
|
|
19322
19568
|
this.transport = new InMemoryBridgeTransport();
|
|
19323
19569
|
this.bridge = new InMemoryAgentBridge(
|
|
@@ -19917,7 +20163,7 @@ var Director = class _Director {
|
|
|
19917
20163
|
})),
|
|
19918
20164
|
usage: this.usage.snapshot()
|
|
19919
20165
|
};
|
|
19920
|
-
await
|
|
20166
|
+
await fsp3.mkdir(path4.dirname(this.manifestPath), { recursive: true });
|
|
19921
20167
|
await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
|
|
19922
20168
|
return this.manifestPath;
|
|
19923
20169
|
}
|
|
@@ -20151,7 +20397,7 @@ var Director = class _Director {
|
|
|
20151
20397
|
const filePath = path4.join(this.sessionsRoot, this.directorRunId, `${subagentId}.jsonl`);
|
|
20152
20398
|
let raw;
|
|
20153
20399
|
try {
|
|
20154
|
-
raw = await
|
|
20400
|
+
raw = await fsp3.readFile(filePath, "utf8");
|
|
20155
20401
|
} catch {
|
|
20156
20402
|
return null;
|
|
20157
20403
|
}
|
|
@@ -20681,7 +20927,7 @@ async function readSubagentPartial(opts, subagentId) {
|
|
|
20681
20927
|
candidates.push(path4.join(opts.sessionsRoot, opts.directorRunId, `${subagentId}.jsonl`));
|
|
20682
20928
|
} else {
|
|
20683
20929
|
try {
|
|
20684
|
-
const entries = await
|
|
20930
|
+
const entries = await fsp3.readdir(opts.sessionsRoot, { withFileTypes: true });
|
|
20685
20931
|
for (const entry of entries) {
|
|
20686
20932
|
if (entry.isDirectory()) {
|
|
20687
20933
|
candidates.push(path4.join(opts.sessionsRoot, entry.name, `${subagentId}.jsonl`));
|
|
@@ -20694,7 +20940,7 @@ async function readSubagentPartial(opts, subagentId) {
|
|
|
20694
20940
|
for (const file of candidates) {
|
|
20695
20941
|
let raw;
|
|
20696
20942
|
try {
|
|
20697
|
-
raw = await
|
|
20943
|
+
raw = await fsp3.readFile(file, "utf8");
|
|
20698
20944
|
} catch {
|
|
20699
20945
|
continue;
|
|
20700
20946
|
}
|
|
@@ -21024,7 +21270,7 @@ var DefaultModelsRegistry = class {
|
|
|
21024
21270
|
async readOverlayFile() {
|
|
21025
21271
|
if (!this.overlayFile) return void 0;
|
|
21026
21272
|
try {
|
|
21027
|
-
const raw = await
|
|
21273
|
+
const raw = await fsp3.readFile(this.overlayFile, "utf8");
|
|
21028
21274
|
return JSON.parse(raw);
|
|
21029
21275
|
} catch {
|
|
21030
21276
|
return void 0;
|
|
@@ -21103,7 +21349,7 @@ var DefaultModelsRegistry = class {
|
|
|
21103
21349
|
}
|
|
21104
21350
|
async readCacheAt(file) {
|
|
21105
21351
|
try {
|
|
21106
|
-
const raw = await
|
|
21352
|
+
const raw = await fsp3.readFile(file, "utf8");
|
|
21107
21353
|
return JSON.parse(raw);
|
|
21108
21354
|
} catch {
|
|
21109
21355
|
return void 0;
|
|
@@ -21489,7 +21735,7 @@ var DefaultModeStore = class {
|
|
|
21489
21735
|
async loadActiveMode() {
|
|
21490
21736
|
try {
|
|
21491
21737
|
const configPath = path4.join(this.configDir, "mode.json");
|
|
21492
|
-
const content = await
|
|
21738
|
+
const content = await fsp3.readFile(configPath, "utf8");
|
|
21493
21739
|
const data = JSON.parse(content);
|
|
21494
21740
|
this.activeModeId = data.activeMode ?? null;
|
|
21495
21741
|
} catch {
|
|
@@ -21498,7 +21744,7 @@ var DefaultModeStore = class {
|
|
|
21498
21744
|
}
|
|
21499
21745
|
async saveActiveMode() {
|
|
21500
21746
|
try {
|
|
21501
|
-
await
|
|
21747
|
+
await fsp3.mkdir(this.configDir, { recursive: true });
|
|
21502
21748
|
const configPath = path4.join(this.configDir, "mode.json");
|
|
21503
21749
|
await atomicWrite(
|
|
21504
21750
|
configPath,
|
|
@@ -21511,13 +21757,13 @@ var DefaultModeStore = class {
|
|
|
21511
21757
|
async function loadProjectModes(modesDir) {
|
|
21512
21758
|
const modes = [];
|
|
21513
21759
|
try {
|
|
21514
|
-
const entries = await
|
|
21760
|
+
const entries = await fsp3.readdir(modesDir);
|
|
21515
21761
|
for (const entry of entries) {
|
|
21516
21762
|
if (!entry.endsWith(".md") && !entry.endsWith(".txt")) continue;
|
|
21517
21763
|
const filePath = path4.join(modesDir, entry);
|
|
21518
|
-
const
|
|
21519
|
-
if (!
|
|
21520
|
-
const content = await
|
|
21764
|
+
const stat8 = await fsp3.stat(filePath);
|
|
21765
|
+
if (!stat8.isFile()) continue;
|
|
21766
|
+
const content = await fsp3.readFile(filePath, "utf8");
|
|
21521
21767
|
const id = path4.basename(entry, path4.extname(entry));
|
|
21522
21768
|
modes.push({
|
|
21523
21769
|
id,
|
|
@@ -21535,7 +21781,7 @@ async function loadUserModes(modesDir) {
|
|
|
21535
21781
|
const modes = [];
|
|
21536
21782
|
try {
|
|
21537
21783
|
const manifestPath = path4.join(modesDir, "modes.json");
|
|
21538
|
-
const content = await
|
|
21784
|
+
const content = await fsp3.readFile(manifestPath, "utf8");
|
|
21539
21785
|
const manifest = JSON.parse(content);
|
|
21540
21786
|
for (const mode of manifest.modes) {
|
|
21541
21787
|
modes.push(mode);
|
|
@@ -21545,11 +21791,41 @@ async function loadUserModes(modesDir) {
|
|
|
21545
21791
|
return modes;
|
|
21546
21792
|
}
|
|
21547
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
|
+
|
|
21548
21823
|
// src/models/provider-model-resolve.ts
|
|
21549
21824
|
function describeCatalogModel(m) {
|
|
21550
21825
|
return {
|
|
21551
21826
|
id: m.id,
|
|
21552
21827
|
name: m.name,
|
|
21828
|
+
...m.description !== void 0 ? { description: m.description } : {},
|
|
21553
21829
|
releaseDate: m.release_date,
|
|
21554
21830
|
contextWindow: m.limit?.context,
|
|
21555
21831
|
inputCost: m.cost?.input,
|
|
@@ -21566,8 +21842,16 @@ function resolveProviderModelList(savedModels, catalog) {
|
|
|
21566
21842
|
if (savedModels && savedModels.length > 0) {
|
|
21567
21843
|
const byId = new Map((catalog?.models ?? []).map((m) => [m.id, m]));
|
|
21568
21844
|
return savedModels.map((id) => {
|
|
21845
|
+
const codex = codexModelMeta(id);
|
|
21569
21846
|
const hit = byId.get(id);
|
|
21570
|
-
|
|
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: [] };
|
|
21571
21855
|
});
|
|
21572
21856
|
}
|
|
21573
21857
|
if (catalog) return catalog.models.map(describeCatalogModel);
|
|
@@ -22638,7 +22922,7 @@ var SpecStore = class {
|
|
|
22638
22922
|
}
|
|
22639
22923
|
async load(id) {
|
|
22640
22924
|
try {
|
|
22641
|
-
const raw = await
|
|
22925
|
+
const raw = await fsp3.readFile(this.filePath(id), "utf8");
|
|
22642
22926
|
return JSON.parse(raw);
|
|
22643
22927
|
} catch {
|
|
22644
22928
|
return null;
|
|
@@ -22650,7 +22934,7 @@ var SpecStore = class {
|
|
|
22650
22934
|
}
|
|
22651
22935
|
async delete(id) {
|
|
22652
22936
|
try {
|
|
22653
|
-
await
|
|
22937
|
+
await fsp3.unlink(this.filePath(id));
|
|
22654
22938
|
await this.removeFromIndex(id);
|
|
22655
22939
|
return true;
|
|
22656
22940
|
} catch {
|
|
@@ -22659,7 +22943,7 @@ var SpecStore = class {
|
|
|
22659
22943
|
}
|
|
22660
22944
|
async exists(id) {
|
|
22661
22945
|
try {
|
|
22662
|
-
await
|
|
22946
|
+
await fsp3.access(this.filePath(id));
|
|
22663
22947
|
return true;
|
|
22664
22948
|
} catch {
|
|
22665
22949
|
return false;
|
|
@@ -22701,7 +22985,7 @@ var SpecStore = class {
|
|
|
22701
22985
|
}
|
|
22702
22986
|
async readIndex() {
|
|
22703
22987
|
try {
|
|
22704
|
-
const raw = await
|
|
22988
|
+
const raw = await fsp3.readFile(this.indexPath, "utf8");
|
|
22705
22989
|
const parsed = JSON.parse(raw);
|
|
22706
22990
|
if (parsed?.version === 1) return parsed;
|
|
22707
22991
|
} catch {
|
|
@@ -22764,7 +23048,7 @@ var TaskGraphStore = class {
|
|
|
22764
23048
|
}
|
|
22765
23049
|
async load(id) {
|
|
22766
23050
|
try {
|
|
22767
|
-
const raw = await
|
|
23051
|
+
const raw = await fsp3.readFile(this.filePath(id), "utf8");
|
|
22768
23052
|
return graphFromJSON(raw);
|
|
22769
23053
|
} catch {
|
|
22770
23054
|
return null;
|
|
@@ -22776,7 +23060,7 @@ var TaskGraphStore = class {
|
|
|
22776
23060
|
}
|
|
22777
23061
|
async delete(id) {
|
|
22778
23062
|
try {
|
|
22779
|
-
await
|
|
23063
|
+
await fsp3.unlink(this.filePath(id));
|
|
22780
23064
|
await this.removeFromIndex(id);
|
|
22781
23065
|
return true;
|
|
22782
23066
|
} catch {
|
|
@@ -22785,7 +23069,7 @@ var TaskGraphStore = class {
|
|
|
22785
23069
|
}
|
|
22786
23070
|
async exists(id) {
|
|
22787
23071
|
try {
|
|
22788
|
-
await
|
|
23072
|
+
await fsp3.access(this.filePath(id));
|
|
22789
23073
|
return true;
|
|
22790
23074
|
} catch {
|
|
22791
23075
|
return false;
|
|
@@ -22796,7 +23080,7 @@ var TaskGraphStore = class {
|
|
|
22796
23080
|
}
|
|
22797
23081
|
async readIndex() {
|
|
22798
23082
|
try {
|
|
22799
|
-
const raw = await
|
|
23083
|
+
const raw = await fsp3.readFile(this.indexPath, "utf8");
|
|
22800
23084
|
const parsed = JSON.parse(raw);
|
|
22801
23085
|
if (parsed?.version === 1) return parsed;
|
|
22802
23086
|
} catch {
|
|
@@ -22868,6 +23152,9 @@ function buildQuestioningPrompt(session, min, max) {
|
|
|
22868
23152
|
"- Adapt based on previous answers",
|
|
22869
23153
|
"- Cover: scope, constraints, edge cases, integrations, security, performance as relevant",
|
|
22870
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.",
|
|
22871
23158
|
"",
|
|
22872
23159
|
`**Question budget:** ${budget}/${max} remaining`,
|
|
22873
23160
|
`**Minimum required:** ${remaining > 0 ? remaining : "met"}`
|
|
@@ -22935,6 +23222,9 @@ function buildImplementationPrompt(session) {
|
|
|
22935
23222
|
"",
|
|
22936
23223
|
"**Instructions for AI:**",
|
|
22937
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.",
|
|
22938
23228
|
"Include:",
|
|
22939
23229
|
"1. Architecture decisions",
|
|
22940
23230
|
"2. File structure changes",
|
|
@@ -23046,10 +23336,10 @@ var AISpecBuilder = class {
|
|
|
23046
23336
|
async saveSession() {
|
|
23047
23337
|
if (!this.sessionPath) return;
|
|
23048
23338
|
try {
|
|
23049
|
-
const
|
|
23050
|
-
const
|
|
23339
|
+
const fsp19 = await import('fs/promises');
|
|
23340
|
+
const path25 = await import('path');
|
|
23051
23341
|
const { atomicWrite: atomicWrite2 } = await Promise.resolve().then(() => (init_atomic_write(), atomic_write_exports));
|
|
23052
|
-
await
|
|
23342
|
+
await fsp19.mkdir(path25.dirname(this.sessionPath), { recursive: true });
|
|
23053
23343
|
await atomicWrite2(this.sessionPath, JSON.stringify(this.session, null, 2));
|
|
23054
23344
|
} catch {
|
|
23055
23345
|
}
|
|
@@ -23058,8 +23348,8 @@ var AISpecBuilder = class {
|
|
|
23058
23348
|
async loadSession() {
|
|
23059
23349
|
if (!this.sessionPath) return false;
|
|
23060
23350
|
try {
|
|
23061
|
-
const
|
|
23062
|
-
const raw = await
|
|
23351
|
+
const fsp19 = await import('fs/promises');
|
|
23352
|
+
const raw = await fsp19.readFile(this.sessionPath, "utf8");
|
|
23063
23353
|
const loaded = JSON.parse(raw);
|
|
23064
23354
|
if (loaded?.id && loaded?.phase && loaded?.title) {
|
|
23065
23355
|
this.session = loaded;
|
|
@@ -23073,8 +23363,8 @@ var AISpecBuilder = class {
|
|
|
23073
23363
|
async deleteSession() {
|
|
23074
23364
|
if (!this.sessionPath) return;
|
|
23075
23365
|
try {
|
|
23076
|
-
const
|
|
23077
|
-
await
|
|
23366
|
+
const fsp19 = await import('fs/promises');
|
|
23367
|
+
await fsp19.unlink(this.sessionPath);
|
|
23078
23368
|
} catch {
|
|
23079
23369
|
}
|
|
23080
23370
|
}
|
|
@@ -23776,15 +24066,15 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
|
|
|
23776
24066
|
maxId = id;
|
|
23777
24067
|
}
|
|
23778
24068
|
}
|
|
23779
|
-
const
|
|
24069
|
+
const path25 = [];
|
|
23780
24070
|
let current = maxId;
|
|
23781
24071
|
const visited = /* @__PURE__ */ new Set();
|
|
23782
24072
|
while (current && !visited.has(current)) {
|
|
23783
24073
|
visited.add(current);
|
|
23784
|
-
|
|
24074
|
+
path25.unshift(current);
|
|
23785
24075
|
current = prev.get(current) ?? null;
|
|
23786
24076
|
}
|
|
23787
|
-
return
|
|
24077
|
+
return path25;
|
|
23788
24078
|
}
|
|
23789
24079
|
function computeParallelGroups(graph, blockedByMap) {
|
|
23790
24080
|
const groups = [];
|
|
@@ -25551,7 +25841,7 @@ var SddBoardStore = class {
|
|
|
25551
25841
|
}
|
|
25552
25842
|
async load(runId) {
|
|
25553
25843
|
try {
|
|
25554
|
-
const raw = await
|
|
25844
|
+
const raw = await fsp3.readFile(this.snapshotPath(runId), "utf8");
|
|
25555
25845
|
return JSON.parse(raw);
|
|
25556
25846
|
} catch {
|
|
25557
25847
|
return null;
|
|
@@ -25569,7 +25859,7 @@ var SddBoardStore = class {
|
|
|
25569
25859
|
async appendEvent(runId, event) {
|
|
25570
25860
|
try {
|
|
25571
25861
|
await ensureDir(this.baseDir);
|
|
25572
|
-
await
|
|
25862
|
+
await fsp3.appendFile(this.eventsPath(runId), `${JSON.stringify(event)}
|
|
25573
25863
|
`, { mode: 384 });
|
|
25574
25864
|
} catch {
|
|
25575
25865
|
}
|
|
@@ -25577,7 +25867,7 @@ var SddBoardStore = class {
|
|
|
25577
25867
|
/** Append a control command (used by readers to steer a CLI-owned run). */
|
|
25578
25868
|
async appendControl(runId, command) {
|
|
25579
25869
|
await ensureDir(this.baseDir);
|
|
25580
|
-
await
|
|
25870
|
+
await fsp3.appendFile(this.controlPath(runId), `${JSON.stringify(command)}
|
|
25581
25871
|
`, { mode: 384 });
|
|
25582
25872
|
}
|
|
25583
25873
|
/** Read + truncate the control queue (the run drains it). Returns parsed commands. */
|
|
@@ -25585,12 +25875,12 @@ var SddBoardStore = class {
|
|
|
25585
25875
|
const p = this.controlPath(runId);
|
|
25586
25876
|
let raw;
|
|
25587
25877
|
try {
|
|
25588
|
-
raw = await
|
|
25878
|
+
raw = await fsp3.readFile(p, "utf8");
|
|
25589
25879
|
} catch {
|
|
25590
25880
|
return [];
|
|
25591
25881
|
}
|
|
25592
25882
|
try {
|
|
25593
|
-
await
|
|
25883
|
+
await fsp3.writeFile(p, "", { mode: 384 });
|
|
25594
25884
|
} catch {
|
|
25595
25885
|
}
|
|
25596
25886
|
return raw.split("\n").filter((l) => l.trim()).map((l) => {
|
|
@@ -25603,9 +25893,9 @@ var SddBoardStore = class {
|
|
|
25603
25893
|
}
|
|
25604
25894
|
async delete(runId) {
|
|
25605
25895
|
await Promise.allSettled([
|
|
25606
|
-
|
|
25607
|
-
|
|
25608
|
-
|
|
25896
|
+
fsp3.unlink(this.snapshotPath(runId)),
|
|
25897
|
+
fsp3.unlink(this.eventsPath(runId)),
|
|
25898
|
+
fsp3.unlink(this.controlPath(runId))
|
|
25609
25899
|
]);
|
|
25610
25900
|
await this.removeFromIndex(runId);
|
|
25611
25901
|
}
|
|
@@ -25615,7 +25905,7 @@ var SddBoardStore = class {
|
|
|
25615
25905
|
}
|
|
25616
25906
|
async readIndex() {
|
|
25617
25907
|
try {
|
|
25618
|
-
const raw = await
|
|
25908
|
+
const raw = await fsp3.readFile(this.indexPath, "utf8");
|
|
25619
25909
|
const parsed = JSON.parse(raw);
|
|
25620
25910
|
if (parsed?.version === 1) return parsed;
|
|
25621
25911
|
} catch {
|
|
@@ -26051,6 +26341,7 @@ var SddInterviewDriver = class {
|
|
|
26051
26341
|
sessionId: s.id,
|
|
26052
26342
|
phase: s.phase,
|
|
26053
26343
|
title: s.title,
|
|
26344
|
+
goal: s.userIntent || s.title,
|
|
26054
26345
|
questionCount: s.questionCount,
|
|
26055
26346
|
minQuestions: this.minQuestions,
|
|
26056
26347
|
maxQuestions: this.maxQuestions,
|
|
@@ -26745,14 +27036,14 @@ async function destroySddProject(opts) {
|
|
|
26745
27036
|
const deleted = [];
|
|
26746
27037
|
const rmDir = async (dir, label) => {
|
|
26747
27038
|
try {
|
|
26748
|
-
await
|
|
27039
|
+
await fsp3.rm(dir, { recursive: true, force: true });
|
|
26749
27040
|
deleted.push(label);
|
|
26750
27041
|
} catch {
|
|
26751
27042
|
}
|
|
26752
27043
|
};
|
|
26753
27044
|
const rmFile = async (file, label) => {
|
|
26754
27045
|
try {
|
|
26755
|
-
await
|
|
27046
|
+
await fsp3.unlink(file);
|
|
26756
27047
|
deleted.push(label);
|
|
26757
27048
|
} catch {
|
|
26758
27049
|
}
|
|
@@ -27108,7 +27399,7 @@ async function startMetricsServer(opts) {
|
|
|
27108
27399
|
const tls = opts.tls;
|
|
27109
27400
|
const useHttps = !!(tls?.cert && tls?.key);
|
|
27110
27401
|
const host = opts.host ?? "127.0.0.1";
|
|
27111
|
-
const
|
|
27402
|
+
const path25 = opts.path ?? "/metrics";
|
|
27112
27403
|
const healthPath = opts.healthPath ?? "/healthz";
|
|
27113
27404
|
const healthRegistry = opts.healthRegistry;
|
|
27114
27405
|
const listener = (req, res) => {
|
|
@@ -27118,7 +27409,7 @@ async function startMetricsServer(opts) {
|
|
|
27118
27409
|
return;
|
|
27119
27410
|
}
|
|
27120
27411
|
const url = req.url.split("?")[0];
|
|
27121
|
-
if (url ===
|
|
27412
|
+
if (url === path25) {
|
|
27122
27413
|
let body;
|
|
27123
27414
|
try {
|
|
27124
27415
|
body = renderPrometheus(opts.sink.snapshot());
|
|
@@ -27182,7 +27473,7 @@ async function startMetricsServer(opts) {
|
|
|
27182
27473
|
const protocol = useHttps ? "https" : "http";
|
|
27183
27474
|
return {
|
|
27184
27475
|
port: boundPort,
|
|
27185
|
-
url: `${protocol}://${host}:${boundPort}${
|
|
27476
|
+
url: `${protocol}://${host}:${boundPort}${path25}`,
|
|
27186
27477
|
close: () => new Promise((resolve8, reject) => {
|
|
27187
27478
|
server.close((err) => err ? reject(err) : resolve8());
|
|
27188
27479
|
})
|
|
@@ -27856,6 +28147,6 @@ var allServers = () => ({
|
|
|
27856
28147
|
ssh: { ...sshManagerServer(), enabled: false }
|
|
27857
28148
|
});
|
|
27858
28149
|
|
|
27859
|
-
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 };
|
|
27860
28151
|
//# sourceMappingURL=index.js.map
|
|
27861
28152
|
//# sourceMappingURL=index.js.map
|