@wrongstack/core 0.273.0 → 0.274.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agent-bridge-BZ2enORi.d.ts → agent-bridge-DFo21wmY.d.ts} +1 -1
- package/dist/{agent-subagent-runner-ehb4xGvd.d.ts → agent-subagent-runner-BwmkIDEd.d.ts} +7 -7
- package/dist/{brain-BxN2k2HP.d.ts → brain-gfZX3the.d.ts} +11 -5
- package/dist/{provider-model-resolve-DFd3IPpw.d.ts → codex-catalog-CooZ6mOl.d.ts} +47 -4
- package/dist/{compactor-72ug-ZRB.d.ts → compactor-riTOds0f.d.ts} +1 -1
- package/dist/{config-C8IYxlO8.d.ts → config-BxcrDzri.d.ts} +60 -4
- package/dist/{context-Dw55zZ_Q.d.ts → context-DERiLofu.d.ts} +60 -1
- package/dist/coordination/index.d.ts +27 -16
- package/dist/coordination/index.js +1641 -1398
- package/dist/coordination/index.js.map +1 -1
- package/dist/defaults/index.d.ts +26 -26
- package/dist/defaults/index.js +2154 -1466
- package/dist/defaults/index.js.map +1 -1
- package/dist/execution/index.d.ts +15 -15
- package/dist/execution/index.js +77 -9
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/prompt-enhancer.d.ts +1 -1
- package/dist/extension/index.d.ts +6 -6
- package/dist/{global-mailbox-C9dsc9Y_.d.ts → global-mailbox-Cr8TW4t_.d.ts} +1 -1
- package/dist/{goal-preamble-NhflDjYb.d.ts → goal-preamble-CEhROp1e.d.ts} +9 -9
- package/dist/{goal-store-Cx363x7Z.d.ts → goal-store-xNSVxmpV.d.ts} +1 -1
- package/dist/hq/index.d.ts +5 -5
- package/dist/{index-B7fHDt0B.d.ts → index-00KPKAlm.d.ts} +5 -5
- package/dist/{index-BbVprU-9.d.ts → index-pDBSBE1r.d.ts} +2 -2
- package/dist/index.d.ts +72 -45
- package/dist/index.js +2840 -1769
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/index.d.ts +6 -6
- package/dist/kernel/index.d.ts +11 -11
- package/dist/kernel/index.js.map +1 -1
- package/dist/{mcp-servers-B6fSRNC1.d.ts → mcp-servers-BIwRiOxe.d.ts} +3 -3
- package/dist/models/index.d.ts +5 -5
- package/dist/models/index.js +40 -2
- package/dist/models/index.js.map +1 -1
- package/dist/{models-registry-4C6Wr91w.d.ts → models-registry-8OorW51H.d.ts} +1 -1
- package/dist/{multi-agent-coordinator-q1skFeNP.d.ts → multi-agent-coordinator-CNx48Zoz.d.ts} +1 -1
- package/dist/{null-fleet-bus-C9rrgQwc.d.ts → null-fleet-bus-B4ZUJYL6.d.ts} +6 -6
- package/dist/observability/index.d.ts +2 -2
- package/dist/{parallel-eternal-engine-CtXly2Sf.d.ts → parallel-eternal-engine-rsIclDqO.d.ts} +9 -9
- package/dist/{path-resolver-Bim6G5Jz.d.ts → path-resolver-BqU-fwzD.d.ts} +3 -3
- package/dist/{permission-CC7XFYWG.d.ts → permission-Dgs3v-Xq.d.ts} +1 -1
- package/dist/{permission-policy-cYR4RJmw.d.ts → permission-policy-Dtht2k0e.d.ts} +2 -2
- package/dist/{pipeline-CNVKuQDQ.d.ts → pipeline-B-dpCFYS.d.ts} +2 -2
- package/dist/{plan-templates-C4wXMmiM.d.ts → plan-templates-B7MxyY2o.d.ts} +58 -5
- package/dist/{provider-runner-BpM0mdBE.d.ts → provider-runner-DrmpBE5l.d.ts} +3 -3
- package/dist/{retry-policy-BV7nzeAd.d.ts → retry-policy-hYxsm10a.d.ts} +1 -1
- package/dist/sdd/index.d.ts +12 -10
- package/dist/sdd/index.js +7 -0
- package/dist/sdd/index.js.map +1 -1
- package/dist/{secret-vault-eMBKfheR.d.ts → secret-vault-DnqIFhPF.d.ts} +1 -1
- package/dist/security/index.d.ts +5 -5
- package/dist/{selector-C4ORTOid.d.ts → selector-D21RjDIg.d.ts} +1 -1
- package/dist/{session-event-bridge-CeNpUL9w.d.ts → session-event-bridge-Dt54CTvq.d.ts} +1 -1
- package/dist/{session-reader-BepLSnGL.d.ts → session-reader-CceH13Kq.d.ts} +5 -4
- package/dist/storage/index.d.ts +46 -12
- package/dist/storage/index.js +2281 -1476
- package/dist/storage/index.js.map +1 -1
- package/dist/tools/index.d.ts +2 -2
- package/dist/types/index.d.ts +19 -19
- package/dist/types/index.js +162 -42
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.d.ts +2 -2
- package/dist/{worktree-manager-BDuXTaWL.d.ts → worktree-manager-DUfBbKzk.d.ts} +1 -1
- package/package.json +1 -1
|
@@ -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,1456 +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;
|
|
9049
|
+
async flush() {
|
|
9050
|
+
if (this.flushTimer) {
|
|
9051
|
+
clearTimeout(this.flushTimer);
|
|
9052
|
+
this.flushTimer = null;
|
|
8993
9053
|
}
|
|
9054
|
+
await this.flushBuffer();
|
|
8994
9055
|
}
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
|
|
9002
|
-
}
|
|
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
|
-
}
|
|
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);
|
|
9043
9064
|
}
|
|
9044
|
-
|
|
9045
|
-
|
|
9065
|
+
/**
|
|
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).
|
|
9071
|
+
*/
|
|
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 = [];
|
|
9046
9077
|
const t0 = Date.now();
|
|
9047
9078
|
let outcome = "success";
|
|
9048
9079
|
let errorMsg;
|
|
9049
|
-
let cacheHit = false;
|
|
9050
9080
|
try {
|
|
9051
|
-
|
|
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;
|
|
9081
|
+
await this.enqueueWrite(batch);
|
|
9158
9082
|
} catch (err) {
|
|
9159
9083
|
outcome = "failure";
|
|
9160
9084
|
errorMsg = toErrorMessage(err);
|
|
9161
|
-
|
|
9162
|
-
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
|
|
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;
|
|
9172
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
|
+
});
|
|
9173
9110
|
}
|
|
9174
9111
|
}
|
|
9175
|
-
|
|
9176
|
-
|
|
9177
|
-
|
|
9178
|
-
|
|
9179
|
-
if (indexed.length > 0) {
|
|
9180
|
-
indexed.sort((a, b) => {
|
|
9181
|
-
if (a.startedAt < b.startedAt) return 1;
|
|
9182
|
-
if (a.startedAt > b.startedAt) return -1;
|
|
9183
|
-
return a.id.localeCompare(b.id);
|
|
9184
|
-
});
|
|
9185
|
-
return indexed.slice(0, limit);
|
|
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);
|
|
9186
9116
|
}
|
|
9187
|
-
return await this.listFromDirectoryScan(limit);
|
|
9188
|
-
} catch {
|
|
9189
|
-
return [];
|
|
9190
9117
|
}
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
indexAppendCount = 0;
|
|
9202
|
-
static COMPACT_EVERY = 30;
|
|
9203
|
-
/** Append a session summary to the index. */
|
|
9204
|
-
async appendToIndex(summary) {
|
|
9205
|
-
try {
|
|
9206
|
-
await ensureDir(this.dir);
|
|
9207
|
-
const line = JSON.stringify(summary) + "\n";
|
|
9208
|
-
await fsp6.appendFile(this.indexFile, line, "utf8");
|
|
9209
|
-
this._indexCache = null;
|
|
9210
|
-
this.indexAppendCount++;
|
|
9211
|
-
if (this.indexAppendCount >= _DefaultSessionStore.COMPACT_EVERY) {
|
|
9212
|
-
await this.compactIndex();
|
|
9213
|
-
this.indexAppendCount = 0;
|
|
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";
|
|
9214
9128
|
}
|
|
9215
|
-
}
|
|
9129
|
+
} else if (event.type === "file_snapshot") {
|
|
9130
|
+
this.fileChangeCount += event.files.length;
|
|
9131
|
+
} else if (event.type === "compaction") {
|
|
9132
|
+
this.compactionCount++;
|
|
9216
9133
|
}
|
|
9217
|
-
|
|
9218
|
-
|
|
9219
|
-
|
|
9220
|
-
|
|
9221
|
-
|
|
9222
|
-
|
|
9223
|
-
|
|
9224
|
-
this.
|
|
9225
|
-
this.
|
|
9226
|
-
}
|
|
9134
|
+
if (event.type === "error" || event.type === "provider_error") {
|
|
9135
|
+
this.outcome = "error";
|
|
9136
|
+
}
|
|
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++;
|
|
9227
9148
|
}
|
|
9228
9149
|
}
|
|
9229
|
-
|
|
9230
|
-
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
|
|
9235
|
-
|
|
9236
|
-
|
|
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;
|
|
9237
9199
|
try {
|
|
9238
|
-
|
|
9239
|
-
if (entries.length === 0) return;
|
|
9240
|
-
const tmp = `${this.indexFile}.compact.tmp`;
|
|
9241
|
-
const lines = entries.map((s) => JSON.stringify(s)).join("\n") + "\n";
|
|
9242
|
-
await fsp6.writeFile(tmp, lines, "utf8");
|
|
9243
|
-
await fsp6.rename(tmp, this.indexFile);
|
|
9244
|
-
this._indexCache = null;
|
|
9200
|
+
await this.onCloseCb?.(this.summary);
|
|
9245
9201
|
} catch (err) {
|
|
9246
|
-
|
|
9247
|
-
|
|
9202
|
+
idxOutcome = "failure";
|
|
9203
|
+
idxError = toErrorMessage(err);
|
|
9248
9204
|
} finally {
|
|
9249
|
-
this.
|
|
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
|
+
});
|
|
9250
9215
|
}
|
|
9251
|
-
}
|
|
9252
|
-
/**
|
|
9253
|
-
* Read the index file and return deduplicated session summaries.
|
|
9254
|
-
* Entries with a matching tombstone are filtered out.
|
|
9255
|
-
* Returns empty array when the index doesn't exist or is corrupt.
|
|
9256
|
-
*/
|
|
9257
|
-
async readIndex() {
|
|
9258
|
-
let stat7;
|
|
9259
9216
|
try {
|
|
9260
|
-
|
|
9261
|
-
stat7 = { mtimeMs: s.mtimeMs, size: s.size };
|
|
9217
|
+
await this.handle.close();
|
|
9262
9218
|
} catch {
|
|
9263
|
-
this._indexCache = null;
|
|
9264
|
-
return [];
|
|
9265
9219
|
}
|
|
9266
|
-
|
|
9267
|
-
|
|
9220
|
+
}
|
|
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 = [];
|
|
9268
9226
|
}
|
|
9269
|
-
|
|
9270
|
-
|
|
9271
|
-
|
|
9272
|
-
|
|
9273
|
-
|
|
9274
|
-
|
|
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;
|
|
9275
9259
|
}
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
|
|
9283
|
-
|
|
9284
|
-
|
|
9285
|
-
|
|
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;
|
|
9269
|
+
try {
|
|
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;
|
|
9286
9312
|
}
|
|
9287
|
-
|
|
9288
|
-
|
|
9313
|
+
fileOffset += bytesRead;
|
|
9314
|
+
if (chunkPos >= bytesRead) {
|
|
9315
|
+
lineStartOffset = fileOffset;
|
|
9289
9316
|
}
|
|
9290
|
-
} catch {
|
|
9291
9317
|
}
|
|
9318
|
+
} finally {
|
|
9319
|
+
await fd?.close();
|
|
9292
9320
|
}
|
|
9293
|
-
|
|
9294
|
-
this.
|
|
9295
|
-
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
* Rebuild the index from disk by scanning all sessions and writing a
|
|
9299
|
-
* fresh _index.jsonl. Useful after manual cleanup or index corruption.
|
|
9300
|
-
*/
|
|
9301
|
-
async rebuildIndex() {
|
|
9302
|
-
const ids = await this.collectSessionIds(this.dir);
|
|
9303
|
-
const summaries = await Promise.all(ids.map((id) => this.summaryFor(id).catch(() => null)));
|
|
9304
|
-
const valid = summaries.filter((s) => s !== null);
|
|
9305
|
-
const tmp = `${this.indexFile}.tmp`;
|
|
9306
|
-
const lines = valid.map((s) => JSON.stringify(s)).join("\n") + "\n";
|
|
9307
|
-
await fsp6.writeFile(tmp, lines, "utf8");
|
|
9308
|
-
await fsp6.rename(tmp, this.indexFile);
|
|
9309
|
-
this._indexCache = null;
|
|
9310
|
-
return valid.length;
|
|
9311
|
-
}
|
|
9312
|
-
async listFromDirectoryScan(limit) {
|
|
9313
|
-
const refs = await this.collectSessionFiles(this.dir);
|
|
9314
|
-
const candidates = await mapWithConcurrency(
|
|
9315
|
-
refs,
|
|
9316
|
-
_DefaultSessionStore.LIST_SCAN_CONCURRENCY,
|
|
9317
|
-
async (ref) => {
|
|
9318
|
-
const manifest = await this.readSummaryManifest(ref.id);
|
|
9319
|
-
if (manifest) return { summary: manifest, needsBackfill: false };
|
|
9320
|
-
const summary = await this.summaryHeaderFor(ref);
|
|
9321
|
-
return summary ? { summary, needsBackfill: true } : null;
|
|
9322
|
-
}
|
|
9323
|
-
);
|
|
9324
|
-
const out = candidates.filter((s) => s !== null);
|
|
9325
|
-
out.sort((a, b) => compareSessionSummaries(a.summary, b.summary));
|
|
9326
|
-
const selected = out.slice(0, limit);
|
|
9327
|
-
const summaries = await mapWithConcurrency(
|
|
9328
|
-
selected,
|
|
9329
|
-
Math.min(_DefaultSessionStore.LIST_SCAN_CONCURRENCY, Math.max(1, limit)),
|
|
9330
|
-
async (candidate) => {
|
|
9331
|
-
if (!candidate.needsBackfill) return candidate.summary;
|
|
9332
|
-
return await this.summaryFor(candidate.summary.id).catch(() => candidate.summary);
|
|
9333
|
-
}
|
|
9334
|
-
);
|
|
9335
|
-
return summaries.filter((s) => s !== null);
|
|
9336
|
-
}
|
|
9337
|
-
async collectSessionFiles(dir, prefix = "", depth = 0) {
|
|
9338
|
-
let entries;
|
|
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);
|
|
9339
9326
|
try {
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
9350
|
-
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
if (entry.name === "_index.jsonl") continue;
|
|
9354
|
-
const base = entry.name.replace(/\.jsonl$/, "");
|
|
9355
|
-
const id = prefix ? `${prefix}/${base}` : base;
|
|
9356
|
-
files.push({ id, filePath: path5.join(dir, entry.name) });
|
|
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;
|
|
9337
|
+
}
|
|
9338
|
+
} else {
|
|
9339
|
+
newlineAfterCheckpoint = totalSize;
|
|
9357
9340
|
}
|
|
9358
|
-
|
|
9359
|
-
|
|
9360
|
-
|
|
9361
|
-
|
|
9362
|
-
|
|
9363
|
-
|
|
9364
|
-
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
|
|
9370
|
-
|
|
9371
|
-
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
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();
|
|
9390
9387
|
}
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9394
|
-
const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
|
|
9395
|
-
return this.collectSessionIds(path5.join(dir, entry.name), childPrefix, depth + 1);
|
|
9396
|
-
})
|
|
9397
|
-
);
|
|
9398
|
-
return [...childIdArrays.flat(), ...fileIds];
|
|
9399
|
-
}
|
|
9400
|
-
async summaryFor(id) {
|
|
9401
|
-
const manifest = this.sessionPath(id, ".summary.json");
|
|
9402
|
-
const t0 = Date.now();
|
|
9403
|
-
let outcome = "success";
|
|
9404
|
-
let errorMsg;
|
|
9405
|
-
const fromManifest = await this.readSummaryManifest(id, t0);
|
|
9406
|
-
if (fromManifest) return fromManifest;
|
|
9407
|
-
try {
|
|
9408
|
-
const full = this.sessionPath(id, ".jsonl");
|
|
9409
|
-
const stat7 = await fsp6.stat(full);
|
|
9410
|
-
const summary = await this.summarize(id, stat7.mtime.toISOString());
|
|
9411
|
-
await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
|
|
9412
|
-
const msg = toErrorMessage(err);
|
|
9413
|
-
this.emitError(id, manifest, "summary_fallback", msg, true);
|
|
9414
|
-
console.warn(JSON.stringify({
|
|
9415
|
-
level: "warn",
|
|
9416
|
-
event: "session_store.manifest_write_failed",
|
|
9417
|
-
sessionId: id,
|
|
9418
|
-
message: msg,
|
|
9419
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9420
|
-
}));
|
|
9421
|
-
});
|
|
9422
|
-
outcome = "failure";
|
|
9423
|
-
errorMsg = "summary fallback \xE2\u20AC\u201D manifest rebuilt";
|
|
9424
|
-
this.emitRead(id, manifest, "summary", outcome, Date.now() - t0, errorMsg);
|
|
9425
|
-
return summary;
|
|
9388
|
+
await src.close();
|
|
9389
|
+
await fsp7.rename(tmpPath, this.filePath);
|
|
9390
|
+
this.handle = await fsp7.open(this.filePath, "a", 384);
|
|
9426
9391
|
} catch (err) {
|
|
9427
|
-
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
return {
|
|
9431
|
-
id,
|
|
9432
|
-
title: "(damaged)",
|
|
9433
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9434
|
-
model: "unknown",
|
|
9435
|
-
provider: "unknown",
|
|
9436
|
-
tokenTotal: 0
|
|
9437
|
-
};
|
|
9392
|
+
await fsp7.unlink(tmpPath).catch(() => void 0);
|
|
9393
|
+
this.handle = await fsp7.open(this.filePath, "a", 384).catch(() => this.handle);
|
|
9394
|
+
throw err;
|
|
9438
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;
|
|
9439
9408
|
}
|
|
9440
|
-
async
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
this.
|
|
9445
|
-
return JSON.parse(raw);
|
|
9446
|
-
} catch {
|
|
9447
|
-
return null;
|
|
9409
|
+
async clearSession() {
|
|
9410
|
+
if (!this.filePath) return;
|
|
9411
|
+
if (this.flushTimer) {
|
|
9412
|
+
clearTimeout(this.flushTimer);
|
|
9413
|
+
this.flushTimer = null;
|
|
9448
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");
|
|
9449
9426
|
}
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9459
|
-
model: "unknown",
|
|
9460
|
-
provider: "unknown",
|
|
9461
|
-
tokenTotal: 0
|
|
9462
|
-
};
|
|
9463
|
-
}
|
|
9464
|
-
mtime = stat7.mtime.toISOString();
|
|
9465
|
-
} catch {
|
|
9466
|
-
return null;
|
|
9467
|
-
}
|
|
9468
|
-
try {
|
|
9469
|
-
for await (const event of this.iterSessionEvents(ref.filePath)) {
|
|
9470
|
-
if (event.type === "session_start") {
|
|
9471
|
-
return {
|
|
9472
|
-
id: ref.id,
|
|
9473
|
-
title: "(empty session)",
|
|
9474
|
-
startedAt: event.ts,
|
|
9475
|
-
model: event.model ?? "unknown",
|
|
9476
|
-
provider: event.provider ?? "unknown",
|
|
9477
|
-
tokenTotal: 0
|
|
9478
|
-
};
|
|
9479
|
-
}
|
|
9480
|
-
}
|
|
9481
|
-
return {
|
|
9482
|
-
id: ref.id,
|
|
9483
|
-
title: "(empty session)",
|
|
9484
|
-
startedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
9485
|
-
model: "unknown",
|
|
9486
|
-
provider: "unknown",
|
|
9487
|
-
tokenTotal: 0
|
|
9488
|
-
};
|
|
9489
|
-
} catch {
|
|
9490
|
-
return {
|
|
9491
|
-
id: ref.id,
|
|
9492
|
-
title: "(damaged)",
|
|
9493
|
-
startedAt: mtime,
|
|
9494
|
-
model: "unknown",
|
|
9495
|
-
provider: "unknown",
|
|
9496
|
-
tokenTotal: 0
|
|
9497
|
-
};
|
|
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");
|
|
9498
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() });
|
|
9499
9443
|
}
|
|
9500
9444
|
/**
|
|
9501
|
-
*
|
|
9502
|
-
*
|
|
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).
|
|
9503
9482
|
*
|
|
9504
|
-
*
|
|
9505
|
-
*
|
|
9506
|
-
* If the session directory itself can't be removed, the error is surfaced
|
|
9507
|
-
* 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.
|
|
9508
9485
|
*/
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
|
|
9524
|
-
|
|
9525
|
-
|
|
9526
|
-
|
|
9527
|
-
|
|
9528
|
-
event: "session_store.delete_failed",
|
|
9529
|
-
sessionId: id,
|
|
9530
|
-
message: msg,
|
|
9531
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9532
|
-
}));
|
|
9533
|
-
}
|
|
9534
|
-
}
|
|
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();
|
|
9535
9505
|
}
|
|
9536
|
-
|
|
9537
|
-
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
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 } : {}
|
|
9544
9517
|
});
|
|
9545
|
-
await this.writeTombstone(id);
|
|
9546
9518
|
}
|
|
9547
|
-
|
|
9548
|
-
|
|
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
|
+
});
|
|
9549
9530
|
}
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
|
|
9553
|
-
|
|
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;
|
|
9554
9576
|
try {
|
|
9555
|
-
|
|
9556
|
-
|
|
9557
|
-
|
|
9558
|
-
|
|
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
|
+
);
|
|
9559
9584
|
}
|
|
9560
|
-
|
|
9561
|
-
|
|
9562
|
-
|
|
9563
|
-
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
}
|
|
9567
|
-
|
|
9568
|
-
|
|
9569
|
-
|
|
9570
|
-
|
|
9571
|
-
|
|
9572
|
-
|
|
9573
|
-
|
|
9574
|
-
|
|
9575
|
-
|
|
9576
|
-
|
|
9577
|
-
|
|
9578
|
-
if (isPrunableJsonl(entry.name)) await pruneFile(this.dir, entry.name, "");
|
|
9579
|
-
continue;
|
|
9580
|
-
}
|
|
9581
|
-
if (!entry.isDirectory()) continue;
|
|
9582
|
-
const dateDir = path5.join(this.dir, entry.name);
|
|
9583
|
-
const files = await fsp6.readdir(dateDir, { withFileTypes: true }).catch(() => []);
|
|
9584
|
-
for (const file of files) {
|
|
9585
|
-
if (!file.isFile() || !isPrunableJsonl(file.name)) continue;
|
|
9586
|
-
await pruneFile(dateDir, file.name, entry.name);
|
|
9587
|
-
}
|
|
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;
|
|
9588
9603
|
}
|
|
9589
|
-
|
|
9590
|
-
|
|
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
|
+
);
|
|
9591
9618
|
}
|
|
9592
|
-
|
|
9593
|
-
|
|
9594
|
-
|
|
9595
|
-
|
|
9596
|
-
|
|
9597
|
-
|
|
9598
|
-
|
|
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)
|
|
9599
9639
|
}
|
|
9600
|
-
|
|
9601
|
-
|
|
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;
|
|
9602
9652
|
}
|
|
9603
|
-
return deleted;
|
|
9604
9653
|
}
|
|
9605
|
-
async
|
|
9606
|
-
await this.ensureShardDir(id);
|
|
9654
|
+
async load(id) {
|
|
9607
9655
|
const file = this.sessionPath(id, ".jsonl");
|
|
9608
|
-
const
|
|
9609
|
-
|
|
9610
|
-
|
|
9611
|
-
|
|
9612
|
-
id,
|
|
9613
|
-
model: "unknown",
|
|
9614
|
-
provider: "unknown"
|
|
9615
|
-
})}
|
|
9616
|
-
`;
|
|
9617
|
-
await fsp6.writeFile(file, record, "utf8");
|
|
9618
|
-
await fsp6.unlink(meta).catch(() => void 0);
|
|
9619
|
-
}
|
|
9620
|
-
async summarize(id, mtime) {
|
|
9656
|
+
const t0 = Date.now();
|
|
9657
|
+
let outcome = "success";
|
|
9658
|
+
let errorMsg;
|
|
9659
|
+
let cacheHit = false;
|
|
9621
9660
|
try {
|
|
9622
|
-
const
|
|
9623
|
-
|
|
9624
|
-
|
|
9625
|
-
|
|
9626
|
-
|
|
9627
|
-
|
|
9628
|
-
|
|
9629
|
-
|
|
9630
|
-
|
|
9631
|
-
|
|
9632
|
-
|
|
9633
|
-
|
|
9634
|
-
|
|
9635
|
-
let
|
|
9636
|
-
let
|
|
9637
|
-
let
|
|
9638
|
-
let
|
|
9639
|
-
|
|
9640
|
-
|
|
9641
|
-
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
|
|
9645
|
-
|
|
9646
|
-
|
|
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
|
+
}
|
|
9647
9733
|
}
|
|
9648
|
-
}
|
|
9649
|
-
|
|
9650
|
-
} else if (e.type === "user_input") {
|
|
9651
|
-
if (title === "(empty session)") title = userInputTitle(e.content);
|
|
9652
|
-
} else if (e.type === "llm_response") {
|
|
9653
|
-
tokenIn += e.usage.input ?? 0;
|
|
9654
|
-
tokenOut += e.usage.output ?? 0;
|
|
9655
|
-
} else if (e.type === "in_flight_start") iterationCount++;
|
|
9656
|
-
else if (e.type === "tool_call_start") {
|
|
9657
|
-
toolCallCount++;
|
|
9658
|
-
toolBreakdown[e.name] = (toolBreakdown[e.name] ?? 0) + 1;
|
|
9659
|
-
} else if (e.type === "tool_result" && e.isError) toolErrorCount++;
|
|
9660
|
-
else if (e.type === "file_snapshot") fileChangeCount += e.files.length;
|
|
9661
|
-
else if (e.type === "error" || e.type === "provider_error") hasError = true;
|
|
9734
|
+
} catch {
|
|
9735
|
+
}
|
|
9662
9736
|
}
|
|
9663
|
-
if (
|
|
9664
|
-
|
|
9665
|
-
|
|
9666
|
-
|
|
9667
|
-
|
|
9668
|
-
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
|
+
});
|
|
9669
9742
|
}
|
|
9670
|
-
|
|
9671
|
-
|
|
9672
|
-
|
|
9673
|
-
|
|
9674
|
-
|
|
9675
|
-
|
|
9676
|
-
|
|
9677
|
-
|
|
9678
|
-
iterationCount: iterationCount > 0 ? iterationCount : void 0,
|
|
9679
|
-
toolCallCount: toolCallCount > 0 ? toolCallCount : void 0,
|
|
9680
|
-
toolErrorCount: toolErrorCount > 0 ? toolErrorCount : void 0,
|
|
9681
|
-
fileChangeCount: fileChangeCount > 0 ? fileChangeCount : void 0,
|
|
9682
|
-
toolBreakdown: Object.keys(toolBreakdown).length > 0 ? toolBreakdown : {},
|
|
9683
|
-
outcome
|
|
9684
|
-
};
|
|
9685
|
-
} catch {
|
|
9686
|
-
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 = {
|
|
9687
9751
|
id,
|
|
9688
|
-
|
|
9689
|
-
|
|
9690
|
-
model:
|
|
9691
|
-
provider:
|
|
9692
|
-
|
|
9752
|
+
startedAt: sessionStartEvent?.ts ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
9753
|
+
endedAt: sessionEndEvent?.ts,
|
|
9754
|
+
model: sessionModel,
|
|
9755
|
+
provider: sessionProvider,
|
|
9756
|
+
pendingToolUses: sessionPendingToolUses
|
|
9693
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
|
+
}
|
|
9694
9783
|
}
|
|
9695
9784
|
}
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
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;
|
|
9808
|
+
try {
|
|
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;
|
|
9699
9816
|
try {
|
|
9700
|
-
|
|
9701
|
-
|
|
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()) {
|
|
9702
9854
|
try {
|
|
9703
|
-
const parsed = JSON.parse(
|
|
9855
|
+
const parsed = JSON.parse(leftover);
|
|
9704
9856
|
if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
|
|
9705
|
-
|
|
9857
|
+
const ev = parsed;
|
|
9858
|
+
if (predicate(ev, eventIndex, ev.ts)) {
|
|
9859
|
+
out.push({ event: ev, eventIndex, ts: ev.ts });
|
|
9860
|
+
}
|
|
9706
9861
|
}
|
|
9707
9862
|
} catch {
|
|
9708
9863
|
}
|
|
9709
9864
|
}
|
|
9865
|
+
return out;
|
|
9710
9866
|
} finally {
|
|
9711
|
-
|
|
9712
|
-
stream.destroy();
|
|
9867
|
+
if (fh) await fh.close().catch(() => void 0);
|
|
9713
9868
|
}
|
|
9714
9869
|
}
|
|
9715
|
-
|
|
9716
|
-
|
|
9717
|
-
|
|
9718
|
-
|
|
9719
|
-
|
|
9720
|
-
|
|
9721
|
-
|
|
9722
|
-
|
|
9723
|
-
|
|
9724
|
-
|
|
9725
|
-
|
|
9726
|
-
|
|
9727
|
-
|
|
9728
|
-
|
|
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 [];
|
|
9729
9885
|
}
|
|
9730
9886
|
}
|
|
9731
|
-
return result;
|
|
9732
|
-
}
|
|
9733
|
-
var FileSessionWriter = class _FileSessionWriter {
|
|
9734
|
-
constructor(id, handle, startedAt, meta, events, opts = {}, traceId) {
|
|
9735
|
-
this.id = id;
|
|
9736
|
-
this.handle = handle;
|
|
9737
|
-
this.startedAt = startedAt;
|
|
9738
|
-
this.meta = meta;
|
|
9739
|
-
this.events = events;
|
|
9740
|
-
this.resumed = opts.resumed ?? false;
|
|
9741
|
-
this.manifestFile = opts.dir ? path5.join(opts.dir, `${path5.basename(id)}.summary.json`) : "";
|
|
9742
|
-
this.filePath = opts.filePath ?? "";
|
|
9743
|
-
this.secretScrubber = opts.secretScrubber;
|
|
9744
|
-
this.onCloseCb = opts.onClose;
|
|
9745
|
-
this.summary = {
|
|
9746
|
-
id,
|
|
9747
|
-
title: "(empty session)",
|
|
9748
|
-
startedAt,
|
|
9749
|
-
model: meta.model ?? "unknown",
|
|
9750
|
-
provider: meta.provider ?? "unknown",
|
|
9751
|
-
tokenTotal: 0
|
|
9752
|
-
};
|
|
9753
|
-
this.traceId = traceId;
|
|
9754
|
-
}
|
|
9755
|
-
id;
|
|
9756
|
-
handle;
|
|
9757
|
-
startedAt;
|
|
9758
|
-
meta;
|
|
9759
|
-
events;
|
|
9760
|
-
closed = false;
|
|
9761
|
-
closePromise = null;
|
|
9762
|
-
manifestFile;
|
|
9763
|
-
summary;
|
|
9764
|
-
tokenIn = 0;
|
|
9765
|
-
tokenOut = 0;
|
|
9766
|
-
filePath;
|
|
9767
|
-
get transcriptPath() {
|
|
9768
|
-
return this.filePath || void 0;
|
|
9769
|
-
}
|
|
9770
|
-
/**
|
|
9771
|
-
* Lazy session_start/session_resumed init, shared by all appenders.
|
|
9772
|
-
* A single promise (not a boolean) so a second append racing the first
|
|
9773
|
-
* can't push its event into the buffer BEFORE the first append's event —
|
|
9774
|
-
* every appender awaits the same init and resumes in FIFO call order.
|
|
9775
|
-
*/
|
|
9776
|
-
initPromise = null;
|
|
9777
|
-
ensureInit() {
|
|
9778
|
-
if (!this.initPromise) this.initPromise = this.writeSessionStartLazy();
|
|
9779
|
-
return this.initPromise;
|
|
9780
|
-
}
|
|
9781
|
-
resumed;
|
|
9782
|
-
appendFailCount = 0;
|
|
9783
|
-
lastAppendWarnAt = 0;
|
|
9784
|
-
secretScrubber;
|
|
9785
|
-
onCloseCb;
|
|
9786
|
-
/** Implements SessionWriter.traceId — propagated from ContextInit.traceId. */
|
|
9787
|
-
traceId;
|
|
9788
|
-
// ── Write buffer — batches events to reduce per-event disk I/O ─────────
|
|
9789
|
-
//
|
|
9790
|
-
// Every append() pushes the scrubbed event into an in-memory buffer instead
|
|
9791
|
-
// of calling handle.appendFile() synchronously. The buffer flushes to disk
|
|
9792
|
-
// when it reaches FLUSH_SIZE events OR after FLUSH_INTERVAL_MS of inactivity.
|
|
9793
|
-
// This cuts the number of disk writes by ~95% without changing the on-disk
|
|
9794
|
-
// format — the JSONL is still one JSON object per line.
|
|
9795
|
-
writeBuffer = [];
|
|
9796
|
-
flushTimer = null;
|
|
9797
|
-
static FLUSH_INTERVAL_MS = 500;
|
|
9798
|
-
static FLUSH_SIZE = 50;
|
|
9799
|
-
// ── Write serialization ─────────────────────────────────────────────────
|
|
9800
|
-
//
|
|
9801
|
-
// All disk writes are funneled through a FIFO promise chain. Without it,
|
|
9802
|
-
// a timer-driven flush racing an explicit flush()/close() issues two
|
|
9803
|
-
// concurrent appendFile() calls on the shared O_APPEND handle — the kernel
|
|
9804
|
-
// may complete them out of order (chronology breaks) or, for large
|
|
9805
|
-
// batches, interleave partial writes (torn JSONL lines). The chain keeps
|
|
9806
|
-
// exactly one write in flight; failures don't break the chain.
|
|
9807
|
-
writeChain = Promise.resolve();
|
|
9808
|
-
/** Enqueue a write on the FIFO chain. Resolves/rejects with that write. */
|
|
9809
|
-
enqueueWrite(data) {
|
|
9810
|
-
const write = this.writeChain.then(() => this.handle.appendFile(data, "utf8"));
|
|
9811
|
-
this.writeChain = write.then(
|
|
9812
|
-
() => void 0,
|
|
9813
|
-
() => void 0
|
|
9814
|
-
);
|
|
9815
|
-
return write;
|
|
9816
|
-
}
|
|
9817
|
-
// ── Enriched summary tracking ──────────────────────────────────────────
|
|
9818
|
-
iterationCount = 0;
|
|
9819
|
-
toolCallCount = 0;
|
|
9820
|
-
toolErrorCount = 0;
|
|
9821
|
-
toolBreakdown = {};
|
|
9822
|
-
fileChangeCount = 0;
|
|
9823
|
-
compactionCount = 0;
|
|
9824
|
-
outcome = void 0;
|
|
9825
9887
|
/**
|
|
9826
|
-
*
|
|
9827
|
-
*
|
|
9828
|
-
* `
|
|
9829
|
-
*
|
|
9830
|
-
*
|
|
9831
|
-
*
|
|
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.
|
|
9832
9896
|
*/
|
|
9833
|
-
|
|
9834
|
-
const
|
|
9835
|
-
if (!s) return event;
|
|
9836
|
-
if (event.type === "user_input") {
|
|
9837
|
-
return {
|
|
9838
|
-
...event,
|
|
9839
|
-
content: typeof event.content === "string" ? s.scrub(event.content) : s.scrubObject(event.content)
|
|
9840
|
-
};
|
|
9841
|
-
}
|
|
9842
|
-
if (event.type === "llm_response") {
|
|
9843
|
-
return { ...event, content: s.scrubObject(event.content) };
|
|
9844
|
-
}
|
|
9845
|
-
return event;
|
|
9846
|
-
}
|
|
9847
|
-
pendingFileSnapshots = [];
|
|
9848
|
-
/** Tracks open tool_use IDs during the current run to serialize on close for resume. */
|
|
9849
|
-
openToolUses = /* @__PURE__ */ new Set();
|
|
9850
|
-
recordFileChange(input) {
|
|
9851
|
-
this.pendingFileSnapshots.push(input);
|
|
9852
|
-
}
|
|
9853
|
-
get pendingToolUses() {
|
|
9854
|
-
return Array.from(this.openToolUses);
|
|
9855
|
-
}
|
|
9856
|
-
async writeSessionStartLazy() {
|
|
9857
|
-
const record = `${JSON.stringify({
|
|
9858
|
-
type: this.resumed ? "session_resumed" : "session_start",
|
|
9859
|
-
ts: this.startedAt,
|
|
9860
|
-
id: this.id,
|
|
9861
|
-
model: this.meta.model ?? "unknown",
|
|
9862
|
-
provider: this.meta.provider ?? "unknown"
|
|
9863
|
-
})}
|
|
9864
|
-
`;
|
|
9897
|
+
async listFiltered(criteria) {
|
|
9898
|
+
const limit = criteria.limit ?? 100;
|
|
9865
9899
|
try {
|
|
9866
|
-
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);
|
|
9867
9913
|
} catch {
|
|
9914
|
+
return [];
|
|
9868
9915
|
}
|
|
9869
9916
|
}
|
|
9870
|
-
|
|
9871
|
-
|
|
9872
|
-
|
|
9873
|
-
|
|
9874
|
-
|
|
9875
|
-
|
|
9876
|
-
|
|
9877
|
-
|
|
9878
|
-
|
|
9879
|
-
|
|
9880
|
-
|
|
9881
|
-
|
|
9882
|
-
|
|
9883
|
-
|
|
9884
|
-
|
|
9885
|
-
|
|
9886
|
-
|
|
9887
|
-
|
|
9888
|
-
|
|
9889
|
-
|
|
9890
|
-
|
|
9891
|
-
|
|
9892
|
-
|
|
9893
|
-
}
|
|
9894
|
-
if (this.writeBuffer.length >= _FileSessionWriter.FLUSH_SIZE) {
|
|
9895
|
-
if (this.flushTimer) {
|
|
9896
|
-
clearTimeout(this.flushTimer);
|
|
9897
|
-
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;
|
|
9898
9940
|
}
|
|
9899
|
-
|
|
9900
|
-
} else {
|
|
9901
|
-
this.scheduleFlush();
|
|
9902
|
-
}
|
|
9903
|
-
}
|
|
9904
|
-
/**
|
|
9905
|
-
* Flush buffered events to disk immediately. Critical events
|
|
9906
|
-
* (user_input, llm_response) call this so they survive SIGKILL/crash
|
|
9907
|
-
* instead of sitting in the in-memory buffer for up to 500ms.
|
|
9908
|
-
*
|
|
9909
|
-
* Idempotent — cancels any pending timer and writes whatever has
|
|
9910
|
-
* accumulated in the buffer. Safe to call even when the buffer
|
|
9911
|
-
* is empty (no-op).
|
|
9912
|
-
*/
|
|
9913
|
-
async flush() {
|
|
9914
|
-
if (this.flushTimer) {
|
|
9915
|
-
clearTimeout(this.flushTimer);
|
|
9916
|
-
this.flushTimer = null;
|
|
9941
|
+
} catch {
|
|
9917
9942
|
}
|
|
9918
|
-
await this.flushBuffer();
|
|
9919
9943
|
}
|
|
9920
|
-
/**
|
|
9921
|
-
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
|
|
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
|
+
}
|
|
9928
9955
|
}
|
|
9929
9956
|
/**
|
|
9930
|
-
*
|
|
9931
|
-
*
|
|
9932
|
-
* append path used — one warning every 5s with a suppressed count.
|
|
9933
|
-
* On failure the buffer is cleared (events are best-effort, same as
|
|
9934
|
-
* 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.
|
|
9935
9959
|
*/
|
|
9936
|
-
async
|
|
9937
|
-
if (this.writeBuffer.length === 0) return;
|
|
9938
|
-
const eventCount = this.writeBuffer.length;
|
|
9939
|
-
const batch = this.writeBuffer.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
9940
|
-
this.writeBuffer = [];
|
|
9960
|
+
async compactIndex() {
|
|
9941
9961
|
const t0 = Date.now();
|
|
9942
9962
|
let outcome = "success";
|
|
9943
9963
|
let errorMsg;
|
|
9944
9964
|
try {
|
|
9945
|
-
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;
|
|
9946
9972
|
} catch (err) {
|
|
9947
9973
|
outcome = "failure";
|
|
9948
9974
|
errorMsg = toErrorMessage(err);
|
|
9949
|
-
this.appendFailCount += eventCount;
|
|
9950
|
-
const now = Date.now();
|
|
9951
|
-
if (now - this.lastAppendWarnAt > 5e3) {
|
|
9952
|
-
const suppressed = this.appendFailCount - 1;
|
|
9953
|
-
const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
|
|
9954
|
-
console.warn(
|
|
9955
|
-
"[session] flush failed:",
|
|
9956
|
-
toErrorMessage(err),
|
|
9957
|
-
tail
|
|
9958
|
-
);
|
|
9959
|
-
this.lastAppendWarnAt = now;
|
|
9960
|
-
this.appendFailCount = 0;
|
|
9961
|
-
}
|
|
9962
9975
|
} finally {
|
|
9963
|
-
this.
|
|
9964
|
-
sessionId: this.id,
|
|
9965
|
-
store: "session",
|
|
9966
|
-
filePath: this.filePath,
|
|
9967
|
-
operation: "flush",
|
|
9968
|
-
outcome,
|
|
9969
|
-
durationMs: Date.now() - t0,
|
|
9970
|
-
...errorMsg !== void 0 ? { error: errorMsg } : {},
|
|
9971
|
-
...eventCount !== void 0 ? { eventCount } : {},
|
|
9972
|
-
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
9973
|
-
});
|
|
9976
|
+
this.emitWrite("~compact~", this.indexFile, "compact", outcome, Date.now() - t0, void 0, errorMsg);
|
|
9974
9977
|
}
|
|
9975
9978
|
}
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
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 {
|
|
9980
10018
|
}
|
|
9981
10019
|
}
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
9991
|
-
|
|
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 });
|
|
9992
10050
|
}
|
|
9993
|
-
} else if (event.type === "file_snapshot") {
|
|
9994
|
-
this.fileChangeCount += event.files.length;
|
|
9995
|
-
} else if (event.type === "compaction") {
|
|
9996
|
-
this.compactionCount++;
|
|
9997
10051
|
}
|
|
9998
|
-
|
|
9999
|
-
|
|
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 [""];
|
|
10000
10067
|
}
|
|
10001
|
-
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
this.summary = { ...this.summary, tokenTotal: this.tokenIn + this.tokenOut };
|
|
10007
|
-
} else if (event.type === "session_end") {
|
|
10008
|
-
const total = event.usage.input + event.usage.output;
|
|
10009
|
-
if (total > 0) this.summary = { ...this.summary, tokenTotal: total };
|
|
10010
|
-
} else if (event.type === "in_flight_start") {
|
|
10011
|
-
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);
|
|
10012
10073
|
}
|
|
10074
|
+
return shardKeys;
|
|
10013
10075
|
}
|
|
10014
|
-
async
|
|
10015
|
-
|
|
10016
|
-
|
|
10017
|
-
|
|
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("/"));
|
|
10018
10115
|
}
|
|
10019
|
-
async
|
|
10020
|
-
|
|
10021
|
-
|
|
10022
|
-
|
|
10023
|
-
|
|
10116
|
+
async collectSessionFiles(dir, prefix = "", depth = 0) {
|
|
10117
|
+
let entries;
|
|
10118
|
+
try {
|
|
10119
|
+
entries = await fsp7.readdir(dir, { withFileTypes: true });
|
|
10120
|
+
} catch {
|
|
10121
|
+
return [];
|
|
10024
10122
|
}
|
|
10025
|
-
|
|
10026
|
-
|
|
10027
|
-
|
|
10028
|
-
|
|
10029
|
-
|
|
10030
|
-
|
|
10031
|
-
|
|
10032
|
-
|
|
10033
|
-
|
|
10034
|
-
|
|
10035
|
-
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
|
|
10043
|
-
|
|
10044
|
-
}
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
|
|
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);
|
|
10058
10169
|
}
|
|
10059
10170
|
}
|
|
10060
|
-
const
|
|
10061
|
-
|
|
10062
|
-
|
|
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;
|
|
10063
10186
|
try {
|
|
10064
|
-
|
|
10065
|
-
|
|
10066
|
-
|
|
10067
|
-
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
|
|
10076
|
-
|
|
10077
|
-
...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
|
+
}));
|
|
10078
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
|
+
};
|
|
10079
10217
|
}
|
|
10218
|
+
}
|
|
10219
|
+
async readSummaryManifest(id, startTime = Date.now()) {
|
|
10220
|
+
const manifest = this.sessionPath(id, ".summary.json");
|
|
10080
10221
|
try {
|
|
10081
|
-
await
|
|
10222
|
+
const raw = await fsp7.readFile(manifest, "utf8");
|
|
10223
|
+
this.emitRead(id, manifest, "summary", "success", Date.now() - startTime);
|
|
10224
|
+
return JSON.parse(raw);
|
|
10082
10225
|
} catch {
|
|
10226
|
+
return null;
|
|
10083
10227
|
}
|
|
10084
10228
|
}
|
|
10085
|
-
async
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
await
|
|
10089
|
-
|
|
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;
|
|
10246
|
+
}
|
|
10247
|
+
try {
|
|
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
|
+
};
|
|
10258
|
+
}
|
|
10259
|
+
}
|
|
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
|
+
};
|
|
10090
10277
|
}
|
|
10091
|
-
await this.append({
|
|
10092
|
-
type: "checkpoint",
|
|
10093
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10094
|
-
promptIndex,
|
|
10095
|
-
promptPreview
|
|
10096
|
-
});
|
|
10097
|
-
this.events?.emit("checkpoint.written", {
|
|
10098
|
-
promptIndex,
|
|
10099
|
-
promptPreview,
|
|
10100
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10101
|
-
fileCount
|
|
10102
|
-
});
|
|
10103
|
-
}
|
|
10104
|
-
async writeFileSnapshot(promptIndex, files) {
|
|
10105
|
-
await this.append({
|
|
10106
|
-
type: "file_snapshot",
|
|
10107
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10108
|
-
promptIndex,
|
|
10109
|
-
files
|
|
10110
|
-
});
|
|
10111
10278
|
}
|
|
10112
10279
|
/**
|
|
10113
|
-
*
|
|
10114
|
-
*
|
|
10115
|
-
*
|
|
10116
|
-
*
|
|
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.
|
|
10117
10287
|
*/
|
|
10118
|
-
async
|
|
10119
|
-
|
|
10120
|
-
|
|
10121
|
-
|
|
10122
|
-
|
|
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
|
+
}));
|
|
10312
|
+
}
|
|
10313
|
+
}
|
|
10123
10314
|
}
|
|
10124
|
-
await
|
|
10125
|
-
|
|
10126
|
-
|
|
10127
|
-
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
|
|
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;
|
|
10133
10333
|
try {
|
|
10134
|
-
|
|
10135
|
-
|
|
10136
|
-
|
|
10137
|
-
|
|
10138
|
-
|
|
10139
|
-
|
|
10140
|
-
|
|
10141
|
-
|
|
10142
|
-
|
|
10143
|
-
|
|
10144
|
-
|
|
10145
|
-
|
|
10146
|
-
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
|
|
10159
|
-
checkpointByteOffset = lineStartOffset;
|
|
10160
|
-
}
|
|
10161
|
-
} else if (targetCheckpointSeen && event.promptIndex !== void 0 && event.promptIndex > targetPromptIndex) {
|
|
10162
|
-
removedCount++;
|
|
10163
|
-
} else if (targetCheckpointSeen && event.promptIndex === void 0) {
|
|
10164
|
-
removedCount++;
|
|
10165
|
-
} else if (!targetCheckpointSeen && event.promptIndex === void 0) {
|
|
10166
|
-
removedCount++;
|
|
10167
|
-
} else if (!targetCheckpointSeen && event.promptIndex !== void 0 && event.promptIndex > targetPromptIndex) {
|
|
10168
|
-
removedCount++;
|
|
10169
|
-
}
|
|
10170
|
-
} catch {
|
|
10171
|
-
}
|
|
10172
|
-
}
|
|
10173
|
-
}
|
|
10174
|
-
chunkPos = idx + 1;
|
|
10175
|
-
lineStartOffset = fileOffset + chunkPos;
|
|
10176
|
-
}
|
|
10177
|
-
fileOffset += bytesRead;
|
|
10178
|
-
if (chunkPos >= bytesRead) {
|
|
10179
|
-
lineStartOffset = fileOffset;
|
|
10180
|
-
}
|
|
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);
|
|
10342
|
+
try {
|
|
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;
|
|
10181
10359
|
}
|
|
10182
|
-
|
|
10183
|
-
|
|
10184
|
-
|
|
10185
|
-
|
|
10186
|
-
|
|
10187
|
-
|
|
10188
|
-
const tmpPath = `${this.filePath}.rewind.tmp`;
|
|
10189
|
-
const src = await fsp6.open(this.filePath, "r", 384);
|
|
10190
|
-
try {
|
|
10191
|
-
const statResult = await src.stat();
|
|
10192
|
-
const totalSize = statResult.size;
|
|
10193
|
-
const prefixBytes = checkpointByteOffset;
|
|
10194
|
-
let newlineAfterCheckpoint = prefixBytes;
|
|
10195
|
-
if (prefixBytes < totalSize) {
|
|
10196
|
-
const probeBuf = Buffer.alloc(Math.min(CHUNK_SIZE, totalSize - prefixBytes));
|
|
10197
|
-
const { bytesRead: probeRead } = await src.read(probeBuf, 0, probeBuf.length, prefixBytes);
|
|
10198
|
-
if (probeRead > 0) {
|
|
10199
|
-
const nl = probeBuf.indexOf("\n");
|
|
10200
|
-
newlineAfterCheckpoint = nl !== -1 ? prefixBytes + nl + 1 : totalSize;
|
|
10201
|
-
}
|
|
10202
|
-
} else {
|
|
10203
|
-
newlineAfterCheckpoint = totalSize;
|
|
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);
|
|
10204
10366
|
}
|
|
10205
|
-
|
|
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);
|
|
10206
10374
|
try {
|
|
10207
|
-
|
|
10208
|
-
|
|
10209
|
-
|
|
10210
|
-
const copyBuf = Buffer.alloc(toCopy);
|
|
10211
|
-
const { bytesRead: r } = await src.read(copyBuf, 0, toCopy, readOffset);
|
|
10212
|
-
if (r === 0) break;
|
|
10213
|
-
await writeFd.write(copyBuf, 0, r);
|
|
10214
|
-
readOffset += r;
|
|
10215
|
-
}
|
|
10216
|
-
const raw = await fsp6.readFile(this.filePath);
|
|
10217
|
-
const tail = raw.subarray(newlineAfterCheckpoint).toString("utf8");
|
|
10218
|
-
for (const line of tail.split("\n")) {
|
|
10219
|
-
if (!line.trim()) continue;
|
|
10220
|
-
try {
|
|
10221
|
-
JSON.parse(line);
|
|
10222
|
-
} catch {
|
|
10223
|
-
await writeFd.write(`${line}
|
|
10224
|
-
`, void 0, "utf8");
|
|
10225
|
-
}
|
|
10375
|
+
const remaining = await fsp7.readdir(dateDir);
|
|
10376
|
+
if (remaining.length === 0) {
|
|
10377
|
+
await fsp7.rmdir(dateDir).catch(() => void 0);
|
|
10226
10378
|
}
|
|
10227
|
-
}
|
|
10228
|
-
await writeFd.close();
|
|
10379
|
+
} catch {
|
|
10229
10380
|
}
|
|
10230
|
-
await src.close();
|
|
10231
|
-
await fsp6.rename(tmpPath, this.filePath);
|
|
10232
|
-
this.handle = await fsp6.open(this.filePath, "a", 384);
|
|
10233
|
-
} catch (err) {
|
|
10234
|
-
await fsp6.unlink(tmpPath).catch(() => void 0);
|
|
10235
|
-
this.handle = await fsp6.open(this.filePath, "a", 384).catch(() => this.handle);
|
|
10236
|
-
throw err;
|
|
10237
10381
|
}
|
|
10238
|
-
|
|
10239
|
-
type: "rewound",
|
|
10240
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10241
|
-
toPromptIndex: targetPromptIndex,
|
|
10242
|
-
revertedFiles: []
|
|
10243
|
-
});
|
|
10244
|
-
this.events?.emit("session.rewound", {
|
|
10245
|
-
toPromptIndex: targetPromptIndex,
|
|
10246
|
-
revertedFiles: [],
|
|
10247
|
-
removedEvents: removedCount
|
|
10248
|
-
});
|
|
10249
|
-
return removedCount;
|
|
10382
|
+
return deleted;
|
|
10250
10383
|
}
|
|
10251
|
-
async
|
|
10252
|
-
|
|
10253
|
-
|
|
10254
|
-
|
|
10255
|
-
this.flushTimer = null;
|
|
10256
|
-
}
|
|
10257
|
-
this.writeBuffer = [];
|
|
10258
|
-
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");
|
|
10259
10388
|
const record = `${JSON.stringify({
|
|
10260
10389
|
type: "session_start",
|
|
10261
10390
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10262
|
-
id
|
|
10263
|
-
model:
|
|
10264
|
-
provider:
|
|
10391
|
+
id,
|
|
10392
|
+
model: "unknown",
|
|
10393
|
+
provider: "unknown"
|
|
10265
10394
|
})}
|
|
10266
10395
|
`;
|
|
10267
|
-
await
|
|
10396
|
+
await fsp7.writeFile(file, record, "utf8");
|
|
10397
|
+
await fsp7.unlink(meta).catch(() => void 0);
|
|
10268
10398
|
}
|
|
10269
|
-
|
|
10270
|
-
|
|
10271
|
-
|
|
10272
|
-
|
|
10273
|
-
|
|
10274
|
-
|
|
10275
|
-
|
|
10276
|
-
|
|
10277
|
-
|
|
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
|
+
};
|
|
10278
10473
|
}
|
|
10279
|
-
await this.append({
|
|
10280
|
-
type: "in_flight_start",
|
|
10281
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10282
|
-
context
|
|
10283
|
-
});
|
|
10284
|
-
this.events?.emit("in_flight.started", { context, ts: (/* @__PURE__ */ new Date()).toISOString() });
|
|
10285
10474
|
}
|
|
10286
|
-
|
|
10287
|
-
|
|
10288
|
-
|
|
10289
|
-
|
|
10290
|
-
|
|
10291
|
-
|
|
10292
|
-
|
|
10293
|
-
|
|
10294
|
-
|
|
10295
|
-
|
|
10296
|
-
|
|
10297
|
-
|
|
10298
|
-
|
|
10299
|
-
|
|
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
|
+
}
|
|
10300
10493
|
}
|
|
10301
10494
|
};
|
|
10302
|
-
function
|
|
10303
|
-
const
|
|
10304
|
-
|
|
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;
|
|
10305
10511
|
}
|
|
10306
10512
|
function compareSessionSummaries(a, b) {
|
|
10307
10513
|
if (a.startedAt < b.startedAt) return 1;
|
|
10308
10514
|
if (a.startedAt > b.startedAt) return -1;
|
|
10309
10515
|
return a.id.localeCompare(b.id);
|
|
10310
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
|
+
}
|
|
10311
10529
|
async function mapWithConcurrency(items, concurrency, fn) {
|
|
10312
10530
|
if (items.length === 0) return [];
|
|
10313
10531
|
const out = new Array(items.length);
|
|
@@ -10333,9 +10551,9 @@ function makeDirectorSessionFactory(opts) {
|
|
|
10333
10551
|
let dir;
|
|
10334
10552
|
if (opts.store) {
|
|
10335
10553
|
store = opts.store;
|
|
10336
|
-
dir = opts.sessionsRoot ?
|
|
10554
|
+
dir = opts.sessionsRoot ? path6.join(opts.sessionsRoot, runId) : "(caller-managed)";
|
|
10337
10555
|
} else if (opts.sessionsRoot) {
|
|
10338
|
-
dir =
|
|
10556
|
+
dir = path6.join(opts.sessionsRoot, runId);
|
|
10339
10557
|
store = new DefaultSessionStore({ dir });
|
|
10340
10558
|
} else {
|
|
10341
10559
|
throw new Error("makeDirectorSessionFactory requires either `store` or `sessionsRoot`");
|
|
@@ -10647,7 +10865,7 @@ var FleetManager = class {
|
|
|
10647
10865
|
})),
|
|
10648
10866
|
usage: this.usage.snapshot()
|
|
10649
10867
|
};
|
|
10650
|
-
await
|
|
10868
|
+
await fsp7.mkdir(path6.dirname(this.manifestPath), { recursive: true });
|
|
10651
10869
|
await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
|
|
10652
10870
|
return this.manifestPath;
|
|
10653
10871
|
}
|
|
@@ -10802,12 +11020,18 @@ var DefaultMailbox = class {
|
|
|
10802
11020
|
_byTo = /* @__PURE__ */ new Map();
|
|
10803
11021
|
/** Secondary index: sender → Set of messages (points into _messageCache). */
|
|
10804
11022
|
_byFrom = /* @__PURE__ */ new Map();
|
|
11023
|
+
/** Counts malformed JSONL lines skipped during parsing for observability. */
|
|
11024
|
+
_corruptionCount = 0;
|
|
10805
11025
|
constructor(sessionDir) {
|
|
10806
|
-
this.filePath =
|
|
11026
|
+
this.filePath = path6.join(sessionDir, MAILBOX_FILE);
|
|
10807
11027
|
}
|
|
10808
11028
|
get mailboxPath() {
|
|
10809
11029
|
return this.filePath;
|
|
10810
11030
|
}
|
|
11031
|
+
/** Returns the count of malformed JSONL lines encountered during reads. */
|
|
11032
|
+
get corruptionCount() {
|
|
11033
|
+
return this._corruptionCount;
|
|
11034
|
+
}
|
|
10811
11035
|
// ── Send ──────────────────────────────────────────────────────────────
|
|
10812
11036
|
async send(input) {
|
|
10813
11037
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -10828,10 +11052,13 @@ var DefaultMailbox = class {
|
|
|
10828
11052
|
taskContext: input.taskContext
|
|
10829
11053
|
};
|
|
10830
11054
|
const line = JSON.stringify(msg) + LINE_SEPARATOR;
|
|
10831
|
-
await
|
|
11055
|
+
await fsp7.mkdir(path6.dirname(this.filePath), { recursive: true });
|
|
10832
11056
|
await withFileLock(this.filePath, async () => {
|
|
10833
|
-
await
|
|
11057
|
+
await fsp7.appendFile(this.filePath, line, "utf8");
|
|
10834
11058
|
this._pushToCache(msg);
|
|
11059
|
+
const { mtime, size } = await this._statUnderLockOrAbsent();
|
|
11060
|
+
this._messageCacheMtime = mtime;
|
|
11061
|
+
this._messageCacheSize = size;
|
|
10835
11062
|
});
|
|
10836
11063
|
return msg;
|
|
10837
11064
|
}
|
|
@@ -10908,9 +11135,10 @@ var DefaultMailbox = class {
|
|
|
10908
11135
|
}
|
|
10909
11136
|
if (changed) {
|
|
10910
11137
|
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
10911
|
-
await
|
|
11138
|
+
await fsp7.writeFile(this.filePath, serialized, "utf8");
|
|
10912
11139
|
}
|
|
10913
|
-
this.
|
|
11140
|
+
const { mtime, size } = await this._statUnderLockOrAbsent();
|
|
11141
|
+
this._setMessageCache(all, mtime, size);
|
|
10914
11142
|
});
|
|
10915
11143
|
return updated;
|
|
10916
11144
|
}
|
|
@@ -10966,8 +11194,9 @@ var DefaultMailbox = class {
|
|
|
10966
11194
|
}
|
|
10967
11195
|
async clearAll() {
|
|
10968
11196
|
await withFileLock(this.filePath, async () => {
|
|
10969
|
-
await
|
|
10970
|
-
this.
|
|
11197
|
+
await fsp7.writeFile(this.filePath, "", "utf8");
|
|
11198
|
+
const { mtime, size } = await this._statUnderLockOrAbsent();
|
|
11199
|
+
this._setMessageCache([], mtime, size);
|
|
10971
11200
|
});
|
|
10972
11201
|
}
|
|
10973
11202
|
async purgeStale(opts) {
|
|
@@ -10998,9 +11227,10 @@ var DefaultMailbox = class {
|
|
|
10998
11227
|
remaining = kept.length;
|
|
10999
11228
|
if (kept.length < all.length) {
|
|
11000
11229
|
const content = kept.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
11001
|
-
await
|
|
11230
|
+
await fsp7.writeFile(this.filePath, content, "utf8");
|
|
11002
11231
|
}
|
|
11003
|
-
this.
|
|
11232
|
+
const { mtime, size } = await this._statUnderLockOrAbsent();
|
|
11233
|
+
this._setMessageCache(kept, mtime, size);
|
|
11004
11234
|
});
|
|
11005
11235
|
return {
|
|
11006
11236
|
completedPurged,
|
|
@@ -11020,7 +11250,7 @@ var DefaultMailbox = class {
|
|
|
11020
11250
|
// ── Internal ──────────────────────────────────────────────────────────
|
|
11021
11251
|
async _readAll() {
|
|
11022
11252
|
try {
|
|
11023
|
-
const raw = await
|
|
11253
|
+
const raw = await fsp7.readFile(this.filePath, "utf8");
|
|
11024
11254
|
return this._parseLines(raw);
|
|
11025
11255
|
} catch (err) {
|
|
11026
11256
|
if (err.code === "ENOENT") return [];
|
|
@@ -11052,7 +11282,9 @@ var DefaultMailbox = class {
|
|
|
11052
11282
|
const msg = parsed;
|
|
11053
11283
|
this._messageCache.push(msg);
|
|
11054
11284
|
this._indexMsg(msg);
|
|
11055
|
-
} catch {
|
|
11285
|
+
} catch (err) {
|
|
11286
|
+
this._corruptionCount++;
|
|
11287
|
+
console.debug(`[mailbox] skipped malformed line during incremental read: ${err.message}`);
|
|
11056
11288
|
}
|
|
11057
11289
|
}
|
|
11058
11290
|
return this._messageCache;
|
|
@@ -11074,19 +11306,38 @@ var DefaultMailbox = class {
|
|
|
11074
11306
|
delete parsed["readAt"];
|
|
11075
11307
|
}
|
|
11076
11308
|
messages.push(parsed);
|
|
11077
|
-
} catch {
|
|
11309
|
+
} catch (err) {
|
|
11310
|
+
this._corruptionCount++;
|
|
11311
|
+
console.debug(`[mailbox] skipped malformed line during full parse: ${err.message}`);
|
|
11078
11312
|
}
|
|
11079
11313
|
}
|
|
11080
11314
|
return messages;
|
|
11081
11315
|
}
|
|
11316
|
+
/**
|
|
11317
|
+
* Stat the mailbox file under the assumption that we are holding the
|
|
11318
|
+
* file lock, and that a write to the file has just completed. Returns
|
|
11319
|
+
* the (mtimeMs, size) pair, or (-1, -1) if the file does not exist
|
|
11320
|
+
* (e.g. ackMany/purgeStale on a session that has never sent a message).
|
|
11321
|
+
*/
|
|
11322
|
+
async _statUnderLockOrAbsent() {
|
|
11323
|
+
try {
|
|
11324
|
+
const st = await fsp7.stat(this.filePath);
|
|
11325
|
+
return { mtime: st.mtimeMs, size: st.size };
|
|
11326
|
+
} catch (err) {
|
|
11327
|
+
if (err.code === "ENOENT") {
|
|
11328
|
+
return { mtime: -1, size: -1 };
|
|
11329
|
+
}
|
|
11330
|
+
throw err;
|
|
11331
|
+
}
|
|
11332
|
+
}
|
|
11082
11333
|
async _readAllCached() {
|
|
11083
11334
|
try {
|
|
11084
|
-
const st = await
|
|
11335
|
+
const st = await fsp7.stat(this.filePath);
|
|
11085
11336
|
if (this._messageCache !== null && this._messageCacheMtime === st.mtimeMs && this._messageCacheSize === st.size) {
|
|
11086
11337
|
return this._messageCache;
|
|
11087
11338
|
}
|
|
11088
11339
|
if (this._messageCache !== null && this._messageCacheSize >= 0 && st.size > this._messageCacheSize) {
|
|
11089
|
-
const fd = await
|
|
11340
|
+
const fd = await fsp7.open(this.filePath, "r");
|
|
11090
11341
|
try {
|
|
11091
11342
|
const updated = await this._readNewMessagesOnly(fd, this._messageCacheSize, st.size);
|
|
11092
11343
|
this._messageCacheMtime = st.mtimeMs;
|
|
@@ -11118,16 +11369,8 @@ var DefaultMailbox = class {
|
|
|
11118
11369
|
}
|
|
11119
11370
|
this._messageCache = messages;
|
|
11120
11371
|
this._buildIndexes(messages);
|
|
11121
|
-
|
|
11122
|
-
|
|
11123
|
-
this._messageCacheSize = size;
|
|
11124
|
-
return;
|
|
11125
|
-
}
|
|
11126
|
-
void fsp6.stat(this.filePath).then((st) => {
|
|
11127
|
-
this._messageCacheMtime = st.mtimeMs;
|
|
11128
|
-
this._messageCacheSize = st.size;
|
|
11129
|
-
}).catch(() => {
|
|
11130
|
-
});
|
|
11372
|
+
this._messageCacheMtime = mtime;
|
|
11373
|
+
this._messageCacheSize = size;
|
|
11131
11374
|
}
|
|
11132
11375
|
_pushToCache(msg) {
|
|
11133
11376
|
if (this._messageCache === null) return;
|
|
@@ -11307,7 +11550,7 @@ var REGISTRY_CACHE_TTL_MS = 2e3;
|
|
|
11307
11550
|
var LINE_SEPARATOR2 = "\n";
|
|
11308
11551
|
var MESSAGE_CACHE_MAX_ENTRIES2 = 1e4;
|
|
11309
11552
|
function resolveProjectDir(projectRoot, globalRoot) {
|
|
11310
|
-
return
|
|
11553
|
+
return path6.join(globalRoot, "projects", projectSlug(projectRoot));
|
|
11311
11554
|
}
|
|
11312
11555
|
var GlobalMailbox = class {
|
|
11313
11556
|
/** Path to the JSONL message file. */
|
|
@@ -11360,14 +11603,14 @@ var GlobalMailbox = class {
|
|
|
11360
11603
|
* @param hqPublisher — optional HQ publisher, or getter, for cross-project telemetry
|
|
11361
11604
|
*/
|
|
11362
11605
|
constructor(projectDir, events, hqPublisher) {
|
|
11363
|
-
this.messagePath =
|
|
11364
|
-
this.registryPath =
|
|
11365
|
-
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);
|
|
11366
11609
|
this._events = events;
|
|
11367
11610
|
this._hqPublisher = hqPublisher;
|
|
11368
11611
|
}
|
|
11369
11612
|
get hqMailboxId() {
|
|
11370
|
-
return `${
|
|
11613
|
+
return `${path6.basename(path6.dirname(this.messagePath))}:mailbox`;
|
|
11371
11614
|
}
|
|
11372
11615
|
get hqPublisher() {
|
|
11373
11616
|
return typeof this._hqPublisher === "function" ? this._hqPublisher() : this._hqPublisher;
|
|
@@ -11404,9 +11647,9 @@ var GlobalMailbox = class {
|
|
|
11404
11647
|
taskContext: input.taskContext
|
|
11405
11648
|
};
|
|
11406
11649
|
const line = JSON.stringify(msg) + LINE_SEPARATOR2;
|
|
11407
|
-
await
|
|
11650
|
+
await fsp7.mkdir(path6.dirname(this.messagePath), { recursive: true });
|
|
11408
11651
|
await withFileLock(this.messagePath, async () => {
|
|
11409
|
-
await
|
|
11652
|
+
await fsp7.appendFile(this.messagePath, line, "utf8");
|
|
11410
11653
|
this._pushToCache(msg);
|
|
11411
11654
|
});
|
|
11412
11655
|
this.publishHqMailboxEvent({ mailboxId: this.hqMailboxId, action: "message.sent", message: msg });
|
|
@@ -11472,7 +11715,7 @@ var GlobalMailbox = class {
|
|
|
11472
11715
|
}
|
|
11473
11716
|
if (changed) {
|
|
11474
11717
|
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR2) + LINE_SEPARATOR2;
|
|
11475
|
-
await
|
|
11718
|
+
await fsp7.writeFile(this.messagePath, serialized, "utf8");
|
|
11476
11719
|
}
|
|
11477
11720
|
cacheSnapshot = all;
|
|
11478
11721
|
});
|
|
@@ -11690,7 +11933,7 @@ var GlobalMailbox = class {
|
|
|
11690
11933
|
}
|
|
11691
11934
|
async clearAll() {
|
|
11692
11935
|
await withFileLock(this.messagePath, async () => {
|
|
11693
|
-
await
|
|
11936
|
+
await fsp7.writeFile(this.messagePath, "", "utf8");
|
|
11694
11937
|
});
|
|
11695
11938
|
this._setMessageCache([]);
|
|
11696
11939
|
}
|
|
@@ -11722,7 +11965,7 @@ var GlobalMailbox = class {
|
|
|
11722
11965
|
remaining = kept.length;
|
|
11723
11966
|
if (kept.length < all.length) {
|
|
11724
11967
|
const content = kept.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR2) + LINE_SEPARATOR2;
|
|
11725
|
-
await
|
|
11968
|
+
await fsp7.writeFile(this.messagePath, content, "utf8");
|
|
11726
11969
|
}
|
|
11727
11970
|
this._setMessageCache(kept);
|
|
11728
11971
|
});
|
|
@@ -11742,7 +11985,7 @@ var GlobalMailbox = class {
|
|
|
11742
11985
|
*/
|
|
11743
11986
|
async _readMessages() {
|
|
11744
11987
|
try {
|
|
11745
|
-
const raw = await
|
|
11988
|
+
const raw = await fsp7.readFile(this.messagePath, "utf8");
|
|
11746
11989
|
return this._parseLines(raw);
|
|
11747
11990
|
} catch (err) {
|
|
11748
11991
|
if (err.code === "ENOENT") return [];
|
|
@@ -11832,12 +12075,12 @@ var GlobalMailbox = class {
|
|
|
11832
12075
|
*/
|
|
11833
12076
|
async _readMessagesCached() {
|
|
11834
12077
|
try {
|
|
11835
|
-
const st = await
|
|
12078
|
+
const st = await fsp7.stat(this.messagePath);
|
|
11836
12079
|
if (this._messageCache !== null && this._messageCacheMtime === st.mtimeMs && this._messageCacheSize === st.size) {
|
|
11837
12080
|
return this._messageCache;
|
|
11838
12081
|
}
|
|
11839
12082
|
if (this._messageCache !== null && this._messageCacheSize >= 0 && st.size > this._messageCacheSize) {
|
|
11840
|
-
const fd = await
|
|
12083
|
+
const fd = await fsp7.open(this.messagePath, "r");
|
|
11841
12084
|
try {
|
|
11842
12085
|
const updated = await this._readNewMessagesOnly(fd, this._messageCacheSize, st.size);
|
|
11843
12086
|
this._messageCacheMtime = st.mtimeMs;
|
|
@@ -11875,7 +12118,7 @@ var GlobalMailbox = class {
|
|
|
11875
12118
|
this._messageCacheMtime = mtime;
|
|
11876
12119
|
this._messageCacheSize = size;
|
|
11877
12120
|
} else {
|
|
11878
|
-
void
|
|
12121
|
+
void fsp7.stat(this.messagePath).then((st) => {
|
|
11879
12122
|
this._messageCacheMtime = st.mtimeMs;
|
|
11880
12123
|
this._messageCacheSize = st.size;
|
|
11881
12124
|
}).catch(() => {
|
|
@@ -11899,14 +12142,14 @@ var GlobalMailbox = class {
|
|
|
11899
12142
|
this._messageCache.push(msg);
|
|
11900
12143
|
}
|
|
11901
12144
|
async _ensureRegistry() {
|
|
11902
|
-
await
|
|
12145
|
+
await fsp7.mkdir(path6.dirname(this.registryPath), { recursive: true });
|
|
11903
12146
|
}
|
|
11904
12147
|
async _readRegistry(opts) {
|
|
11905
12148
|
if (!opts?.fresh && this._registryCache && Date.now() - this._registryCacheAt < REGISTRY_CACHE_TTL_MS) {
|
|
11906
12149
|
return new Map(this._registryCache);
|
|
11907
12150
|
}
|
|
11908
12151
|
try {
|
|
11909
|
-
const raw = await
|
|
12152
|
+
const raw = await fsp7.readFile(this.registryPath, "utf8");
|
|
11910
12153
|
const data = JSON.parse(raw);
|
|
11911
12154
|
const map = /* @__PURE__ */ new Map();
|
|
11912
12155
|
for (const [id, agent] of Object.entries(data)) {
|
|
@@ -11939,19 +12182,19 @@ var GlobalMailbox = class {
|
|
|
11939
12182
|
obj[id] = agent;
|
|
11940
12183
|
}
|
|
11941
12184
|
const tmp = `${this.registryPath}.${randomUUID().slice(0, 8)}.tmp`;
|
|
11942
|
-
await
|
|
11943
|
-
await
|
|
12185
|
+
await fsp7.writeFile(tmp, JSON.stringify(obj), "utf8");
|
|
12186
|
+
await fsp7.rename(tmp, this.registryPath);
|
|
11944
12187
|
}
|
|
11945
12188
|
// ── Client registry internals ───────────────────────────────────────────
|
|
11946
12189
|
async _ensureClientRegistry() {
|
|
11947
|
-
await
|
|
12190
|
+
await fsp7.mkdir(path6.dirname(this.clientRegistryPath), { recursive: true });
|
|
11948
12191
|
}
|
|
11949
12192
|
async _readClientRegistry(opts) {
|
|
11950
12193
|
if (!opts?.fresh && this._clientRegistryCache && Date.now() - this._clientRegistryCacheAt < REGISTRY_CACHE_TTL_MS) {
|
|
11951
12194
|
return new Map(this._clientRegistryCache);
|
|
11952
12195
|
}
|
|
11953
12196
|
try {
|
|
11954
|
-
const raw = await
|
|
12197
|
+
const raw = await fsp7.readFile(this.clientRegistryPath, "utf8");
|
|
11955
12198
|
const data = JSON.parse(raw);
|
|
11956
12199
|
const map = /* @__PURE__ */ new Map();
|
|
11957
12200
|
for (const [id, client] of Object.entries(data)) {
|
|
@@ -11984,8 +12227,8 @@ var GlobalMailbox = class {
|
|
|
11984
12227
|
obj[id] = client;
|
|
11985
12228
|
}
|
|
11986
12229
|
const tmp = `${this.clientRegistryPath}.${randomUUID().slice(0, 8)}.tmp`;
|
|
11987
|
-
await
|
|
11988
|
-
await
|
|
12230
|
+
await fsp7.writeFile(tmp, JSON.stringify(obj), "utf8");
|
|
12231
|
+
await fsp7.rename(tmp, this.clientRegistryPath);
|
|
11989
12232
|
}
|
|
11990
12233
|
};
|
|
11991
12234
|
function defaultResolveProjectDir(ctx) {
|
|
@@ -12458,13 +12701,13 @@ function makeDependencyWatcherConfig(opts) {
|
|
|
12458
12701
|
const globPatterns = patterns.filter((p) => p.includes("*"));
|
|
12459
12702
|
const plainPatterns = patterns.filter((p) => !p.includes("*"));
|
|
12460
12703
|
function matchesPattern(filePath) {
|
|
12461
|
-
const
|
|
12462
|
-
if (plainPatterns.includes(
|
|
12704
|
+
const basename7 = filePath.split("/").pop()?.split("\\").pop() ?? "";
|
|
12705
|
+
if (plainPatterns.includes(basename7)) return true;
|
|
12463
12706
|
for (const gp of globPatterns) {
|
|
12464
12707
|
const regex = new RegExp(
|
|
12465
12708
|
"^" + gp.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$"
|
|
12466
12709
|
);
|
|
12467
|
-
if (regex.test(
|
|
12710
|
+
if (regex.test(basename7)) return true;
|
|
12468
12711
|
}
|
|
12469
12712
|
return false;
|
|
12470
12713
|
}
|
|
@@ -12591,11 +12834,11 @@ function createMailboxHooks(opts) {
|
|
|
12591
12834
|
var DEFAULT_MAX_ENTRIES = 1e4;
|
|
12592
12835
|
var LOG_FILENAME = "package-authors.json";
|
|
12593
12836
|
function logPath(storageDir) {
|
|
12594
|
-
return
|
|
12837
|
+
return path6.join(storageDir, LOG_FILENAME);
|
|
12595
12838
|
}
|
|
12596
12839
|
async function loadLog(storageDir, projectRoot) {
|
|
12597
12840
|
try {
|
|
12598
|
-
const raw = await
|
|
12841
|
+
const raw = await fsp7.readFile(logPath(storageDir), "utf-8");
|
|
12599
12842
|
const parsed = JSON.parse(raw);
|
|
12600
12843
|
if (!parsed.entries || !Array.isArray(parsed.entries)) {
|
|
12601
12844
|
return { projectRoot, entries: [] };
|
|
@@ -12609,13 +12852,13 @@ async function loadLog(storageDir, projectRoot) {
|
|
|
12609
12852
|
}
|
|
12610
12853
|
}
|
|
12611
12854
|
async function saveLog(storageDir, log) {
|
|
12612
|
-
await
|
|
12855
|
+
await fsp7.mkdir(storageDir, { recursive: true });
|
|
12613
12856
|
const tmp = `${logPath(storageDir)}.tmp.${Date.now()}`;
|
|
12614
|
-
await
|
|
12615
|
-
await
|
|
12857
|
+
await fsp7.writeFile(tmp, JSON.stringify(log, null, 2) + "\n", "utf-8");
|
|
12858
|
+
await fsp7.rename(tmp, logPath(storageDir));
|
|
12616
12859
|
}
|
|
12617
12860
|
function detectEcosystem(manifestPath) {
|
|
12618
|
-
const name =
|
|
12861
|
+
const name = path6.basename(manifestPath).toLowerCase();
|
|
12619
12862
|
if (name === "package.json") return "npm";
|
|
12620
12863
|
if (name === "go.mod") return "go";
|
|
12621
12864
|
if (name === "cargo.toml") return "cargo";
|
|
@@ -12898,8 +13141,8 @@ var KnowledgeGraph = class {
|
|
|
12898
13141
|
return this.index;
|
|
12899
13142
|
}
|
|
12900
13143
|
constructor(sessionDir) {
|
|
12901
|
-
this.filePath =
|
|
12902
|
-
this.graphFilePath =
|
|
13144
|
+
this.filePath = path6.join(sessionDir, "_knowledge_graph");
|
|
13145
|
+
this.graphFilePath = path6.join(this.filePath, "graph.jsonl");
|
|
12903
13146
|
}
|
|
12904
13147
|
// ── Write ──────────────────────────────────────────────────────────────
|
|
12905
13148
|
/**
|
|
@@ -13080,23 +13323,23 @@ var KnowledgeGraph = class {
|
|
|
13080
13323
|
}
|
|
13081
13324
|
}
|
|
13082
13325
|
async _persist(node) {
|
|
13083
|
-
await
|
|
13326
|
+
await fsp7.mkdir(this.filePath, { recursive: true });
|
|
13084
13327
|
const line = JSON.stringify(node) + "\n";
|
|
13085
13328
|
await withFileLock(this.graphFilePath, async () => {
|
|
13086
|
-
await
|
|
13329
|
+
await fsp7.appendFile(this.graphFilePath, line, "utf8");
|
|
13087
13330
|
});
|
|
13088
13331
|
}
|
|
13089
13332
|
async _append(node) {
|
|
13090
|
-
await
|
|
13333
|
+
await fsp7.mkdir(this.filePath, { recursive: true });
|
|
13091
13334
|
const line = JSON.stringify({ op: "update", node }) + "\n";
|
|
13092
13335
|
await withFileLock(this.graphFilePath, async () => {
|
|
13093
|
-
await
|
|
13336
|
+
await fsp7.appendFile(this.graphFilePath, line, "utf8");
|
|
13094
13337
|
});
|
|
13095
13338
|
}
|
|
13096
13339
|
/** Rebuild in-memory state from the log file. Call on startup. */
|
|
13097
13340
|
async load() {
|
|
13098
13341
|
try {
|
|
13099
|
-
const content = await
|
|
13342
|
+
const content = await fsp7.readFile(this.graphFilePath, "utf8");
|
|
13100
13343
|
const lines = content.split("\n").filter(Boolean);
|
|
13101
13344
|
for (const line of lines) {
|
|
13102
13345
|
try {
|
|
@@ -15527,11 +15770,11 @@ var AgentMonitorService = class {
|
|
|
15527
15770
|
this._onEntry?.(entry);
|
|
15528
15771
|
}
|
|
15529
15772
|
async _appendToFile(subagentId, entry) {
|
|
15530
|
-
const dir =
|
|
15531
|
-
await
|
|
15532
|
-
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");
|
|
15533
15776
|
const line = JSON.stringify(entry) + "\n";
|
|
15534
|
-
await
|
|
15777
|
+
await fsp7.appendFile(filePath, line, { encoding: "utf8" });
|
|
15535
15778
|
}
|
|
15536
15779
|
_uid() {
|
|
15537
15780
|
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|