@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID, createHash, randomBytes } from 'crypto';
|
|
2
|
-
import * as
|
|
3
|
-
import * as
|
|
2
|
+
import * as fsp7 from 'fs/promises';
|
|
3
|
+
import * as path6 from 'path';
|
|
4
4
|
import { isAbsolute, resolve } from 'path';
|
|
5
5
|
import * as os from 'os';
|
|
6
6
|
import { hostname } from 'os';
|
|
@@ -154,17 +154,17 @@ function formatHumanPrompt(request) {
|
|
|
154
154
|
return lines.join("\n");
|
|
155
155
|
}
|
|
156
156
|
async function atomicWrite(targetPath, content, opts = {}) {
|
|
157
|
-
const dir =
|
|
158
|
-
await
|
|
159
|
-
const tmp =
|
|
157
|
+
const dir = path6.dirname(targetPath);
|
|
158
|
+
await fsp7.mkdir(dir, { recursive: true });
|
|
159
|
+
const tmp = path6.join(dir, `.${path6.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
|
|
160
160
|
try {
|
|
161
161
|
if (typeof content === "string") {
|
|
162
|
-
await
|
|
162
|
+
await fsp7.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
|
|
163
163
|
} else {
|
|
164
|
-
await
|
|
164
|
+
await fsp7.writeFile(tmp, content, { flag: "wx" });
|
|
165
165
|
}
|
|
166
166
|
try {
|
|
167
|
-
const fh = await
|
|
167
|
+
const fh = await fsp7.open(tmp, "r+");
|
|
168
168
|
try {
|
|
169
169
|
await fh.sync();
|
|
170
170
|
} finally {
|
|
@@ -174,50 +174,50 @@ async function atomicWrite(targetPath, content, opts = {}) {
|
|
|
174
174
|
}
|
|
175
175
|
let mode;
|
|
176
176
|
try {
|
|
177
|
-
const stat7 = await
|
|
177
|
+
const stat7 = await fsp7.stat(targetPath);
|
|
178
178
|
mode = stat7.mode & 511;
|
|
179
179
|
} catch {
|
|
180
180
|
mode = opts.mode;
|
|
181
181
|
}
|
|
182
182
|
if (mode !== void 0) {
|
|
183
|
-
await
|
|
183
|
+
await fsp7.chmod(tmp, mode);
|
|
184
184
|
}
|
|
185
185
|
await renameWithRetry(tmp, targetPath);
|
|
186
186
|
} catch (err) {
|
|
187
187
|
try {
|
|
188
|
-
await
|
|
188
|
+
await fsp7.unlink(tmp);
|
|
189
189
|
} catch {
|
|
190
190
|
}
|
|
191
191
|
throw err;
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
async function ensureDir(dir) {
|
|
195
|
-
await
|
|
195
|
+
await fsp7.mkdir(dir, { recursive: true });
|
|
196
196
|
}
|
|
197
197
|
async function withFileLock(targetPath, fn, opts = {}) {
|
|
198
|
-
const dir =
|
|
199
|
-
await
|
|
200
|
-
const lockPath =
|
|
198
|
+
const dir = path6.dirname(targetPath);
|
|
199
|
+
await fsp7.mkdir(dir, { recursive: true });
|
|
200
|
+
const lockPath = path6.join(dir, `.${path6.basename(targetPath)}.lock`);
|
|
201
201
|
const timeoutMs = opts.timeoutMs ?? 5e3;
|
|
202
202
|
const staleMs = opts.staleMs ?? 3e4;
|
|
203
203
|
const started = Date.now();
|
|
204
204
|
let handle;
|
|
205
205
|
for (; ; ) {
|
|
206
206
|
try {
|
|
207
|
-
handle = await
|
|
207
|
+
handle = await fsp7.open(lockPath, "wx");
|
|
208
208
|
await handle.writeFile(`${process.pid}:${Date.now()}`);
|
|
209
209
|
break;
|
|
210
210
|
} catch (err) {
|
|
211
211
|
const code = err.code;
|
|
212
212
|
if (code === "ENOENT") {
|
|
213
|
-
await
|
|
213
|
+
await fsp7.mkdir(dir, { recursive: true });
|
|
214
214
|
continue;
|
|
215
215
|
}
|
|
216
216
|
if (code !== "EEXIST") throw err;
|
|
217
217
|
try {
|
|
218
|
-
const stat7 = await
|
|
218
|
+
const stat7 = await fsp7.stat(lockPath);
|
|
219
219
|
if (Date.now() - stat7.mtimeMs > staleMs) {
|
|
220
|
-
await
|
|
220
|
+
await fsp7.unlink(lockPath);
|
|
221
221
|
continue;
|
|
222
222
|
}
|
|
223
223
|
} catch {
|
|
@@ -237,7 +237,7 @@ async function withFileLock(targetPath, fn, opts = {}) {
|
|
|
237
237
|
} catch {
|
|
238
238
|
}
|
|
239
239
|
try {
|
|
240
|
-
await
|
|
240
|
+
await fsp7.unlink(lockPath);
|
|
241
241
|
} catch {
|
|
242
242
|
}
|
|
243
243
|
}
|
|
@@ -245,14 +245,14 @@ async function withFileLock(targetPath, fn, opts = {}) {
|
|
|
245
245
|
var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
|
|
246
246
|
async function renameWithRetry(from, to) {
|
|
247
247
|
if (process.platform !== "win32") {
|
|
248
|
-
await
|
|
248
|
+
await fsp7.rename(from, to);
|
|
249
249
|
return;
|
|
250
250
|
}
|
|
251
251
|
const delays = [10, 25, 60, 120, 250];
|
|
252
252
|
let lastErr;
|
|
253
253
|
for (let i = 0; i <= delays.length; i++) {
|
|
254
254
|
try {
|
|
255
|
-
await
|
|
255
|
+
await fsp7.rename(from, to);
|
|
256
256
|
return;
|
|
257
257
|
} catch (err) {
|
|
258
258
|
lastErr = err;
|
|
@@ -275,7 +275,7 @@ function toErrorMessage(err) {
|
|
|
275
275
|
async function acquireDirectorStateLock(lockPath, processId = process.pid) {
|
|
276
276
|
let existing;
|
|
277
277
|
try {
|
|
278
|
-
existing = await
|
|
278
|
+
existing = await fsp7.readFile(lockPath, "utf8");
|
|
279
279
|
} catch {
|
|
280
280
|
}
|
|
281
281
|
if (existing) {
|
|
@@ -299,7 +299,7 @@ async function acquireDirectorStateLock(lockPath, processId = process.pid) {
|
|
|
299
299
|
}
|
|
300
300
|
async function releaseDirectorStateLock(lockPath) {
|
|
301
301
|
try {
|
|
302
|
-
await
|
|
302
|
+
await fsp7.unlink(lockPath);
|
|
303
303
|
} catch {
|
|
304
304
|
}
|
|
305
305
|
}
|
|
@@ -525,7 +525,7 @@ async function expandGlob(pattern) {
|
|
|
525
525
|
async function walk(dir, pat) {
|
|
526
526
|
let entries;
|
|
527
527
|
try {
|
|
528
|
-
entries = await
|
|
528
|
+
entries = await fsp7.readdir(dir);
|
|
529
529
|
} catch {
|
|
530
530
|
return;
|
|
531
531
|
}
|
|
@@ -547,7 +547,7 @@ async function expandGlob(pattern) {
|
|
|
547
547
|
for (const e of entries) {
|
|
548
548
|
const full = `${dir}${SEP}${e}`;
|
|
549
549
|
try {
|
|
550
|
-
const stat7 = await
|
|
550
|
+
const stat7 = await fsp7.stat(full);
|
|
551
551
|
if (stat7.isDirectory()) await walk(full, rest);
|
|
552
552
|
} catch {
|
|
553
553
|
}
|
|
@@ -565,7 +565,7 @@ async function expandGlob(pattern) {
|
|
|
565
565
|
if (entries.includes(seg)) {
|
|
566
566
|
const full = `${dir}${SEP}${seg}`;
|
|
567
567
|
try {
|
|
568
|
-
const stat7 = await
|
|
568
|
+
const stat7 = await fsp7.stat(full);
|
|
569
569
|
if (stat7.isDirectory()) await walk(full, rest);
|
|
570
570
|
} catch {
|
|
571
571
|
}
|
|
@@ -673,8 +673,8 @@ function truncate(s, max) {
|
|
|
673
673
|
return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
|
|
674
674
|
}
|
|
675
675
|
function projectSlug(absRoot) {
|
|
676
|
-
const base = slugify(
|
|
677
|
-
const hash = createHash("sha256").update(
|
|
676
|
+
const base = slugify(path6.basename(absRoot));
|
|
677
|
+
const hash = createHash("sha256").update(path6.resolve(absRoot)).digest("hex").slice(0, 6);
|
|
678
678
|
return `${base}-${hash}`;
|
|
679
679
|
}
|
|
680
680
|
function slugify(name) {
|
|
@@ -682,8 +682,8 @@ function slugify(name) {
|
|
|
682
682
|
}
|
|
683
683
|
function wstackGlobalRoot() {
|
|
684
684
|
const fromEnv = process.env["WRONGSTACK_HOME"];
|
|
685
|
-
if (fromEnv && fromEnv.trim().length > 0) return
|
|
686
|
-
return
|
|
685
|
+
if (fromEnv && fromEnv.trim().length > 0) return path6.resolve(fromEnv);
|
|
686
|
+
return path6.join(os.homedir(), ".wrongstack");
|
|
687
687
|
}
|
|
688
688
|
|
|
689
689
|
// src/types/errors.ts
|
|
@@ -999,8 +999,8 @@ var CollabSession = class extends EventEmitter {
|
|
|
999
999
|
for (const filePath of allFiles) {
|
|
1000
1000
|
try {
|
|
1001
1001
|
const [content, stat7] = await Promise.all([
|
|
1002
|
-
|
|
1003
|
-
|
|
1002
|
+
fsp7.readFile(filePath, "utf8"),
|
|
1003
|
+
fsp7.stat(filePath)
|
|
1004
1004
|
]);
|
|
1005
1005
|
const ext = filePath.split(".").pop() ?? "";
|
|
1006
1006
|
const language = ext === "ts" || ext === "tsx" ? "typescript" : ext === "js" || ext === "jsx" ? "javascript" : ext === "md" ? "markdown" : ext === "json" ? "json" : void 0;
|
|
@@ -1422,7 +1422,7 @@ Emit each evaluation immediately. Do not wait until you have read all reports.`;
|
|
|
1422
1422
|
for (const file of this.snapshot.files) {
|
|
1423
1423
|
if (file.snapshotMtimeMs === void 0 && file.snapshotSizeBytes === void 0) continue;
|
|
1424
1424
|
try {
|
|
1425
|
-
const stat7 = await
|
|
1425
|
+
const stat7 = await fsp7.stat(file.path);
|
|
1426
1426
|
const mtimeChanged = file.snapshotMtimeMs !== void 0 && stat7.mtimeMs > file.snapshotMtimeMs + 1;
|
|
1427
1427
|
const sizeChanged = file.snapshotSizeBytes !== void 0 && stat7.size !== file.snapshotSizeBytes;
|
|
1428
1428
|
if (mtimeChanged || sizeChanged) {
|
|
@@ -7235,7 +7235,7 @@ var Director = class _Director {
|
|
|
7235
7235
|
this.fleetManager = opts.fleetManager;
|
|
7236
7236
|
this.logger = opts.logger;
|
|
7237
7237
|
if (this.sharedScratchpadPath) {
|
|
7238
|
-
void
|
|
7238
|
+
void fsp7.mkdir(this.sharedScratchpadPath, { recursive: true }).catch((err) => this.logShutdownError("shared_scratchpad_mkdir", err));
|
|
7239
7239
|
}
|
|
7240
7240
|
this.transport = new InMemoryBridgeTransport();
|
|
7241
7241
|
this.bridge = new InMemoryAgentBridge(
|
|
@@ -7835,7 +7835,7 @@ var Director = class _Director {
|
|
|
7835
7835
|
})),
|
|
7836
7836
|
usage: this.usage.snapshot()
|
|
7837
7837
|
};
|
|
7838
|
-
await
|
|
7838
|
+
await fsp7.mkdir(path6.dirname(this.manifestPath), { recursive: true });
|
|
7839
7839
|
await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
|
|
7840
7840
|
return this.manifestPath;
|
|
7841
7841
|
}
|
|
@@ -8066,10 +8066,10 @@ var Director = class _Director {
|
|
|
8066
8066
|
*/
|
|
8067
8067
|
async readSession(subagentId, tail) {
|
|
8068
8068
|
if (!this.sessionsRoot) return null;
|
|
8069
|
-
const filePath =
|
|
8069
|
+
const filePath = path6.join(this.sessionsRoot, this.directorRunId, `${subagentId}.jsonl`);
|
|
8070
8070
|
let raw;
|
|
8071
8071
|
try {
|
|
8072
|
-
raw = await
|
|
8072
|
+
raw = await fsp7.readFile(filePath, "utf8");
|
|
8073
8073
|
} catch {
|
|
8074
8074
|
return null;
|
|
8075
8075
|
}
|
|
@@ -8596,13 +8596,13 @@ async function readSubagentPartial(opts, subagentId) {
|
|
|
8596
8596
|
if (!opts.sessionsRoot) return void 0;
|
|
8597
8597
|
const candidates = [];
|
|
8598
8598
|
if (opts.directorRunId) {
|
|
8599
|
-
candidates.push(
|
|
8599
|
+
candidates.push(path6.join(opts.sessionsRoot, opts.directorRunId, `${subagentId}.jsonl`));
|
|
8600
8600
|
} else {
|
|
8601
8601
|
try {
|
|
8602
|
-
const entries = await
|
|
8602
|
+
const entries = await fsp7.readdir(opts.sessionsRoot, { withFileTypes: true });
|
|
8603
8603
|
for (const entry of entries) {
|
|
8604
8604
|
if (entry.isDirectory()) {
|
|
8605
|
-
candidates.push(
|
|
8605
|
+
candidates.push(path6.join(opts.sessionsRoot, entry.name, `${subagentId}.jsonl`));
|
|
8606
8606
|
}
|
|
8607
8607
|
}
|
|
8608
8608
|
} catch {
|
|
@@ -8612,7 +8612,7 @@ async function readSubagentPartial(opts, subagentId) {
|
|
|
8612
8612
|
for (const file of candidates) {
|
|
8613
8613
|
let raw;
|
|
8614
8614
|
try {
|
|
8615
|
-
raw = await
|
|
8615
|
+
raw = await fsp7.readFile(file, "utf8");
|
|
8616
8616
|
} catch {
|
|
8617
8617
|
continue;
|
|
8618
8618
|
}
|
|
@@ -8858,1541 +8858,1674 @@ function makeAgentSubagentRunner(opts) {
|
|
|
8858
8858
|
function defaultFormatTaskInput(task) {
|
|
8859
8859
|
return task.description ?? "";
|
|
8860
8860
|
}
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
const time = startedAt.slice(11, 19).replace(/:/g, "-");
|
|
8867
|
-
const suffix = randomBytes(2).toString("hex");
|
|
8868
|
-
const modelPart = model ? `_${sanitizeModel(model)}` : "";
|
|
8869
|
-
return `${date}/${time}Z${modelPart}_${suffix}`;
|
|
8861
|
+
|
|
8862
|
+
// src/storage/session-helpers.ts
|
|
8863
|
+
function userInputTitle(content) {
|
|
8864
|
+
const text = typeof content === "string" ? content : content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
|
|
8865
|
+
return (text || "(non-text input)").slice(0, 60);
|
|
8870
8866
|
}
|
|
8871
8867
|
|
|
8872
|
-
// src/storage/session-
|
|
8873
|
-
var
|
|
8874
|
-
|
|
8868
|
+
// src/storage/file-session-writer.ts
|
|
8869
|
+
var FileSessionWriter = class _FileSessionWriter {
|
|
8870
|
+
constructor(id, handle, startedAt, meta, events, opts = {}, traceId) {
|
|
8871
|
+
this.id = id;
|
|
8872
|
+
this.handle = handle;
|
|
8873
|
+
this.startedAt = startedAt;
|
|
8874
|
+
this.meta = meta;
|
|
8875
|
+
this.events = events;
|
|
8876
|
+
this.resumed = opts.resumed ?? false;
|
|
8877
|
+
this.manifestFile = opts.dir ? path6.join(opts.dir, `${path6.basename(id)}.summary.json`) : "";
|
|
8878
|
+
this.filePath = opts.filePath ?? "";
|
|
8879
|
+
this.secretScrubber = opts.secretScrubber;
|
|
8880
|
+
this.onCloseCb = opts.onClose;
|
|
8881
|
+
this.summary = {
|
|
8882
|
+
id,
|
|
8883
|
+
title: "(empty session)",
|
|
8884
|
+
startedAt,
|
|
8885
|
+
model: meta.model ?? "unknown",
|
|
8886
|
+
provider: meta.provider ?? "unknown",
|
|
8887
|
+
tokenTotal: 0
|
|
8888
|
+
};
|
|
8889
|
+
this.traceId = traceId;
|
|
8890
|
+
}
|
|
8891
|
+
id;
|
|
8892
|
+
handle;
|
|
8893
|
+
startedAt;
|
|
8894
|
+
meta;
|
|
8875
8895
|
events;
|
|
8876
|
-
|
|
8896
|
+
closed = false;
|
|
8897
|
+
closePromise = null;
|
|
8898
|
+
manifestFile;
|
|
8899
|
+
summary;
|
|
8900
|
+
tokenIn = 0;
|
|
8901
|
+
tokenOut = 0;
|
|
8902
|
+
filePath;
|
|
8903
|
+
get transcriptPath() {
|
|
8904
|
+
return this.filePath || void 0;
|
|
8905
|
+
}
|
|
8877
8906
|
/**
|
|
8878
|
-
*
|
|
8879
|
-
*
|
|
8880
|
-
*
|
|
8881
|
-
*
|
|
8882
|
-
* store's lifetime (e.g., webui session detail views, list() fallbacks).
|
|
8883
|
-
*
|
|
8884
|
-
* Max size is capped to prevent unbounded memory growth in long-running
|
|
8885
|
-
* processes. When the limit is reached, the oldest entry is evicted.
|
|
8907
|
+
* Lazy session_start/session_resumed init, shared by all appenders.
|
|
8908
|
+
* A single promise (not a boolean) so a second append racing the first
|
|
8909
|
+
* can't push its event into the buffer BEFORE the first append's event —
|
|
8910
|
+
* every appender awaits the same init and resumes in FIFO call order.
|
|
8886
8911
|
*/
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8912
|
+
initPromise = null;
|
|
8913
|
+
ensureInit() {
|
|
8914
|
+
if (!this.initPromise) this.initPromise = this.writeSessionStartLazy();
|
|
8915
|
+
return this.initPromise;
|
|
8916
|
+
}
|
|
8917
|
+
resumed;
|
|
8918
|
+
appendFailCount = 0;
|
|
8919
|
+
lastAppendWarnAt = 0;
|
|
8920
|
+
secretScrubber;
|
|
8921
|
+
onCloseCb;
|
|
8922
|
+
/** Implements SessionWriter.traceId — propagated from ContextInit.traceId. */
|
|
8923
|
+
traceId;
|
|
8924
|
+
// ── Write buffer — batches events to reduce per-event disk I/O ──────────────
|
|
8925
|
+
//
|
|
8926
|
+
// Every append() pushes the scrubbed event into an in-memory buffer instead
|
|
8927
|
+
// of calling handle.appendFile() synchronously. The buffer flushes to disk
|
|
8928
|
+
// when it reaches FLUSH_SIZE events OR after FLUSH_INTERVAL_MS of inactivity.
|
|
8929
|
+
// This cuts the number of disk writes by ~95% without changing the on-disk
|
|
8930
|
+
// format — the JSONL is still one JSON object per line.
|
|
8931
|
+
writeBuffer = [];
|
|
8932
|
+
flushTimer = null;
|
|
8933
|
+
static FLUSH_INTERVAL_MS = 500;
|
|
8934
|
+
static FLUSH_SIZE = 50;
|
|
8935
|
+
// ── Write serialization ─────────────────────────────────────────────────────
|
|
8936
|
+
//
|
|
8937
|
+
// All disk writes are funneled through a FIFO promise chain. Without it,
|
|
8938
|
+
// a timer-driven flush racing an explicit flush()/close() issues two
|
|
8939
|
+
// concurrent appendFile() calls on the shared O_APPEND handle — the kernel
|
|
8940
|
+
// may complete them out of order (chronology breaks) or, for large
|
|
8941
|
+
// batches, interleave partial writes (torn JSONL lines). The chain keeps
|
|
8942
|
+
// exactly one write in flight; failures don't break the chain.
|
|
8943
|
+
writeChain = Promise.resolve();
|
|
8944
|
+
/** Enqueue a write on the FIFO chain. Resolves/rejects with that write. */
|
|
8945
|
+
enqueueWrite(data) {
|
|
8946
|
+
const write = this.writeChain.then(() => this.handle.appendFile(data, "utf8"));
|
|
8947
|
+
this.writeChain = write.then(
|
|
8948
|
+
() => void 0,
|
|
8949
|
+
() => void 0
|
|
8950
|
+
);
|
|
8951
|
+
return write;
|
|
8895
8952
|
}
|
|
8953
|
+
// ── Enriched summary tracking ───────────────────────────────────────────────
|
|
8954
|
+
iterationCount = 0;
|
|
8955
|
+
toolCallCount = 0;
|
|
8956
|
+
toolErrorCount = 0;
|
|
8957
|
+
toolBreakdown = {};
|
|
8958
|
+
fileChangeCount = 0;
|
|
8959
|
+
compactionCount = 0;
|
|
8960
|
+
outcome = void 0;
|
|
8896
8961
|
/**
|
|
8897
|
-
*
|
|
8898
|
-
* the
|
|
8962
|
+
* Scrub secrets out of conversation-turn events before they are observed
|
|
8963
|
+
* for the summary, written to the JSONL log, or surfaced on resume. Only
|
|
8964
|
+
* `user_input` / `llm_response` carry free-form user/model text; other event
|
|
8965
|
+
* types either have no secret-bearing content or are already scrubbed
|
|
8966
|
+
* upstream (tool results). Returns the event unchanged when no scrubber is
|
|
8967
|
+
* configured.
|
|
8899
8968
|
*/
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
|
|
8904
|
-
|
|
8969
|
+
scrubEvent(event) {
|
|
8970
|
+
const s = this.secretScrubber;
|
|
8971
|
+
if (!s) return event;
|
|
8972
|
+
if (event.type === "user_input") {
|
|
8973
|
+
return {
|
|
8974
|
+
...event,
|
|
8975
|
+
content: typeof event.content === "string" ? s.scrub(event.content) : s.scrubObject(event.content)
|
|
8976
|
+
};
|
|
8977
|
+
}
|
|
8978
|
+
if (event.type === "llm_response") {
|
|
8979
|
+
return { ...event, content: s.scrubObject(event.content) };
|
|
8905
8980
|
}
|
|
8981
|
+
return event;
|
|
8906
8982
|
}
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
filePath,
|
|
8913
|
-
operation,
|
|
8914
|
-
outcome,
|
|
8915
|
-
durationMs,
|
|
8916
|
-
...error !== void 0 ? { error } : {}
|
|
8917
|
-
});
|
|
8983
|
+
pendingFileSnapshots = [];
|
|
8984
|
+
/** Tracks open tool_use IDs during the current run to serialize on close for resume. */
|
|
8985
|
+
openToolUses = /* @__PURE__ */ new Set();
|
|
8986
|
+
recordFileChange(input) {
|
|
8987
|
+
this.pendingFileSnapshots.push(input);
|
|
8918
8988
|
}
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
sessionId,
|
|
8922
|
-
store: "session",
|
|
8923
|
-
filePath,
|
|
8924
|
-
operation,
|
|
8925
|
-
outcome,
|
|
8926
|
-
durationMs,
|
|
8927
|
-
...eventCount !== void 0 ? { eventCount } : {},
|
|
8928
|
-
...error !== void 0 ? { error } : {}
|
|
8929
|
-
});
|
|
8989
|
+
get pendingToolUses() {
|
|
8990
|
+
return Array.from(this.openToolUses);
|
|
8930
8991
|
}
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8992
|
+
async writeSessionStartLazy() {
|
|
8993
|
+
const record = `${JSON.stringify({
|
|
8994
|
+
type: this.resumed ? "session_resumed" : "session_start",
|
|
8995
|
+
ts: this.startedAt,
|
|
8996
|
+
id: this.id,
|
|
8997
|
+
model: this.meta.model ?? "unknown",
|
|
8998
|
+
provider: this.meta.provider ?? "unknown"
|
|
8999
|
+
})}
|
|
9000
|
+
`;
|
|
9001
|
+
try {
|
|
9002
|
+
await this.enqueueWrite(record);
|
|
9003
|
+
} catch {
|
|
9004
|
+
}
|
|
8940
9005
|
}
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
|
|
9006
|
+
async append(event) {
|
|
9007
|
+
if (this.closed) return;
|
|
9008
|
+
await this.ensureInit();
|
|
9009
|
+
const scrubbed = this.scrubEvent(event);
|
|
9010
|
+
this.observeForSummary(scrubbed);
|
|
9011
|
+
this.writeBuffer.push(scrubbed);
|
|
9012
|
+
if (this.writeBuffer.length >= _FileSessionWriter.FLUSH_SIZE) {
|
|
9013
|
+
if (this.flushTimer) {
|
|
9014
|
+
clearTimeout(this.flushTimer);
|
|
9015
|
+
this.flushTimer = null;
|
|
9016
|
+
}
|
|
9017
|
+
await this.flushBuffer();
|
|
9018
|
+
} else {
|
|
9019
|
+
this.scheduleFlush();
|
|
9020
|
+
}
|
|
8944
9021
|
}
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
9022
|
+
async appendBatch(events) {
|
|
9023
|
+
if (this.closed || events.length === 0) return;
|
|
9024
|
+
await this.ensureInit();
|
|
9025
|
+
for (const event of events) {
|
|
9026
|
+
const scrubbed = this.scrubEvent(event);
|
|
9027
|
+
this.observeForSummary(scrubbed);
|
|
9028
|
+
this.writeBuffer.push(scrubbed);
|
|
9029
|
+
}
|
|
9030
|
+
if (this.writeBuffer.length >= _FileSessionWriter.FLUSH_SIZE) {
|
|
9031
|
+
if (this.flushTimer) {
|
|
9032
|
+
clearTimeout(this.flushTimer);
|
|
9033
|
+
this.flushTimer = null;
|
|
9034
|
+
}
|
|
9035
|
+
await this.flushBuffer();
|
|
9036
|
+
} else {
|
|
9037
|
+
this.scheduleFlush();
|
|
9038
|
+
}
|
|
8948
9039
|
}
|
|
8949
9040
|
/**
|
|
8950
|
-
*
|
|
8951
|
-
*
|
|
8952
|
-
*
|
|
9041
|
+
* Flush buffered events to disk immediately. Critical events
|
|
9042
|
+
* (user_input, llm_response) call this so they survive SIGKILL/crash
|
|
9043
|
+
* instead of sitting in the in-memory buffer for up to 500ms.
|
|
9044
|
+
*
|
|
9045
|
+
* Idempotent — cancels any pending timer and writes whatever has
|
|
9046
|
+
* accumulated in the buffer. Safe to call even when the buffer
|
|
9047
|
+
* is empty (no-op).
|
|
8953
9048
|
*/
|
|
8954
|
-
async
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
}
|
|
8959
|
-
async create(meta) {
|
|
8960
|
-
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8961
|
-
const id = meta.id && meta.id.length > 0 ? meta.id : generateSessionId(startedAt, meta.model ?? meta.provider);
|
|
8962
|
-
const shardDir = await this.ensureShardDir(id);
|
|
8963
|
-
const file = path5.join(shardDir, `${path5.basename(id)}.jsonl`);
|
|
8964
|
-
const t0 = Date.now();
|
|
8965
|
-
let handle;
|
|
8966
|
-
try {
|
|
8967
|
-
handle = await fsp6.open(file, "a", 384);
|
|
8968
|
-
} catch (err) {
|
|
8969
|
-
this.emitError(id, file, "create", toErrorMessage(err), false);
|
|
8970
|
-
throw new Error(
|
|
8971
|
-
`Failed to open session file: ${toErrorMessage(err)}`,
|
|
8972
|
-
{ cause: err }
|
|
8973
|
-
);
|
|
8974
|
-
}
|
|
8975
|
-
try {
|
|
8976
|
-
const writer = new FileSessionWriter(id, handle, startedAt, meta, this.events, {
|
|
8977
|
-
dir: shardDir,
|
|
8978
|
-
filePath: file,
|
|
8979
|
-
secretScrubber: this.secretScrubber,
|
|
8980
|
-
onClose: (s) => this.appendToIndex(s)
|
|
8981
|
-
});
|
|
8982
|
-
this.emitWrite(id, file, "create", "success", Date.now() - t0);
|
|
8983
|
-
return writer;
|
|
8984
|
-
} catch (err) {
|
|
8985
|
-
await handle.close().catch((e) => console.warn(JSON.stringify({
|
|
8986
|
-
level: "warn",
|
|
8987
|
-
event: "session_store.handle_close_failed",
|
|
8988
|
-
message: e instanceof Error ? e.message : String(e),
|
|
8989
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8990
|
-
})));
|
|
8991
|
-
this.emitError(id, file, "create", toErrorMessage(err), true);
|
|
8992
|
-
throw err;
|
|
8993
|
-
}
|
|
8994
|
-
}
|
|
8995
|
-
async resume(id) {
|
|
8996
|
-
const file = this.sessionPath(id, ".jsonl");
|
|
8997
|
-
const t0 = Date.now();
|
|
8998
|
-
const data = await this.load(id);
|
|
8999
|
-
let handle;
|
|
9000
|
-
try {
|
|
9001
|
-
handle = await fsp6.open(file, "a", 384);
|
|
9002
|
-
} catch (err) {
|
|
9003
|
-
this.emitError(id, file, "resume", toErrorMessage(err), false);
|
|
9004
|
-
throw new Error(
|
|
9005
|
-
`Failed to open session "${id}" for append: ${toErrorMessage(err)}`,
|
|
9006
|
-
{ cause: err }
|
|
9007
|
-
);
|
|
9008
|
-
}
|
|
9009
|
-
try {
|
|
9010
|
-
const writer = new FileSessionWriter(
|
|
9011
|
-
id,
|
|
9012
|
-
handle,
|
|
9013
|
-
(/* @__PURE__ */ new Date()).toISOString(),
|
|
9014
|
-
{
|
|
9015
|
-
id,
|
|
9016
|
-
model: data.metadata.model,
|
|
9017
|
-
provider: data.metadata.provider
|
|
9018
|
-
},
|
|
9019
|
-
this.events,
|
|
9020
|
-
{
|
|
9021
|
-
resumed: true,
|
|
9022
|
-
// Shard directory (sessions/<date>/) — must match create() so the
|
|
9023
|
-
// .summary.json sidecar lands next to the JSONL instead of the
|
|
9024
|
-
// sessions root (where summaryFor() would never find it).
|
|
9025
|
-
dir: path5.dirname(file),
|
|
9026
|
-
filePath: file,
|
|
9027
|
-
secretScrubber: this.secretScrubber,
|
|
9028
|
-
onClose: (s) => this.appendToIndex(s)
|
|
9029
|
-
}
|
|
9030
|
-
);
|
|
9031
|
-
this.emitWrite(id, file, "resume", "success", Date.now() - t0);
|
|
9032
|
-
return { writer, data };
|
|
9033
|
-
} catch (err) {
|
|
9034
|
-
await handle.close().catch((e) => console.warn(JSON.stringify({
|
|
9035
|
-
level: "warn",
|
|
9036
|
-
event: "session_store.handle_close_failed",
|
|
9037
|
-
message: e instanceof Error ? e.message : String(e),
|
|
9038
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9039
|
-
})));
|
|
9040
|
-
this.emitError(id, file, "resume", toErrorMessage(err), true);
|
|
9041
|
-
throw err;
|
|
9042
|
-
}
|
|
9043
|
-
}
|
|
9044
|
-
async load(id) {
|
|
9045
|
-
const file = this.sessionPath(id, ".jsonl");
|
|
9046
|
-
const t0 = Date.now();
|
|
9047
|
-
let outcome = "success";
|
|
9048
|
-
let errorMsg;
|
|
9049
|
-
let cacheHit = false;
|
|
9050
|
-
try {
|
|
9051
|
-
const s = await fsp6.stat(file);
|
|
9052
|
-
const stat7 = { mtimeMs: s.mtimeMs, size: s.size };
|
|
9053
|
-
const cached = this._loadCache.get(id);
|
|
9054
|
-
if (cached && cached.mtimeMs === stat7.mtimeMs && cached.size === stat7.size) {
|
|
9055
|
-
cacheHit = true;
|
|
9056
|
-
this._loadCache.delete(id);
|
|
9057
|
-
this._loadCache.set(id, cached);
|
|
9058
|
-
return cached.data;
|
|
9059
|
-
}
|
|
9060
|
-
const raw = await fsp6.readFile(file, "utf8");
|
|
9061
|
-
const lines = raw.split("\n").filter((l) => l.trim());
|
|
9062
|
-
const events = [];
|
|
9063
|
-
let sessionStartEvent;
|
|
9064
|
-
let sessionEndEvent;
|
|
9065
|
-
let sessionModel;
|
|
9066
|
-
let sessionProvider;
|
|
9067
|
-
let sessionPendingToolUses;
|
|
9068
|
-
const messages = [];
|
|
9069
|
-
const openToolUses = /* @__PURE__ */ new Set();
|
|
9070
|
-
let usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
9071
|
-
for (const line of lines) {
|
|
9072
|
-
try {
|
|
9073
|
-
const parsed = JSON.parse(line);
|
|
9074
|
-
if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
|
|
9075
|
-
const ev = parsed;
|
|
9076
|
-
events.push(ev);
|
|
9077
|
-
if (ev.type === "session_start" && !sessionStartEvent) {
|
|
9078
|
-
sessionStartEvent = ev;
|
|
9079
|
-
sessionModel = ev.model;
|
|
9080
|
-
sessionProvider = ev.provider;
|
|
9081
|
-
}
|
|
9082
|
-
if (ev.type === "session_end") {
|
|
9083
|
-
sessionEndEvent = ev;
|
|
9084
|
-
sessionPendingToolUses = ev.pendingToolUses;
|
|
9085
|
-
}
|
|
9086
|
-
if (ev.type === "user_input") {
|
|
9087
|
-
openToolUses.clear();
|
|
9088
|
-
messages.push({ role: "user", content: ev.content, ts: ev.ts });
|
|
9089
|
-
} else if (ev.type === "llm_response") {
|
|
9090
|
-
messages.push({ role: "assistant", content: ev.content, ts: ev.ts });
|
|
9091
|
-
for (const b of ev.content) {
|
|
9092
|
-
if (b.type === "tool_use") openToolUses.add(b.id);
|
|
9093
|
-
}
|
|
9094
|
-
usage = {
|
|
9095
|
-
input: usage.input + (ev.usage.input ?? 0),
|
|
9096
|
-
output: usage.output + (ev.usage.output ?? 0),
|
|
9097
|
-
cacheRead: (usage.cacheRead ?? 0) + (ev.usage.cacheRead ?? 0),
|
|
9098
|
-
cacheWrite: (usage.cacheWrite ?? 0) + (ev.usage.cacheWrite ?? 0)
|
|
9099
|
-
};
|
|
9100
|
-
} else if (ev.type === "tool_result") {
|
|
9101
|
-
if (!openToolUses.has(ev.id)) {
|
|
9102
|
-
this.events?.emit("session.damaged", {
|
|
9103
|
-
sessionId: id,
|
|
9104
|
-
detail: `Orphan tool_result "${ev.id}" has no matching tool_use`
|
|
9105
|
-
});
|
|
9106
|
-
continue;
|
|
9107
|
-
}
|
|
9108
|
-
openToolUses.delete(ev.id);
|
|
9109
|
-
const resultBlock = {
|
|
9110
|
-
type: "tool_result",
|
|
9111
|
-
tool_use_id: ev.id,
|
|
9112
|
-
content: typeof ev.content === "string" ? ev.content : JSON.stringify(ev.content),
|
|
9113
|
-
is_error: ev.isError
|
|
9114
|
-
};
|
|
9115
|
-
const last = messages[messages.length - 1];
|
|
9116
|
-
const lastIsToolResultUser = last?.role === "user" && Array.isArray(last.content) && last.content.every((b) => b.type === "tool_result");
|
|
9117
|
-
if (lastIsToolResultUser && Array.isArray(last.content)) {
|
|
9118
|
-
last.content.push(resultBlock);
|
|
9119
|
-
} else {
|
|
9120
|
-
messages.push({ role: "user", content: [resultBlock], ts: ev.ts });
|
|
9121
|
-
}
|
|
9122
|
-
}
|
|
9123
|
-
}
|
|
9124
|
-
} catch {
|
|
9125
|
-
}
|
|
9126
|
-
}
|
|
9127
|
-
if (openToolUses.size > 0) {
|
|
9128
|
-
this.events?.emit("session.damaged", {
|
|
9129
|
-
sessionId: id,
|
|
9130
|
-
detail: `${openToolUses.size} tool_use blocks without matching results - replay repaired`
|
|
9131
|
-
});
|
|
9132
|
-
}
|
|
9133
|
-
const repaired = repairToolUseAdjacency(messages);
|
|
9134
|
-
if (repaired.report.changed) {
|
|
9135
|
-
this.events?.emit("session.damaged", {
|
|
9136
|
-
sessionId: id,
|
|
9137
|
-
detail: `Repaired replay adjacency: removed ${repaired.report.removedToolUses.length} tool_use, ${repaired.report.removedToolResults.length} tool_result, ${repaired.report.removedMessages} empty messages`
|
|
9138
|
-
});
|
|
9139
|
-
}
|
|
9140
|
-
const meta = {
|
|
9141
|
-
id,
|
|
9142
|
-
startedAt: sessionStartEvent?.ts ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
9143
|
-
endedAt: sessionEndEvent?.ts,
|
|
9144
|
-
model: sessionModel,
|
|
9145
|
-
provider: sessionProvider,
|
|
9146
|
-
pendingToolUses: sessionPendingToolUses
|
|
9147
|
-
};
|
|
9148
|
-
const toolCallEnds = extractToolCallEnds(events);
|
|
9149
|
-
const data = { metadata: meta, events, messages: repaired.messages, usage, toolCallEnds };
|
|
9150
|
-
if (this._loadCache.size >= _DefaultSessionStore.LOAD_CACHE_MAX_ENTRIES) {
|
|
9151
|
-
const oldest = this._loadCache.keys().next().value;
|
|
9152
|
-
if (oldest !== void 0) {
|
|
9153
|
-
this._loadCache.delete(oldest);
|
|
9154
|
-
}
|
|
9155
|
-
}
|
|
9156
|
-
this._loadCache.set(id, { mtimeMs: stat7.mtimeMs, size: stat7.size, data });
|
|
9157
|
-
return data;
|
|
9158
|
-
} catch (err) {
|
|
9159
|
-
outcome = "failure";
|
|
9160
|
-
errorMsg = toErrorMessage(err);
|
|
9161
|
-
throw err;
|
|
9162
|
-
} finally {
|
|
9163
|
-
this.emitRead(id, file, "load", outcome, Date.now() - t0, errorMsg);
|
|
9164
|
-
if (cacheHit) {
|
|
9165
|
-
this.events?.emit("storage.cache_hit", {
|
|
9166
|
-
sessionId: id,
|
|
9167
|
-
store: "session",
|
|
9168
|
-
filePath: file,
|
|
9169
|
-
operation: "load",
|
|
9170
|
-
durationMs: Date.now() - t0
|
|
9171
|
-
});
|
|
9172
|
-
}
|
|
9173
|
-
}
|
|
9174
|
-
}
|
|
9175
|
-
/**
|
|
9176
|
-
* Streaming search over a session's JSONL. Walks the file once, parses
|
|
9177
|
-
* each event lazily, and yields only the events that match `predicate`.
|
|
9178
|
-
* Stops as soon as `opts.limit` matches are collected.
|
|
9179
|
-
*
|
|
9180
|
-
* Why this exists: `load()` parses the entire file into memory and
|
|
9181
|
-
* rebuilds `messages`/`toolCallEnds` for every caller. `search()` only
|
|
9182
|
-
* needs to know which events contain matching text — a per-line
|
|
9183
|
-
* predicate is enough. The full parse work (and the `_loadCache` poll)
|
|
9184
|
-
* is wasted in that case.
|
|
9185
|
-
*
|
|
9186
|
-
* Memory: O(hits) regardless of file size. Disk: one linear scan,
|
|
9187
|
-
* terminated at `limit` if the caller asked for one.
|
|
9188
|
-
*
|
|
9189
|
-
* Errors: missing file yields []. Corrupt lines are skipped (same
|
|
9190
|
-
* policy as `load()`). Aborting via `signal` rejects with `AbortError`.
|
|
9191
|
-
*/
|
|
9192
|
-
async searchEvents(id, predicate, opts) {
|
|
9193
|
-
const file = this.sessionPath(id, ".jsonl");
|
|
9194
|
-
const limit = opts?.limit;
|
|
9195
|
-
const signal = opts?.signal;
|
|
9196
|
-
const out = [];
|
|
9197
|
-
let stat7;
|
|
9198
|
-
try {
|
|
9199
|
-
stat7 = await fsp6.stat(file);
|
|
9200
|
-
} catch (err) {
|
|
9201
|
-
if (err.code === "ENOENT") return [];
|
|
9202
|
-
throw err;
|
|
9203
|
-
}
|
|
9204
|
-
if (stat7.size === 0) return [];
|
|
9205
|
-
let fh;
|
|
9206
|
-
try {
|
|
9207
|
-
fh = await fsp6.open(file, "r");
|
|
9208
|
-
const CHUNK = 64 * 1024;
|
|
9209
|
-
const buf = Buffer.alloc(CHUNK);
|
|
9210
|
-
let leftover = "";
|
|
9211
|
-
let eventIndex = 0;
|
|
9212
|
-
for (let position = 0; ; position += buf.byteLength) {
|
|
9213
|
-
if (signal?.aborted) {
|
|
9214
|
-
const reason = signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
9215
|
-
throw reason;
|
|
9216
|
-
}
|
|
9217
|
-
const { bytesRead } = await fh.read(buf, 0, CHUNK, position);
|
|
9218
|
-
if (bytesRead === 0) break;
|
|
9219
|
-
const text = leftover + buf.subarray(0, bytesRead).toString("utf8");
|
|
9220
|
-
const parts = text.split("\n");
|
|
9221
|
-
leftover = parts.pop() ?? "";
|
|
9222
|
-
for (const line of parts) {
|
|
9223
|
-
if (!line) continue;
|
|
9224
|
-
let ev;
|
|
9225
|
-
try {
|
|
9226
|
-
const parsed = JSON.parse(line);
|
|
9227
|
-
if (parsed === null || typeof parsed !== "object" || typeof parsed.type !== "string" || typeof parsed.ts !== "string") {
|
|
9228
|
-
continue;
|
|
9229
|
-
}
|
|
9230
|
-
ev = parsed;
|
|
9231
|
-
} catch {
|
|
9232
|
-
continue;
|
|
9233
|
-
}
|
|
9234
|
-
if (predicate(ev, eventIndex, ev.ts)) {
|
|
9235
|
-
out.push({ event: ev, eventIndex, ts: ev.ts });
|
|
9236
|
-
if (limit !== void 0 && out.length >= limit) {
|
|
9237
|
-
return out;
|
|
9238
|
-
}
|
|
9239
|
-
}
|
|
9240
|
-
eventIndex++;
|
|
9241
|
-
}
|
|
9242
|
-
}
|
|
9243
|
-
if (leftover.trim()) {
|
|
9244
|
-
try {
|
|
9245
|
-
const parsed = JSON.parse(leftover);
|
|
9246
|
-
if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
|
|
9247
|
-
const ev = parsed;
|
|
9248
|
-
if (predicate(ev, eventIndex, ev.ts)) {
|
|
9249
|
-
out.push({ event: ev, eventIndex, ts: ev.ts });
|
|
9250
|
-
}
|
|
9251
|
-
}
|
|
9252
|
-
} catch {
|
|
9253
|
-
}
|
|
9254
|
-
}
|
|
9255
|
-
return out;
|
|
9256
|
-
} finally {
|
|
9257
|
-
if (fh) await fh.close().catch(() => void 0);
|
|
9258
|
-
}
|
|
9259
|
-
}
|
|
9260
|
-
async list(limit = 20) {
|
|
9261
|
-
try {
|
|
9262
|
-
await ensureDir(this.dir);
|
|
9263
|
-
const indexed = await this.readIndex();
|
|
9264
|
-
if (indexed.length > 0) {
|
|
9265
|
-
indexed.sort((a, b) => {
|
|
9266
|
-
if (a.startedAt < b.startedAt) return 1;
|
|
9267
|
-
if (a.startedAt > b.startedAt) return -1;
|
|
9268
|
-
return a.id.localeCompare(b.id);
|
|
9269
|
-
});
|
|
9270
|
-
return indexed.slice(0, limit);
|
|
9271
|
-
}
|
|
9272
|
-
return await this.listFromDirectoryScan(limit);
|
|
9273
|
-
} catch {
|
|
9274
|
-
return [];
|
|
9275
|
-
}
|
|
9276
|
-
}
|
|
9277
|
-
// ── Session index (_index.jsonl) ─────────────────────────────────────────
|
|
9278
|
-
//
|
|
9279
|
-
// One JSON line per closed session, appended atomically on close().
|
|
9280
|
-
// When a session is deleted, a tombstone {action:"delete",id:"..."} is
|
|
9281
|
-
// appended. On read, tombstones filter out matching session entries.
|
|
9282
|
-
// This keeps listing O(lines-in-index) instead of O(files-on-disk).
|
|
9283
|
-
//
|
|
9284
|
-
// The index auto-compacts every N appends to prevent unbounded growth
|
|
9285
|
-
// from tombstones and duplicate entries (resume cycles).
|
|
9286
|
-
indexAppendCount = 0;
|
|
9287
|
-
static COMPACT_EVERY = 30;
|
|
9288
|
-
/** Append a session summary to the index. */
|
|
9289
|
-
async appendToIndex(summary) {
|
|
9290
|
-
try {
|
|
9291
|
-
await ensureDir(this.dir);
|
|
9292
|
-
const line = JSON.stringify(summary) + "\n";
|
|
9293
|
-
await fsp6.appendFile(this.indexFile, line, "utf8");
|
|
9294
|
-
this._indexCache = null;
|
|
9295
|
-
this.indexAppendCount++;
|
|
9296
|
-
if (this.indexAppendCount >= _DefaultSessionStore.COMPACT_EVERY) {
|
|
9297
|
-
await this.compactIndex();
|
|
9298
|
-
this.indexAppendCount = 0;
|
|
9299
|
-
}
|
|
9300
|
-
} catch {
|
|
9049
|
+
async flush() {
|
|
9050
|
+
if (this.flushTimer) {
|
|
9051
|
+
clearTimeout(this.flushTimer);
|
|
9052
|
+
this.flushTimer = null;
|
|
9301
9053
|
}
|
|
9054
|
+
await this.flushBuffer();
|
|
9302
9055
|
}
|
|
9303
|
-
/**
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
} catch {
|
|
9312
|
-
}
|
|
9056
|
+
/** Schedule a deferred flush. No-op if a timer is already pending. */
|
|
9057
|
+
scheduleFlush() {
|
|
9058
|
+
if (this.flushTimer) return;
|
|
9059
|
+
this.flushTimer = setTimeout(() => {
|
|
9060
|
+
this.flushTimer = null;
|
|
9061
|
+
this.flushBuffer().catch(() => {
|
|
9062
|
+
});
|
|
9063
|
+
}, _FileSessionWriter.FLUSH_INTERVAL_MS);
|
|
9313
9064
|
}
|
|
9314
9065
|
/**
|
|
9315
|
-
*
|
|
9316
|
-
*
|
|
9066
|
+
* Flush all buffered events to disk as a single appendFile call.
|
|
9067
|
+
* Errors use the same throttled-warning pattern the old per-event
|
|
9068
|
+
* append path used — one warning every 5s with a suppressed count.
|
|
9069
|
+
* On failure the buffer is cleared (events are best-effort, same as
|
|
9070
|
+
* the old per-event path where a failed write was silently dropped).
|
|
9317
9071
|
*/
|
|
9318
|
-
async
|
|
9072
|
+
async flushBuffer() {
|
|
9073
|
+
if (this.writeBuffer.length === 0) return;
|
|
9074
|
+
const eventCount = this.writeBuffer.length;
|
|
9075
|
+
const batch = this.writeBuffer.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
9076
|
+
this.writeBuffer = [];
|
|
9319
9077
|
const t0 = Date.now();
|
|
9320
9078
|
let outcome = "success";
|
|
9321
9079
|
let errorMsg;
|
|
9322
9080
|
try {
|
|
9323
|
-
|
|
9324
|
-
if (entries.length === 0) return;
|
|
9325
|
-
const tmp = `${this.indexFile}.compact.tmp`;
|
|
9326
|
-
const lines = entries.map((s) => JSON.stringify(s)).join("\n") + "\n";
|
|
9327
|
-
await fsp6.writeFile(tmp, lines, "utf8");
|
|
9328
|
-
await fsp6.rename(tmp, this.indexFile);
|
|
9329
|
-
this._indexCache = null;
|
|
9081
|
+
await this.enqueueWrite(batch);
|
|
9330
9082
|
} catch (err) {
|
|
9331
9083
|
outcome = "failure";
|
|
9332
9084
|
errorMsg = toErrorMessage(err);
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
const s = await fsp6.stat(this.indexFile);
|
|
9346
|
-
stat7 = { mtimeMs: s.mtimeMs, size: s.size };
|
|
9347
|
-
} catch {
|
|
9348
|
-
this._indexCache = null;
|
|
9349
|
-
return [];
|
|
9350
|
-
}
|
|
9351
|
-
if (this._indexCache !== null && this._indexCache.mtimeMs === stat7.mtimeMs && this._indexCache.size === stat7.size) {
|
|
9352
|
-
return [...this._indexCache.summaries];
|
|
9353
|
-
}
|
|
9354
|
-
let raw;
|
|
9355
|
-
try {
|
|
9356
|
-
raw = await fsp6.readFile(this.indexFile, "utf8");
|
|
9357
|
-
} catch {
|
|
9358
|
-
this._indexCache = null;
|
|
9359
|
-
return [];
|
|
9360
|
-
}
|
|
9361
|
-
const deleted = /* @__PURE__ */ new Set();
|
|
9362
|
-
const seen = /* @__PURE__ */ new Map();
|
|
9363
|
-
for (const line of raw.split("\n")) {
|
|
9364
|
-
if (!line.trim()) continue;
|
|
9365
|
-
try {
|
|
9366
|
-
const entry = JSON.parse(line);
|
|
9367
|
-
if (entry.action === "delete" && entry.id) {
|
|
9368
|
-
deleted.add(entry.id);
|
|
9369
|
-
seen.delete(entry.id);
|
|
9370
|
-
continue;
|
|
9371
|
-
}
|
|
9372
|
-
if (entry.id && !deleted.has(entry.id)) {
|
|
9373
|
-
seen.set(entry.id, entry);
|
|
9374
|
-
}
|
|
9375
|
-
} catch {
|
|
9085
|
+
this.appendFailCount += eventCount;
|
|
9086
|
+
const now = Date.now();
|
|
9087
|
+
if (now - this.lastAppendWarnAt > 5e3) {
|
|
9088
|
+
const suppressed = this.appendFailCount - 1;
|
|
9089
|
+
const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
|
|
9090
|
+
console.warn(
|
|
9091
|
+
"[session] flush failed:",
|
|
9092
|
+
toErrorMessage(err),
|
|
9093
|
+
tail
|
|
9094
|
+
);
|
|
9095
|
+
this.lastAppendWarnAt = now;
|
|
9096
|
+
this.appendFailCount = 0;
|
|
9376
9097
|
}
|
|
9098
|
+
} finally {
|
|
9099
|
+
this.events?.emit("storage.write", {
|
|
9100
|
+
sessionId: this.id,
|
|
9101
|
+
store: "session",
|
|
9102
|
+
filePath: this.filePath,
|
|
9103
|
+
operation: "flush",
|
|
9104
|
+
outcome,
|
|
9105
|
+
durationMs: Date.now() - t0,
|
|
9106
|
+
...errorMsg !== void 0 ? { error: errorMsg } : {},
|
|
9107
|
+
...eventCount !== void 0 ? { eventCount } : {},
|
|
9108
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
9109
|
+
});
|
|
9377
9110
|
}
|
|
9378
|
-
const summaries = Array.from(seen.values());
|
|
9379
|
-
this._indexCache = { ...stat7, summaries };
|
|
9380
|
-
return [...summaries];
|
|
9381
|
-
}
|
|
9382
|
-
/**
|
|
9383
|
-
* Rebuild the index from disk by scanning all sessions and writing a
|
|
9384
|
-
* fresh _index.jsonl. Useful after manual cleanup or index corruption.
|
|
9385
|
-
*/
|
|
9386
|
-
async rebuildIndex() {
|
|
9387
|
-
const ids = await this.collectSessionIds(this.dir);
|
|
9388
|
-
const summaries = await Promise.all(ids.map((id) => this.summaryFor(id).catch(() => null)));
|
|
9389
|
-
const valid = summaries.filter((s) => s !== null);
|
|
9390
|
-
const tmp = `${this.indexFile}.tmp`;
|
|
9391
|
-
const lines = valid.map((s) => JSON.stringify(s)).join("\n") + "\n";
|
|
9392
|
-
await fsp6.writeFile(tmp, lines, "utf8");
|
|
9393
|
-
await fsp6.rename(tmp, this.indexFile);
|
|
9394
|
-
this._indexCache = null;
|
|
9395
|
-
return valid.length;
|
|
9396
9111
|
}
|
|
9397
|
-
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
9401
|
-
_DefaultSessionStore.LIST_SCAN_CONCURRENCY,
|
|
9402
|
-
async (ref) => {
|
|
9403
|
-
const manifest = await this.readSummaryManifest(ref.id);
|
|
9404
|
-
if (manifest) return { summary: manifest, needsBackfill: false };
|
|
9405
|
-
const summary = await this.summaryHeaderFor(ref);
|
|
9406
|
-
return summary ? { summary, needsBackfill: true } : null;
|
|
9407
|
-
}
|
|
9408
|
-
);
|
|
9409
|
-
const out = candidates.filter((s) => s !== null);
|
|
9410
|
-
out.sort((a, b) => compareSessionSummaries(a.summary, b.summary));
|
|
9411
|
-
const selected = out.slice(0, limit);
|
|
9412
|
-
const summaries = await mapWithConcurrency(
|
|
9413
|
-
selected,
|
|
9414
|
-
Math.min(_DefaultSessionStore.LIST_SCAN_CONCURRENCY, Math.max(1, limit)),
|
|
9415
|
-
async (candidate) => {
|
|
9416
|
-
if (!candidate.needsBackfill) return candidate.summary;
|
|
9417
|
-
return await this.summaryFor(candidate.summary.id).catch(() => candidate.summary);
|
|
9112
|
+
observeForSummary(event) {
|
|
9113
|
+
if (event.type === "llm_response") {
|
|
9114
|
+
for (const block of event.content) {
|
|
9115
|
+
if (block.type === "tool_use") this.openToolUses.add(block.id);
|
|
9418
9116
|
}
|
|
9419
|
-
);
|
|
9420
|
-
return summaries.filter((s) => s !== null);
|
|
9421
|
-
}
|
|
9422
|
-
async collectSessionFiles(dir, prefix = "", depth = 0) {
|
|
9423
|
-
let entries;
|
|
9424
|
-
try {
|
|
9425
|
-
entries = await fsp6.readdir(dir, { withFileTypes: true });
|
|
9426
|
-
} catch {
|
|
9427
|
-
return [];
|
|
9428
9117
|
}
|
|
9429
|
-
|
|
9430
|
-
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
const base = entry.name.replace(/\.jsonl$/, "");
|
|
9440
|
-
const id = prefix ? `${prefix}/${base}` : base;
|
|
9441
|
-
files.push({ id, filePath: path5.join(dir, entry.name) });
|
|
9118
|
+
if (event.type === "tool_use") {
|
|
9119
|
+
this.openToolUses.add(event.id);
|
|
9120
|
+
} else if (event.type === "tool_call_start") {
|
|
9121
|
+
this.toolCallCount++;
|
|
9122
|
+
this.toolBreakdown[event.name] = (this.toolBreakdown[event.name] ?? 0) + 1;
|
|
9123
|
+
} else if (event.type === "tool_result") {
|
|
9124
|
+
this.openToolUses.delete(event.id);
|
|
9125
|
+
if (event.isError) {
|
|
9126
|
+
this.toolErrorCount++;
|
|
9127
|
+
this.outcome = "error";
|
|
9442
9128
|
}
|
|
9129
|
+
} else if (event.type === "file_snapshot") {
|
|
9130
|
+
this.fileChangeCount += event.files.length;
|
|
9131
|
+
} else if (event.type === "compaction") {
|
|
9132
|
+
this.compactionCount++;
|
|
9443
9133
|
}
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
|
|
9447
|
-
return this.collectSessionFiles(path5.join(dir, entry.name), childPrefix, depth + 1);
|
|
9448
|
-
})
|
|
9449
|
-
);
|
|
9450
|
-
return [...childFileArrays.flat(), ...files];
|
|
9451
|
-
}
|
|
9452
|
-
/** Recursively collect session IDs from date-shard subdirectories.
|
|
9453
|
-
* IDs include the date-prefix path (e.g. "2026-06-06/17-46-57Z_…").
|
|
9454
|
-
* Skips `.jsonl`/`.summary.json` root files, dot-files, and
|
|
9455
|
-
* sub-directories that belong to fleet/subagent sessions. */
|
|
9456
|
-
async collectSessionIds(dir, prefix = "", depth = 0) {
|
|
9457
|
-
let entries;
|
|
9458
|
-
try {
|
|
9459
|
-
entries = await fsp6.readdir(dir, { withFileTypes: true });
|
|
9460
|
-
} catch {
|
|
9461
|
-
return [];
|
|
9134
|
+
if (event.type === "error" || event.type === "provider_error") {
|
|
9135
|
+
this.outcome = "error";
|
|
9462
9136
|
}
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
fileIds.push(prefix ? `${prefix}/${base}` : base);
|
|
9475
|
-
}
|
|
9137
|
+
if (event.type === "user_input" && this.summary.title === "(empty session)") {
|
|
9138
|
+
this.summary = { ...this.summary, title: userInputTitle(event.content) };
|
|
9139
|
+
} else if (event.type === "llm_response") {
|
|
9140
|
+
this.tokenIn += event.usage.input;
|
|
9141
|
+
this.tokenOut += event.usage.output;
|
|
9142
|
+
this.summary = { ...this.summary, tokenTotal: this.tokenIn + this.tokenOut };
|
|
9143
|
+
} else if (event.type === "session_end") {
|
|
9144
|
+
const total = event.usage.input + event.usage.output;
|
|
9145
|
+
if (total > 0) this.summary = { ...this.summary, tokenTotal: total };
|
|
9146
|
+
} else if (event.type === "in_flight_start") {
|
|
9147
|
+
this.iterationCount++;
|
|
9476
9148
|
}
|
|
9477
|
-
const childIdArrays = await Promise.all(
|
|
9478
|
-
dirEntries.map((entry) => {
|
|
9479
|
-
const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
|
|
9480
|
-
return this.collectSessionIds(path5.join(dir, entry.name), childPrefix, depth + 1);
|
|
9481
|
-
})
|
|
9482
|
-
);
|
|
9483
|
-
return [...childIdArrays.flat(), ...fileIds];
|
|
9484
9149
|
}
|
|
9485
|
-
async
|
|
9486
|
-
|
|
9487
|
-
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
|
|
9495
|
-
|
|
9496
|
-
|
|
9497
|
-
|
|
9498
|
-
|
|
9499
|
-
|
|
9500
|
-
|
|
9501
|
-
|
|
9502
|
-
|
|
9503
|
-
|
|
9504
|
-
|
|
9505
|
-
|
|
9506
|
-
}
|
|
9507
|
-
outcome
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
9150
|
+
async close() {
|
|
9151
|
+
if (this.closePromise) return this.closePromise;
|
|
9152
|
+
this.closePromise = this.doClose();
|
|
9153
|
+
return this.closePromise;
|
|
9154
|
+
}
|
|
9155
|
+
async doClose() {
|
|
9156
|
+
this.closed = true;
|
|
9157
|
+
if (this.flushTimer) {
|
|
9158
|
+
clearTimeout(this.flushTimer);
|
|
9159
|
+
this.flushTimer = null;
|
|
9160
|
+
}
|
|
9161
|
+
await this.flushBuffer();
|
|
9162
|
+
await this.writeChain;
|
|
9163
|
+
this.summary = {
|
|
9164
|
+
...this.summary,
|
|
9165
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9166
|
+
iterationCount: this.iterationCount,
|
|
9167
|
+
toolCallCount: this.toolCallCount,
|
|
9168
|
+
toolErrorCount: this.toolErrorCount,
|
|
9169
|
+
fileChangeCount: this.fileChangeCount,
|
|
9170
|
+
compactionCount: this.compactionCount > 0 ? this.compactionCount : void 0,
|
|
9171
|
+
toolBreakdown: { ...this.toolBreakdown },
|
|
9172
|
+
outcome: this.outcome ?? "completed"
|
|
9173
|
+
};
|
|
9174
|
+
if (this.manifestFile) {
|
|
9175
|
+
const t0 = Date.now();
|
|
9176
|
+
let outcome = "success";
|
|
9177
|
+
let errorMsg;
|
|
9178
|
+
try {
|
|
9179
|
+
await atomicWrite(this.manifestFile, JSON.stringify(this.summary), { mode: 384 });
|
|
9180
|
+
} catch (err) {
|
|
9181
|
+
outcome = "failure";
|
|
9182
|
+
errorMsg = toErrorMessage(err);
|
|
9183
|
+
} finally {
|
|
9184
|
+
this.events?.emit("storage.write", {
|
|
9185
|
+
sessionId: this.id,
|
|
9186
|
+
store: "session",
|
|
9187
|
+
filePath: this.manifestFile,
|
|
9188
|
+
operation: "close",
|
|
9189
|
+
outcome,
|
|
9190
|
+
durationMs: Date.now() - t0,
|
|
9191
|
+
...errorMsg !== void 0 ? { error: errorMsg } : {},
|
|
9192
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
9193
|
+
});
|
|
9194
|
+
}
|
|
9195
|
+
}
|
|
9196
|
+
const idxT0 = Date.now();
|
|
9197
|
+
let idxOutcome = "success";
|
|
9198
|
+
let idxError;
|
|
9199
|
+
try {
|
|
9200
|
+
await this.onCloseCb?.(this.summary);
|
|
9511
9201
|
} catch (err) {
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
id,
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9202
|
+
idxOutcome = "failure";
|
|
9203
|
+
idxError = toErrorMessage(err);
|
|
9204
|
+
} finally {
|
|
9205
|
+
this.events?.emit("storage.write", {
|
|
9206
|
+
sessionId: this.summary.id,
|
|
9207
|
+
store: "session",
|
|
9208
|
+
filePath: this.filePath,
|
|
9209
|
+
operation: "index_append",
|
|
9210
|
+
outcome: idxOutcome,
|
|
9211
|
+
durationMs: Date.now() - idxT0,
|
|
9212
|
+
...idxError !== void 0 ? { error: idxError } : {},
|
|
9213
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
9214
|
+
});
|
|
9523
9215
|
}
|
|
9524
|
-
}
|
|
9525
|
-
async readSummaryManifest(id, startTime = Date.now()) {
|
|
9526
|
-
const manifest = this.sessionPath(id, ".summary.json");
|
|
9527
9216
|
try {
|
|
9528
|
-
|
|
9529
|
-
this.emitRead(id, manifest, "summary", "success", Date.now() - startTime);
|
|
9530
|
-
return JSON.parse(raw);
|
|
9217
|
+
await this.handle.close();
|
|
9531
9218
|
} catch {
|
|
9532
|
-
return null;
|
|
9533
9219
|
}
|
|
9534
9220
|
}
|
|
9535
|
-
async
|
|
9536
|
-
|
|
9221
|
+
async writeCheckpoint(promptIndex, promptPreview) {
|
|
9222
|
+
const fileCount = this.pendingFileSnapshots.length;
|
|
9223
|
+
if (fileCount > 0) {
|
|
9224
|
+
await this.writeFileSnapshot(promptIndex, [...this.pendingFileSnapshots]);
|
|
9225
|
+
this.pendingFileSnapshots = [];
|
|
9226
|
+
}
|
|
9227
|
+
await this.append({
|
|
9228
|
+
type: "checkpoint",
|
|
9229
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9230
|
+
promptIndex,
|
|
9231
|
+
promptPreview
|
|
9232
|
+
});
|
|
9233
|
+
this.events?.emit("checkpoint.written", {
|
|
9234
|
+
promptIndex,
|
|
9235
|
+
promptPreview,
|
|
9236
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9237
|
+
fileCount
|
|
9238
|
+
});
|
|
9239
|
+
}
|
|
9240
|
+
async writeFileSnapshot(promptIndex, files) {
|
|
9241
|
+
await this.append({
|
|
9242
|
+
type: "file_snapshot",
|
|
9243
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9244
|
+
promptIndex,
|
|
9245
|
+
files
|
|
9246
|
+
});
|
|
9247
|
+
}
|
|
9248
|
+
/**
|
|
9249
|
+
* Truncate the session file to the checkpoint with the given promptIndex,
|
|
9250
|
+
* removing all events that follow it. Uses a single-pass byte-offset scan
|
|
9251
|
+
* so post-checkpoint content is never read or parsed — O(1) memory instead
|
|
9252
|
+
* of O(N) JSON.parse calls over the full file.
|
|
9253
|
+
*/
|
|
9254
|
+
async truncateToCheckpoint(targetPromptIndex) {
|
|
9255
|
+
if (!this.filePath) return 0;
|
|
9256
|
+
if (this.flushTimer) {
|
|
9257
|
+
clearTimeout(this.flushTimer);
|
|
9258
|
+
this.flushTimer = null;
|
|
9259
|
+
}
|
|
9260
|
+
await this.flushBuffer();
|
|
9261
|
+
await this.writeChain;
|
|
9262
|
+
const CHUNK_SIZE = 65536;
|
|
9263
|
+
let fd;
|
|
9264
|
+
let fileOffset = 0;
|
|
9265
|
+
let lineStartOffset = 0;
|
|
9266
|
+
let checkpointByteOffset = -1;
|
|
9267
|
+
let removedCount = 0;
|
|
9268
|
+
let targetCheckpointSeen = false;
|
|
9537
9269
|
try {
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
|
|
9546
|
-
|
|
9547
|
-
|
|
9270
|
+
fd = await fsp7.open(this.filePath, "r", 384);
|
|
9271
|
+
while (true) {
|
|
9272
|
+
const buf = Buffer.alloc(CHUNK_SIZE);
|
|
9273
|
+
const { bytesRead } = await fd.read(buf, 0, CHUNK_SIZE, fileOffset);
|
|
9274
|
+
if (bytesRead === 0) break;
|
|
9275
|
+
let chunkPos = 0;
|
|
9276
|
+
while (chunkPos < bytesRead) {
|
|
9277
|
+
const idx = buf.indexOf("\n", chunkPos);
|
|
9278
|
+
if (idx === -1) {
|
|
9279
|
+
lineStartOffset = fileOffset + chunkPos;
|
|
9280
|
+
break;
|
|
9281
|
+
}
|
|
9282
|
+
if (checkpointByteOffset !== -1) {
|
|
9283
|
+
removedCount++;
|
|
9284
|
+
} else {
|
|
9285
|
+
const lineBytes = buf.subarray(chunkPos, idx);
|
|
9286
|
+
const line = new TextDecoder("utf-8", { fatal: false }).decode(lineBytes);
|
|
9287
|
+
if (line.trim()) {
|
|
9288
|
+
try {
|
|
9289
|
+
const event = JSON.parse(line);
|
|
9290
|
+
if (event.type === "checkpoint") {
|
|
9291
|
+
if (event.promptIndex === targetPromptIndex) {
|
|
9292
|
+
checkpointByteOffset = lineStartOffset;
|
|
9293
|
+
targetCheckpointSeen = true;
|
|
9294
|
+
} else if (event.promptIndex !== void 0 && event.promptIndex > targetPromptIndex) {
|
|
9295
|
+
checkpointByteOffset = lineStartOffset;
|
|
9296
|
+
}
|
|
9297
|
+
} else if (targetCheckpointSeen && event.promptIndex !== void 0 && event.promptIndex > targetPromptIndex) {
|
|
9298
|
+
removedCount++;
|
|
9299
|
+
} else if (targetCheckpointSeen && event.promptIndex === void 0) {
|
|
9300
|
+
removedCount++;
|
|
9301
|
+
} else if (!targetCheckpointSeen && event.promptIndex === void 0) {
|
|
9302
|
+
removedCount++;
|
|
9303
|
+
} else if (!targetCheckpointSeen && event.promptIndex !== void 0 && event.promptIndex > targetPromptIndex) {
|
|
9304
|
+
removedCount++;
|
|
9305
|
+
}
|
|
9306
|
+
} catch {
|
|
9307
|
+
}
|
|
9308
|
+
}
|
|
9309
|
+
}
|
|
9310
|
+
chunkPos = idx + 1;
|
|
9311
|
+
lineStartOffset = fileOffset + chunkPos;
|
|
9312
|
+
}
|
|
9313
|
+
fileOffset += bytesRead;
|
|
9314
|
+
if (chunkPos >= bytesRead) {
|
|
9315
|
+
lineStartOffset = fileOffset;
|
|
9316
|
+
}
|
|
9548
9317
|
}
|
|
9549
|
-
|
|
9550
|
-
|
|
9551
|
-
return null;
|
|
9318
|
+
} finally {
|
|
9319
|
+
await fd?.close();
|
|
9552
9320
|
}
|
|
9321
|
+
if (checkpointByteOffset === -1) return 0;
|
|
9322
|
+
await this.writeChain;
|
|
9323
|
+
await this.handle.close();
|
|
9324
|
+
const tmpPath = `${this.filePath}.rewind.tmp`;
|
|
9325
|
+
const src = await fsp7.open(this.filePath, "r", 384);
|
|
9553
9326
|
try {
|
|
9554
|
-
|
|
9555
|
-
|
|
9556
|
-
|
|
9557
|
-
|
|
9558
|
-
|
|
9559
|
-
|
|
9560
|
-
|
|
9561
|
-
|
|
9562
|
-
|
|
9563
|
-
|
|
9327
|
+
const statResult = await src.stat();
|
|
9328
|
+
const totalSize = statResult.size;
|
|
9329
|
+
const prefixBytes = checkpointByteOffset;
|
|
9330
|
+
let newlineAfterCheckpoint = prefixBytes;
|
|
9331
|
+
if (prefixBytes < totalSize) {
|
|
9332
|
+
const probeBuf = Buffer.alloc(Math.min(CHUNK_SIZE, totalSize - prefixBytes));
|
|
9333
|
+
const { bytesRead: probeRead } = await src.read(probeBuf, 0, probeBuf.length, prefixBytes);
|
|
9334
|
+
if (probeRead > 0) {
|
|
9335
|
+
const nl = probeBuf.indexOf("\n");
|
|
9336
|
+
newlineAfterCheckpoint = nl !== -1 ? prefixBytes + nl + 1 : totalSize;
|
|
9564
9337
|
}
|
|
9338
|
+
} else {
|
|
9339
|
+
newlineAfterCheckpoint = totalSize;
|
|
9565
9340
|
}
|
|
9566
|
-
|
|
9567
|
-
|
|
9568
|
-
|
|
9569
|
-
|
|
9570
|
-
|
|
9571
|
-
|
|
9572
|
-
|
|
9573
|
-
|
|
9574
|
-
|
|
9575
|
-
|
|
9576
|
-
|
|
9577
|
-
|
|
9578
|
-
|
|
9579
|
-
|
|
9580
|
-
|
|
9581
|
-
|
|
9582
|
-
|
|
9341
|
+
const writeFd = await fsp7.open(tmpPath, "w", 384);
|
|
9342
|
+
try {
|
|
9343
|
+
let readOffset = 0;
|
|
9344
|
+
while (readOffset < newlineAfterCheckpoint) {
|
|
9345
|
+
const toCopy = Math.min(CHUNK_SIZE, newlineAfterCheckpoint - readOffset);
|
|
9346
|
+
const copyBuf = Buffer.alloc(toCopy);
|
|
9347
|
+
const { bytesRead: r } = await src.read(copyBuf, 0, toCopy, readOffset);
|
|
9348
|
+
if (r === 0) break;
|
|
9349
|
+
await writeFd.write(copyBuf, 0, r);
|
|
9350
|
+
readOffset += r;
|
|
9351
|
+
}
|
|
9352
|
+
let tailOffset = newlineAfterCheckpoint;
|
|
9353
|
+
let leftover = "";
|
|
9354
|
+
while (tailOffset < totalSize) {
|
|
9355
|
+
const toRead = Math.min(CHUNK_SIZE, totalSize - tailOffset);
|
|
9356
|
+
const tailBuf = Buffer.alloc(toRead);
|
|
9357
|
+
const { bytesRead: tr } = await src.read(tailBuf, 0, toRead, tailOffset);
|
|
9358
|
+
if (tr === 0) break;
|
|
9359
|
+
const chunk = leftover + tailBuf.subarray(0, tr).toString("utf8");
|
|
9360
|
+
const lastNl = chunk.lastIndexOf("\n");
|
|
9361
|
+
if (lastNl === -1) {
|
|
9362
|
+
leftover = chunk;
|
|
9363
|
+
} else {
|
|
9364
|
+
for (const line of chunk.slice(0, lastNl + 1).split("\n")) {
|
|
9365
|
+
if (!line.trim()) continue;
|
|
9366
|
+
try {
|
|
9367
|
+
JSON.parse(line);
|
|
9368
|
+
} catch {
|
|
9369
|
+
await writeFd.write(`${line}
|
|
9370
|
+
`, void 0, "utf8");
|
|
9371
|
+
}
|
|
9372
|
+
}
|
|
9373
|
+
leftover = chunk.slice(lastNl + 1);
|
|
9374
|
+
}
|
|
9375
|
+
tailOffset += tr;
|
|
9376
|
+
}
|
|
9377
|
+
if (leftover.trim()) {
|
|
9378
|
+
try {
|
|
9379
|
+
JSON.parse(leftover);
|
|
9380
|
+
} catch {
|
|
9381
|
+
await writeFd.write(`${leftover}
|
|
9382
|
+
`, void 0, "utf8");
|
|
9383
|
+
}
|
|
9384
|
+
}
|
|
9385
|
+
} finally {
|
|
9386
|
+
await writeFd.close();
|
|
9387
|
+
}
|
|
9388
|
+
await src.close();
|
|
9389
|
+
await fsp7.rename(tmpPath, this.filePath);
|
|
9390
|
+
this.handle = await fsp7.open(this.filePath, "a", 384);
|
|
9391
|
+
} catch (err) {
|
|
9392
|
+
await fsp7.unlink(tmpPath).catch(() => void 0);
|
|
9393
|
+
this.handle = await fsp7.open(this.filePath, "a", 384).catch(() => this.handle);
|
|
9394
|
+
throw err;
|
|
9395
|
+
}
|
|
9396
|
+
await this.append({
|
|
9397
|
+
type: "rewound",
|
|
9398
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9399
|
+
toPromptIndex: targetPromptIndex,
|
|
9400
|
+
revertedFiles: []
|
|
9401
|
+
});
|
|
9402
|
+
this.events?.emit("session.rewound", {
|
|
9403
|
+
toPromptIndex: targetPromptIndex,
|
|
9404
|
+
revertedFiles: [],
|
|
9405
|
+
removedEvents: removedCount
|
|
9406
|
+
});
|
|
9407
|
+
return removedCount;
|
|
9408
|
+
}
|
|
9409
|
+
async clearSession() {
|
|
9410
|
+
if (!this.filePath) return;
|
|
9411
|
+
if (this.flushTimer) {
|
|
9412
|
+
clearTimeout(this.flushTimer);
|
|
9413
|
+
this.flushTimer = null;
|
|
9414
|
+
}
|
|
9415
|
+
this.writeBuffer = [];
|
|
9416
|
+
await this.writeChain;
|
|
9417
|
+
const record = `${JSON.stringify({
|
|
9418
|
+
type: "session_start",
|
|
9419
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9420
|
+
id: this.id,
|
|
9421
|
+
model: this.meta.model ?? "unknown",
|
|
9422
|
+
provider: this.meta.provider ?? "unknown"
|
|
9423
|
+
})}
|
|
9424
|
+
`;
|
|
9425
|
+
await fsp7.writeFile(this.filePath, record, "utf8");
|
|
9426
|
+
}
|
|
9427
|
+
/**
|
|
9428
|
+
* Write an in-flight marker. The agent loop should call
|
|
9429
|
+
* this at the start of each long-running operation; a matching
|
|
9430
|
+
* `clearInFlightMarker` follows on clean exit. A stale marker
|
|
9431
|
+
* (no end) is what `SessionRecovery.detectStale` looks for.
|
|
9432
|
+
*/
|
|
9433
|
+
async writeInFlightMarker(context) {
|
|
9434
|
+
if (!context || context.length > 500) {
|
|
9435
|
+
throw new Error("In-flight context must be 1..500 chars");
|
|
9583
9436
|
}
|
|
9437
|
+
await this.append({
|
|
9438
|
+
type: "in_flight_start",
|
|
9439
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9440
|
+
context
|
|
9441
|
+
});
|
|
9442
|
+
this.events?.emit("in_flight.started", { context, ts: (/* @__PURE__ */ new Date()).toISOString() });
|
|
9584
9443
|
}
|
|
9585
9444
|
/**
|
|
9586
|
-
*
|
|
9587
|
-
*
|
|
9445
|
+
* Close the in-flight marker. Idempotent in spirit
|
|
9446
|
+
* (you can call it after a successful iteration even if you
|
|
9447
|
+
* didn't open one this round) — but the session log records
|
|
9448
|
+
* every call so postmortem tooling can see "the agent finished
|
|
9449
|
+
* cleanly X times, then died without finishing Y".
|
|
9450
|
+
*/
|
|
9451
|
+
async clearInFlightMarker(reason) {
|
|
9452
|
+
await this.append({
|
|
9453
|
+
type: "in_flight_end",
|
|
9454
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9455
|
+
reason
|
|
9456
|
+
});
|
|
9457
|
+
this.events?.emit("in_flight.ended", { reason, ts: (/* @__PURE__ */ new Date()).toISOString() });
|
|
9458
|
+
}
|
|
9459
|
+
};
|
|
9460
|
+
function sanitizeModel(model) {
|
|
9461
|
+
return model.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
9462
|
+
}
|
|
9463
|
+
function generateSessionId(startedAt, model) {
|
|
9464
|
+
const date = startedAt.slice(0, 10);
|
|
9465
|
+
const time = startedAt.slice(11, 19).replace(/:/g, "-");
|
|
9466
|
+
const suffix = randomBytes(2).toString("hex");
|
|
9467
|
+
const modelPart = model ? `_${sanitizeModel(model)}` : "";
|
|
9468
|
+
return `${date}/${time}Z${modelPart}_${suffix}`;
|
|
9469
|
+
}
|
|
9470
|
+
|
|
9471
|
+
// src/storage/session-store.ts
|
|
9472
|
+
var DefaultSessionStore = class _DefaultSessionStore {
|
|
9473
|
+
dir;
|
|
9474
|
+
events;
|
|
9475
|
+
secretScrubber;
|
|
9476
|
+
/**
|
|
9477
|
+
* In-memory cache for load() results, keyed by session ID. The cache is
|
|
9478
|
+
* invalidated when the file's mtimeMs or size changes (indicating the
|
|
9479
|
+
* file was written to). This eliminates redundant full-file reads and
|
|
9480
|
+
* JSON parses when the same session is loaded multiple times within the
|
|
9481
|
+
* store's lifetime (e.g., webui session detail views, list() fallbacks).
|
|
9588
9482
|
*
|
|
9589
|
-
*
|
|
9590
|
-
*
|
|
9591
|
-
* If the session directory itself can't be removed, the error is surfaced
|
|
9592
|
-
* to the caller so prune() can report it.
|
|
9483
|
+
* Max size is capped to prevent unbounded memory growth in long-running
|
|
9484
|
+
* processes. When the limit is reached, the oldest entry is evicted.
|
|
9593
9485
|
*/
|
|
9594
|
-
|
|
9595
|
-
|
|
9596
|
-
|
|
9597
|
-
|
|
9598
|
-
|
|
9599
|
-
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
|
|
9604
|
-
|
|
9605
|
-
|
|
9606
|
-
|
|
9607
|
-
|
|
9608
|
-
|
|
9609
|
-
|
|
9610
|
-
|
|
9611
|
-
|
|
9612
|
-
|
|
9613
|
-
event: "session_store.delete_failed",
|
|
9614
|
-
sessionId: id,
|
|
9615
|
-
message: msg,
|
|
9616
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9617
|
-
}));
|
|
9618
|
-
}
|
|
9619
|
-
}
|
|
9486
|
+
_loadCache = /* @__PURE__ */ new Map();
|
|
9487
|
+
_indexCache = null;
|
|
9488
|
+
shardManifestCache = /* @__PURE__ */ new Map();
|
|
9489
|
+
static LOAD_CACHE_MAX_ENTRIES = 50;
|
|
9490
|
+
static LIST_SCAN_CONCURRENCY = 32;
|
|
9491
|
+
constructor(opts) {
|
|
9492
|
+
this.dir = opts.dir;
|
|
9493
|
+
this.events = opts.events;
|
|
9494
|
+
this.secretScrubber = opts.secretScrubber;
|
|
9495
|
+
}
|
|
9496
|
+
/**
|
|
9497
|
+
* Clear the load() cache. Useful for testing or when the caller knows
|
|
9498
|
+
* the file has changed externally (e.g., another process wrote to it).
|
|
9499
|
+
*/
|
|
9500
|
+
clearLoadCache(sessionId) {
|
|
9501
|
+
if (sessionId !== void 0) {
|
|
9502
|
+
this._loadCache.delete(sessionId);
|
|
9503
|
+
} else {
|
|
9504
|
+
this._loadCache.clear();
|
|
9620
9505
|
}
|
|
9621
|
-
|
|
9622
|
-
|
|
9623
|
-
|
|
9624
|
-
|
|
9625
|
-
|
|
9626
|
-
|
|
9627
|
-
|
|
9628
|
-
|
|
9506
|
+
}
|
|
9507
|
+
// ── Storage event helpers ───────────────────────────────────────────────────
|
|
9508
|
+
emitRead(sessionId, filePath, operation, outcome, durationMs, error) {
|
|
9509
|
+
this.events?.emit("storage.read", {
|
|
9510
|
+
sessionId,
|
|
9511
|
+
store: "session",
|
|
9512
|
+
filePath,
|
|
9513
|
+
operation,
|
|
9514
|
+
outcome,
|
|
9515
|
+
durationMs,
|
|
9516
|
+
...error !== void 0 ? { error } : {}
|
|
9629
9517
|
});
|
|
9630
|
-
await this.writeTombstone(id);
|
|
9631
9518
|
}
|
|
9632
|
-
|
|
9633
|
-
|
|
9519
|
+
emitWrite(sessionId, filePath, operation, outcome, durationMs, eventCount, error) {
|
|
9520
|
+
this.events?.emit("storage.write", {
|
|
9521
|
+
sessionId,
|
|
9522
|
+
store: "session",
|
|
9523
|
+
filePath,
|
|
9524
|
+
operation,
|
|
9525
|
+
outcome,
|
|
9526
|
+
durationMs,
|
|
9527
|
+
...eventCount !== void 0 ? { eventCount } : {},
|
|
9528
|
+
...error !== void 0 ? { error } : {}
|
|
9529
|
+
});
|
|
9634
9530
|
}
|
|
9635
|
-
|
|
9636
|
-
|
|
9637
|
-
|
|
9638
|
-
|
|
9531
|
+
emitError(sessionId, filePath, operation, error, recoverable) {
|
|
9532
|
+
this.events?.emit("storage.error", {
|
|
9533
|
+
sessionId,
|
|
9534
|
+
store: "session",
|
|
9535
|
+
filePath,
|
|
9536
|
+
operation,
|
|
9537
|
+
error,
|
|
9538
|
+
recoverable
|
|
9539
|
+
});
|
|
9540
|
+
}
|
|
9541
|
+
/** Absolute path to the session index file. */
|
|
9542
|
+
get indexFile() {
|
|
9543
|
+
return path6.join(this.dir, "_index.jsonl");
|
|
9544
|
+
}
|
|
9545
|
+
/** Join session ID to its absolute path within the store directory. */
|
|
9546
|
+
sessionPath(id, ext) {
|
|
9547
|
+
return path6.join(this.dir, `${id}${ext}`);
|
|
9548
|
+
}
|
|
9549
|
+
shardManifestPath(shardKey) {
|
|
9550
|
+
return shardKey ? path6.join(this.dir, shardKey, "_manifest.json") : path6.join(this.dir, "_manifest.json");
|
|
9551
|
+
}
|
|
9552
|
+
shardKeyForSessionId(id) {
|
|
9553
|
+
const dirName = path6.dirname(id);
|
|
9554
|
+
return dirName === "." ? "" : dirName;
|
|
9555
|
+
}
|
|
9556
|
+
invalidateShardManifestBySessionId(id) {
|
|
9557
|
+
this.shardManifestCache.delete(this.shardKeyForSessionId(id));
|
|
9558
|
+
}
|
|
9559
|
+
/**
|
|
9560
|
+
* Ensure the directory implied by the session ID exists. When the ID
|
|
9561
|
+
* contains a date prefix like `2026-06-06/...`, this creates the date
|
|
9562
|
+
* subdirectory so sessions group naturally by day.
|
|
9563
|
+
*/
|
|
9564
|
+
async ensureShardDir(id) {
|
|
9565
|
+
const dirPath = path6.dirname(path6.join(this.dir, id));
|
|
9566
|
+
await ensureDir(dirPath);
|
|
9567
|
+
return dirPath;
|
|
9568
|
+
}
|
|
9569
|
+
async create(meta) {
|
|
9570
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9571
|
+
const id = meta.id && meta.id.length > 0 ? meta.id : generateSessionId(startedAt, meta.model ?? meta.provider);
|
|
9572
|
+
const shardDir = await this.ensureShardDir(id);
|
|
9573
|
+
const file = path6.join(shardDir, `${path6.basename(id)}.jsonl`);
|
|
9574
|
+
const t0 = Date.now();
|
|
9575
|
+
let handle;
|
|
9639
9576
|
try {
|
|
9640
|
-
|
|
9641
|
-
|
|
9642
|
-
|
|
9643
|
-
|
|
9577
|
+
handle = await fsp7.open(file, "a", 384);
|
|
9578
|
+
} catch (err) {
|
|
9579
|
+
this.emitError(id, file, "create", toErrorMessage(err), false);
|
|
9580
|
+
throw new Error(
|
|
9581
|
+
`Failed to open session file: ${toErrorMessage(err)}`,
|
|
9582
|
+
{ cause: err }
|
|
9583
|
+
);
|
|
9644
9584
|
}
|
|
9645
|
-
|
|
9646
|
-
|
|
9647
|
-
|
|
9648
|
-
|
|
9649
|
-
|
|
9650
|
-
|
|
9651
|
-
}
|
|
9652
|
-
|
|
9653
|
-
|
|
9654
|
-
|
|
9655
|
-
|
|
9656
|
-
|
|
9657
|
-
|
|
9658
|
-
|
|
9659
|
-
|
|
9660
|
-
|
|
9661
|
-
|
|
9662
|
-
|
|
9663
|
-
if (isPrunableJsonl(entry.name)) await pruneFile(this.dir, entry.name, "");
|
|
9664
|
-
continue;
|
|
9665
|
-
}
|
|
9666
|
-
if (!entry.isDirectory()) continue;
|
|
9667
|
-
const dateDir = path5.join(this.dir, entry.name);
|
|
9668
|
-
const files = await fsp6.readdir(dateDir, { withFileTypes: true }).catch(() => []);
|
|
9669
|
-
for (const file of files) {
|
|
9670
|
-
if (!file.isFile() || !isPrunableJsonl(file.name)) continue;
|
|
9671
|
-
await pruneFile(dateDir, file.name, entry.name);
|
|
9672
|
-
}
|
|
9585
|
+
try {
|
|
9586
|
+
const writer = new FileSessionWriter(id, handle, startedAt, meta, this.events, {
|
|
9587
|
+
dir: shardDir,
|
|
9588
|
+
filePath: file,
|
|
9589
|
+
secretScrubber: this.secretScrubber,
|
|
9590
|
+
onClose: (s) => this.appendToIndex(s)
|
|
9591
|
+
});
|
|
9592
|
+
this.emitWrite(id, file, "create", "success", Date.now() - t0);
|
|
9593
|
+
return writer;
|
|
9594
|
+
} catch (err) {
|
|
9595
|
+
await handle.close().catch((e) => console.warn(JSON.stringify({
|
|
9596
|
+
level: "warn",
|
|
9597
|
+
event: "session_store.handle_close_failed",
|
|
9598
|
+
message: e instanceof Error ? e.message : String(e),
|
|
9599
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9600
|
+
})));
|
|
9601
|
+
this.emitError(id, file, "create", toErrorMessage(err), true);
|
|
9602
|
+
throw err;
|
|
9673
9603
|
}
|
|
9674
|
-
|
|
9675
|
-
|
|
9604
|
+
}
|
|
9605
|
+
async resume(id) {
|
|
9606
|
+
const file = this.sessionPath(id, ".jsonl");
|
|
9607
|
+
const t0 = Date.now();
|
|
9608
|
+
const data = await this.load(id);
|
|
9609
|
+
let handle;
|
|
9610
|
+
try {
|
|
9611
|
+
handle = await fsp7.open(file, "a", 384);
|
|
9612
|
+
} catch (err) {
|
|
9613
|
+
this.emitError(id, file, "resume", toErrorMessage(err), false);
|
|
9614
|
+
throw new Error(
|
|
9615
|
+
`Failed to open session "${id}" for append: ${toErrorMessage(err)}`,
|
|
9616
|
+
{ cause: err }
|
|
9617
|
+
);
|
|
9676
9618
|
}
|
|
9677
|
-
|
|
9678
|
-
|
|
9679
|
-
|
|
9680
|
-
|
|
9681
|
-
|
|
9682
|
-
|
|
9683
|
-
|
|
9619
|
+
try {
|
|
9620
|
+
const writer = new FileSessionWriter(
|
|
9621
|
+
id,
|
|
9622
|
+
handle,
|
|
9623
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
9624
|
+
{
|
|
9625
|
+
id,
|
|
9626
|
+
model: data.metadata.model,
|
|
9627
|
+
provider: data.metadata.provider
|
|
9628
|
+
},
|
|
9629
|
+
this.events,
|
|
9630
|
+
{
|
|
9631
|
+
resumed: true,
|
|
9632
|
+
// Shard directory (sessions/<date>/) — must match create() so the
|
|
9633
|
+
// .summary.json sidecar lands next to the JSONL instead of the
|
|
9634
|
+
// sessions root (where summaryFor() would never find it).
|
|
9635
|
+
dir: path6.dirname(file),
|
|
9636
|
+
filePath: file,
|
|
9637
|
+
secretScrubber: this.secretScrubber,
|
|
9638
|
+
onClose: (s) => this.appendToIndex(s)
|
|
9684
9639
|
}
|
|
9685
|
-
|
|
9686
|
-
|
|
9640
|
+
);
|
|
9641
|
+
this.emitWrite(id, file, "resume", "success", Date.now() - t0);
|
|
9642
|
+
return { writer, data };
|
|
9643
|
+
} catch (err) {
|
|
9644
|
+
await handle.close().catch((e) => console.warn(JSON.stringify({
|
|
9645
|
+
level: "warn",
|
|
9646
|
+
event: "session_store.handle_close_failed",
|
|
9647
|
+
message: e instanceof Error ? e.message : String(e),
|
|
9648
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9649
|
+
})));
|
|
9650
|
+
this.emitError(id, file, "resume", toErrorMessage(err), true);
|
|
9651
|
+
throw err;
|
|
9687
9652
|
}
|
|
9688
|
-
return deleted;
|
|
9689
9653
|
}
|
|
9690
|
-
async
|
|
9691
|
-
await this.ensureShardDir(id);
|
|
9654
|
+
async load(id) {
|
|
9692
9655
|
const file = this.sessionPath(id, ".jsonl");
|
|
9693
|
-
const
|
|
9694
|
-
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
id,
|
|
9698
|
-
model: "unknown",
|
|
9699
|
-
provider: "unknown"
|
|
9700
|
-
})}
|
|
9701
|
-
`;
|
|
9702
|
-
await fsp6.writeFile(file, record, "utf8");
|
|
9703
|
-
await fsp6.unlink(meta).catch(() => void 0);
|
|
9704
|
-
}
|
|
9705
|
-
async summarize(id, mtime) {
|
|
9656
|
+
const t0 = Date.now();
|
|
9657
|
+
let outcome = "success";
|
|
9658
|
+
let errorMsg;
|
|
9659
|
+
let cacheHit = false;
|
|
9706
9660
|
try {
|
|
9707
|
-
const
|
|
9708
|
-
|
|
9709
|
-
|
|
9710
|
-
|
|
9711
|
-
|
|
9712
|
-
|
|
9713
|
-
|
|
9714
|
-
|
|
9715
|
-
|
|
9716
|
-
|
|
9717
|
-
|
|
9718
|
-
|
|
9719
|
-
|
|
9720
|
-
let
|
|
9721
|
-
let
|
|
9722
|
-
let
|
|
9723
|
-
let
|
|
9724
|
-
|
|
9725
|
-
|
|
9726
|
-
|
|
9727
|
-
|
|
9728
|
-
|
|
9729
|
-
|
|
9730
|
-
|
|
9731
|
-
|
|
9661
|
+
const s = await fsp7.stat(file);
|
|
9662
|
+
const stat7 = { mtimeMs: s.mtimeMs, size: s.size };
|
|
9663
|
+
const cached = this._loadCache.get(id);
|
|
9664
|
+
if (cached && cached.mtimeMs === stat7.mtimeMs && cached.size === stat7.size) {
|
|
9665
|
+
cacheHit = true;
|
|
9666
|
+
this._loadCache.delete(id);
|
|
9667
|
+
this._loadCache.set(id, cached);
|
|
9668
|
+
return cached.data;
|
|
9669
|
+
}
|
|
9670
|
+
const raw = await fsp7.readFile(file, "utf8");
|
|
9671
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
9672
|
+
const events = [];
|
|
9673
|
+
let sessionStartEvent;
|
|
9674
|
+
let sessionEndEvent;
|
|
9675
|
+
let sessionModel;
|
|
9676
|
+
let sessionProvider;
|
|
9677
|
+
let sessionPendingToolUses;
|
|
9678
|
+
const messages = [];
|
|
9679
|
+
const openToolUses = /* @__PURE__ */ new Set();
|
|
9680
|
+
let usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
9681
|
+
for (const line of lines) {
|
|
9682
|
+
try {
|
|
9683
|
+
const parsed = JSON.parse(line);
|
|
9684
|
+
if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
|
|
9685
|
+
const ev = parsed;
|
|
9686
|
+
events.push(ev);
|
|
9687
|
+
if (ev.type === "session_start" && !sessionStartEvent) {
|
|
9688
|
+
sessionStartEvent = ev;
|
|
9689
|
+
sessionModel = ev.model;
|
|
9690
|
+
sessionProvider = ev.provider;
|
|
9691
|
+
}
|
|
9692
|
+
if (ev.type === "session_end") {
|
|
9693
|
+
sessionEndEvent = ev;
|
|
9694
|
+
sessionPendingToolUses = ev.pendingToolUses;
|
|
9695
|
+
}
|
|
9696
|
+
if (ev.type === "user_input") {
|
|
9697
|
+
openToolUses.clear();
|
|
9698
|
+
messages.push({ role: "user", content: ev.content, ts: ev.ts });
|
|
9699
|
+
} else if (ev.type === "llm_response") {
|
|
9700
|
+
messages.push({ role: "assistant", content: ev.content, ts: ev.ts });
|
|
9701
|
+
for (const b of ev.content) {
|
|
9702
|
+
if (b.type === "tool_use") openToolUses.add(b.id);
|
|
9703
|
+
}
|
|
9704
|
+
usage = {
|
|
9705
|
+
input: usage.input + (ev.usage.input ?? 0),
|
|
9706
|
+
output: usage.output + (ev.usage.output ?? 0),
|
|
9707
|
+
cacheRead: (usage.cacheRead ?? 0) + (ev.usage.cacheRead ?? 0),
|
|
9708
|
+
cacheWrite: (usage.cacheWrite ?? 0) + (ev.usage.cacheWrite ?? 0)
|
|
9709
|
+
};
|
|
9710
|
+
} else if (ev.type === "tool_result") {
|
|
9711
|
+
if (!openToolUses.has(ev.id)) {
|
|
9712
|
+
this.events?.emit("session.damaged", {
|
|
9713
|
+
sessionId: id,
|
|
9714
|
+
detail: `Orphan tool_result "${ev.id}" has no matching tool_use`
|
|
9715
|
+
});
|
|
9716
|
+
continue;
|
|
9717
|
+
}
|
|
9718
|
+
openToolUses.delete(ev.id);
|
|
9719
|
+
const resultBlock = {
|
|
9720
|
+
type: "tool_result",
|
|
9721
|
+
tool_use_id: ev.id,
|
|
9722
|
+
content: typeof ev.content === "string" ? ev.content : JSON.stringify(ev.content),
|
|
9723
|
+
is_error: ev.isError
|
|
9724
|
+
};
|
|
9725
|
+
const last = messages[messages.length - 1];
|
|
9726
|
+
const lastIsToolResultUser = last?.role === "user" && Array.isArray(last.content) && last.content.every((b) => b.type === "tool_result");
|
|
9727
|
+
if (lastIsToolResultUser && Array.isArray(last.content)) {
|
|
9728
|
+
last.content.push(resultBlock);
|
|
9729
|
+
} else {
|
|
9730
|
+
messages.push({ role: "user", content: [resultBlock], ts: ev.ts });
|
|
9731
|
+
}
|
|
9732
|
+
}
|
|
9732
9733
|
}
|
|
9733
|
-
}
|
|
9734
|
-
|
|
9735
|
-
} else if (e.type === "user_input") {
|
|
9736
|
-
if (title === "(empty session)") title = userInputTitle(e.content);
|
|
9737
|
-
} else if (e.type === "llm_response") {
|
|
9738
|
-
tokenIn += e.usage.input ?? 0;
|
|
9739
|
-
tokenOut += e.usage.output ?? 0;
|
|
9740
|
-
} else if (e.type === "in_flight_start") iterationCount++;
|
|
9741
|
-
else if (e.type === "tool_call_start") {
|
|
9742
|
-
toolCallCount++;
|
|
9743
|
-
toolBreakdown[e.name] = (toolBreakdown[e.name] ?? 0) + 1;
|
|
9744
|
-
} else if (e.type === "tool_result" && e.isError) toolErrorCount++;
|
|
9745
|
-
else if (e.type === "file_snapshot") fileChangeCount += e.files.length;
|
|
9746
|
-
else if (e.type === "error" || e.type === "provider_error") hasError = true;
|
|
9734
|
+
} catch {
|
|
9735
|
+
}
|
|
9747
9736
|
}
|
|
9748
|
-
if (
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
outcome = "error";
|
|
9737
|
+
if (openToolUses.size > 0) {
|
|
9738
|
+
this.events?.emit("session.damaged", {
|
|
9739
|
+
sessionId: id,
|
|
9740
|
+
detail: `${openToolUses.size} tool_use blocks without matching results - replay repaired`
|
|
9741
|
+
});
|
|
9754
9742
|
}
|
|
9755
|
-
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
|
|
9761
|
-
|
|
9762
|
-
|
|
9763
|
-
iterationCount: iterationCount > 0 ? iterationCount : void 0,
|
|
9764
|
-
toolCallCount: toolCallCount > 0 ? toolCallCount : void 0,
|
|
9765
|
-
toolErrorCount: toolErrorCount > 0 ? toolErrorCount : void 0,
|
|
9766
|
-
fileChangeCount: fileChangeCount > 0 ? fileChangeCount : void 0,
|
|
9767
|
-
toolBreakdown: Object.keys(toolBreakdown).length > 0 ? toolBreakdown : {},
|
|
9768
|
-
outcome
|
|
9769
|
-
};
|
|
9770
|
-
} catch {
|
|
9771
|
-
return {
|
|
9743
|
+
const repaired = repairToolUseAdjacency(messages);
|
|
9744
|
+
if (repaired.report.changed) {
|
|
9745
|
+
this.events?.emit("session.damaged", {
|
|
9746
|
+
sessionId: id,
|
|
9747
|
+
detail: `Repaired replay adjacency: removed ${repaired.report.removedToolUses.length} tool_use, ${repaired.report.removedToolResults.length} tool_result, ${repaired.report.removedMessages} empty messages`
|
|
9748
|
+
});
|
|
9749
|
+
}
|
|
9750
|
+
const meta = {
|
|
9772
9751
|
id,
|
|
9773
|
-
|
|
9774
|
-
|
|
9775
|
-
model:
|
|
9776
|
-
provider:
|
|
9777
|
-
|
|
9752
|
+
startedAt: sessionStartEvent?.ts ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
9753
|
+
endedAt: sessionEndEvent?.ts,
|
|
9754
|
+
model: sessionModel,
|
|
9755
|
+
provider: sessionProvider,
|
|
9756
|
+
pendingToolUses: sessionPendingToolUses
|
|
9778
9757
|
};
|
|
9758
|
+
const toolCallEnds = extractToolCallEnds(events);
|
|
9759
|
+
const data = { metadata: meta, events, messages: repaired.messages, usage, toolCallEnds };
|
|
9760
|
+
if (this._loadCache.size >= _DefaultSessionStore.LOAD_CACHE_MAX_ENTRIES) {
|
|
9761
|
+
const oldest = this._loadCache.keys().next().value;
|
|
9762
|
+
if (oldest !== void 0) {
|
|
9763
|
+
this._loadCache.delete(oldest);
|
|
9764
|
+
}
|
|
9765
|
+
}
|
|
9766
|
+
this._loadCache.set(id, { mtimeMs: stat7.mtimeMs, size: stat7.size, data });
|
|
9767
|
+
return data;
|
|
9768
|
+
} catch (err) {
|
|
9769
|
+
outcome = "failure";
|
|
9770
|
+
errorMsg = toErrorMessage(err);
|
|
9771
|
+
throw err;
|
|
9772
|
+
} finally {
|
|
9773
|
+
this.emitRead(id, file, "load", outcome, Date.now() - t0, errorMsg);
|
|
9774
|
+
if (cacheHit) {
|
|
9775
|
+
this.events?.emit("storage.cache_hit", {
|
|
9776
|
+
sessionId: id,
|
|
9777
|
+
store: "session",
|
|
9778
|
+
filePath: file,
|
|
9779
|
+
operation: "load",
|
|
9780
|
+
durationMs: Date.now() - t0
|
|
9781
|
+
});
|
|
9782
|
+
}
|
|
9779
9783
|
}
|
|
9780
9784
|
}
|
|
9781
|
-
|
|
9782
|
-
|
|
9783
|
-
|
|
9785
|
+
/**
|
|
9786
|
+
* Streaming search over a session's JSONL. Walks the file once, parses
|
|
9787
|
+
* each event lazily, and yields only the events that match `predicate`.
|
|
9788
|
+
* Stops as soon as `opts.limit` matches are collected.
|
|
9789
|
+
*
|
|
9790
|
+
* Why this exists: `load()` parses the entire file into memory and
|
|
9791
|
+
* rebuilds `messages`/`toolCallEnds` for every caller. `search()` only
|
|
9792
|
+
* needs to know which events contain matching text — a per-line
|
|
9793
|
+
* predicate is enough. The full parse work (and the `_loadCache` poll)
|
|
9794
|
+
* is wasted in that case.
|
|
9795
|
+
*
|
|
9796
|
+
* Memory: O(hits) regardless of file size. Disk: one linear scan,
|
|
9797
|
+
* terminated at `limit` if the caller asked for one.
|
|
9798
|
+
*
|
|
9799
|
+
* Errors: missing file yields []. Corrupt lines are skipped (same
|
|
9800
|
+
* policy as `load()`). Aborting via `signal` rejects with `AbortError`.
|
|
9801
|
+
*/
|
|
9802
|
+
async searchEvents(id, predicate, opts) {
|
|
9803
|
+
const file = this.sessionPath(id, ".jsonl");
|
|
9804
|
+
const limit = opts?.limit;
|
|
9805
|
+
const signal = opts?.signal;
|
|
9806
|
+
const out = [];
|
|
9807
|
+
let stat7;
|
|
9784
9808
|
try {
|
|
9785
|
-
|
|
9786
|
-
|
|
9809
|
+
stat7 = await fsp7.stat(file);
|
|
9810
|
+
} catch (err) {
|
|
9811
|
+
if (err.code === "ENOENT") return [];
|
|
9812
|
+
throw err;
|
|
9813
|
+
}
|
|
9814
|
+
if (stat7.size === 0) return [];
|
|
9815
|
+
let fh;
|
|
9816
|
+
try {
|
|
9817
|
+
fh = await fsp7.open(file, "r");
|
|
9818
|
+
const CHUNK = 64 * 1024;
|
|
9819
|
+
const buf = Buffer.alloc(CHUNK);
|
|
9820
|
+
let leftover = "";
|
|
9821
|
+
let eventIndex = 0;
|
|
9822
|
+
for (let position = 0; ; position += buf.byteLength) {
|
|
9823
|
+
if (signal?.aborted) {
|
|
9824
|
+
const reason = signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
9825
|
+
throw reason;
|
|
9826
|
+
}
|
|
9827
|
+
const { bytesRead } = await fh.read(buf, 0, CHUNK, position);
|
|
9828
|
+
if (bytesRead === 0) break;
|
|
9829
|
+
const text = leftover + buf.subarray(0, bytesRead).toString("utf8");
|
|
9830
|
+
const parts = text.split("\n");
|
|
9831
|
+
leftover = parts.pop() ?? "";
|
|
9832
|
+
for (const line of parts) {
|
|
9833
|
+
if (!line) continue;
|
|
9834
|
+
let ev;
|
|
9835
|
+
try {
|
|
9836
|
+
const parsed = JSON.parse(line);
|
|
9837
|
+
if (parsed === null || typeof parsed !== "object" || typeof parsed.type !== "string" || typeof parsed.ts !== "string") {
|
|
9838
|
+
continue;
|
|
9839
|
+
}
|
|
9840
|
+
ev = parsed;
|
|
9841
|
+
} catch {
|
|
9842
|
+
continue;
|
|
9843
|
+
}
|
|
9844
|
+
if (predicate(ev, eventIndex, ev.ts)) {
|
|
9845
|
+
out.push({ event: ev, eventIndex, ts: ev.ts });
|
|
9846
|
+
if (limit !== void 0 && out.length >= limit) {
|
|
9847
|
+
return out;
|
|
9848
|
+
}
|
|
9849
|
+
}
|
|
9850
|
+
eventIndex++;
|
|
9851
|
+
}
|
|
9852
|
+
}
|
|
9853
|
+
if (leftover.trim()) {
|
|
9787
9854
|
try {
|
|
9788
|
-
const parsed = JSON.parse(
|
|
9855
|
+
const parsed = JSON.parse(leftover);
|
|
9789
9856
|
if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
|
|
9790
|
-
|
|
9857
|
+
const ev = parsed;
|
|
9858
|
+
if (predicate(ev, eventIndex, ev.ts)) {
|
|
9859
|
+
out.push({ event: ev, eventIndex, ts: ev.ts });
|
|
9860
|
+
}
|
|
9791
9861
|
}
|
|
9792
9862
|
} catch {
|
|
9793
9863
|
}
|
|
9794
9864
|
}
|
|
9865
|
+
return out;
|
|
9795
9866
|
} finally {
|
|
9796
|
-
|
|
9797
|
-
stream.destroy();
|
|
9867
|
+
if (fh) await fh.close().catch(() => void 0);
|
|
9798
9868
|
}
|
|
9799
9869
|
}
|
|
9800
|
-
|
|
9801
|
-
|
|
9802
|
-
|
|
9803
|
-
|
|
9804
|
-
|
|
9805
|
-
|
|
9806
|
-
|
|
9807
|
-
|
|
9808
|
-
|
|
9809
|
-
|
|
9810
|
-
|
|
9811
|
-
|
|
9812
|
-
|
|
9813
|
-
|
|
9870
|
+
async list(limit = 20) {
|
|
9871
|
+
try {
|
|
9872
|
+
await ensureDir(this.dir);
|
|
9873
|
+
const indexed = await this.readIndex();
|
|
9874
|
+
if (indexed.length > 0) {
|
|
9875
|
+
indexed.sort((a, b) => {
|
|
9876
|
+
if (a.startedAt < b.startedAt) return 1;
|
|
9877
|
+
if (a.startedAt > b.startedAt) return -1;
|
|
9878
|
+
return a.id.localeCompare(b.id);
|
|
9879
|
+
});
|
|
9880
|
+
return indexed.slice(0, limit);
|
|
9881
|
+
}
|
|
9882
|
+
return await this.listFromDirectoryScan(limit);
|
|
9883
|
+
} catch {
|
|
9884
|
+
return [];
|
|
9814
9885
|
}
|
|
9815
9886
|
}
|
|
9816
|
-
return result;
|
|
9817
|
-
}
|
|
9818
|
-
var FileSessionWriter = class _FileSessionWriter {
|
|
9819
|
-
constructor(id, handle, startedAt, meta, events, opts = {}, traceId) {
|
|
9820
|
-
this.id = id;
|
|
9821
|
-
this.handle = handle;
|
|
9822
|
-
this.startedAt = startedAt;
|
|
9823
|
-
this.meta = meta;
|
|
9824
|
-
this.events = events;
|
|
9825
|
-
this.resumed = opts.resumed ?? false;
|
|
9826
|
-
this.manifestFile = opts.dir ? path5.join(opts.dir, `${path5.basename(id)}.summary.json`) : "";
|
|
9827
|
-
this.filePath = opts.filePath ?? "";
|
|
9828
|
-
this.secretScrubber = opts.secretScrubber;
|
|
9829
|
-
this.onCloseCb = opts.onClose;
|
|
9830
|
-
this.summary = {
|
|
9831
|
-
id,
|
|
9832
|
-
title: "(empty session)",
|
|
9833
|
-
startedAt,
|
|
9834
|
-
model: meta.model ?? "unknown",
|
|
9835
|
-
provider: meta.provider ?? "unknown",
|
|
9836
|
-
tokenTotal: 0
|
|
9837
|
-
};
|
|
9838
|
-
this.traceId = traceId;
|
|
9839
|
-
}
|
|
9840
|
-
id;
|
|
9841
|
-
handle;
|
|
9842
|
-
startedAt;
|
|
9843
|
-
meta;
|
|
9844
|
-
events;
|
|
9845
|
-
closed = false;
|
|
9846
|
-
closePromise = null;
|
|
9847
|
-
manifestFile;
|
|
9848
|
-
summary;
|
|
9849
|
-
tokenIn = 0;
|
|
9850
|
-
tokenOut = 0;
|
|
9851
|
-
filePath;
|
|
9852
|
-
get transcriptPath() {
|
|
9853
|
-
return this.filePath || void 0;
|
|
9854
|
-
}
|
|
9855
|
-
/**
|
|
9856
|
-
* Lazy session_start/session_resumed init, shared by all appenders.
|
|
9857
|
-
* A single promise (not a boolean) so a second append racing the first
|
|
9858
|
-
* can't push its event into the buffer BEFORE the first append's event —
|
|
9859
|
-
* every appender awaits the same init and resumes in FIFO call order.
|
|
9860
|
-
*/
|
|
9861
|
-
initPromise = null;
|
|
9862
|
-
ensureInit() {
|
|
9863
|
-
if (!this.initPromise) this.initPromise = this.writeSessionStartLazy();
|
|
9864
|
-
return this.initPromise;
|
|
9865
|
-
}
|
|
9866
|
-
resumed;
|
|
9867
|
-
appendFailCount = 0;
|
|
9868
|
-
lastAppendWarnAt = 0;
|
|
9869
|
-
secretScrubber;
|
|
9870
|
-
onCloseCb;
|
|
9871
|
-
/** Implements SessionWriter.traceId — propagated from ContextInit.traceId. */
|
|
9872
|
-
traceId;
|
|
9873
|
-
// ── Write buffer — batches events to reduce per-event disk I/O ─────────
|
|
9874
|
-
//
|
|
9875
|
-
// Every append() pushes the scrubbed event into an in-memory buffer instead
|
|
9876
|
-
// of calling handle.appendFile() synchronously. The buffer flushes to disk
|
|
9877
|
-
// when it reaches FLUSH_SIZE events OR after FLUSH_INTERVAL_MS of inactivity.
|
|
9878
|
-
// This cuts the number of disk writes by ~95% without changing the on-disk
|
|
9879
|
-
// format — the JSONL is still one JSON object per line.
|
|
9880
|
-
writeBuffer = [];
|
|
9881
|
-
flushTimer = null;
|
|
9882
|
-
static FLUSH_INTERVAL_MS = 500;
|
|
9883
|
-
static FLUSH_SIZE = 50;
|
|
9884
|
-
// ── Write serialization ─────────────────────────────────────────────────
|
|
9885
|
-
//
|
|
9886
|
-
// All disk writes are funneled through a FIFO promise chain. Without it,
|
|
9887
|
-
// a timer-driven flush racing an explicit flush()/close() issues two
|
|
9888
|
-
// concurrent appendFile() calls on the shared O_APPEND handle — the kernel
|
|
9889
|
-
// may complete them out of order (chronology breaks) or, for large
|
|
9890
|
-
// batches, interleave partial writes (torn JSONL lines). The chain keeps
|
|
9891
|
-
// exactly one write in flight; failures don't break the chain.
|
|
9892
|
-
writeChain = Promise.resolve();
|
|
9893
|
-
/** Enqueue a write on the FIFO chain. Resolves/rejects with that write. */
|
|
9894
|
-
enqueueWrite(data) {
|
|
9895
|
-
const write = this.writeChain.then(() => this.handle.appendFile(data, "utf8"));
|
|
9896
|
-
this.writeChain = write.then(
|
|
9897
|
-
() => void 0,
|
|
9898
|
-
() => void 0
|
|
9899
|
-
);
|
|
9900
|
-
return write;
|
|
9901
|
-
}
|
|
9902
|
-
// ── Enriched summary tracking ──────────────────────────────────────────
|
|
9903
|
-
iterationCount = 0;
|
|
9904
|
-
toolCallCount = 0;
|
|
9905
|
-
toolErrorCount = 0;
|
|
9906
|
-
toolBreakdown = {};
|
|
9907
|
-
fileChangeCount = 0;
|
|
9908
|
-
compactionCount = 0;
|
|
9909
|
-
outcome = void 0;
|
|
9910
9887
|
/**
|
|
9911
|
-
*
|
|
9912
|
-
*
|
|
9913
|
-
* `
|
|
9914
|
-
*
|
|
9915
|
-
*
|
|
9916
|
-
*
|
|
9888
|
+
* List sessions matching filter criteria, using the cached index.
|
|
9889
|
+
* Filters are applied BEFORE sorting and slicing, so the caller gets
|
|
9890
|
+
* exactly `limit` matching sessions — not a slice of a larger fetch.
|
|
9891
|
+
*
|
|
9892
|
+
* This avoids the DefaultSessionReader pattern of fetching 1000 sessions
|
|
9893
|
+
* then linear-filtering: the index is already in memory (readIndex
|
|
9894
|
+
* caches it), and the filter runs over the cached array without any
|
|
9895
|
+
* additional disk I/O.
|
|
9917
9896
|
*/
|
|
9918
|
-
|
|
9919
|
-
const
|
|
9920
|
-
if (!s) return event;
|
|
9921
|
-
if (event.type === "user_input") {
|
|
9922
|
-
return {
|
|
9923
|
-
...event,
|
|
9924
|
-
content: typeof event.content === "string" ? s.scrub(event.content) : s.scrubObject(event.content)
|
|
9925
|
-
};
|
|
9926
|
-
}
|
|
9927
|
-
if (event.type === "llm_response") {
|
|
9928
|
-
return { ...event, content: s.scrubObject(event.content) };
|
|
9929
|
-
}
|
|
9930
|
-
return event;
|
|
9931
|
-
}
|
|
9932
|
-
pendingFileSnapshots = [];
|
|
9933
|
-
/** Tracks open tool_use IDs during the current run to serialize on close for resume. */
|
|
9934
|
-
openToolUses = /* @__PURE__ */ new Set();
|
|
9935
|
-
recordFileChange(input) {
|
|
9936
|
-
this.pendingFileSnapshots.push(input);
|
|
9937
|
-
}
|
|
9938
|
-
get pendingToolUses() {
|
|
9939
|
-
return Array.from(this.openToolUses);
|
|
9940
|
-
}
|
|
9941
|
-
async writeSessionStartLazy() {
|
|
9942
|
-
const record = `${JSON.stringify({
|
|
9943
|
-
type: this.resumed ? "session_resumed" : "session_start",
|
|
9944
|
-
ts: this.startedAt,
|
|
9945
|
-
id: this.id,
|
|
9946
|
-
model: this.meta.model ?? "unknown",
|
|
9947
|
-
provider: this.meta.provider ?? "unknown"
|
|
9948
|
-
})}
|
|
9949
|
-
`;
|
|
9897
|
+
async listFiltered(criteria) {
|
|
9898
|
+
const limit = criteria.limit ?? 100;
|
|
9950
9899
|
try {
|
|
9951
|
-
await this.
|
|
9900
|
+
await ensureDir(this.dir);
|
|
9901
|
+
const indexed = await this.readIndex();
|
|
9902
|
+
if (indexed.length === 0) {
|
|
9903
|
+
const raw = await this.list(Math.max(limit, 100));
|
|
9904
|
+
return raw.filter((s) => matchesSessionFilter(s, criteria)).slice(0, limit);
|
|
9905
|
+
}
|
|
9906
|
+
const filtered = indexed.filter((s) => matchesSessionFilter(s, criteria));
|
|
9907
|
+
filtered.sort((a, b) => {
|
|
9908
|
+
if (a.startedAt < b.startedAt) return 1;
|
|
9909
|
+
if (a.startedAt > b.startedAt) return -1;
|
|
9910
|
+
return a.id.localeCompare(b.id);
|
|
9911
|
+
});
|
|
9912
|
+
return filtered.slice(0, limit);
|
|
9952
9913
|
} catch {
|
|
9914
|
+
return [];
|
|
9953
9915
|
}
|
|
9954
9916
|
}
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
|
|
9958
|
-
|
|
9959
|
-
|
|
9960
|
-
|
|
9961
|
-
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
|
|
9965
|
-
|
|
9966
|
-
|
|
9967
|
-
|
|
9968
|
-
|
|
9969
|
-
|
|
9970
|
-
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
if (this.flushTimer) {
|
|
9981
|
-
clearTimeout(this.flushTimer);
|
|
9982
|
-
this.flushTimer = null;
|
|
9983
|
-
}
|
|
9984
|
-
await this.flushBuffer();
|
|
9985
|
-
} else {
|
|
9986
|
-
this.scheduleFlush();
|
|
9987
|
-
}
|
|
9988
|
-
}
|
|
9989
|
-
/**
|
|
9990
|
-
* Flush buffered events to disk immediately. Critical events
|
|
9991
|
-
* (user_input, llm_response) call this so they survive SIGKILL/crash
|
|
9992
|
-
* instead of sitting in the in-memory buffer for up to 500ms.
|
|
9993
|
-
*
|
|
9994
|
-
* Idempotent — cancels any pending timer and writes whatever has
|
|
9995
|
-
* accumulated in the buffer. Safe to call even when the buffer
|
|
9996
|
-
* is empty (no-op).
|
|
9997
|
-
*/
|
|
9998
|
-
async flush() {
|
|
9999
|
-
if (this.flushTimer) {
|
|
10000
|
-
clearTimeout(this.flushTimer);
|
|
10001
|
-
this.flushTimer = null;
|
|
9917
|
+
// ── Session index (_index.jsonl) ─────────────────────────────────────────
|
|
9918
|
+
//
|
|
9919
|
+
// One JSON line per closed session, appended atomically on close().
|
|
9920
|
+
// When a session is deleted, a tombstone {action:"delete",id:"..."} is
|
|
9921
|
+
// appended. On read, tombstones filter out matching session entries.
|
|
9922
|
+
// This keeps listing O(lines-in-index) instead of O(files-on-disk).
|
|
9923
|
+
//
|
|
9924
|
+
// The index auto-compacts every N appends to prevent unbounded growth
|
|
9925
|
+
// from tombstones and duplicate entries (resume cycles).
|
|
9926
|
+
indexAppendCount = 0;
|
|
9927
|
+
static COMPACT_EVERY = 30;
|
|
9928
|
+
/** Append a session summary to the index. */
|
|
9929
|
+
async appendToIndex(summary) {
|
|
9930
|
+
try {
|
|
9931
|
+
await ensureDir(this.dir);
|
|
9932
|
+
const line = JSON.stringify(summary) + "\n";
|
|
9933
|
+
await fsp7.appendFile(this.indexFile, line, "utf8");
|
|
9934
|
+
this._indexCache = null;
|
|
9935
|
+
this.invalidateShardManifestBySessionId(summary.id);
|
|
9936
|
+
this.indexAppendCount++;
|
|
9937
|
+
if (this.indexAppendCount >= _DefaultSessionStore.COMPACT_EVERY) {
|
|
9938
|
+
await this.compactIndex();
|
|
9939
|
+
this.indexAppendCount = 0;
|
|
9940
|
+
}
|
|
9941
|
+
} catch {
|
|
10002
9942
|
}
|
|
10003
|
-
await this.flushBuffer();
|
|
10004
9943
|
}
|
|
10005
|
-
/**
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
10009
|
-
|
|
10010
|
-
|
|
10011
|
-
|
|
10012
|
-
|
|
9944
|
+
/** Append a tombstone entry for a deleted session. */
|
|
9945
|
+
async writeTombstone(id) {
|
|
9946
|
+
try {
|
|
9947
|
+
await ensureDir(this.dir);
|
|
9948
|
+
const line = JSON.stringify({ action: "delete", id }) + "\n";
|
|
9949
|
+
await fsp7.appendFile(this.indexFile, line, "utf8");
|
|
9950
|
+
this._indexCache = null;
|
|
9951
|
+
this.invalidateShardManifestBySessionId(id);
|
|
9952
|
+
this.indexAppendCount++;
|
|
9953
|
+
} catch {
|
|
9954
|
+
}
|
|
10013
9955
|
}
|
|
10014
9956
|
/**
|
|
10015
|
-
*
|
|
10016
|
-
*
|
|
10017
|
-
* append path used — one warning every 5s with a suppressed count.
|
|
10018
|
-
* On failure the buffer is cleared (events are best-effort, same as
|
|
10019
|
-
* the old per-event path where a failed write was silently dropped).
|
|
9957
|
+
* Compact the index: read all entries, drop tombstones, deduplicate
|
|
9958
|
+
* (keep latest per session), and rewrite. Atomic via temp+rename.
|
|
10020
9959
|
*/
|
|
10021
|
-
async
|
|
10022
|
-
if (this.writeBuffer.length === 0) return;
|
|
10023
|
-
const eventCount = this.writeBuffer.length;
|
|
10024
|
-
const batch = this.writeBuffer.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
10025
|
-
this.writeBuffer = [];
|
|
9960
|
+
async compactIndex() {
|
|
10026
9961
|
const t0 = Date.now();
|
|
10027
9962
|
let outcome = "success";
|
|
10028
9963
|
let errorMsg;
|
|
10029
9964
|
try {
|
|
10030
|
-
await this.
|
|
9965
|
+
const entries = await this.readIndex();
|
|
9966
|
+
if (entries.length === 0) return;
|
|
9967
|
+
const tmp = `${this.indexFile}.compact.tmp`;
|
|
9968
|
+
const lines = entries.map((s) => JSON.stringify(s)).join("\n") + "\n";
|
|
9969
|
+
await fsp7.writeFile(tmp, lines, "utf8");
|
|
9970
|
+
await fsp7.rename(tmp, this.indexFile);
|
|
9971
|
+
this._indexCache = null;
|
|
10031
9972
|
} catch (err) {
|
|
10032
9973
|
outcome = "failure";
|
|
10033
9974
|
errorMsg = toErrorMessage(err);
|
|
10034
|
-
this.appendFailCount += eventCount;
|
|
10035
|
-
const now = Date.now();
|
|
10036
|
-
if (now - this.lastAppendWarnAt > 5e3) {
|
|
10037
|
-
const suppressed = this.appendFailCount - 1;
|
|
10038
|
-
const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
|
|
10039
|
-
console.warn(
|
|
10040
|
-
"[session] flush failed:",
|
|
10041
|
-
toErrorMessage(err),
|
|
10042
|
-
tail
|
|
10043
|
-
);
|
|
10044
|
-
this.lastAppendWarnAt = now;
|
|
10045
|
-
this.appendFailCount = 0;
|
|
10046
|
-
}
|
|
10047
9975
|
} finally {
|
|
10048
|
-
this.
|
|
10049
|
-
sessionId: this.id,
|
|
10050
|
-
store: "session",
|
|
10051
|
-
filePath: this.filePath,
|
|
10052
|
-
operation: "flush",
|
|
10053
|
-
outcome,
|
|
10054
|
-
durationMs: Date.now() - t0,
|
|
10055
|
-
...errorMsg !== void 0 ? { error: errorMsg } : {},
|
|
10056
|
-
...eventCount !== void 0 ? { eventCount } : {},
|
|
10057
|
-
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
10058
|
-
});
|
|
9976
|
+
this.emitWrite("~compact~", this.indexFile, "compact", outcome, Date.now() - t0, void 0, errorMsg);
|
|
10059
9977
|
}
|
|
10060
9978
|
}
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
9979
|
+
/**
|
|
9980
|
+
* Read the index file and return deduplicated session summaries.
|
|
9981
|
+
* Entries with a matching tombstone are filtered out.
|
|
9982
|
+
* Returns empty array when the index doesn't exist or is corrupt.
|
|
9983
|
+
*/
|
|
9984
|
+
async readIndex() {
|
|
9985
|
+
let stat7;
|
|
9986
|
+
try {
|
|
9987
|
+
const s = await fsp7.stat(this.indexFile);
|
|
9988
|
+
stat7 = { mtimeMs: s.mtimeMs, size: s.size };
|
|
9989
|
+
} catch {
|
|
9990
|
+
this._indexCache = null;
|
|
9991
|
+
return [];
|
|
9992
|
+
}
|
|
9993
|
+
if (this._indexCache !== null && this._indexCache.mtimeMs === stat7.mtimeMs && this._indexCache.size === stat7.size) {
|
|
9994
|
+
return [...this._indexCache.summaries];
|
|
9995
|
+
}
|
|
9996
|
+
let raw;
|
|
9997
|
+
try {
|
|
9998
|
+
raw = await fsp7.readFile(this.indexFile, "utf8");
|
|
9999
|
+
} catch {
|
|
10000
|
+
this._indexCache = null;
|
|
10001
|
+
return [];
|
|
10002
|
+
}
|
|
10003
|
+
const deleted = /* @__PURE__ */ new Set();
|
|
10004
|
+
const seen = /* @__PURE__ */ new Map();
|
|
10005
|
+
for (const line of raw.split("\n")) {
|
|
10006
|
+
if (!line.trim()) continue;
|
|
10007
|
+
try {
|
|
10008
|
+
const entry = JSON.parse(line);
|
|
10009
|
+
if (entry.action === "delete" && entry.id) {
|
|
10010
|
+
deleted.add(entry.id);
|
|
10011
|
+
seen.delete(entry.id);
|
|
10012
|
+
continue;
|
|
10013
|
+
}
|
|
10014
|
+
if (entry.id && !deleted.has(entry.id)) {
|
|
10015
|
+
seen.set(entry.id, entry);
|
|
10016
|
+
}
|
|
10017
|
+
} catch {
|
|
10065
10018
|
}
|
|
10066
10019
|
}
|
|
10067
|
-
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
|
|
10076
|
-
|
|
10020
|
+
const summaries = Array.from(seen.values());
|
|
10021
|
+
this._indexCache = { ...stat7, summaries };
|
|
10022
|
+
return [...summaries];
|
|
10023
|
+
}
|
|
10024
|
+
/**
|
|
10025
|
+
* Rebuild the index from disk by scanning all sessions and writing a
|
|
10026
|
+
* fresh _index.jsonl. Useful after manual cleanup or index corruption.
|
|
10027
|
+
*/
|
|
10028
|
+
async rebuildIndex() {
|
|
10029
|
+
const ids = await this.collectSessionIds(this.dir);
|
|
10030
|
+
const summaries = await Promise.all(ids.map((id) => this.summaryFor(id).catch(() => null)));
|
|
10031
|
+
const valid = summaries.filter((s) => s !== null);
|
|
10032
|
+
const tmp = `${this.indexFile}.tmp`;
|
|
10033
|
+
const lines = valid.map((s) => JSON.stringify(s)).join("\n") + "\n";
|
|
10034
|
+
await fsp7.writeFile(tmp, lines, "utf8");
|
|
10035
|
+
await fsp7.rename(tmp, this.indexFile);
|
|
10036
|
+
this._indexCache = null;
|
|
10037
|
+
return valid.length;
|
|
10038
|
+
}
|
|
10039
|
+
async listFromDirectoryScan(limit) {
|
|
10040
|
+
const shardKeys = await this.collectShardKeys();
|
|
10041
|
+
const shardEntries = await mapWithConcurrency(
|
|
10042
|
+
shardKeys,
|
|
10043
|
+
_DefaultSessionStore.LIST_SCAN_CONCURRENCY,
|
|
10044
|
+
async (shardKey) => await this.readOrBuildShardManifest(shardKey)
|
|
10045
|
+
);
|
|
10046
|
+
const out = [];
|
|
10047
|
+
for (const entry of shardEntries) {
|
|
10048
|
+
for (const summary of entry.summaries) {
|
|
10049
|
+
out.push({ summary, needsBackfill: false });
|
|
10077
10050
|
}
|
|
10078
|
-
} else if (event.type === "file_snapshot") {
|
|
10079
|
-
this.fileChangeCount += event.files.length;
|
|
10080
|
-
} else if (event.type === "compaction") {
|
|
10081
|
-
this.compactionCount++;
|
|
10082
10051
|
}
|
|
10083
|
-
|
|
10084
|
-
|
|
10052
|
+
out.sort((a, b) => compareSessionSummaries(a.summary, b.summary));
|
|
10053
|
+
const selected = out.slice(0, limit);
|
|
10054
|
+
const summaries = await mapWithConcurrency(
|
|
10055
|
+
selected,
|
|
10056
|
+
Math.min(_DefaultSessionStore.LIST_SCAN_CONCURRENCY, Math.max(1, limit)),
|
|
10057
|
+
async (candidate) => candidate.summary
|
|
10058
|
+
);
|
|
10059
|
+
return summaries.filter((s) => s !== null);
|
|
10060
|
+
}
|
|
10061
|
+
async collectShardKeys() {
|
|
10062
|
+
let entries;
|
|
10063
|
+
try {
|
|
10064
|
+
entries = await fsp7.readdir(this.dir, { withFileTypes: true });
|
|
10065
|
+
} catch {
|
|
10066
|
+
return [""];
|
|
10085
10067
|
}
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
this.summary = { ...this.summary, tokenTotal: this.tokenIn + this.tokenOut };
|
|
10092
|
-
} else if (event.type === "session_end") {
|
|
10093
|
-
const total = event.usage.input + event.usage.output;
|
|
10094
|
-
if (total > 0) this.summary = { ...this.summary, tokenTotal: total };
|
|
10095
|
-
} else if (event.type === "in_flight_start") {
|
|
10096
|
-
this.iterationCount++;
|
|
10068
|
+
const shardKeys = [""];
|
|
10069
|
+
for (const entry of entries) {
|
|
10070
|
+
if (entry.name.startsWith(".") && entry.name !== ".wrongstack") continue;
|
|
10071
|
+
if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments") continue;
|
|
10072
|
+
if (entry.isDirectory()) shardKeys.push(entry.name);
|
|
10097
10073
|
}
|
|
10074
|
+
return shardKeys;
|
|
10098
10075
|
}
|
|
10099
|
-
async
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10076
|
+
async readOrBuildShardManifest(shardKey) {
|
|
10077
|
+
const cached = this.shardManifestCache.get(shardKey);
|
|
10078
|
+
if (cached) return cached;
|
|
10079
|
+
const manifestPath = this.shardManifestPath(shardKey);
|
|
10080
|
+
try {
|
|
10081
|
+
const raw = await fsp7.readFile(manifestPath, "utf8");
|
|
10082
|
+
const parsed = JSON.parse(raw);
|
|
10083
|
+
const entry2 = {
|
|
10084
|
+
summaries: Array.isArray(parsed.summaries) ? parsed.summaries : [],
|
|
10085
|
+
ids: Array.isArray(parsed.ids) ? parsed.ids : []
|
|
10086
|
+
};
|
|
10087
|
+
this.shardManifestCache.set(shardKey, entry2);
|
|
10088
|
+
return entry2;
|
|
10089
|
+
} catch {
|
|
10090
|
+
}
|
|
10091
|
+
const refs = await this.collectSessionFilesInShard(shardKey);
|
|
10092
|
+
const candidates = await mapWithConcurrency(
|
|
10093
|
+
refs,
|
|
10094
|
+
_DefaultSessionStore.LIST_SCAN_CONCURRENCY,
|
|
10095
|
+
async (ref) => {
|
|
10096
|
+
const manifest = await this.readSummaryManifest(ref.id);
|
|
10097
|
+
if (manifest) return { summary: manifest, needsBackfill: false };
|
|
10098
|
+
const summary = await this.summaryHeaderFor(ref);
|
|
10099
|
+
if (!summary) return null;
|
|
10100
|
+
const hydrated = await this.summaryFor(summary.id).catch(() => summary);
|
|
10101
|
+
return { summary: hydrated, needsBackfill: false };
|
|
10102
|
+
}
|
|
10103
|
+
);
|
|
10104
|
+
const summaries = candidates.filter((candidate) => candidate !== null).map((candidate) => candidate.summary);
|
|
10105
|
+
summaries.sort(compareSessionSummaries);
|
|
10106
|
+
const entry = { summaries, ids: summaries.map((summary) => summary.id) };
|
|
10107
|
+
this.shardManifestCache.set(shardKey, entry);
|
|
10108
|
+
await atomicWrite(manifestPath, JSON.stringify(entry), { mode: 384 }).catch(() => void 0);
|
|
10109
|
+
return entry;
|
|
10110
|
+
}
|
|
10111
|
+
async collectSessionFilesInShard(shardKey) {
|
|
10112
|
+
const dir = shardKey ? path6.join(this.dir, shardKey) : this.dir;
|
|
10113
|
+
const entries = await this.collectSessionFiles(dir, shardKey);
|
|
10114
|
+
return shardKey ? entries.filter((entry) => entry.id.startsWith(`${shardKey}/`)) : entries.filter((entry) => !entry.id.includes("/"));
|
|
10103
10115
|
}
|
|
10104
|
-
async
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10116
|
+
async collectSessionFiles(dir, prefix = "", depth = 0) {
|
|
10117
|
+
let entries;
|
|
10118
|
+
try {
|
|
10119
|
+
entries = await fsp7.readdir(dir, { withFileTypes: true });
|
|
10120
|
+
} catch {
|
|
10121
|
+
return [];
|
|
10109
10122
|
}
|
|
10110
|
-
|
|
10111
|
-
|
|
10112
|
-
|
|
10113
|
-
|
|
10114
|
-
|
|
10115
|
-
|
|
10116
|
-
|
|
10117
|
-
|
|
10118
|
-
|
|
10119
|
-
|
|
10120
|
-
|
|
10121
|
-
|
|
10122
|
-
|
|
10123
|
-
|
|
10124
|
-
|
|
10125
|
-
|
|
10126
|
-
|
|
10127
|
-
|
|
10128
|
-
|
|
10129
|
-
}
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
|
|
10133
|
-
|
|
10134
|
-
|
|
10135
|
-
|
|
10136
|
-
|
|
10137
|
-
|
|
10138
|
-
|
|
10139
|
-
|
|
10140
|
-
|
|
10141
|
-
|
|
10142
|
-
|
|
10123
|
+
const dirEntries = [];
|
|
10124
|
+
const files = [];
|
|
10125
|
+
for (const entry of entries) {
|
|
10126
|
+
if (entry.name.startsWith(".") && entry.name !== ".wrongstack") continue;
|
|
10127
|
+
if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments")
|
|
10128
|
+
continue;
|
|
10129
|
+
if (entry.isDirectory()) {
|
|
10130
|
+
dirEntries.push(entry);
|
|
10131
|
+
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
10132
|
+
if (entry.name === "_index.jsonl") continue;
|
|
10133
|
+
const base = entry.name.replace(/\.jsonl$/, "");
|
|
10134
|
+
const id = prefix ? `${prefix}/${base}` : base;
|
|
10135
|
+
files.push({ id, filePath: path6.join(dir, entry.name) });
|
|
10136
|
+
}
|
|
10137
|
+
}
|
|
10138
|
+
const childFileArrays = await Promise.all(
|
|
10139
|
+
dirEntries.map((entry) => {
|
|
10140
|
+
const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
|
|
10141
|
+
return this.collectSessionFiles(path6.join(dir, entry.name), childPrefix, depth + 1);
|
|
10142
|
+
})
|
|
10143
|
+
);
|
|
10144
|
+
return [...childFileArrays.flat(), ...files];
|
|
10145
|
+
}
|
|
10146
|
+
/** Recursively collect session IDs from date-shard subdirectories.
|
|
10147
|
+
* IDs include the date-prefix path (e.g. "2026-06-06/17-46-57Z_…").
|
|
10148
|
+
* Skips `.jsonl`/`.summary.json` root files, dot-files, and
|
|
10149
|
+
* sub-directories that belong to fleet/subagent sessions. */
|
|
10150
|
+
async collectSessionIds(dir, prefix = "", depth = 0) {
|
|
10151
|
+
let entries;
|
|
10152
|
+
try {
|
|
10153
|
+
entries = await fsp7.readdir(dir, { withFileTypes: true });
|
|
10154
|
+
} catch {
|
|
10155
|
+
return [];
|
|
10156
|
+
}
|
|
10157
|
+
const dirEntries = [];
|
|
10158
|
+
const fileIds = [];
|
|
10159
|
+
for (const entry of entries) {
|
|
10160
|
+
if (entry.name.startsWith(".") && entry.name !== ".wrongstack") continue;
|
|
10161
|
+
if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments")
|
|
10162
|
+
continue;
|
|
10163
|
+
if (entry.isDirectory()) {
|
|
10164
|
+
dirEntries.push(entry);
|
|
10165
|
+
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
10166
|
+
if (entry.name === "_index.jsonl") continue;
|
|
10167
|
+
const base = entry.name.replace(/\.jsonl$/, "");
|
|
10168
|
+
fileIds.push(prefix ? `${prefix}/${base}` : base);
|
|
10143
10169
|
}
|
|
10144
10170
|
}
|
|
10145
|
-
const
|
|
10146
|
-
|
|
10147
|
-
|
|
10171
|
+
const childIdArrays = await Promise.all(
|
|
10172
|
+
dirEntries.map((entry) => {
|
|
10173
|
+
const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
|
|
10174
|
+
return this.collectSessionIds(path6.join(dir, entry.name), childPrefix, depth + 1);
|
|
10175
|
+
})
|
|
10176
|
+
);
|
|
10177
|
+
return [...childIdArrays.flat(), ...fileIds];
|
|
10178
|
+
}
|
|
10179
|
+
async summaryFor(id) {
|
|
10180
|
+
const manifest = this.sessionPath(id, ".summary.json");
|
|
10181
|
+
const t0 = Date.now();
|
|
10182
|
+
let outcome = "success";
|
|
10183
|
+
let errorMsg;
|
|
10184
|
+
const fromManifest = await this.readSummaryManifest(id, t0);
|
|
10185
|
+
if (fromManifest) return fromManifest;
|
|
10148
10186
|
try {
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
10187
|
+
const full = this.sessionPath(id, ".jsonl");
|
|
10188
|
+
const stat7 = await fsp7.stat(full);
|
|
10189
|
+
const summary = await this.summarize(id, stat7.mtime.toISOString());
|
|
10190
|
+
await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
|
|
10191
|
+
const msg = toErrorMessage(err);
|
|
10192
|
+
this.emitError(id, manifest, "summary_fallback", msg, true);
|
|
10193
|
+
console.warn(JSON.stringify({
|
|
10194
|
+
level: "warn",
|
|
10195
|
+
event: "session_store.manifest_write_failed",
|
|
10196
|
+
sessionId: id,
|
|
10197
|
+
message: msg,
|
|
10198
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
10199
|
+
}));
|
|
10163
10200
|
});
|
|
10201
|
+
outcome = "failure";
|
|
10202
|
+
errorMsg = "summary fallback \xE2\u20AC\u201D manifest rebuilt";
|
|
10203
|
+
this.emitRead(id, manifest, "summary", outcome, Date.now() - t0, errorMsg);
|
|
10204
|
+
return summary;
|
|
10205
|
+
} catch (err) {
|
|
10206
|
+
outcome = "failure";
|
|
10207
|
+
errorMsg = toErrorMessage(err);
|
|
10208
|
+
this.emitRead(id, manifest, "summary", outcome, Date.now() - t0, errorMsg);
|
|
10209
|
+
return {
|
|
10210
|
+
id,
|
|
10211
|
+
title: "(damaged)",
|
|
10212
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10213
|
+
model: "unknown",
|
|
10214
|
+
provider: "unknown",
|
|
10215
|
+
tokenTotal: 0
|
|
10216
|
+
};
|
|
10164
10217
|
}
|
|
10218
|
+
}
|
|
10219
|
+
async readSummaryManifest(id, startTime = Date.now()) {
|
|
10220
|
+
const manifest = this.sessionPath(id, ".summary.json");
|
|
10165
10221
|
try {
|
|
10166
|
-
await
|
|
10222
|
+
const raw = await fsp7.readFile(manifest, "utf8");
|
|
10223
|
+
this.emitRead(id, manifest, "summary", "success", Date.now() - startTime);
|
|
10224
|
+
return JSON.parse(raw);
|
|
10167
10225
|
} catch {
|
|
10226
|
+
return null;
|
|
10168
10227
|
}
|
|
10169
10228
|
}
|
|
10170
|
-
async
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
await
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
|
|
10183
|
-
|
|
10184
|
-
|
|
10185
|
-
|
|
10186
|
-
|
|
10187
|
-
});
|
|
10188
|
-
}
|
|
10189
|
-
async writeFileSnapshot(promptIndex, files) {
|
|
10190
|
-
await this.append({
|
|
10191
|
-
type: "file_snapshot",
|
|
10192
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10193
|
-
promptIndex,
|
|
10194
|
-
files
|
|
10195
|
-
});
|
|
10196
|
-
}
|
|
10197
|
-
/**
|
|
10198
|
-
* Truncate the session file to the checkpoint with the given promptIndex,
|
|
10199
|
-
* removing all events that follow it. Uses a single-pass byte-offset scan
|
|
10200
|
-
* so post-checkpoint content is never read or parsed — O(1) memory instead
|
|
10201
|
-
* of O(N) JSON.parse calls over the full file.
|
|
10202
|
-
*/
|
|
10203
|
-
async truncateToCheckpoint(targetPromptIndex) {
|
|
10204
|
-
if (!this.filePath) return 0;
|
|
10205
|
-
if (this.flushTimer) {
|
|
10206
|
-
clearTimeout(this.flushTimer);
|
|
10207
|
-
this.flushTimer = null;
|
|
10229
|
+
async summaryHeaderFor(ref) {
|
|
10230
|
+
let mtime = (/* @__PURE__ */ new Date(0)).toISOString();
|
|
10231
|
+
try {
|
|
10232
|
+
const stat7 = await fsp7.stat(ref.filePath);
|
|
10233
|
+
if (!stat7.isFile()) {
|
|
10234
|
+
return {
|
|
10235
|
+
id: ref.id,
|
|
10236
|
+
title: "(damaged)",
|
|
10237
|
+
startedAt: stat7.mtime.toISOString(),
|
|
10238
|
+
model: "unknown",
|
|
10239
|
+
provider: "unknown",
|
|
10240
|
+
tokenTotal: 0
|
|
10241
|
+
};
|
|
10242
|
+
}
|
|
10243
|
+
mtime = stat7.mtime.toISOString();
|
|
10244
|
+
} catch {
|
|
10245
|
+
return null;
|
|
10208
10246
|
}
|
|
10209
|
-
await this.flushBuffer();
|
|
10210
|
-
await this.writeChain;
|
|
10211
|
-
const CHUNK_SIZE = 65536;
|
|
10212
|
-
let fd;
|
|
10213
|
-
let fileOffset = 0;
|
|
10214
|
-
let lineStartOffset = 0;
|
|
10215
|
-
let checkpointByteOffset = -1;
|
|
10216
|
-
let removedCount = 0;
|
|
10217
|
-
let targetCheckpointSeen = false;
|
|
10218
10247
|
try {
|
|
10219
|
-
|
|
10220
|
-
|
|
10221
|
-
|
|
10222
|
-
|
|
10223
|
-
|
|
10224
|
-
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
|
|
10229
|
-
break;
|
|
10230
|
-
}
|
|
10231
|
-
if (checkpointByteOffset !== -1) {
|
|
10232
|
-
removedCount++;
|
|
10233
|
-
} else {
|
|
10234
|
-
const lineBytes = buf.subarray(chunkPos, idx);
|
|
10235
|
-
const line = new TextDecoder("utf-8", { fatal: false }).decode(lineBytes);
|
|
10236
|
-
if (line.trim()) {
|
|
10237
|
-
try {
|
|
10238
|
-
const event = JSON.parse(line);
|
|
10239
|
-
if (event.type === "checkpoint") {
|
|
10240
|
-
if (event.promptIndex === targetPromptIndex) {
|
|
10241
|
-
checkpointByteOffset = lineStartOffset;
|
|
10242
|
-
targetCheckpointSeen = true;
|
|
10243
|
-
} else if (event.promptIndex !== void 0 && event.promptIndex > targetPromptIndex) {
|
|
10244
|
-
checkpointByteOffset = lineStartOffset;
|
|
10245
|
-
}
|
|
10246
|
-
} else if (targetCheckpointSeen && event.promptIndex !== void 0 && event.promptIndex > targetPromptIndex) {
|
|
10247
|
-
removedCount++;
|
|
10248
|
-
} else if (targetCheckpointSeen && event.promptIndex === void 0) {
|
|
10249
|
-
removedCount++;
|
|
10250
|
-
} else if (!targetCheckpointSeen && event.promptIndex === void 0) {
|
|
10251
|
-
removedCount++;
|
|
10252
|
-
} else if (!targetCheckpointSeen && event.promptIndex !== void 0 && event.promptIndex > targetPromptIndex) {
|
|
10253
|
-
removedCount++;
|
|
10254
|
-
}
|
|
10255
|
-
} catch {
|
|
10256
|
-
}
|
|
10257
|
-
}
|
|
10258
|
-
}
|
|
10259
|
-
chunkPos = idx + 1;
|
|
10260
|
-
lineStartOffset = fileOffset + chunkPos;
|
|
10261
|
-
}
|
|
10262
|
-
fileOffset += bytesRead;
|
|
10263
|
-
if (chunkPos >= bytesRead) {
|
|
10264
|
-
lineStartOffset = fileOffset;
|
|
10248
|
+
for await (const event of this.iterSessionEvents(ref.filePath)) {
|
|
10249
|
+
if (event.type === "session_start") {
|
|
10250
|
+
return {
|
|
10251
|
+
id: ref.id,
|
|
10252
|
+
title: "(empty session)",
|
|
10253
|
+
startedAt: event.ts,
|
|
10254
|
+
model: event.model ?? "unknown",
|
|
10255
|
+
provider: event.provider ?? "unknown",
|
|
10256
|
+
tokenTotal: 0
|
|
10257
|
+
};
|
|
10265
10258
|
}
|
|
10266
10259
|
}
|
|
10267
|
-
|
|
10268
|
-
|
|
10260
|
+
return {
|
|
10261
|
+
id: ref.id,
|
|
10262
|
+
title: "(empty session)",
|
|
10263
|
+
startedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
10264
|
+
model: "unknown",
|
|
10265
|
+
provider: "unknown",
|
|
10266
|
+
tokenTotal: 0
|
|
10267
|
+
};
|
|
10268
|
+
} catch {
|
|
10269
|
+
return {
|
|
10270
|
+
id: ref.id,
|
|
10271
|
+
title: "(damaged)",
|
|
10272
|
+
startedAt: mtime,
|
|
10273
|
+
model: "unknown",
|
|
10274
|
+
provider: "unknown",
|
|
10275
|
+
tokenTotal: 0
|
|
10276
|
+
};
|
|
10269
10277
|
}
|
|
10270
|
-
|
|
10271
|
-
|
|
10272
|
-
|
|
10273
|
-
|
|
10274
|
-
|
|
10275
|
-
|
|
10276
|
-
|
|
10277
|
-
|
|
10278
|
-
|
|
10279
|
-
|
|
10280
|
-
|
|
10281
|
-
|
|
10282
|
-
|
|
10283
|
-
|
|
10284
|
-
|
|
10285
|
-
|
|
10278
|
+
}
|
|
10279
|
+
/**
|
|
10280
|
+
* Delete a session and all associated files: JSONL, summary, plan/todos
|
|
10281
|
+
* sidecars, and the session directory (fleet.json, shared/, subagents/).
|
|
10282
|
+
*
|
|
10283
|
+
* Individual file deletions are best-effort (logged as structured warnings),
|
|
10284
|
+
* but a tombstone is always written so readIndex() filters this session out.
|
|
10285
|
+
* If the session directory itself can't be removed, the error is surfaced
|
|
10286
|
+
* to the caller so prune() can report it.
|
|
10287
|
+
*/
|
|
10288
|
+
async deleteSession(id) {
|
|
10289
|
+
const jsonlPath = this.sessionPath(id, ".jsonl");
|
|
10290
|
+
const summaryPath = this.sessionPath(id, ".summary.json");
|
|
10291
|
+
const shardDir = path6.dirname(path6.join(this.dir, id));
|
|
10292
|
+
const base = path6.basename(id);
|
|
10293
|
+
const sessDir = path6.join(shardDir, base);
|
|
10294
|
+
const deletions = [
|
|
10295
|
+
fsp7.unlink(jsonlPath),
|
|
10296
|
+
fsp7.unlink(summaryPath),
|
|
10297
|
+
fsp7.unlink(path6.join(shardDir, `${base}.plan.json`)),
|
|
10298
|
+
fsp7.unlink(path6.join(shardDir, `${base}.todos.json`))
|
|
10299
|
+
];
|
|
10300
|
+
const results = await Promise.allSettled(deletions);
|
|
10301
|
+
for (const r of results) {
|
|
10302
|
+
if (r.status === "rejected") {
|
|
10303
|
+
const msg = r.reason instanceof Error ? r.reason.message : String(r.reason);
|
|
10304
|
+
if (r.reason?.code !== "ENOENT") {
|
|
10305
|
+
console.warn(JSON.stringify({
|
|
10306
|
+
level: "warn",
|
|
10307
|
+
event: "session_store.delete_failed",
|
|
10308
|
+
sessionId: id,
|
|
10309
|
+
message: msg,
|
|
10310
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
10311
|
+
}));
|
|
10286
10312
|
}
|
|
10287
|
-
} else {
|
|
10288
|
-
newlineAfterCheckpoint = totalSize;
|
|
10289
10313
|
}
|
|
10290
|
-
|
|
10314
|
+
}
|
|
10315
|
+
await fsp7.rm(sessDir, { recursive: true, force: true }).catch((err) => {
|
|
10316
|
+
console.warn(JSON.stringify({
|
|
10317
|
+
level: "warn",
|
|
10318
|
+
event: "session_store.rmdir_failed",
|
|
10319
|
+
sessionId: id,
|
|
10320
|
+
message: toErrorMessage(err),
|
|
10321
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
10322
|
+
}));
|
|
10323
|
+
});
|
|
10324
|
+
await this.writeTombstone(id);
|
|
10325
|
+
}
|
|
10326
|
+
async delete(id) {
|
|
10327
|
+
await this.deleteSession(id);
|
|
10328
|
+
}
|
|
10329
|
+
async prune(maxAgeDays = 30) {
|
|
10330
|
+
const cutoff = Date.now() - maxAgeDays * 864e5;
|
|
10331
|
+
let deleted = 0;
|
|
10332
|
+
let activeSessionId = null;
|
|
10333
|
+
try {
|
|
10334
|
+
const raw = await fsp7.readFile(path6.join(this.dir, "active.json"), "utf8");
|
|
10335
|
+
const active = JSON.parse(raw);
|
|
10336
|
+
activeSessionId = active.sessionId ?? null;
|
|
10337
|
+
} catch {
|
|
10338
|
+
}
|
|
10339
|
+
const isPrunableJsonl = (name) => name.endsWith(".jsonl") && name !== "_index.jsonl" && name !== "_mailbox.jsonl" && !name.endsWith(".replay.jsonl") && !name.endsWith(".audit.jsonl");
|
|
10340
|
+
const pruneFile = async (dir, name, prefix) => {
|
|
10341
|
+
const jsonlPath = path6.join(dir, name);
|
|
10291
10342
|
try {
|
|
10292
|
-
|
|
10293
|
-
|
|
10294
|
-
|
|
10295
|
-
|
|
10296
|
-
|
|
10297
|
-
|
|
10298
|
-
|
|
10299
|
-
|
|
10300
|
-
|
|
10301
|
-
|
|
10302
|
-
|
|
10303
|
-
|
|
10304
|
-
|
|
10305
|
-
|
|
10306
|
-
|
|
10307
|
-
|
|
10308
|
-
|
|
10309
|
-
|
|
10310
|
-
|
|
10343
|
+
const stat7 = await fsp7.stat(jsonlPath);
|
|
10344
|
+
if (stat7.mtimeMs >= cutoff) return;
|
|
10345
|
+
} catch {
|
|
10346
|
+
return;
|
|
10347
|
+
}
|
|
10348
|
+
const base = name.replace(/\.jsonl$/, "");
|
|
10349
|
+
const id = prefix ? `${prefix}/${base}` : base;
|
|
10350
|
+
if (activeSessionId && id === activeSessionId) return;
|
|
10351
|
+
await this.deleteSession(id);
|
|
10352
|
+
deleted++;
|
|
10353
|
+
};
|
|
10354
|
+
const entries = await fsp7.readdir(this.dir, { withFileTypes: true }).catch(() => []);
|
|
10355
|
+
for (const entry of entries) {
|
|
10356
|
+
if (entry.isFile()) {
|
|
10357
|
+
if (isPrunableJsonl(entry.name)) await pruneFile(this.dir, entry.name, "");
|
|
10358
|
+
continue;
|
|
10359
|
+
}
|
|
10360
|
+
if (!entry.isDirectory()) continue;
|
|
10361
|
+
const dateDir = path6.join(this.dir, entry.name);
|
|
10362
|
+
const files = await fsp7.readdir(dateDir, { withFileTypes: true }).catch(() => []);
|
|
10363
|
+
for (const file of files) {
|
|
10364
|
+
if (!file.isFile() || !isPrunableJsonl(file.name)) continue;
|
|
10365
|
+
await pruneFile(dateDir, file.name, entry.name);
|
|
10366
|
+
}
|
|
10367
|
+
}
|
|
10368
|
+
if (deleted > 0) {
|
|
10369
|
+
await this.compactIndex().catch(() => void 0);
|
|
10370
|
+
}
|
|
10371
|
+
for (const entry of entries) {
|
|
10372
|
+
if (!entry.isDirectory()) continue;
|
|
10373
|
+
const dateDir = path6.join(this.dir, entry.name);
|
|
10374
|
+
try {
|
|
10375
|
+
const remaining = await fsp7.readdir(dateDir);
|
|
10376
|
+
if (remaining.length === 0) {
|
|
10377
|
+
await fsp7.rmdir(dateDir).catch(() => void 0);
|
|
10311
10378
|
}
|
|
10312
|
-
}
|
|
10313
|
-
await writeFd.close();
|
|
10379
|
+
} catch {
|
|
10314
10380
|
}
|
|
10315
|
-
await src.close();
|
|
10316
|
-
await fsp6.rename(tmpPath, this.filePath);
|
|
10317
|
-
this.handle = await fsp6.open(this.filePath, "a", 384);
|
|
10318
|
-
} catch (err) {
|
|
10319
|
-
await fsp6.unlink(tmpPath).catch(() => void 0);
|
|
10320
|
-
this.handle = await fsp6.open(this.filePath, "a", 384).catch(() => this.handle);
|
|
10321
|
-
throw err;
|
|
10322
10381
|
}
|
|
10323
|
-
|
|
10324
|
-
type: "rewound",
|
|
10325
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10326
|
-
toPromptIndex: targetPromptIndex,
|
|
10327
|
-
revertedFiles: []
|
|
10328
|
-
});
|
|
10329
|
-
this.events?.emit("session.rewound", {
|
|
10330
|
-
toPromptIndex: targetPromptIndex,
|
|
10331
|
-
revertedFiles: [],
|
|
10332
|
-
removedEvents: removedCount
|
|
10333
|
-
});
|
|
10334
|
-
return removedCount;
|
|
10382
|
+
return deleted;
|
|
10335
10383
|
}
|
|
10336
|
-
async
|
|
10337
|
-
|
|
10338
|
-
|
|
10339
|
-
|
|
10340
|
-
this.flushTimer = null;
|
|
10341
|
-
}
|
|
10342
|
-
this.writeBuffer = [];
|
|
10343
|
-
await this.writeChain;
|
|
10384
|
+
async clearHistory(id) {
|
|
10385
|
+
await this.ensureShardDir(id);
|
|
10386
|
+
const file = this.sessionPath(id, ".jsonl");
|
|
10387
|
+
const meta = this.sessionPath(id, ".summary.json");
|
|
10344
10388
|
const record = `${JSON.stringify({
|
|
10345
10389
|
type: "session_start",
|
|
10346
10390
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10347
|
-
id
|
|
10348
|
-
model:
|
|
10349
|
-
provider:
|
|
10391
|
+
id,
|
|
10392
|
+
model: "unknown",
|
|
10393
|
+
provider: "unknown"
|
|
10350
10394
|
})}
|
|
10351
10395
|
`;
|
|
10352
|
-
await
|
|
10396
|
+
await fsp7.writeFile(file, record, "utf8");
|
|
10397
|
+
await fsp7.unlink(meta).catch(() => void 0);
|
|
10353
10398
|
}
|
|
10354
|
-
|
|
10355
|
-
|
|
10356
|
-
|
|
10357
|
-
|
|
10358
|
-
|
|
10359
|
-
|
|
10360
|
-
|
|
10361
|
-
|
|
10362
|
-
|
|
10399
|
+
async summarize(id, mtime) {
|
|
10400
|
+
try {
|
|
10401
|
+
const file = this.sessionPath(id, ".jsonl");
|
|
10402
|
+
let title = "(empty session)";
|
|
10403
|
+
let startedAt = (/* @__PURE__ */ new Date(0)).toISOString();
|
|
10404
|
+
let endedAt;
|
|
10405
|
+
let model = "unknown";
|
|
10406
|
+
let provider = "unknown";
|
|
10407
|
+
let tokenIn = 0;
|
|
10408
|
+
let tokenOut = 0;
|
|
10409
|
+
let iterationCount = 0;
|
|
10410
|
+
let toolCallCount = 0;
|
|
10411
|
+
let toolErrorCount = 0;
|
|
10412
|
+
let fileChangeCount = 0;
|
|
10413
|
+
const toolBreakdown = {};
|
|
10414
|
+
let outcome;
|
|
10415
|
+
let lastEventType;
|
|
10416
|
+
let hasError = false;
|
|
10417
|
+
let sawStart = false;
|
|
10418
|
+
for await (const e of this.iterSessionEvents(file)) {
|
|
10419
|
+
lastEventType = e.type;
|
|
10420
|
+
if (e.type === "session_start") {
|
|
10421
|
+
if (!sawStart) {
|
|
10422
|
+
sawStart = true;
|
|
10423
|
+
startedAt = e.ts;
|
|
10424
|
+
model = e.model ?? "unknown";
|
|
10425
|
+
provider = e.provider ?? "unknown";
|
|
10426
|
+
}
|
|
10427
|
+
} else if (e.type === "session_end") {
|
|
10428
|
+
endedAt = e.ts;
|
|
10429
|
+
} else if (e.type === "user_input") {
|
|
10430
|
+
if (title === "(empty session)") title = userInputTitle(e.content);
|
|
10431
|
+
} else if (e.type === "llm_response") {
|
|
10432
|
+
tokenIn += e.usage.input ?? 0;
|
|
10433
|
+
tokenOut += e.usage.output ?? 0;
|
|
10434
|
+
} else if (e.type === "in_flight_start") iterationCount++;
|
|
10435
|
+
else if (e.type === "tool_call_start") {
|
|
10436
|
+
toolCallCount++;
|
|
10437
|
+
toolBreakdown[e.name] = (toolBreakdown[e.name] ?? 0) + 1;
|
|
10438
|
+
} else if (e.type === "tool_result" && e.isError) toolErrorCount++;
|
|
10439
|
+
else if (e.type === "file_snapshot") fileChangeCount += e.files.length;
|
|
10440
|
+
else if (e.type === "error" || e.type === "provider_error") hasError = true;
|
|
10441
|
+
}
|
|
10442
|
+
if (lastEventType === "session_end") {
|
|
10443
|
+
outcome = "completed";
|
|
10444
|
+
} else if (lastEventType === "in_flight_start") {
|
|
10445
|
+
outcome = "aborted";
|
|
10446
|
+
} else if (hasError) {
|
|
10447
|
+
outcome = "error";
|
|
10448
|
+
}
|
|
10449
|
+
return {
|
|
10450
|
+
id,
|
|
10451
|
+
title,
|
|
10452
|
+
startedAt,
|
|
10453
|
+
endedAt,
|
|
10454
|
+
model,
|
|
10455
|
+
provider,
|
|
10456
|
+
tokenTotal: tokenIn + tokenOut,
|
|
10457
|
+
iterationCount: iterationCount > 0 ? iterationCount : void 0,
|
|
10458
|
+
toolCallCount: toolCallCount > 0 ? toolCallCount : void 0,
|
|
10459
|
+
toolErrorCount: toolErrorCount > 0 ? toolErrorCount : void 0,
|
|
10460
|
+
fileChangeCount: fileChangeCount > 0 ? fileChangeCount : void 0,
|
|
10461
|
+
toolBreakdown: Object.keys(toolBreakdown).length > 0 ? toolBreakdown : {},
|
|
10462
|
+
outcome
|
|
10463
|
+
};
|
|
10464
|
+
} catch {
|
|
10465
|
+
return {
|
|
10466
|
+
id,
|
|
10467
|
+
title: "(damaged)",
|
|
10468
|
+
startedAt: mtime,
|
|
10469
|
+
model: "unknown",
|
|
10470
|
+
provider: "unknown",
|
|
10471
|
+
tokenTotal: 0
|
|
10472
|
+
};
|
|
10363
10473
|
}
|
|
10364
|
-
await this.append({
|
|
10365
|
-
type: "in_flight_start",
|
|
10366
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10367
|
-
context
|
|
10368
|
-
});
|
|
10369
|
-
this.events?.emit("in_flight.started", { context, ts: (/* @__PURE__ */ new Date()).toISOString() });
|
|
10370
10474
|
}
|
|
10371
|
-
|
|
10372
|
-
|
|
10373
|
-
|
|
10374
|
-
|
|
10375
|
-
|
|
10376
|
-
|
|
10377
|
-
|
|
10378
|
-
|
|
10379
|
-
|
|
10380
|
-
|
|
10381
|
-
|
|
10382
|
-
|
|
10383
|
-
|
|
10384
|
-
|
|
10475
|
+
async *iterSessionEvents(file) {
|
|
10476
|
+
const stream = createReadStream(file, { encoding: "utf8" });
|
|
10477
|
+
const lines = createInterface({ input: stream, crlfDelay: Infinity });
|
|
10478
|
+
try {
|
|
10479
|
+
for await (const line of lines) {
|
|
10480
|
+
if (!line.trim()) continue;
|
|
10481
|
+
try {
|
|
10482
|
+
const parsed = JSON.parse(line);
|
|
10483
|
+
if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
|
|
10484
|
+
yield parsed;
|
|
10485
|
+
}
|
|
10486
|
+
} catch {
|
|
10487
|
+
}
|
|
10488
|
+
}
|
|
10489
|
+
} finally {
|
|
10490
|
+
lines.close();
|
|
10491
|
+
stream.destroy();
|
|
10492
|
+
}
|
|
10385
10493
|
}
|
|
10386
10494
|
};
|
|
10387
|
-
function
|
|
10388
|
-
const
|
|
10389
|
-
|
|
10495
|
+
function extractToolCallEnds(events) {
|
|
10496
|
+
const result = [];
|
|
10497
|
+
for (const e of events) {
|
|
10498
|
+
if (e.type === "tool_call_end") {
|
|
10499
|
+
result.push({
|
|
10500
|
+
name: e.name,
|
|
10501
|
+
id: e.id,
|
|
10502
|
+
durationMs: e.durationMs,
|
|
10503
|
+
ok: e.ok ?? false,
|
|
10504
|
+
outputBytes: e.outputBytes,
|
|
10505
|
+
outputTokens: e.outputTokens,
|
|
10506
|
+
outputLines: e.outputLines
|
|
10507
|
+
});
|
|
10508
|
+
}
|
|
10509
|
+
}
|
|
10510
|
+
return result;
|
|
10390
10511
|
}
|
|
10391
10512
|
function compareSessionSummaries(a, b) {
|
|
10392
10513
|
if (a.startedAt < b.startedAt) return 1;
|
|
10393
10514
|
if (a.startedAt > b.startedAt) return -1;
|
|
10394
10515
|
return a.id.localeCompare(b.id);
|
|
10395
10516
|
}
|
|
10517
|
+
function matchesSessionFilter(s, criteria) {
|
|
10518
|
+
if (criteria.since && s.startedAt < criteria.since) return false;
|
|
10519
|
+
if (criteria.until && s.startedAt > criteria.until) return false;
|
|
10520
|
+
if (criteria.provider && s.provider !== criteria.provider) return false;
|
|
10521
|
+
if (criteria.model && s.model !== criteria.model) return false;
|
|
10522
|
+
if (criteria.minTokens !== void 0 && s.tokenTotal < criteria.minTokens) return false;
|
|
10523
|
+
if (criteria.titleContains) {
|
|
10524
|
+
const needle = criteria.titleContains.toLowerCase();
|
|
10525
|
+
if (!s.title.toLowerCase().includes(needle)) return false;
|
|
10526
|
+
}
|
|
10527
|
+
return true;
|
|
10528
|
+
}
|
|
10396
10529
|
async function mapWithConcurrency(items, concurrency, fn) {
|
|
10397
10530
|
if (items.length === 0) return [];
|
|
10398
10531
|
const out = new Array(items.length);
|
|
@@ -10418,9 +10551,9 @@ function makeDirectorSessionFactory(opts) {
|
|
|
10418
10551
|
let dir;
|
|
10419
10552
|
if (opts.store) {
|
|
10420
10553
|
store = opts.store;
|
|
10421
|
-
dir = opts.sessionsRoot ?
|
|
10554
|
+
dir = opts.sessionsRoot ? path6.join(opts.sessionsRoot, runId) : "(caller-managed)";
|
|
10422
10555
|
} else if (opts.sessionsRoot) {
|
|
10423
|
-
dir =
|
|
10556
|
+
dir = path6.join(opts.sessionsRoot, runId);
|
|
10424
10557
|
store = new DefaultSessionStore({ dir });
|
|
10425
10558
|
} else {
|
|
10426
10559
|
throw new Error("makeDirectorSessionFactory requires either `store` or `sessionsRoot`");
|
|
@@ -10732,7 +10865,7 @@ var FleetManager = class {
|
|
|
10732
10865
|
})),
|
|
10733
10866
|
usage: this.usage.snapshot()
|
|
10734
10867
|
};
|
|
10735
|
-
await
|
|
10868
|
+
await fsp7.mkdir(path6.dirname(this.manifestPath), { recursive: true });
|
|
10736
10869
|
await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
|
|
10737
10870
|
return this.manifestPath;
|
|
10738
10871
|
}
|
|
@@ -10887,12 +11020,18 @@ var DefaultMailbox = class {
|
|
|
10887
11020
|
_byTo = /* @__PURE__ */ new Map();
|
|
10888
11021
|
/** Secondary index: sender → Set of messages (points into _messageCache). */
|
|
10889
11022
|
_byFrom = /* @__PURE__ */ new Map();
|
|
11023
|
+
/** Counts malformed JSONL lines skipped during parsing for observability. */
|
|
11024
|
+
_corruptionCount = 0;
|
|
10890
11025
|
constructor(sessionDir) {
|
|
10891
|
-
this.filePath =
|
|
11026
|
+
this.filePath = path6.join(sessionDir, MAILBOX_FILE);
|
|
10892
11027
|
}
|
|
10893
11028
|
get mailboxPath() {
|
|
10894
11029
|
return this.filePath;
|
|
10895
11030
|
}
|
|
11031
|
+
/** Returns the count of malformed JSONL lines encountered during reads. */
|
|
11032
|
+
get corruptionCount() {
|
|
11033
|
+
return this._corruptionCount;
|
|
11034
|
+
}
|
|
10896
11035
|
// ── Send ──────────────────────────────────────────────────────────────
|
|
10897
11036
|
async send(input) {
|
|
10898
11037
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -10913,9 +11052,9 @@ var DefaultMailbox = class {
|
|
|
10913
11052
|
taskContext: input.taskContext
|
|
10914
11053
|
};
|
|
10915
11054
|
const line = JSON.stringify(msg) + LINE_SEPARATOR;
|
|
10916
|
-
await
|
|
11055
|
+
await fsp7.mkdir(path6.dirname(this.filePath), { recursive: true });
|
|
10917
11056
|
await withFileLock(this.filePath, async () => {
|
|
10918
|
-
await
|
|
11057
|
+
await fsp7.appendFile(this.filePath, line, "utf8");
|
|
10919
11058
|
this._pushToCache(msg);
|
|
10920
11059
|
const { mtime, size } = await this._statUnderLockOrAbsent();
|
|
10921
11060
|
this._messageCacheMtime = mtime;
|
|
@@ -10996,7 +11135,7 @@ var DefaultMailbox = class {
|
|
|
10996
11135
|
}
|
|
10997
11136
|
if (changed) {
|
|
10998
11137
|
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
10999
|
-
await
|
|
11138
|
+
await fsp7.writeFile(this.filePath, serialized, "utf8");
|
|
11000
11139
|
}
|
|
11001
11140
|
const { mtime, size } = await this._statUnderLockOrAbsent();
|
|
11002
11141
|
this._setMessageCache(all, mtime, size);
|
|
@@ -11055,7 +11194,7 @@ var DefaultMailbox = class {
|
|
|
11055
11194
|
}
|
|
11056
11195
|
async clearAll() {
|
|
11057
11196
|
await withFileLock(this.filePath, async () => {
|
|
11058
|
-
await
|
|
11197
|
+
await fsp7.writeFile(this.filePath, "", "utf8");
|
|
11059
11198
|
const { mtime, size } = await this._statUnderLockOrAbsent();
|
|
11060
11199
|
this._setMessageCache([], mtime, size);
|
|
11061
11200
|
});
|
|
@@ -11088,7 +11227,7 @@ var DefaultMailbox = class {
|
|
|
11088
11227
|
remaining = kept.length;
|
|
11089
11228
|
if (kept.length < all.length) {
|
|
11090
11229
|
const content = kept.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
11091
|
-
await
|
|
11230
|
+
await fsp7.writeFile(this.filePath, content, "utf8");
|
|
11092
11231
|
}
|
|
11093
11232
|
const { mtime, size } = await this._statUnderLockOrAbsent();
|
|
11094
11233
|
this._setMessageCache(kept, mtime, size);
|
|
@@ -11111,7 +11250,7 @@ var DefaultMailbox = class {
|
|
|
11111
11250
|
// ── Internal ──────────────────────────────────────────────────────────
|
|
11112
11251
|
async _readAll() {
|
|
11113
11252
|
try {
|
|
11114
|
-
const raw = await
|
|
11253
|
+
const raw = await fsp7.readFile(this.filePath, "utf8");
|
|
11115
11254
|
return this._parseLines(raw);
|
|
11116
11255
|
} catch (err) {
|
|
11117
11256
|
if (err.code === "ENOENT") return [];
|
|
@@ -11143,7 +11282,9 @@ var DefaultMailbox = class {
|
|
|
11143
11282
|
const msg = parsed;
|
|
11144
11283
|
this._messageCache.push(msg);
|
|
11145
11284
|
this._indexMsg(msg);
|
|
11146
|
-
} catch {
|
|
11285
|
+
} catch (err) {
|
|
11286
|
+
this._corruptionCount++;
|
|
11287
|
+
console.debug(`[mailbox] skipped malformed line during incremental read: ${err.message}`);
|
|
11147
11288
|
}
|
|
11148
11289
|
}
|
|
11149
11290
|
return this._messageCache;
|
|
@@ -11165,7 +11306,9 @@ var DefaultMailbox = class {
|
|
|
11165
11306
|
delete parsed["readAt"];
|
|
11166
11307
|
}
|
|
11167
11308
|
messages.push(parsed);
|
|
11168
|
-
} catch {
|
|
11309
|
+
} catch (err) {
|
|
11310
|
+
this._corruptionCount++;
|
|
11311
|
+
console.debug(`[mailbox] skipped malformed line during full parse: ${err.message}`);
|
|
11169
11312
|
}
|
|
11170
11313
|
}
|
|
11171
11314
|
return messages;
|
|
@@ -11178,7 +11321,7 @@ var DefaultMailbox = class {
|
|
|
11178
11321
|
*/
|
|
11179
11322
|
async _statUnderLockOrAbsent() {
|
|
11180
11323
|
try {
|
|
11181
|
-
const st = await
|
|
11324
|
+
const st = await fsp7.stat(this.filePath);
|
|
11182
11325
|
return { mtime: st.mtimeMs, size: st.size };
|
|
11183
11326
|
} catch (err) {
|
|
11184
11327
|
if (err.code === "ENOENT") {
|
|
@@ -11189,12 +11332,12 @@ var DefaultMailbox = class {
|
|
|
11189
11332
|
}
|
|
11190
11333
|
async _readAllCached() {
|
|
11191
11334
|
try {
|
|
11192
|
-
const st = await
|
|
11335
|
+
const st = await fsp7.stat(this.filePath);
|
|
11193
11336
|
if (this._messageCache !== null && this._messageCacheMtime === st.mtimeMs && this._messageCacheSize === st.size) {
|
|
11194
11337
|
return this._messageCache;
|
|
11195
11338
|
}
|
|
11196
11339
|
if (this._messageCache !== null && this._messageCacheSize >= 0 && st.size > this._messageCacheSize) {
|
|
11197
|
-
const fd = await
|
|
11340
|
+
const fd = await fsp7.open(this.filePath, "r");
|
|
11198
11341
|
try {
|
|
11199
11342
|
const updated = await this._readNewMessagesOnly(fd, this._messageCacheSize, st.size);
|
|
11200
11343
|
this._messageCacheMtime = st.mtimeMs;
|
|
@@ -11407,7 +11550,7 @@ var REGISTRY_CACHE_TTL_MS = 2e3;
|
|
|
11407
11550
|
var LINE_SEPARATOR2 = "\n";
|
|
11408
11551
|
var MESSAGE_CACHE_MAX_ENTRIES2 = 1e4;
|
|
11409
11552
|
function resolveProjectDir(projectRoot, globalRoot) {
|
|
11410
|
-
return
|
|
11553
|
+
return path6.join(globalRoot, "projects", projectSlug(projectRoot));
|
|
11411
11554
|
}
|
|
11412
11555
|
var GlobalMailbox = class {
|
|
11413
11556
|
/** Path to the JSONL message file. */
|
|
@@ -11460,14 +11603,14 @@ var GlobalMailbox = class {
|
|
|
11460
11603
|
* @param hqPublisher — optional HQ publisher, or getter, for cross-project telemetry
|
|
11461
11604
|
*/
|
|
11462
11605
|
constructor(projectDir, events, hqPublisher) {
|
|
11463
|
-
this.messagePath =
|
|
11464
|
-
this.registryPath =
|
|
11465
|
-
this.clientRegistryPath =
|
|
11606
|
+
this.messagePath = path6.join(projectDir, MAILBOX_FILE2);
|
|
11607
|
+
this.registryPath = path6.join(projectDir, "_mailbox.registry.json");
|
|
11608
|
+
this.clientRegistryPath = path6.join(projectDir, CLIENT_REGISTRY_FILE);
|
|
11466
11609
|
this._events = events;
|
|
11467
11610
|
this._hqPublisher = hqPublisher;
|
|
11468
11611
|
}
|
|
11469
11612
|
get hqMailboxId() {
|
|
11470
|
-
return `${
|
|
11613
|
+
return `${path6.basename(path6.dirname(this.messagePath))}:mailbox`;
|
|
11471
11614
|
}
|
|
11472
11615
|
get hqPublisher() {
|
|
11473
11616
|
return typeof this._hqPublisher === "function" ? this._hqPublisher() : this._hqPublisher;
|
|
@@ -11504,9 +11647,9 @@ var GlobalMailbox = class {
|
|
|
11504
11647
|
taskContext: input.taskContext
|
|
11505
11648
|
};
|
|
11506
11649
|
const line = JSON.stringify(msg) + LINE_SEPARATOR2;
|
|
11507
|
-
await
|
|
11650
|
+
await fsp7.mkdir(path6.dirname(this.messagePath), { recursive: true });
|
|
11508
11651
|
await withFileLock(this.messagePath, async () => {
|
|
11509
|
-
await
|
|
11652
|
+
await fsp7.appendFile(this.messagePath, line, "utf8");
|
|
11510
11653
|
this._pushToCache(msg);
|
|
11511
11654
|
});
|
|
11512
11655
|
this.publishHqMailboxEvent({ mailboxId: this.hqMailboxId, action: "message.sent", message: msg });
|
|
@@ -11572,7 +11715,7 @@ var GlobalMailbox = class {
|
|
|
11572
11715
|
}
|
|
11573
11716
|
if (changed) {
|
|
11574
11717
|
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR2) + LINE_SEPARATOR2;
|
|
11575
|
-
await
|
|
11718
|
+
await fsp7.writeFile(this.messagePath, serialized, "utf8");
|
|
11576
11719
|
}
|
|
11577
11720
|
cacheSnapshot = all;
|
|
11578
11721
|
});
|
|
@@ -11790,7 +11933,7 @@ var GlobalMailbox = class {
|
|
|
11790
11933
|
}
|
|
11791
11934
|
async clearAll() {
|
|
11792
11935
|
await withFileLock(this.messagePath, async () => {
|
|
11793
|
-
await
|
|
11936
|
+
await fsp7.writeFile(this.messagePath, "", "utf8");
|
|
11794
11937
|
});
|
|
11795
11938
|
this._setMessageCache([]);
|
|
11796
11939
|
}
|
|
@@ -11822,7 +11965,7 @@ var GlobalMailbox = class {
|
|
|
11822
11965
|
remaining = kept.length;
|
|
11823
11966
|
if (kept.length < all.length) {
|
|
11824
11967
|
const content = kept.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR2) + LINE_SEPARATOR2;
|
|
11825
|
-
await
|
|
11968
|
+
await fsp7.writeFile(this.messagePath, content, "utf8");
|
|
11826
11969
|
}
|
|
11827
11970
|
this._setMessageCache(kept);
|
|
11828
11971
|
});
|
|
@@ -11842,7 +11985,7 @@ var GlobalMailbox = class {
|
|
|
11842
11985
|
*/
|
|
11843
11986
|
async _readMessages() {
|
|
11844
11987
|
try {
|
|
11845
|
-
const raw = await
|
|
11988
|
+
const raw = await fsp7.readFile(this.messagePath, "utf8");
|
|
11846
11989
|
return this._parseLines(raw);
|
|
11847
11990
|
} catch (err) {
|
|
11848
11991
|
if (err.code === "ENOENT") return [];
|
|
@@ -11932,12 +12075,12 @@ var GlobalMailbox = class {
|
|
|
11932
12075
|
*/
|
|
11933
12076
|
async _readMessagesCached() {
|
|
11934
12077
|
try {
|
|
11935
|
-
const st = await
|
|
12078
|
+
const st = await fsp7.stat(this.messagePath);
|
|
11936
12079
|
if (this._messageCache !== null && this._messageCacheMtime === st.mtimeMs && this._messageCacheSize === st.size) {
|
|
11937
12080
|
return this._messageCache;
|
|
11938
12081
|
}
|
|
11939
12082
|
if (this._messageCache !== null && this._messageCacheSize >= 0 && st.size > this._messageCacheSize) {
|
|
11940
|
-
const fd = await
|
|
12083
|
+
const fd = await fsp7.open(this.messagePath, "r");
|
|
11941
12084
|
try {
|
|
11942
12085
|
const updated = await this._readNewMessagesOnly(fd, this._messageCacheSize, st.size);
|
|
11943
12086
|
this._messageCacheMtime = st.mtimeMs;
|
|
@@ -11975,7 +12118,7 @@ var GlobalMailbox = class {
|
|
|
11975
12118
|
this._messageCacheMtime = mtime;
|
|
11976
12119
|
this._messageCacheSize = size;
|
|
11977
12120
|
} else {
|
|
11978
|
-
void
|
|
12121
|
+
void fsp7.stat(this.messagePath).then((st) => {
|
|
11979
12122
|
this._messageCacheMtime = st.mtimeMs;
|
|
11980
12123
|
this._messageCacheSize = st.size;
|
|
11981
12124
|
}).catch(() => {
|
|
@@ -11999,14 +12142,14 @@ var GlobalMailbox = class {
|
|
|
11999
12142
|
this._messageCache.push(msg);
|
|
12000
12143
|
}
|
|
12001
12144
|
async _ensureRegistry() {
|
|
12002
|
-
await
|
|
12145
|
+
await fsp7.mkdir(path6.dirname(this.registryPath), { recursive: true });
|
|
12003
12146
|
}
|
|
12004
12147
|
async _readRegistry(opts) {
|
|
12005
12148
|
if (!opts?.fresh && this._registryCache && Date.now() - this._registryCacheAt < REGISTRY_CACHE_TTL_MS) {
|
|
12006
12149
|
return new Map(this._registryCache);
|
|
12007
12150
|
}
|
|
12008
12151
|
try {
|
|
12009
|
-
const raw = await
|
|
12152
|
+
const raw = await fsp7.readFile(this.registryPath, "utf8");
|
|
12010
12153
|
const data = JSON.parse(raw);
|
|
12011
12154
|
const map = /* @__PURE__ */ new Map();
|
|
12012
12155
|
for (const [id, agent] of Object.entries(data)) {
|
|
@@ -12039,19 +12182,19 @@ var GlobalMailbox = class {
|
|
|
12039
12182
|
obj[id] = agent;
|
|
12040
12183
|
}
|
|
12041
12184
|
const tmp = `${this.registryPath}.${randomUUID().slice(0, 8)}.tmp`;
|
|
12042
|
-
await
|
|
12043
|
-
await
|
|
12185
|
+
await fsp7.writeFile(tmp, JSON.stringify(obj), "utf8");
|
|
12186
|
+
await fsp7.rename(tmp, this.registryPath);
|
|
12044
12187
|
}
|
|
12045
12188
|
// ── Client registry internals ───────────────────────────────────────────
|
|
12046
12189
|
async _ensureClientRegistry() {
|
|
12047
|
-
await
|
|
12190
|
+
await fsp7.mkdir(path6.dirname(this.clientRegistryPath), { recursive: true });
|
|
12048
12191
|
}
|
|
12049
12192
|
async _readClientRegistry(opts) {
|
|
12050
12193
|
if (!opts?.fresh && this._clientRegistryCache && Date.now() - this._clientRegistryCacheAt < REGISTRY_CACHE_TTL_MS) {
|
|
12051
12194
|
return new Map(this._clientRegistryCache);
|
|
12052
12195
|
}
|
|
12053
12196
|
try {
|
|
12054
|
-
const raw = await
|
|
12197
|
+
const raw = await fsp7.readFile(this.clientRegistryPath, "utf8");
|
|
12055
12198
|
const data = JSON.parse(raw);
|
|
12056
12199
|
const map = /* @__PURE__ */ new Map();
|
|
12057
12200
|
for (const [id, client] of Object.entries(data)) {
|
|
@@ -12084,8 +12227,8 @@ var GlobalMailbox = class {
|
|
|
12084
12227
|
obj[id] = client;
|
|
12085
12228
|
}
|
|
12086
12229
|
const tmp = `${this.clientRegistryPath}.${randomUUID().slice(0, 8)}.tmp`;
|
|
12087
|
-
await
|
|
12088
|
-
await
|
|
12230
|
+
await fsp7.writeFile(tmp, JSON.stringify(obj), "utf8");
|
|
12231
|
+
await fsp7.rename(tmp, this.clientRegistryPath);
|
|
12089
12232
|
}
|
|
12090
12233
|
};
|
|
12091
12234
|
function defaultResolveProjectDir(ctx) {
|
|
@@ -12558,13 +12701,13 @@ function makeDependencyWatcherConfig(opts) {
|
|
|
12558
12701
|
const globPatterns = patterns.filter((p) => p.includes("*"));
|
|
12559
12702
|
const plainPatterns = patterns.filter((p) => !p.includes("*"));
|
|
12560
12703
|
function matchesPattern(filePath) {
|
|
12561
|
-
const
|
|
12562
|
-
if (plainPatterns.includes(
|
|
12704
|
+
const basename7 = filePath.split("/").pop()?.split("\\").pop() ?? "";
|
|
12705
|
+
if (plainPatterns.includes(basename7)) return true;
|
|
12563
12706
|
for (const gp of globPatterns) {
|
|
12564
12707
|
const regex = new RegExp(
|
|
12565
12708
|
"^" + gp.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$"
|
|
12566
12709
|
);
|
|
12567
|
-
if (regex.test(
|
|
12710
|
+
if (regex.test(basename7)) return true;
|
|
12568
12711
|
}
|
|
12569
12712
|
return false;
|
|
12570
12713
|
}
|
|
@@ -12691,11 +12834,11 @@ function createMailboxHooks(opts) {
|
|
|
12691
12834
|
var DEFAULT_MAX_ENTRIES = 1e4;
|
|
12692
12835
|
var LOG_FILENAME = "package-authors.json";
|
|
12693
12836
|
function logPath(storageDir) {
|
|
12694
|
-
return
|
|
12837
|
+
return path6.join(storageDir, LOG_FILENAME);
|
|
12695
12838
|
}
|
|
12696
12839
|
async function loadLog(storageDir, projectRoot) {
|
|
12697
12840
|
try {
|
|
12698
|
-
const raw = await
|
|
12841
|
+
const raw = await fsp7.readFile(logPath(storageDir), "utf-8");
|
|
12699
12842
|
const parsed = JSON.parse(raw);
|
|
12700
12843
|
if (!parsed.entries || !Array.isArray(parsed.entries)) {
|
|
12701
12844
|
return { projectRoot, entries: [] };
|
|
@@ -12709,13 +12852,13 @@ async function loadLog(storageDir, projectRoot) {
|
|
|
12709
12852
|
}
|
|
12710
12853
|
}
|
|
12711
12854
|
async function saveLog(storageDir, log) {
|
|
12712
|
-
await
|
|
12855
|
+
await fsp7.mkdir(storageDir, { recursive: true });
|
|
12713
12856
|
const tmp = `${logPath(storageDir)}.tmp.${Date.now()}`;
|
|
12714
|
-
await
|
|
12715
|
-
await
|
|
12857
|
+
await fsp7.writeFile(tmp, JSON.stringify(log, null, 2) + "\n", "utf-8");
|
|
12858
|
+
await fsp7.rename(tmp, logPath(storageDir));
|
|
12716
12859
|
}
|
|
12717
12860
|
function detectEcosystem(manifestPath) {
|
|
12718
|
-
const name =
|
|
12861
|
+
const name = path6.basename(manifestPath).toLowerCase();
|
|
12719
12862
|
if (name === "package.json") return "npm";
|
|
12720
12863
|
if (name === "go.mod") return "go";
|
|
12721
12864
|
if (name === "cargo.toml") return "cargo";
|
|
@@ -12998,8 +13141,8 @@ var KnowledgeGraph = class {
|
|
|
12998
13141
|
return this.index;
|
|
12999
13142
|
}
|
|
13000
13143
|
constructor(sessionDir) {
|
|
13001
|
-
this.filePath =
|
|
13002
|
-
this.graphFilePath =
|
|
13144
|
+
this.filePath = path6.join(sessionDir, "_knowledge_graph");
|
|
13145
|
+
this.graphFilePath = path6.join(this.filePath, "graph.jsonl");
|
|
13003
13146
|
}
|
|
13004
13147
|
// ── Write ──────────────────────────────────────────────────────────────
|
|
13005
13148
|
/**
|
|
@@ -13180,23 +13323,23 @@ var KnowledgeGraph = class {
|
|
|
13180
13323
|
}
|
|
13181
13324
|
}
|
|
13182
13325
|
async _persist(node) {
|
|
13183
|
-
await
|
|
13326
|
+
await fsp7.mkdir(this.filePath, { recursive: true });
|
|
13184
13327
|
const line = JSON.stringify(node) + "\n";
|
|
13185
13328
|
await withFileLock(this.graphFilePath, async () => {
|
|
13186
|
-
await
|
|
13329
|
+
await fsp7.appendFile(this.graphFilePath, line, "utf8");
|
|
13187
13330
|
});
|
|
13188
13331
|
}
|
|
13189
13332
|
async _append(node) {
|
|
13190
|
-
await
|
|
13333
|
+
await fsp7.mkdir(this.filePath, { recursive: true });
|
|
13191
13334
|
const line = JSON.stringify({ op: "update", node }) + "\n";
|
|
13192
13335
|
await withFileLock(this.graphFilePath, async () => {
|
|
13193
|
-
await
|
|
13336
|
+
await fsp7.appendFile(this.graphFilePath, line, "utf8");
|
|
13194
13337
|
});
|
|
13195
13338
|
}
|
|
13196
13339
|
/** Rebuild in-memory state from the log file. Call on startup. */
|
|
13197
13340
|
async load() {
|
|
13198
13341
|
try {
|
|
13199
|
-
const content = await
|
|
13342
|
+
const content = await fsp7.readFile(this.graphFilePath, "utf8");
|
|
13200
13343
|
const lines = content.split("\n").filter(Boolean);
|
|
13201
13344
|
for (const line of lines) {
|
|
13202
13345
|
try {
|
|
@@ -15627,11 +15770,11 @@ var AgentMonitorService = class {
|
|
|
15627
15770
|
this._onEntry?.(entry);
|
|
15628
15771
|
}
|
|
15629
15772
|
async _appendToFile(subagentId, entry) {
|
|
15630
|
-
const dir =
|
|
15631
|
-
await
|
|
15632
|
-
const filePath =
|
|
15773
|
+
const dir = path6.join(this._transcriptsDir, subagentId);
|
|
15774
|
+
await fsp7.mkdir(dir, { recursive: true });
|
|
15775
|
+
const filePath = path6.join(dir, "transcript.jsonl");
|
|
15633
15776
|
const line = JSON.stringify(entry) + "\n";
|
|
15634
|
-
await
|
|
15777
|
+
await fsp7.appendFile(filePath, line, { encoding: "utf8" });
|
|
15635
15778
|
}
|
|
15636
15779
|
_uid() {
|
|
15637
15780
|
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|