@wrongstack/core 0.273.0 → 0.274.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agent-bridge-BZ2enORi.d.ts → agent-bridge-DFo21wmY.d.ts} +1 -1
- package/dist/{agent-subagent-runner-ehb4xGvd.d.ts → agent-subagent-runner-BwmkIDEd.d.ts} +7 -7
- package/dist/{brain-BxN2k2HP.d.ts → brain-gfZX3the.d.ts} +11 -5
- package/dist/{provider-model-resolve-DFd3IPpw.d.ts → codex-catalog-CooZ6mOl.d.ts} +47 -4
- package/dist/{compactor-72ug-ZRB.d.ts → compactor-riTOds0f.d.ts} +1 -1
- package/dist/{config-C8IYxlO8.d.ts → config-BxcrDzri.d.ts} +60 -4
- package/dist/{context-Dw55zZ_Q.d.ts → context-DERiLofu.d.ts} +60 -1
- package/dist/coordination/index.d.ts +27 -16
- package/dist/coordination/index.js +1641 -1398
- package/dist/coordination/index.js.map +1 -1
- package/dist/defaults/index.d.ts +26 -26
- package/dist/defaults/index.js +2154 -1466
- package/dist/defaults/index.js.map +1 -1
- package/dist/execution/index.d.ts +15 -15
- package/dist/execution/index.js +77 -9
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/prompt-enhancer.d.ts +1 -1
- package/dist/extension/index.d.ts +6 -6
- package/dist/{global-mailbox-C9dsc9Y_.d.ts → global-mailbox-Cr8TW4t_.d.ts} +1 -1
- package/dist/{goal-preamble-NhflDjYb.d.ts → goal-preamble-CEhROp1e.d.ts} +9 -9
- package/dist/{goal-store-Cx363x7Z.d.ts → goal-store-xNSVxmpV.d.ts} +1 -1
- package/dist/hq/index.d.ts +5 -5
- package/dist/{index-B7fHDt0B.d.ts → index-00KPKAlm.d.ts} +5 -5
- package/dist/{index-BbVprU-9.d.ts → index-pDBSBE1r.d.ts} +2 -2
- package/dist/index.d.ts +72 -45
- package/dist/index.js +2840 -1769
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/index.d.ts +6 -6
- package/dist/kernel/index.d.ts +11 -11
- package/dist/kernel/index.js.map +1 -1
- package/dist/{mcp-servers-B6fSRNC1.d.ts → mcp-servers-BIwRiOxe.d.ts} +3 -3
- package/dist/models/index.d.ts +5 -5
- package/dist/models/index.js +40 -2
- package/dist/models/index.js.map +1 -1
- package/dist/{models-registry-4C6Wr91w.d.ts → models-registry-8OorW51H.d.ts} +1 -1
- package/dist/{multi-agent-coordinator-q1skFeNP.d.ts → multi-agent-coordinator-CNx48Zoz.d.ts} +1 -1
- package/dist/{null-fleet-bus-C9rrgQwc.d.ts → null-fleet-bus-B4ZUJYL6.d.ts} +6 -6
- package/dist/observability/index.d.ts +2 -2
- package/dist/{parallel-eternal-engine-CtXly2Sf.d.ts → parallel-eternal-engine-rsIclDqO.d.ts} +9 -9
- package/dist/{path-resolver-Bim6G5Jz.d.ts → path-resolver-BqU-fwzD.d.ts} +3 -3
- package/dist/{permission-CC7XFYWG.d.ts → permission-Dgs3v-Xq.d.ts} +1 -1
- package/dist/{permission-policy-cYR4RJmw.d.ts → permission-policy-Dtht2k0e.d.ts} +2 -2
- package/dist/{pipeline-CNVKuQDQ.d.ts → pipeline-B-dpCFYS.d.ts} +2 -2
- package/dist/{plan-templates-C4wXMmiM.d.ts → plan-templates-B7MxyY2o.d.ts} +58 -5
- package/dist/{provider-runner-BpM0mdBE.d.ts → provider-runner-DrmpBE5l.d.ts} +3 -3
- package/dist/{retry-policy-BV7nzeAd.d.ts → retry-policy-hYxsm10a.d.ts} +1 -1
- package/dist/sdd/index.d.ts +12 -10
- package/dist/sdd/index.js +7 -0
- package/dist/sdd/index.js.map +1 -1
- package/dist/{secret-vault-eMBKfheR.d.ts → secret-vault-DnqIFhPF.d.ts} +1 -1
- package/dist/security/index.d.ts +5 -5
- package/dist/{selector-C4ORTOid.d.ts → selector-D21RjDIg.d.ts} +1 -1
- package/dist/{session-event-bridge-CeNpUL9w.d.ts → session-event-bridge-Dt54CTvq.d.ts} +1 -1
- package/dist/{session-reader-BepLSnGL.d.ts → session-reader-CceH13Kq.d.ts} +5 -4
- package/dist/storage/index.d.ts +46 -12
- package/dist/storage/index.js +2281 -1476
- package/dist/storage/index.js.map +1 -1
- package/dist/tools/index.d.ts +2 -2
- package/dist/types/index.d.ts +19 -19
- package/dist/types/index.js +162 -42
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.d.ts +2 -2
- package/dist/{worktree-manager-BDuXTaWL.d.ts → worktree-manager-DUfBbKzk.d.ts} +1 -1
- package/package.json +1 -1
package/dist/storage/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createReadStream } from 'fs';
|
|
2
|
-
import * as
|
|
2
|
+
import * as fsp2 from 'fs/promises';
|
|
3
3
|
import * as path2 from 'path';
|
|
4
4
|
import { createInterface } from 'readline';
|
|
5
5
|
import { randomBytes, randomUUID, createHash } from 'crypto';
|
|
@@ -9,16 +9,16 @@ import { hostname } from 'os';
|
|
|
9
9
|
// src/storage/session-store.ts
|
|
10
10
|
async function atomicWrite(targetPath, content, opts = {}) {
|
|
11
11
|
const dir = path2.dirname(targetPath);
|
|
12
|
-
await
|
|
12
|
+
await fsp2.mkdir(dir, { recursive: true });
|
|
13
13
|
const tmp = path2.join(dir, `.${path2.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
|
|
14
14
|
try {
|
|
15
15
|
if (typeof content === "string") {
|
|
16
|
-
await
|
|
16
|
+
await fsp2.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
|
|
17
17
|
} else {
|
|
18
|
-
await
|
|
18
|
+
await fsp2.writeFile(tmp, content, { flag: "wx" });
|
|
19
19
|
}
|
|
20
20
|
try {
|
|
21
|
-
const fh = await
|
|
21
|
+
const fh = await fsp2.open(tmp, "r+");
|
|
22
22
|
try {
|
|
23
23
|
await fh.sync();
|
|
24
24
|
} finally {
|
|
@@ -28,29 +28,29 @@ async function atomicWrite(targetPath, content, opts = {}) {
|
|
|
28
28
|
}
|
|
29
29
|
let mode;
|
|
30
30
|
try {
|
|
31
|
-
const
|
|
32
|
-
mode =
|
|
31
|
+
const stat10 = await fsp2.stat(targetPath);
|
|
32
|
+
mode = stat10.mode & 511;
|
|
33
33
|
} catch {
|
|
34
34
|
mode = opts.mode;
|
|
35
35
|
}
|
|
36
36
|
if (mode !== void 0) {
|
|
37
|
-
await
|
|
37
|
+
await fsp2.chmod(tmp, mode);
|
|
38
38
|
}
|
|
39
39
|
await renameWithRetry(tmp, targetPath);
|
|
40
40
|
} catch (err) {
|
|
41
41
|
try {
|
|
42
|
-
await
|
|
42
|
+
await fsp2.unlink(tmp);
|
|
43
43
|
} catch {
|
|
44
44
|
}
|
|
45
45
|
throw err;
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
async function ensureDir(dir) {
|
|
49
|
-
await
|
|
49
|
+
await fsp2.mkdir(dir, { recursive: true });
|
|
50
50
|
}
|
|
51
51
|
async function withFileLock(targetPath, fn, opts = {}) {
|
|
52
52
|
const dir = path2.dirname(targetPath);
|
|
53
|
-
await
|
|
53
|
+
await fsp2.mkdir(dir, { recursive: true });
|
|
54
54
|
const lockPath = path2.join(dir, `.${path2.basename(targetPath)}.lock`);
|
|
55
55
|
const timeoutMs = opts.timeoutMs ?? 5e3;
|
|
56
56
|
const staleMs = opts.staleMs ?? 3e4;
|
|
@@ -58,20 +58,20 @@ async function withFileLock(targetPath, fn, opts = {}) {
|
|
|
58
58
|
let handle;
|
|
59
59
|
for (; ; ) {
|
|
60
60
|
try {
|
|
61
|
-
handle = await
|
|
61
|
+
handle = await fsp2.open(lockPath, "wx");
|
|
62
62
|
await handle.writeFile(`${process.pid}:${Date.now()}`);
|
|
63
63
|
break;
|
|
64
64
|
} catch (err) {
|
|
65
65
|
const code = err.code;
|
|
66
66
|
if (code === "ENOENT") {
|
|
67
|
-
await
|
|
67
|
+
await fsp2.mkdir(dir, { recursive: true });
|
|
68
68
|
continue;
|
|
69
69
|
}
|
|
70
70
|
if (code !== "EEXIST") throw err;
|
|
71
71
|
try {
|
|
72
|
-
const
|
|
73
|
-
if (Date.now() -
|
|
74
|
-
await
|
|
72
|
+
const stat10 = await fsp2.stat(lockPath);
|
|
73
|
+
if (Date.now() - stat10.mtimeMs > staleMs) {
|
|
74
|
+
await fsp2.unlink(lockPath);
|
|
75
75
|
continue;
|
|
76
76
|
}
|
|
77
77
|
} catch {
|
|
@@ -91,7 +91,7 @@ async function withFileLock(targetPath, fn, opts = {}) {
|
|
|
91
91
|
} catch {
|
|
92
92
|
}
|
|
93
93
|
try {
|
|
94
|
-
await
|
|
94
|
+
await fsp2.unlink(lockPath);
|
|
95
95
|
} catch {
|
|
96
96
|
}
|
|
97
97
|
}
|
|
@@ -99,14 +99,14 @@ async function withFileLock(targetPath, fn, opts = {}) {
|
|
|
99
99
|
var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
|
|
100
100
|
async function renameWithRetry(from, to) {
|
|
101
101
|
if (process.platform !== "win32") {
|
|
102
|
-
await
|
|
102
|
+
await fsp2.rename(from, to);
|
|
103
103
|
return;
|
|
104
104
|
}
|
|
105
105
|
const delays = [10, 25, 60, 120, 250];
|
|
106
106
|
let lastErr;
|
|
107
107
|
for (let i = 0; i <= delays.length; i++) {
|
|
108
108
|
try {
|
|
109
|
-
await
|
|
109
|
+
await fsp2.rename(from, to);
|
|
110
110
|
return;
|
|
111
111
|
} catch (err) {
|
|
112
112
|
lastErr = err;
|
|
@@ -240,7 +240,7 @@ function envFlag(value) {
|
|
|
240
240
|
return !/^(0|false|no|off)$/i.test(value.trim());
|
|
241
241
|
}
|
|
242
242
|
var COLOR = isColorTty();
|
|
243
|
-
var wrap = (
|
|
243
|
+
var wrap = (open8, close) => (s) => COLOR ? `\x1B[${open8}m${s}\x1B[${close}m` : s;
|
|
244
244
|
var color = {
|
|
245
245
|
reset: wrap("0", "0"),
|
|
246
246
|
bold: wrap("1", "22"),
|
|
@@ -432,1456 +432,1674 @@ function resolveWstackPaths(opts) {
|
|
|
432
432
|
projectStatus: (projectHash2) => path2.join(globalRoot, "projects", projectHash2, "status.json")
|
|
433
433
|
};
|
|
434
434
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
const time = startedAt.slice(11, 19).replace(/:/g, "-");
|
|
441
|
-
const suffix = randomBytes(2).toString("hex");
|
|
442
|
-
const modelPart = model ? `_${sanitizeModel(model)}` : "";
|
|
443
|
-
return `${date}/${time}Z${modelPart}_${suffix}`;
|
|
435
|
+
|
|
436
|
+
// src/storage/session-helpers.ts
|
|
437
|
+
function userInputTitle(content) {
|
|
438
|
+
const text = typeof content === "string" ? content : content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
|
|
439
|
+
return (text || "(non-text input)").slice(0, 60);
|
|
444
440
|
}
|
|
445
441
|
|
|
446
|
-
// src/storage/session-
|
|
447
|
-
var
|
|
448
|
-
|
|
442
|
+
// src/storage/file-session-writer.ts
|
|
443
|
+
var FileSessionWriter = class _FileSessionWriter {
|
|
444
|
+
constructor(id, handle, startedAt, meta, events, opts = {}, traceId) {
|
|
445
|
+
this.id = id;
|
|
446
|
+
this.handle = handle;
|
|
447
|
+
this.startedAt = startedAt;
|
|
448
|
+
this.meta = meta;
|
|
449
|
+
this.events = events;
|
|
450
|
+
this.resumed = opts.resumed ?? false;
|
|
451
|
+
this.manifestFile = opts.dir ? path2.join(opts.dir, `${path2.basename(id)}.summary.json`) : "";
|
|
452
|
+
this.filePath = opts.filePath ?? "";
|
|
453
|
+
this.secretScrubber = opts.secretScrubber;
|
|
454
|
+
this.onCloseCb = opts.onClose;
|
|
455
|
+
this.summary = {
|
|
456
|
+
id,
|
|
457
|
+
title: "(empty session)",
|
|
458
|
+
startedAt,
|
|
459
|
+
model: meta.model ?? "unknown",
|
|
460
|
+
provider: meta.provider ?? "unknown",
|
|
461
|
+
tokenTotal: 0
|
|
462
|
+
};
|
|
463
|
+
this.traceId = traceId;
|
|
464
|
+
}
|
|
465
|
+
id;
|
|
466
|
+
handle;
|
|
467
|
+
startedAt;
|
|
468
|
+
meta;
|
|
449
469
|
events;
|
|
450
|
-
|
|
470
|
+
closed = false;
|
|
471
|
+
closePromise = null;
|
|
472
|
+
manifestFile;
|
|
473
|
+
summary;
|
|
474
|
+
tokenIn = 0;
|
|
475
|
+
tokenOut = 0;
|
|
476
|
+
filePath;
|
|
477
|
+
get transcriptPath() {
|
|
478
|
+
return this.filePath || void 0;
|
|
479
|
+
}
|
|
451
480
|
/**
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
* store's lifetime (e.g., webui session detail views, list() fallbacks).
|
|
457
|
-
*
|
|
458
|
-
* Max size is capped to prevent unbounded memory growth in long-running
|
|
459
|
-
* processes. When the limit is reached, the oldest entry is evicted.
|
|
481
|
+
* Lazy session_start/session_resumed init, shared by all appenders.
|
|
482
|
+
* A single promise (not a boolean) so a second append racing the first
|
|
483
|
+
* can't push its event into the buffer BEFORE the first append's event —
|
|
484
|
+
* every appender awaits the same init and resumes in FIFO call order.
|
|
460
485
|
*/
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
486
|
+
initPromise = null;
|
|
487
|
+
ensureInit() {
|
|
488
|
+
if (!this.initPromise) this.initPromise = this.writeSessionStartLazy();
|
|
489
|
+
return this.initPromise;
|
|
490
|
+
}
|
|
491
|
+
resumed;
|
|
492
|
+
appendFailCount = 0;
|
|
493
|
+
lastAppendWarnAt = 0;
|
|
494
|
+
secretScrubber;
|
|
495
|
+
onCloseCb;
|
|
496
|
+
/** Implements SessionWriter.traceId — propagated from ContextInit.traceId. */
|
|
497
|
+
traceId;
|
|
498
|
+
// ── Write buffer — batches events to reduce per-event disk I/O ──────────────
|
|
499
|
+
//
|
|
500
|
+
// Every append() pushes the scrubbed event into an in-memory buffer instead
|
|
501
|
+
// of calling handle.appendFile() synchronously. The buffer flushes to disk
|
|
502
|
+
// when it reaches FLUSH_SIZE events OR after FLUSH_INTERVAL_MS of inactivity.
|
|
503
|
+
// This cuts the number of disk writes by ~95% without changing the on-disk
|
|
504
|
+
// format — the JSONL is still one JSON object per line.
|
|
505
|
+
writeBuffer = [];
|
|
506
|
+
flushTimer = null;
|
|
507
|
+
static FLUSH_INTERVAL_MS = 500;
|
|
508
|
+
static FLUSH_SIZE = 50;
|
|
509
|
+
// ── Write serialization ─────────────────────────────────────────────────────
|
|
510
|
+
//
|
|
511
|
+
// All disk writes are funneled through a FIFO promise chain. Without it,
|
|
512
|
+
// a timer-driven flush racing an explicit flush()/close() issues two
|
|
513
|
+
// concurrent appendFile() calls on the shared O_APPEND handle — the kernel
|
|
514
|
+
// may complete them out of order (chronology breaks) or, for large
|
|
515
|
+
// batches, interleave partial writes (torn JSONL lines). The chain keeps
|
|
516
|
+
// exactly one write in flight; failures don't break the chain.
|
|
517
|
+
writeChain = Promise.resolve();
|
|
518
|
+
/** Enqueue a write on the FIFO chain. Resolves/rejects with that write. */
|
|
519
|
+
enqueueWrite(data) {
|
|
520
|
+
const write = this.writeChain.then(() => this.handle.appendFile(data, "utf8"));
|
|
521
|
+
this.writeChain = write.then(
|
|
522
|
+
() => void 0,
|
|
523
|
+
() => void 0
|
|
524
|
+
);
|
|
525
|
+
return write;
|
|
469
526
|
}
|
|
527
|
+
// ── Enriched summary tracking ───────────────────────────────────────────────
|
|
528
|
+
iterationCount = 0;
|
|
529
|
+
toolCallCount = 0;
|
|
530
|
+
toolErrorCount = 0;
|
|
531
|
+
toolBreakdown = {};
|
|
532
|
+
fileChangeCount = 0;
|
|
533
|
+
compactionCount = 0;
|
|
534
|
+
outcome = void 0;
|
|
470
535
|
/**
|
|
471
|
-
*
|
|
472
|
-
* the
|
|
536
|
+
* Scrub secrets out of conversation-turn events before they are observed
|
|
537
|
+
* for the summary, written to the JSONL log, or surfaced on resume. Only
|
|
538
|
+
* `user_input` / `llm_response` carry free-form user/model text; other event
|
|
539
|
+
* types either have no secret-bearing content or are already scrubbed
|
|
540
|
+
* upstream (tool results). Returns the event unchanged when no scrubber is
|
|
541
|
+
* configured.
|
|
473
542
|
*/
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
543
|
+
scrubEvent(event) {
|
|
544
|
+
const s = this.secretScrubber;
|
|
545
|
+
if (!s) return event;
|
|
546
|
+
if (event.type === "user_input") {
|
|
547
|
+
return {
|
|
548
|
+
...event,
|
|
549
|
+
content: typeof event.content === "string" ? s.scrub(event.content) : s.scrubObject(event.content)
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
if (event.type === "llm_response") {
|
|
553
|
+
return { ...event, content: s.scrubObject(event.content) };
|
|
479
554
|
}
|
|
555
|
+
return event;
|
|
480
556
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
filePath,
|
|
487
|
-
operation,
|
|
488
|
-
outcome,
|
|
489
|
-
durationMs,
|
|
490
|
-
...error !== void 0 ? { error } : {}
|
|
491
|
-
});
|
|
557
|
+
pendingFileSnapshots = [];
|
|
558
|
+
/** Tracks open tool_use IDs during the current run to serialize on close for resume. */
|
|
559
|
+
openToolUses = /* @__PURE__ */ new Set();
|
|
560
|
+
recordFileChange(input) {
|
|
561
|
+
this.pendingFileSnapshots.push(input);
|
|
492
562
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
sessionId,
|
|
496
|
-
store: "session",
|
|
497
|
-
filePath,
|
|
498
|
-
operation,
|
|
499
|
-
outcome,
|
|
500
|
-
durationMs,
|
|
501
|
-
...eventCount !== void 0 ? { eventCount } : {},
|
|
502
|
-
...error !== void 0 ? { error } : {}
|
|
503
|
-
});
|
|
563
|
+
get pendingToolUses() {
|
|
564
|
+
return Array.from(this.openToolUses);
|
|
504
565
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
566
|
+
async writeSessionStartLazy() {
|
|
567
|
+
const record = `${JSON.stringify({
|
|
568
|
+
type: this.resumed ? "session_resumed" : "session_start",
|
|
569
|
+
ts: this.startedAt,
|
|
570
|
+
id: this.id,
|
|
571
|
+
model: this.meta.model ?? "unknown",
|
|
572
|
+
provider: this.meta.provider ?? "unknown"
|
|
573
|
+
})}
|
|
574
|
+
`;
|
|
575
|
+
try {
|
|
576
|
+
await this.enqueueWrite(record);
|
|
577
|
+
} catch {
|
|
578
|
+
}
|
|
514
579
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
580
|
+
async append(event) {
|
|
581
|
+
if (this.closed) return;
|
|
582
|
+
await this.ensureInit();
|
|
583
|
+
const scrubbed = this.scrubEvent(event);
|
|
584
|
+
this.observeForSummary(scrubbed);
|
|
585
|
+
this.writeBuffer.push(scrubbed);
|
|
586
|
+
if (this.writeBuffer.length >= _FileSessionWriter.FLUSH_SIZE) {
|
|
587
|
+
if (this.flushTimer) {
|
|
588
|
+
clearTimeout(this.flushTimer);
|
|
589
|
+
this.flushTimer = null;
|
|
590
|
+
}
|
|
591
|
+
await this.flushBuffer();
|
|
592
|
+
} else {
|
|
593
|
+
this.scheduleFlush();
|
|
594
|
+
}
|
|
518
595
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
596
|
+
async appendBatch(events) {
|
|
597
|
+
if (this.closed || events.length === 0) return;
|
|
598
|
+
await this.ensureInit();
|
|
599
|
+
for (const event of events) {
|
|
600
|
+
const scrubbed = this.scrubEvent(event);
|
|
601
|
+
this.observeForSummary(scrubbed);
|
|
602
|
+
this.writeBuffer.push(scrubbed);
|
|
603
|
+
}
|
|
604
|
+
if (this.writeBuffer.length >= _FileSessionWriter.FLUSH_SIZE) {
|
|
605
|
+
if (this.flushTimer) {
|
|
606
|
+
clearTimeout(this.flushTimer);
|
|
607
|
+
this.flushTimer = null;
|
|
608
|
+
}
|
|
609
|
+
await this.flushBuffer();
|
|
610
|
+
} else {
|
|
611
|
+
this.scheduleFlush();
|
|
612
|
+
}
|
|
522
613
|
}
|
|
523
614
|
/**
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
615
|
+
* Flush buffered events to disk immediately. Critical events
|
|
616
|
+
* (user_input, llm_response) call this so they survive SIGKILL/crash
|
|
617
|
+
* instead of sitting in the in-memory buffer for up to 500ms.
|
|
618
|
+
*
|
|
619
|
+
* Idempotent — cancels any pending timer and writes whatever has
|
|
620
|
+
* accumulated in the buffer. Safe to call even when the buffer
|
|
621
|
+
* is empty (no-op).
|
|
527
622
|
*/
|
|
528
|
-
async
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
623
|
+
async flush() {
|
|
624
|
+
if (this.flushTimer) {
|
|
625
|
+
clearTimeout(this.flushTimer);
|
|
626
|
+
this.flushTimer = null;
|
|
627
|
+
}
|
|
628
|
+
await this.flushBuffer();
|
|
532
629
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
let handle;
|
|
540
|
-
try {
|
|
541
|
-
handle = await fsp.open(file, "a", 384);
|
|
542
|
-
} catch (err) {
|
|
543
|
-
this.emitError(id, file, "create", toErrorMessage(err), false);
|
|
544
|
-
throw new Error(
|
|
545
|
-
`Failed to open session file: ${toErrorMessage(err)}`,
|
|
546
|
-
{ cause: err }
|
|
547
|
-
);
|
|
548
|
-
}
|
|
549
|
-
try {
|
|
550
|
-
const writer = new FileSessionWriter(id, handle, startedAt, meta, this.events, {
|
|
551
|
-
dir: shardDir,
|
|
552
|
-
filePath: file,
|
|
553
|
-
secretScrubber: this.secretScrubber,
|
|
554
|
-
onClose: (s) => this.appendToIndex(s)
|
|
630
|
+
/** Schedule a deferred flush. No-op if a timer is already pending. */
|
|
631
|
+
scheduleFlush() {
|
|
632
|
+
if (this.flushTimer) return;
|
|
633
|
+
this.flushTimer = setTimeout(() => {
|
|
634
|
+
this.flushTimer = null;
|
|
635
|
+
this.flushBuffer().catch(() => {
|
|
555
636
|
});
|
|
556
|
-
|
|
557
|
-
return writer;
|
|
558
|
-
} catch (err) {
|
|
559
|
-
await handle.close().catch((e) => console.warn(JSON.stringify({
|
|
560
|
-
level: "warn",
|
|
561
|
-
event: "session_store.handle_close_failed",
|
|
562
|
-
message: e instanceof Error ? e.message : String(e),
|
|
563
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
564
|
-
})));
|
|
565
|
-
this.emitError(id, file, "create", toErrorMessage(err), true);
|
|
566
|
-
throw err;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
async resume(id) {
|
|
570
|
-
const file = this.sessionPath(id, ".jsonl");
|
|
571
|
-
const t0 = Date.now();
|
|
572
|
-
const data = await this.load(id);
|
|
573
|
-
let handle;
|
|
574
|
-
try {
|
|
575
|
-
handle = await fsp.open(file, "a", 384);
|
|
576
|
-
} catch (err) {
|
|
577
|
-
this.emitError(id, file, "resume", toErrorMessage(err), false);
|
|
578
|
-
throw new Error(
|
|
579
|
-
`Failed to open session "${id}" for append: ${toErrorMessage(err)}`,
|
|
580
|
-
{ cause: err }
|
|
581
|
-
);
|
|
582
|
-
}
|
|
583
|
-
try {
|
|
584
|
-
const writer = new FileSessionWriter(
|
|
585
|
-
id,
|
|
586
|
-
handle,
|
|
587
|
-
(/* @__PURE__ */ new Date()).toISOString(),
|
|
588
|
-
{
|
|
589
|
-
id,
|
|
590
|
-
model: data.metadata.model,
|
|
591
|
-
provider: data.metadata.provider
|
|
592
|
-
},
|
|
593
|
-
this.events,
|
|
594
|
-
{
|
|
595
|
-
resumed: true,
|
|
596
|
-
// Shard directory (sessions/<date>/) — must match create() so the
|
|
597
|
-
// .summary.json sidecar lands next to the JSONL instead of the
|
|
598
|
-
// sessions root (where summaryFor() would never find it).
|
|
599
|
-
dir: path2.dirname(file),
|
|
600
|
-
filePath: file,
|
|
601
|
-
secretScrubber: this.secretScrubber,
|
|
602
|
-
onClose: (s) => this.appendToIndex(s)
|
|
603
|
-
}
|
|
604
|
-
);
|
|
605
|
-
this.emitWrite(id, file, "resume", "success", Date.now() - t0);
|
|
606
|
-
return { writer, data };
|
|
607
|
-
} catch (err) {
|
|
608
|
-
await handle.close().catch((e) => console.warn(JSON.stringify({
|
|
609
|
-
level: "warn",
|
|
610
|
-
event: "session_store.handle_close_failed",
|
|
611
|
-
message: e instanceof Error ? e.message : String(e),
|
|
612
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
613
|
-
})));
|
|
614
|
-
this.emitError(id, file, "resume", toErrorMessage(err), true);
|
|
615
|
-
throw err;
|
|
616
|
-
}
|
|
637
|
+
}, _FileSessionWriter.FLUSH_INTERVAL_MS);
|
|
617
638
|
}
|
|
618
|
-
|
|
619
|
-
|
|
639
|
+
/**
|
|
640
|
+
* Flush all buffered events to disk as a single appendFile call.
|
|
641
|
+
* Errors use the same throttled-warning pattern the old per-event
|
|
642
|
+
* append path used — one warning every 5s with a suppressed count.
|
|
643
|
+
* On failure the buffer is cleared (events are best-effort, same as
|
|
644
|
+
* the old per-event path where a failed write was silently dropped).
|
|
645
|
+
*/
|
|
646
|
+
async flushBuffer() {
|
|
647
|
+
if (this.writeBuffer.length === 0) return;
|
|
648
|
+
const eventCount = this.writeBuffer.length;
|
|
649
|
+
const batch = this.writeBuffer.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
650
|
+
this.writeBuffer = [];
|
|
620
651
|
const t0 = Date.now();
|
|
621
652
|
let outcome = "success";
|
|
622
653
|
let errorMsg;
|
|
623
|
-
let cacheHit = false;
|
|
624
654
|
try {
|
|
625
|
-
|
|
626
|
-
const stat7 = { mtimeMs: s.mtimeMs, size: s.size };
|
|
627
|
-
const cached = this._loadCache.get(id);
|
|
628
|
-
if (cached && cached.mtimeMs === stat7.mtimeMs && cached.size === stat7.size) {
|
|
629
|
-
cacheHit = true;
|
|
630
|
-
this._loadCache.delete(id);
|
|
631
|
-
this._loadCache.set(id, cached);
|
|
632
|
-
return cached.data;
|
|
633
|
-
}
|
|
634
|
-
const raw = await fsp.readFile(file, "utf8");
|
|
635
|
-
const lines = raw.split("\n").filter((l) => l.trim());
|
|
636
|
-
const events = [];
|
|
637
|
-
let sessionStartEvent;
|
|
638
|
-
let sessionEndEvent;
|
|
639
|
-
let sessionModel;
|
|
640
|
-
let sessionProvider;
|
|
641
|
-
let sessionPendingToolUses;
|
|
642
|
-
const messages = [];
|
|
643
|
-
const openToolUses = /* @__PURE__ */ new Set();
|
|
644
|
-
let usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
645
|
-
for (const line of lines) {
|
|
646
|
-
try {
|
|
647
|
-
const parsed = JSON.parse(line);
|
|
648
|
-
if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
|
|
649
|
-
const ev = parsed;
|
|
650
|
-
events.push(ev);
|
|
651
|
-
if (ev.type === "session_start" && !sessionStartEvent) {
|
|
652
|
-
sessionStartEvent = ev;
|
|
653
|
-
sessionModel = ev.model;
|
|
654
|
-
sessionProvider = ev.provider;
|
|
655
|
-
}
|
|
656
|
-
if (ev.type === "session_end") {
|
|
657
|
-
sessionEndEvent = ev;
|
|
658
|
-
sessionPendingToolUses = ev.pendingToolUses;
|
|
659
|
-
}
|
|
660
|
-
if (ev.type === "user_input") {
|
|
661
|
-
openToolUses.clear();
|
|
662
|
-
messages.push({ role: "user", content: ev.content, ts: ev.ts });
|
|
663
|
-
} else if (ev.type === "llm_response") {
|
|
664
|
-
messages.push({ role: "assistant", content: ev.content, ts: ev.ts });
|
|
665
|
-
for (const b of ev.content) {
|
|
666
|
-
if (b.type === "tool_use") openToolUses.add(b.id);
|
|
667
|
-
}
|
|
668
|
-
usage = {
|
|
669
|
-
input: usage.input + (ev.usage.input ?? 0),
|
|
670
|
-
output: usage.output + (ev.usage.output ?? 0),
|
|
671
|
-
cacheRead: (usage.cacheRead ?? 0) + (ev.usage.cacheRead ?? 0),
|
|
672
|
-
cacheWrite: (usage.cacheWrite ?? 0) + (ev.usage.cacheWrite ?? 0)
|
|
673
|
-
};
|
|
674
|
-
} else if (ev.type === "tool_result") {
|
|
675
|
-
if (!openToolUses.has(ev.id)) {
|
|
676
|
-
this.events?.emit("session.damaged", {
|
|
677
|
-
sessionId: id,
|
|
678
|
-
detail: `Orphan tool_result "${ev.id}" has no matching tool_use`
|
|
679
|
-
});
|
|
680
|
-
continue;
|
|
681
|
-
}
|
|
682
|
-
openToolUses.delete(ev.id);
|
|
683
|
-
const resultBlock = {
|
|
684
|
-
type: "tool_result",
|
|
685
|
-
tool_use_id: ev.id,
|
|
686
|
-
content: typeof ev.content === "string" ? ev.content : JSON.stringify(ev.content),
|
|
687
|
-
is_error: ev.isError
|
|
688
|
-
};
|
|
689
|
-
const last = messages[messages.length - 1];
|
|
690
|
-
const lastIsToolResultUser = last?.role === "user" && Array.isArray(last.content) && last.content.every((b) => b.type === "tool_result");
|
|
691
|
-
if (lastIsToolResultUser && Array.isArray(last.content)) {
|
|
692
|
-
last.content.push(resultBlock);
|
|
693
|
-
} else {
|
|
694
|
-
messages.push({ role: "user", content: [resultBlock], ts: ev.ts });
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
} catch {
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
if (openToolUses.size > 0) {
|
|
702
|
-
this.events?.emit("session.damaged", {
|
|
703
|
-
sessionId: id,
|
|
704
|
-
detail: `${openToolUses.size} tool_use blocks without matching results - replay repaired`
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
const repaired = repairToolUseAdjacency(messages);
|
|
708
|
-
if (repaired.report.changed) {
|
|
709
|
-
this.events?.emit("session.damaged", {
|
|
710
|
-
sessionId: id,
|
|
711
|
-
detail: `Repaired replay adjacency: removed ${repaired.report.removedToolUses.length} tool_use, ${repaired.report.removedToolResults.length} tool_result, ${repaired.report.removedMessages} empty messages`
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
const meta = {
|
|
715
|
-
id,
|
|
716
|
-
startedAt: sessionStartEvent?.ts ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
717
|
-
endedAt: sessionEndEvent?.ts,
|
|
718
|
-
model: sessionModel,
|
|
719
|
-
provider: sessionProvider,
|
|
720
|
-
pendingToolUses: sessionPendingToolUses
|
|
721
|
-
};
|
|
722
|
-
const toolCallEnds = extractToolCallEnds(events);
|
|
723
|
-
const data = { metadata: meta, events, messages: repaired.messages, usage, toolCallEnds };
|
|
724
|
-
if (this._loadCache.size >= _DefaultSessionStore.LOAD_CACHE_MAX_ENTRIES) {
|
|
725
|
-
const oldest = this._loadCache.keys().next().value;
|
|
726
|
-
if (oldest !== void 0) {
|
|
727
|
-
this._loadCache.delete(oldest);
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
this._loadCache.set(id, { mtimeMs: stat7.mtimeMs, size: stat7.size, data });
|
|
731
|
-
return data;
|
|
655
|
+
await this.enqueueWrite(batch);
|
|
732
656
|
} catch (err) {
|
|
733
657
|
outcome = "failure";
|
|
734
658
|
errorMsg = toErrorMessage(err);
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
659
|
+
this.appendFailCount += eventCount;
|
|
660
|
+
const now = Date.now();
|
|
661
|
+
if (now - this.lastAppendWarnAt > 5e3) {
|
|
662
|
+
const suppressed = this.appendFailCount - 1;
|
|
663
|
+
const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
|
|
664
|
+
console.warn(
|
|
665
|
+
"[session] flush failed:",
|
|
666
|
+
toErrorMessage(err),
|
|
667
|
+
tail
|
|
668
|
+
);
|
|
669
|
+
this.lastAppendWarnAt = now;
|
|
670
|
+
this.appendFailCount = 0;
|
|
746
671
|
}
|
|
672
|
+
} finally {
|
|
673
|
+
this.events?.emit("storage.write", {
|
|
674
|
+
sessionId: this.id,
|
|
675
|
+
store: "session",
|
|
676
|
+
filePath: this.filePath,
|
|
677
|
+
operation: "flush",
|
|
678
|
+
outcome,
|
|
679
|
+
durationMs: Date.now() - t0,
|
|
680
|
+
...errorMsg !== void 0 ? { error: errorMsg } : {},
|
|
681
|
+
...eventCount !== void 0 ? { eventCount } : {},
|
|
682
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
683
|
+
});
|
|
747
684
|
}
|
|
748
685
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
if (indexed.length > 0) {
|
|
754
|
-
indexed.sort((a, b) => {
|
|
755
|
-
if (a.startedAt < b.startedAt) return 1;
|
|
756
|
-
if (a.startedAt > b.startedAt) return -1;
|
|
757
|
-
return a.id.localeCompare(b.id);
|
|
758
|
-
});
|
|
759
|
-
return indexed.slice(0, limit);
|
|
686
|
+
observeForSummary(event) {
|
|
687
|
+
if (event.type === "llm_response") {
|
|
688
|
+
for (const block of event.content) {
|
|
689
|
+
if (block.type === "tool_use") this.openToolUses.add(block.id);
|
|
760
690
|
}
|
|
761
|
-
return await this.listFromDirectoryScan(limit);
|
|
762
|
-
} catch {
|
|
763
|
-
return [];
|
|
764
691
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
indexAppendCount = 0;
|
|
776
|
-
static COMPACT_EVERY = 30;
|
|
777
|
-
/** Append a session summary to the index. */
|
|
778
|
-
async appendToIndex(summary) {
|
|
779
|
-
try {
|
|
780
|
-
await ensureDir(this.dir);
|
|
781
|
-
const line = JSON.stringify(summary) + "\n";
|
|
782
|
-
await fsp.appendFile(this.indexFile, line, "utf8");
|
|
783
|
-
this._indexCache = null;
|
|
784
|
-
this.indexAppendCount++;
|
|
785
|
-
if (this.indexAppendCount >= _DefaultSessionStore.COMPACT_EVERY) {
|
|
786
|
-
await this.compactIndex();
|
|
787
|
-
this.indexAppendCount = 0;
|
|
692
|
+
if (event.type === "tool_use") {
|
|
693
|
+
this.openToolUses.add(event.id);
|
|
694
|
+
} else if (event.type === "tool_call_start") {
|
|
695
|
+
this.toolCallCount++;
|
|
696
|
+
this.toolBreakdown[event.name] = (this.toolBreakdown[event.name] ?? 0) + 1;
|
|
697
|
+
} else if (event.type === "tool_result") {
|
|
698
|
+
this.openToolUses.delete(event.id);
|
|
699
|
+
if (event.isError) {
|
|
700
|
+
this.toolErrorCount++;
|
|
701
|
+
this.outcome = "error";
|
|
788
702
|
}
|
|
789
|
-
}
|
|
703
|
+
} else if (event.type === "file_snapshot") {
|
|
704
|
+
this.fileChangeCount += event.files.length;
|
|
705
|
+
} else if (event.type === "compaction") {
|
|
706
|
+
this.compactionCount++;
|
|
790
707
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
this.
|
|
799
|
-
this.
|
|
800
|
-
}
|
|
708
|
+
if (event.type === "error" || event.type === "provider_error") {
|
|
709
|
+
this.outcome = "error";
|
|
710
|
+
}
|
|
711
|
+
if (event.type === "user_input" && this.summary.title === "(empty session)") {
|
|
712
|
+
this.summary = { ...this.summary, title: userInputTitle(event.content) };
|
|
713
|
+
} else if (event.type === "llm_response") {
|
|
714
|
+
this.tokenIn += event.usage.input;
|
|
715
|
+
this.tokenOut += event.usage.output;
|
|
716
|
+
this.summary = { ...this.summary, tokenTotal: this.tokenIn + this.tokenOut };
|
|
717
|
+
} else if (event.type === "session_end") {
|
|
718
|
+
const total = event.usage.input + event.usage.output;
|
|
719
|
+
if (total > 0) this.summary = { ...this.summary, tokenTotal: total };
|
|
720
|
+
} else if (event.type === "in_flight_start") {
|
|
721
|
+
this.iterationCount++;
|
|
801
722
|
}
|
|
802
723
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
724
|
+
async close() {
|
|
725
|
+
if (this.closePromise) return this.closePromise;
|
|
726
|
+
this.closePromise = this.doClose();
|
|
727
|
+
return this.closePromise;
|
|
728
|
+
}
|
|
729
|
+
async doClose() {
|
|
730
|
+
this.closed = true;
|
|
731
|
+
if (this.flushTimer) {
|
|
732
|
+
clearTimeout(this.flushTimer);
|
|
733
|
+
this.flushTimer = null;
|
|
734
|
+
}
|
|
735
|
+
await this.flushBuffer();
|
|
736
|
+
await this.writeChain;
|
|
737
|
+
this.summary = {
|
|
738
|
+
...this.summary,
|
|
739
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
740
|
+
iterationCount: this.iterationCount,
|
|
741
|
+
toolCallCount: this.toolCallCount,
|
|
742
|
+
toolErrorCount: this.toolErrorCount,
|
|
743
|
+
fileChangeCount: this.fileChangeCount,
|
|
744
|
+
compactionCount: this.compactionCount > 0 ? this.compactionCount : void 0,
|
|
745
|
+
toolBreakdown: { ...this.toolBreakdown },
|
|
746
|
+
outcome: this.outcome ?? "completed"
|
|
747
|
+
};
|
|
748
|
+
if (this.manifestFile) {
|
|
749
|
+
const t0 = Date.now();
|
|
750
|
+
let outcome = "success";
|
|
751
|
+
let errorMsg;
|
|
752
|
+
try {
|
|
753
|
+
await atomicWrite(this.manifestFile, JSON.stringify(this.summary), { mode: 384 });
|
|
754
|
+
} catch (err) {
|
|
755
|
+
outcome = "failure";
|
|
756
|
+
errorMsg = toErrorMessage(err);
|
|
757
|
+
} finally {
|
|
758
|
+
this.events?.emit("storage.write", {
|
|
759
|
+
sessionId: this.id,
|
|
760
|
+
store: "session",
|
|
761
|
+
filePath: this.manifestFile,
|
|
762
|
+
operation: "close",
|
|
763
|
+
outcome,
|
|
764
|
+
durationMs: Date.now() - t0,
|
|
765
|
+
...errorMsg !== void 0 ? { error: errorMsg } : {},
|
|
766
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
const idxT0 = Date.now();
|
|
771
|
+
let idxOutcome = "success";
|
|
772
|
+
let idxError;
|
|
811
773
|
try {
|
|
812
|
-
|
|
813
|
-
if (entries.length === 0) return;
|
|
814
|
-
const tmp = `${this.indexFile}.compact.tmp`;
|
|
815
|
-
const lines = entries.map((s) => JSON.stringify(s)).join("\n") + "\n";
|
|
816
|
-
await fsp.writeFile(tmp, lines, "utf8");
|
|
817
|
-
await fsp.rename(tmp, this.indexFile);
|
|
818
|
-
this._indexCache = null;
|
|
774
|
+
await this.onCloseCb?.(this.summary);
|
|
819
775
|
} catch (err) {
|
|
820
|
-
|
|
821
|
-
|
|
776
|
+
idxOutcome = "failure";
|
|
777
|
+
idxError = toErrorMessage(err);
|
|
822
778
|
} finally {
|
|
823
|
-
this.
|
|
779
|
+
this.events?.emit("storage.write", {
|
|
780
|
+
sessionId: this.summary.id,
|
|
781
|
+
store: "session",
|
|
782
|
+
filePath: this.filePath,
|
|
783
|
+
operation: "index_append",
|
|
784
|
+
outcome: idxOutcome,
|
|
785
|
+
durationMs: Date.now() - idxT0,
|
|
786
|
+
...idxError !== void 0 ? { error: idxError } : {},
|
|
787
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
788
|
+
});
|
|
824
789
|
}
|
|
825
|
-
}
|
|
826
|
-
/**
|
|
827
|
-
* Read the index file and return deduplicated session summaries.
|
|
828
|
-
* Entries with a matching tombstone are filtered out.
|
|
829
|
-
* Returns empty array when the index doesn't exist or is corrupt.
|
|
830
|
-
*/
|
|
831
|
-
async readIndex() {
|
|
832
|
-
let stat7;
|
|
833
790
|
try {
|
|
834
|
-
|
|
835
|
-
stat7 = { mtimeMs: s.mtimeMs, size: s.size };
|
|
791
|
+
await this.handle.close();
|
|
836
792
|
} catch {
|
|
837
|
-
this._indexCache = null;
|
|
838
|
-
return [];
|
|
839
793
|
}
|
|
840
|
-
|
|
841
|
-
|
|
794
|
+
}
|
|
795
|
+
async writeCheckpoint(promptIndex, promptPreview) {
|
|
796
|
+
const fileCount = this.pendingFileSnapshots.length;
|
|
797
|
+
if (fileCount > 0) {
|
|
798
|
+
await this.writeFileSnapshot(promptIndex, [...this.pendingFileSnapshots]);
|
|
799
|
+
this.pendingFileSnapshots = [];
|
|
842
800
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
801
|
+
await this.append({
|
|
802
|
+
type: "checkpoint",
|
|
803
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
804
|
+
promptIndex,
|
|
805
|
+
promptPreview
|
|
806
|
+
});
|
|
807
|
+
this.events?.emit("checkpoint.written", {
|
|
808
|
+
promptIndex,
|
|
809
|
+
promptPreview,
|
|
810
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
811
|
+
fileCount
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
async writeFileSnapshot(promptIndex, files) {
|
|
815
|
+
await this.append({
|
|
816
|
+
type: "file_snapshot",
|
|
817
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
818
|
+
promptIndex,
|
|
819
|
+
files
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Truncate the session file to the checkpoint with the given promptIndex,
|
|
824
|
+
* removing all events that follow it. Uses a single-pass byte-offset scan
|
|
825
|
+
* so post-checkpoint content is never read or parsed — O(1) memory instead
|
|
826
|
+
* of O(N) JSON.parse calls over the full file.
|
|
827
|
+
*/
|
|
828
|
+
async truncateToCheckpoint(targetPromptIndex) {
|
|
829
|
+
if (!this.filePath) return 0;
|
|
830
|
+
if (this.flushTimer) {
|
|
831
|
+
clearTimeout(this.flushTimer);
|
|
832
|
+
this.flushTimer = null;
|
|
849
833
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
834
|
+
await this.flushBuffer();
|
|
835
|
+
await this.writeChain;
|
|
836
|
+
const CHUNK_SIZE = 65536;
|
|
837
|
+
let fd;
|
|
838
|
+
let fileOffset = 0;
|
|
839
|
+
let lineStartOffset = 0;
|
|
840
|
+
let checkpointByteOffset = -1;
|
|
841
|
+
let removedCount = 0;
|
|
842
|
+
let targetCheckpointSeen = false;
|
|
843
|
+
try {
|
|
844
|
+
fd = await fsp2.open(this.filePath, "r", 384);
|
|
845
|
+
while (true) {
|
|
846
|
+
const buf = Buffer.alloc(CHUNK_SIZE);
|
|
847
|
+
const { bytesRead } = await fd.read(buf, 0, CHUNK_SIZE, fileOffset);
|
|
848
|
+
if (bytesRead === 0) break;
|
|
849
|
+
let chunkPos = 0;
|
|
850
|
+
while (chunkPos < bytesRead) {
|
|
851
|
+
const idx = buf.indexOf("\n", chunkPos);
|
|
852
|
+
if (idx === -1) {
|
|
853
|
+
lineStartOffset = fileOffset + chunkPos;
|
|
854
|
+
break;
|
|
855
|
+
}
|
|
856
|
+
if (checkpointByteOffset !== -1) {
|
|
857
|
+
removedCount++;
|
|
858
|
+
} else {
|
|
859
|
+
const lineBytes = buf.subarray(chunkPos, idx);
|
|
860
|
+
const line = new TextDecoder("utf-8", { fatal: false }).decode(lineBytes);
|
|
861
|
+
if (line.trim()) {
|
|
862
|
+
try {
|
|
863
|
+
const event = JSON.parse(line);
|
|
864
|
+
if (event.type === "checkpoint") {
|
|
865
|
+
if (event.promptIndex === targetPromptIndex) {
|
|
866
|
+
checkpointByteOffset = lineStartOffset;
|
|
867
|
+
targetCheckpointSeen = true;
|
|
868
|
+
} else if (event.promptIndex !== void 0 && event.promptIndex > targetPromptIndex) {
|
|
869
|
+
checkpointByteOffset = lineStartOffset;
|
|
870
|
+
}
|
|
871
|
+
} else if (targetCheckpointSeen && event.promptIndex !== void 0 && event.promptIndex > targetPromptIndex) {
|
|
872
|
+
removedCount++;
|
|
873
|
+
} else if (targetCheckpointSeen && event.promptIndex === void 0) {
|
|
874
|
+
removedCount++;
|
|
875
|
+
} else if (!targetCheckpointSeen && event.promptIndex === void 0) {
|
|
876
|
+
removedCount++;
|
|
877
|
+
} else if (!targetCheckpointSeen && event.promptIndex !== void 0 && event.promptIndex > targetPromptIndex) {
|
|
878
|
+
removedCount++;
|
|
879
|
+
}
|
|
880
|
+
} catch {
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
chunkPos = idx + 1;
|
|
885
|
+
lineStartOffset = fileOffset + chunkPos;
|
|
860
886
|
}
|
|
861
|
-
|
|
862
|
-
|
|
887
|
+
fileOffset += bytesRead;
|
|
888
|
+
if (chunkPos >= bytesRead) {
|
|
889
|
+
lineStartOffset = fileOffset;
|
|
863
890
|
}
|
|
864
|
-
} catch {
|
|
865
891
|
}
|
|
892
|
+
} finally {
|
|
893
|
+
await fd?.close();
|
|
866
894
|
}
|
|
867
|
-
|
|
868
|
-
this.
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
* Rebuild the index from disk by scanning all sessions and writing a
|
|
873
|
-
* fresh _index.jsonl. Useful after manual cleanup or index corruption.
|
|
874
|
-
*/
|
|
875
|
-
async rebuildIndex() {
|
|
876
|
-
const ids = await this.collectSessionIds(this.dir);
|
|
877
|
-
const summaries = await Promise.all(ids.map((id) => this.summaryFor(id).catch(() => null)));
|
|
878
|
-
const valid = summaries.filter((s) => s !== null);
|
|
879
|
-
const tmp = `${this.indexFile}.tmp`;
|
|
880
|
-
const lines = valid.map((s) => JSON.stringify(s)).join("\n") + "\n";
|
|
881
|
-
await fsp.writeFile(tmp, lines, "utf8");
|
|
882
|
-
await fsp.rename(tmp, this.indexFile);
|
|
883
|
-
this._indexCache = null;
|
|
884
|
-
return valid.length;
|
|
885
|
-
}
|
|
886
|
-
async listFromDirectoryScan(limit) {
|
|
887
|
-
const refs = await this.collectSessionFiles(this.dir);
|
|
888
|
-
const candidates = await mapWithConcurrency(
|
|
889
|
-
refs,
|
|
890
|
-
_DefaultSessionStore.LIST_SCAN_CONCURRENCY,
|
|
891
|
-
async (ref) => {
|
|
892
|
-
const manifest = await this.readSummaryManifest(ref.id);
|
|
893
|
-
if (manifest) return { summary: manifest, needsBackfill: false };
|
|
894
|
-
const summary = await this.summaryHeaderFor(ref);
|
|
895
|
-
return summary ? { summary, needsBackfill: true } : null;
|
|
896
|
-
}
|
|
897
|
-
);
|
|
898
|
-
const out = candidates.filter((s) => s !== null);
|
|
899
|
-
out.sort((a, b) => compareSessionSummaries(a.summary, b.summary));
|
|
900
|
-
const selected = out.slice(0, limit);
|
|
901
|
-
const summaries = await mapWithConcurrency(
|
|
902
|
-
selected,
|
|
903
|
-
Math.min(_DefaultSessionStore.LIST_SCAN_CONCURRENCY, Math.max(1, limit)),
|
|
904
|
-
async (candidate) => {
|
|
905
|
-
if (!candidate.needsBackfill) return candidate.summary;
|
|
906
|
-
return await this.summaryFor(candidate.summary.id).catch(() => candidate.summary);
|
|
907
|
-
}
|
|
908
|
-
);
|
|
909
|
-
return summaries.filter((s) => s !== null);
|
|
910
|
-
}
|
|
911
|
-
async collectSessionFiles(dir, prefix = "", depth = 0) {
|
|
912
|
-
let entries;
|
|
895
|
+
if (checkpointByteOffset === -1) return 0;
|
|
896
|
+
await this.writeChain;
|
|
897
|
+
await this.handle.close();
|
|
898
|
+
const tmpPath = `${this.filePath}.rewind.tmp`;
|
|
899
|
+
const src = await fsp2.open(this.filePath, "r", 384);
|
|
913
900
|
try {
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
if (entry.name === "_index.jsonl") continue;
|
|
928
|
-
const base = entry.name.replace(/\.jsonl$/, "");
|
|
929
|
-
const id = prefix ? `${prefix}/${base}` : base;
|
|
930
|
-
files.push({ id, filePath: path2.join(dir, entry.name) });
|
|
901
|
+
const statResult = await src.stat();
|
|
902
|
+
const totalSize = statResult.size;
|
|
903
|
+
const prefixBytes = checkpointByteOffset;
|
|
904
|
+
let newlineAfterCheckpoint = prefixBytes;
|
|
905
|
+
if (prefixBytes < totalSize) {
|
|
906
|
+
const probeBuf = Buffer.alloc(Math.min(CHUNK_SIZE, totalSize - prefixBytes));
|
|
907
|
+
const { bytesRead: probeRead } = await src.read(probeBuf, 0, probeBuf.length, prefixBytes);
|
|
908
|
+
if (probeRead > 0) {
|
|
909
|
+
const nl = probeBuf.indexOf("\n");
|
|
910
|
+
newlineAfterCheckpoint = nl !== -1 ? prefixBytes + nl + 1 : totalSize;
|
|
911
|
+
}
|
|
912
|
+
} else {
|
|
913
|
+
newlineAfterCheckpoint = totalSize;
|
|
931
914
|
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
915
|
+
const writeFd = await fsp2.open(tmpPath, "w", 384);
|
|
916
|
+
try {
|
|
917
|
+
let readOffset = 0;
|
|
918
|
+
while (readOffset < newlineAfterCheckpoint) {
|
|
919
|
+
const toCopy = Math.min(CHUNK_SIZE, newlineAfterCheckpoint - readOffset);
|
|
920
|
+
const copyBuf = Buffer.alloc(toCopy);
|
|
921
|
+
const { bytesRead: r } = await src.read(copyBuf, 0, toCopy, readOffset);
|
|
922
|
+
if (r === 0) break;
|
|
923
|
+
await writeFd.write(copyBuf, 0, r);
|
|
924
|
+
readOffset += r;
|
|
925
|
+
}
|
|
926
|
+
let tailOffset = newlineAfterCheckpoint;
|
|
927
|
+
let leftover = "";
|
|
928
|
+
while (tailOffset < totalSize) {
|
|
929
|
+
const toRead = Math.min(CHUNK_SIZE, totalSize - tailOffset);
|
|
930
|
+
const tailBuf = Buffer.alloc(toRead);
|
|
931
|
+
const { bytesRead: tr } = await src.read(tailBuf, 0, toRead, tailOffset);
|
|
932
|
+
if (tr === 0) break;
|
|
933
|
+
const chunk = leftover + tailBuf.subarray(0, tr).toString("utf8");
|
|
934
|
+
const lastNl = chunk.lastIndexOf("\n");
|
|
935
|
+
if (lastNl === -1) {
|
|
936
|
+
leftover = chunk;
|
|
937
|
+
} else {
|
|
938
|
+
for (const line of chunk.slice(0, lastNl + 1).split("\n")) {
|
|
939
|
+
if (!line.trim()) continue;
|
|
940
|
+
try {
|
|
941
|
+
JSON.parse(line);
|
|
942
|
+
} catch {
|
|
943
|
+
await writeFd.write(`${line}
|
|
944
|
+
`, void 0, "utf8");
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
leftover = chunk.slice(lastNl + 1);
|
|
948
|
+
}
|
|
949
|
+
tailOffset += tr;
|
|
950
|
+
}
|
|
951
|
+
if (leftover.trim()) {
|
|
952
|
+
try {
|
|
953
|
+
JSON.parse(leftover);
|
|
954
|
+
} catch {
|
|
955
|
+
await writeFd.write(`${leftover}
|
|
956
|
+
`, void 0, "utf8");
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
} finally {
|
|
960
|
+
await writeFd.close();
|
|
964
961
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
|
|
969
|
-
return this.collectSessionIds(path2.join(dir, entry.name), childPrefix, depth + 1);
|
|
970
|
-
})
|
|
971
|
-
);
|
|
972
|
-
return [...childIdArrays.flat(), ...fileIds];
|
|
973
|
-
}
|
|
974
|
-
async summaryFor(id) {
|
|
975
|
-
const manifest = this.sessionPath(id, ".summary.json");
|
|
976
|
-
const t0 = Date.now();
|
|
977
|
-
let outcome = "success";
|
|
978
|
-
let errorMsg;
|
|
979
|
-
const fromManifest = await this.readSummaryManifest(id, t0);
|
|
980
|
-
if (fromManifest) return fromManifest;
|
|
981
|
-
try {
|
|
982
|
-
const full = this.sessionPath(id, ".jsonl");
|
|
983
|
-
const stat7 = await fsp.stat(full);
|
|
984
|
-
const summary = await this.summarize(id, stat7.mtime.toISOString());
|
|
985
|
-
await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
|
|
986
|
-
const msg = toErrorMessage(err);
|
|
987
|
-
this.emitError(id, manifest, "summary_fallback", msg, true);
|
|
988
|
-
console.warn(JSON.stringify({
|
|
989
|
-
level: "warn",
|
|
990
|
-
event: "session_store.manifest_write_failed",
|
|
991
|
-
sessionId: id,
|
|
992
|
-
message: msg,
|
|
993
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
994
|
-
}));
|
|
995
|
-
});
|
|
996
|
-
outcome = "failure";
|
|
997
|
-
errorMsg = "summary fallback \xE2\u20AC\u201D manifest rebuilt";
|
|
998
|
-
this.emitRead(id, manifest, "summary", outcome, Date.now() - t0, errorMsg);
|
|
999
|
-
return summary;
|
|
962
|
+
await src.close();
|
|
963
|
+
await fsp2.rename(tmpPath, this.filePath);
|
|
964
|
+
this.handle = await fsp2.open(this.filePath, "a", 384);
|
|
1000
965
|
} catch (err) {
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
return {
|
|
1005
|
-
id,
|
|
1006
|
-
title: "(damaged)",
|
|
1007
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1008
|
-
model: "unknown",
|
|
1009
|
-
provider: "unknown",
|
|
1010
|
-
tokenTotal: 0
|
|
1011
|
-
};
|
|
966
|
+
await fsp2.unlink(tmpPath).catch(() => void 0);
|
|
967
|
+
this.handle = await fsp2.open(this.filePath, "a", 384).catch(() => this.handle);
|
|
968
|
+
throw err;
|
|
1012
969
|
}
|
|
970
|
+
await this.append({
|
|
971
|
+
type: "rewound",
|
|
972
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
973
|
+
toPromptIndex: targetPromptIndex,
|
|
974
|
+
revertedFiles: []
|
|
975
|
+
});
|
|
976
|
+
this.events?.emit("session.rewound", {
|
|
977
|
+
toPromptIndex: targetPromptIndex,
|
|
978
|
+
revertedFiles: [],
|
|
979
|
+
removedEvents: removedCount
|
|
980
|
+
});
|
|
981
|
+
return removedCount;
|
|
1013
982
|
}
|
|
1014
|
-
async
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
this.
|
|
1019
|
-
return JSON.parse(raw);
|
|
1020
|
-
} catch {
|
|
1021
|
-
return null;
|
|
983
|
+
async clearSession() {
|
|
984
|
+
if (!this.filePath) return;
|
|
985
|
+
if (this.flushTimer) {
|
|
986
|
+
clearTimeout(this.flushTimer);
|
|
987
|
+
this.flushTimer = null;
|
|
1022
988
|
}
|
|
989
|
+
this.writeBuffer = [];
|
|
990
|
+
await this.writeChain;
|
|
991
|
+
const record = `${JSON.stringify({
|
|
992
|
+
type: "session_start",
|
|
993
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
994
|
+
id: this.id,
|
|
995
|
+
model: this.meta.model ?? "unknown",
|
|
996
|
+
provider: this.meta.provider ?? "unknown"
|
|
997
|
+
})}
|
|
998
|
+
`;
|
|
999
|
+
await fsp2.writeFile(this.filePath, record, "utf8");
|
|
1023
1000
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
model: "unknown",
|
|
1034
|
-
provider: "unknown",
|
|
1035
|
-
tokenTotal: 0
|
|
1036
|
-
};
|
|
1037
|
-
}
|
|
1038
|
-
mtime = stat7.mtime.toISOString();
|
|
1039
|
-
} catch {
|
|
1040
|
-
return null;
|
|
1041
|
-
}
|
|
1042
|
-
try {
|
|
1043
|
-
for await (const event of this.iterSessionEvents(ref.filePath)) {
|
|
1044
|
-
if (event.type === "session_start") {
|
|
1045
|
-
return {
|
|
1046
|
-
id: ref.id,
|
|
1047
|
-
title: "(empty session)",
|
|
1048
|
-
startedAt: event.ts,
|
|
1049
|
-
model: event.model ?? "unknown",
|
|
1050
|
-
provider: event.provider ?? "unknown",
|
|
1051
|
-
tokenTotal: 0
|
|
1052
|
-
};
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
return {
|
|
1056
|
-
id: ref.id,
|
|
1057
|
-
title: "(empty session)",
|
|
1058
|
-
startedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1059
|
-
model: "unknown",
|
|
1060
|
-
provider: "unknown",
|
|
1061
|
-
tokenTotal: 0
|
|
1062
|
-
};
|
|
1063
|
-
} catch {
|
|
1064
|
-
return {
|
|
1065
|
-
id: ref.id,
|
|
1066
|
-
title: "(damaged)",
|
|
1067
|
-
startedAt: mtime,
|
|
1068
|
-
model: "unknown",
|
|
1069
|
-
provider: "unknown",
|
|
1070
|
-
tokenTotal: 0
|
|
1071
|
-
};
|
|
1001
|
+
/**
|
|
1002
|
+
* Write an in-flight marker. The agent loop should call
|
|
1003
|
+
* this at the start of each long-running operation; a matching
|
|
1004
|
+
* `clearInFlightMarker` follows on clean exit. A stale marker
|
|
1005
|
+
* (no end) is what `SessionRecovery.detectStale` looks for.
|
|
1006
|
+
*/
|
|
1007
|
+
async writeInFlightMarker(context) {
|
|
1008
|
+
if (!context || context.length > 500) {
|
|
1009
|
+
throw new Error("In-flight context must be 1..500 chars");
|
|
1072
1010
|
}
|
|
1011
|
+
await this.append({
|
|
1012
|
+
type: "in_flight_start",
|
|
1013
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1014
|
+
context
|
|
1015
|
+
});
|
|
1016
|
+
this.events?.emit("in_flight.started", { context, ts: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1073
1017
|
}
|
|
1074
1018
|
/**
|
|
1075
|
-
*
|
|
1076
|
-
*
|
|
1019
|
+
* Close the in-flight marker. Idempotent in spirit
|
|
1020
|
+
* (you can call it after a successful iteration even if you
|
|
1021
|
+
* didn't open one this round) — but the session log records
|
|
1022
|
+
* every call so postmortem tooling can see "the agent finished
|
|
1023
|
+
* cleanly X times, then died without finishing Y".
|
|
1024
|
+
*/
|
|
1025
|
+
async clearInFlightMarker(reason) {
|
|
1026
|
+
await this.append({
|
|
1027
|
+
type: "in_flight_end",
|
|
1028
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1029
|
+
reason
|
|
1030
|
+
});
|
|
1031
|
+
this.events?.emit("in_flight.ended", { reason, ts: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
function sanitizeModel(model) {
|
|
1035
|
+
return model.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
1036
|
+
}
|
|
1037
|
+
function generateSessionId(startedAt, model) {
|
|
1038
|
+
const date = startedAt.slice(0, 10);
|
|
1039
|
+
const time = startedAt.slice(11, 19).replace(/:/g, "-");
|
|
1040
|
+
const suffix = randomBytes(2).toString("hex");
|
|
1041
|
+
const modelPart = model ? `_${sanitizeModel(model)}` : "";
|
|
1042
|
+
return `${date}/${time}Z${modelPart}_${suffix}`;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// src/storage/session-store.ts
|
|
1046
|
+
var DefaultSessionStore = class _DefaultSessionStore {
|
|
1047
|
+
dir;
|
|
1048
|
+
events;
|
|
1049
|
+
secretScrubber;
|
|
1050
|
+
/**
|
|
1051
|
+
* In-memory cache for load() results, keyed by session ID. The cache is
|
|
1052
|
+
* invalidated when the file's mtimeMs or size changes (indicating the
|
|
1053
|
+
* file was written to). This eliminates redundant full-file reads and
|
|
1054
|
+
* JSON parses when the same session is loaded multiple times within the
|
|
1055
|
+
* store's lifetime (e.g., webui session detail views, list() fallbacks).
|
|
1077
1056
|
*
|
|
1078
|
-
*
|
|
1079
|
-
*
|
|
1080
|
-
* If the session directory itself can't be removed, the error is surfaced
|
|
1081
|
-
* to the caller so prune() can report it.
|
|
1057
|
+
* Max size is capped to prevent unbounded memory growth in long-running
|
|
1058
|
+
* processes. When the limit is reached, the oldest entry is evicted.
|
|
1082
1059
|
*/
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
event: "session_store.delete_failed",
|
|
1103
|
-
sessionId: id,
|
|
1104
|
-
message: msg,
|
|
1105
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1106
|
-
}));
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1060
|
+
_loadCache = /* @__PURE__ */ new Map();
|
|
1061
|
+
_indexCache = null;
|
|
1062
|
+
shardManifestCache = /* @__PURE__ */ new Map();
|
|
1063
|
+
static LOAD_CACHE_MAX_ENTRIES = 50;
|
|
1064
|
+
static LIST_SCAN_CONCURRENCY = 32;
|
|
1065
|
+
constructor(opts) {
|
|
1066
|
+
this.dir = opts.dir;
|
|
1067
|
+
this.events = opts.events;
|
|
1068
|
+
this.secretScrubber = opts.secretScrubber;
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Clear the load() cache. Useful for testing or when the caller knows
|
|
1072
|
+
* the file has changed externally (e.g., another process wrote to it).
|
|
1073
|
+
*/
|
|
1074
|
+
clearLoadCache(sessionId) {
|
|
1075
|
+
if (sessionId !== void 0) {
|
|
1076
|
+
this._loadCache.delete(sessionId);
|
|
1077
|
+
} else {
|
|
1078
|
+
this._loadCache.clear();
|
|
1109
1079
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1080
|
+
}
|
|
1081
|
+
// ── Storage event helpers ───────────────────────────────────────────────────
|
|
1082
|
+
emitRead(sessionId, filePath, operation, outcome, durationMs, error) {
|
|
1083
|
+
this.events?.emit("storage.read", {
|
|
1084
|
+
sessionId,
|
|
1085
|
+
store: "session",
|
|
1086
|
+
filePath,
|
|
1087
|
+
operation,
|
|
1088
|
+
outcome,
|
|
1089
|
+
durationMs,
|
|
1090
|
+
...error !== void 0 ? { error } : {}
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
emitWrite(sessionId, filePath, operation, outcome, durationMs, eventCount, error) {
|
|
1094
|
+
this.events?.emit("storage.write", {
|
|
1095
|
+
sessionId,
|
|
1096
|
+
store: "session",
|
|
1097
|
+
filePath,
|
|
1098
|
+
operation,
|
|
1099
|
+
outcome,
|
|
1100
|
+
durationMs,
|
|
1101
|
+
...eventCount !== void 0 ? { eventCount } : {},
|
|
1102
|
+
...error !== void 0 ? { error } : {}
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
emitError(sessionId, filePath, operation, error, recoverable) {
|
|
1106
|
+
this.events?.emit("storage.error", {
|
|
1107
|
+
sessionId,
|
|
1108
|
+
store: "session",
|
|
1109
|
+
filePath,
|
|
1110
|
+
operation,
|
|
1111
|
+
error,
|
|
1112
|
+
recoverable
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
/** Absolute path to the session index file. */
|
|
1116
|
+
get indexFile() {
|
|
1117
|
+
return path2.join(this.dir, "_index.jsonl");
|
|
1118
|
+
}
|
|
1119
|
+
/** Join session ID to its absolute path within the store directory. */
|
|
1120
|
+
sessionPath(id, ext) {
|
|
1121
|
+
return path2.join(this.dir, `${id}${ext}`);
|
|
1122
|
+
}
|
|
1123
|
+
shardManifestPath(shardKey) {
|
|
1124
|
+
return shardKey ? path2.join(this.dir, shardKey, "_manifest.json") : path2.join(this.dir, "_manifest.json");
|
|
1125
|
+
}
|
|
1126
|
+
shardKeyForSessionId(id) {
|
|
1127
|
+
const dirName = path2.dirname(id);
|
|
1128
|
+
return dirName === "." ? "" : dirName;
|
|
1129
|
+
}
|
|
1130
|
+
invalidateShardManifestBySessionId(id) {
|
|
1131
|
+
this.shardManifestCache.delete(this.shardKeyForSessionId(id));
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Ensure the directory implied by the session ID exists. When the ID
|
|
1135
|
+
* contains a date prefix like `2026-06-06/...`, this creates the date
|
|
1136
|
+
* subdirectory so sessions group naturally by day.
|
|
1137
|
+
*/
|
|
1138
|
+
async ensureShardDir(id) {
|
|
1139
|
+
const dirPath = path2.dirname(path2.join(this.dir, id));
|
|
1140
|
+
await ensureDir(dirPath);
|
|
1141
|
+
return dirPath;
|
|
1142
|
+
}
|
|
1143
|
+
async create(meta) {
|
|
1144
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1145
|
+
const id = meta.id && meta.id.length > 0 ? meta.id : generateSessionId(startedAt, meta.model ?? meta.provider);
|
|
1146
|
+
const shardDir = await this.ensureShardDir(id);
|
|
1147
|
+
const file = path2.join(shardDir, `${path2.basename(id)}.jsonl`);
|
|
1148
|
+
const t0 = Date.now();
|
|
1149
|
+
let handle;
|
|
1150
|
+
try {
|
|
1151
|
+
handle = await fsp2.open(file, "a", 384);
|
|
1152
|
+
} catch (err) {
|
|
1153
|
+
this.emitError(id, file, "create", toErrorMessage(err), false);
|
|
1154
|
+
throw new Error(
|
|
1155
|
+
`Failed to open session file: ${toErrorMessage(err)}`,
|
|
1156
|
+
{ cause: err }
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
try {
|
|
1160
|
+
const writer = new FileSessionWriter(id, handle, startedAt, meta, this.events, {
|
|
1161
|
+
dir: shardDir,
|
|
1162
|
+
filePath: file,
|
|
1163
|
+
secretScrubber: this.secretScrubber,
|
|
1164
|
+
onClose: (s) => this.appendToIndex(s)
|
|
1165
|
+
});
|
|
1166
|
+
this.emitWrite(id, file, "create", "success", Date.now() - t0);
|
|
1167
|
+
return writer;
|
|
1168
|
+
} catch (err) {
|
|
1169
|
+
await handle.close().catch((e) => console.warn(JSON.stringify({
|
|
1112
1170
|
level: "warn",
|
|
1113
|
-
event: "session_store.
|
|
1114
|
-
|
|
1115
|
-
message: toErrorMessage(err),
|
|
1171
|
+
event: "session_store.handle_close_failed",
|
|
1172
|
+
message: e instanceof Error ? e.message : String(e),
|
|
1116
1173
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1117
|
-
}));
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
async delete(id) {
|
|
1122
|
-
await this.deleteSession(id);
|
|
1174
|
+
})));
|
|
1175
|
+
this.emitError(id, file, "create", toErrorMessage(err), true);
|
|
1176
|
+
throw err;
|
|
1177
|
+
}
|
|
1123
1178
|
}
|
|
1124
|
-
async
|
|
1125
|
-
const
|
|
1126
|
-
|
|
1127
|
-
|
|
1179
|
+
async resume(id) {
|
|
1180
|
+
const file = this.sessionPath(id, ".jsonl");
|
|
1181
|
+
const t0 = Date.now();
|
|
1182
|
+
const data = await this.load(id);
|
|
1183
|
+
let handle;
|
|
1128
1184
|
try {
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
const jsonlPath = path2.join(dir, name);
|
|
1137
|
-
try {
|
|
1138
|
-
const stat7 = await fsp.stat(jsonlPath);
|
|
1139
|
-
if (stat7.mtimeMs >= cutoff) return;
|
|
1140
|
-
} catch {
|
|
1141
|
-
return;
|
|
1142
|
-
}
|
|
1143
|
-
const base = name.replace(/\.jsonl$/, "");
|
|
1144
|
-
const id = prefix ? `${prefix}/${base}` : base;
|
|
1145
|
-
if (activeSessionId && id === activeSessionId) return;
|
|
1146
|
-
await this.deleteSession(id);
|
|
1147
|
-
deleted++;
|
|
1148
|
-
};
|
|
1149
|
-
const entries = await fsp.readdir(this.dir, { withFileTypes: true }).catch(() => []);
|
|
1150
|
-
for (const entry of entries) {
|
|
1151
|
-
if (entry.isFile()) {
|
|
1152
|
-
if (isPrunableJsonl(entry.name)) await pruneFile(this.dir, entry.name, "");
|
|
1153
|
-
continue;
|
|
1154
|
-
}
|
|
1155
|
-
if (!entry.isDirectory()) continue;
|
|
1156
|
-
const dateDir = path2.join(this.dir, entry.name);
|
|
1157
|
-
const files = await fsp.readdir(dateDir, { withFileTypes: true }).catch(() => []);
|
|
1158
|
-
for (const file of files) {
|
|
1159
|
-
if (!file.isFile() || !isPrunableJsonl(file.name)) continue;
|
|
1160
|
-
await pruneFile(dateDir, file.name, entry.name);
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
if (deleted > 0) {
|
|
1164
|
-
await this.compactIndex().catch(() => void 0);
|
|
1185
|
+
handle = await fsp2.open(file, "a", 384);
|
|
1186
|
+
} catch (err) {
|
|
1187
|
+
this.emitError(id, file, "resume", toErrorMessage(err), false);
|
|
1188
|
+
throw new Error(
|
|
1189
|
+
`Failed to open session "${id}" for append: ${toErrorMessage(err)}`,
|
|
1190
|
+
{ cause: err }
|
|
1191
|
+
);
|
|
1165
1192
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1193
|
+
try {
|
|
1194
|
+
const writer = new FileSessionWriter(
|
|
1195
|
+
id,
|
|
1196
|
+
handle,
|
|
1197
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
1198
|
+
{
|
|
1199
|
+
id,
|
|
1200
|
+
model: data.metadata.model,
|
|
1201
|
+
provider: data.metadata.provider
|
|
1202
|
+
},
|
|
1203
|
+
this.events,
|
|
1204
|
+
{
|
|
1205
|
+
resumed: true,
|
|
1206
|
+
// Shard directory (sessions/<date>/) — must match create() so the
|
|
1207
|
+
// .summary.json sidecar lands next to the JSONL instead of the
|
|
1208
|
+
// sessions root (where summaryFor() would never find it).
|
|
1209
|
+
dir: path2.dirname(file),
|
|
1210
|
+
filePath: file,
|
|
1211
|
+
secretScrubber: this.secretScrubber,
|
|
1212
|
+
onClose: (s) => this.appendToIndex(s)
|
|
1173
1213
|
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1214
|
+
);
|
|
1215
|
+
this.emitWrite(id, file, "resume", "success", Date.now() - t0);
|
|
1216
|
+
return { writer, data };
|
|
1217
|
+
} catch (err) {
|
|
1218
|
+
await handle.close().catch((e) => console.warn(JSON.stringify({
|
|
1219
|
+
level: "warn",
|
|
1220
|
+
event: "session_store.handle_close_failed",
|
|
1221
|
+
message: e instanceof Error ? e.message : String(e),
|
|
1222
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1223
|
+
})));
|
|
1224
|
+
this.emitError(id, file, "resume", toErrorMessage(err), true);
|
|
1225
|
+
throw err;
|
|
1176
1226
|
}
|
|
1177
|
-
return deleted;
|
|
1178
1227
|
}
|
|
1179
|
-
async
|
|
1180
|
-
await this.ensureShardDir(id);
|
|
1228
|
+
async load(id) {
|
|
1181
1229
|
const file = this.sessionPath(id, ".jsonl");
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
id,
|
|
1187
|
-
model: "unknown",
|
|
1188
|
-
provider: "unknown"
|
|
1189
|
-
})}
|
|
1190
|
-
`;
|
|
1191
|
-
await fsp.writeFile(file, record, "utf8");
|
|
1192
|
-
await fsp.unlink(meta).catch(() => void 0);
|
|
1193
|
-
}
|
|
1194
|
-
async summarize(id, mtime) {
|
|
1230
|
+
const t0 = Date.now();
|
|
1231
|
+
let outcome = "success";
|
|
1232
|
+
let errorMsg;
|
|
1233
|
+
let cacheHit = false;
|
|
1195
1234
|
try {
|
|
1196
|
-
const
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
let
|
|
1210
|
-
let
|
|
1211
|
-
let
|
|
1212
|
-
let
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1235
|
+
const s = await fsp2.stat(file);
|
|
1236
|
+
const stat10 = { mtimeMs: s.mtimeMs, size: s.size };
|
|
1237
|
+
const cached = this._loadCache.get(id);
|
|
1238
|
+
if (cached && cached.mtimeMs === stat10.mtimeMs && cached.size === stat10.size) {
|
|
1239
|
+
cacheHit = true;
|
|
1240
|
+
this._loadCache.delete(id);
|
|
1241
|
+
this._loadCache.set(id, cached);
|
|
1242
|
+
return cached.data;
|
|
1243
|
+
}
|
|
1244
|
+
const raw = await fsp2.readFile(file, "utf8");
|
|
1245
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
1246
|
+
const events = [];
|
|
1247
|
+
let sessionStartEvent;
|
|
1248
|
+
let sessionEndEvent;
|
|
1249
|
+
let sessionModel;
|
|
1250
|
+
let sessionProvider;
|
|
1251
|
+
let sessionPendingToolUses;
|
|
1252
|
+
const messages = [];
|
|
1253
|
+
const openToolUses = /* @__PURE__ */ new Set();
|
|
1254
|
+
let usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
1255
|
+
for (const line of lines) {
|
|
1256
|
+
try {
|
|
1257
|
+
const parsed = JSON.parse(line);
|
|
1258
|
+
if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
|
|
1259
|
+
const ev = parsed;
|
|
1260
|
+
events.push(ev);
|
|
1261
|
+
if (ev.type === "session_start" && !sessionStartEvent) {
|
|
1262
|
+
sessionStartEvent = ev;
|
|
1263
|
+
sessionModel = ev.model;
|
|
1264
|
+
sessionProvider = ev.provider;
|
|
1265
|
+
}
|
|
1266
|
+
if (ev.type === "session_end") {
|
|
1267
|
+
sessionEndEvent = ev;
|
|
1268
|
+
sessionPendingToolUses = ev.pendingToolUses;
|
|
1269
|
+
}
|
|
1270
|
+
if (ev.type === "user_input") {
|
|
1271
|
+
openToolUses.clear();
|
|
1272
|
+
messages.push({ role: "user", content: ev.content, ts: ev.ts });
|
|
1273
|
+
} else if (ev.type === "llm_response") {
|
|
1274
|
+
messages.push({ role: "assistant", content: ev.content, ts: ev.ts });
|
|
1275
|
+
for (const b of ev.content) {
|
|
1276
|
+
if (b.type === "tool_use") openToolUses.add(b.id);
|
|
1277
|
+
}
|
|
1278
|
+
usage = {
|
|
1279
|
+
input: usage.input + (ev.usage.input ?? 0),
|
|
1280
|
+
output: usage.output + (ev.usage.output ?? 0),
|
|
1281
|
+
cacheRead: (usage.cacheRead ?? 0) + (ev.usage.cacheRead ?? 0),
|
|
1282
|
+
cacheWrite: (usage.cacheWrite ?? 0) + (ev.usage.cacheWrite ?? 0)
|
|
1283
|
+
};
|
|
1284
|
+
} else if (ev.type === "tool_result") {
|
|
1285
|
+
if (!openToolUses.has(ev.id)) {
|
|
1286
|
+
this.events?.emit("session.damaged", {
|
|
1287
|
+
sessionId: id,
|
|
1288
|
+
detail: `Orphan tool_result "${ev.id}" has no matching tool_use`
|
|
1289
|
+
});
|
|
1290
|
+
continue;
|
|
1291
|
+
}
|
|
1292
|
+
openToolUses.delete(ev.id);
|
|
1293
|
+
const resultBlock = {
|
|
1294
|
+
type: "tool_result",
|
|
1295
|
+
tool_use_id: ev.id,
|
|
1296
|
+
content: typeof ev.content === "string" ? ev.content : JSON.stringify(ev.content),
|
|
1297
|
+
is_error: ev.isError
|
|
1298
|
+
};
|
|
1299
|
+
const last = messages[messages.length - 1];
|
|
1300
|
+
const lastIsToolResultUser = last?.role === "user" && Array.isArray(last.content) && last.content.every((b) => b.type === "tool_result");
|
|
1301
|
+
if (lastIsToolResultUser && Array.isArray(last.content)) {
|
|
1302
|
+
last.content.push(resultBlock);
|
|
1303
|
+
} else {
|
|
1304
|
+
messages.push({ role: "user", content: [resultBlock], ts: ev.ts });
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1221
1307
|
}
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
} else if (e.type === "user_input") {
|
|
1225
|
-
if (title === "(empty session)") title = userInputTitle(e.content);
|
|
1226
|
-
} else if (e.type === "llm_response") {
|
|
1227
|
-
tokenIn += e.usage.input ?? 0;
|
|
1228
|
-
tokenOut += e.usage.output ?? 0;
|
|
1229
|
-
} else if (e.type === "in_flight_start") iterationCount++;
|
|
1230
|
-
else if (e.type === "tool_call_start") {
|
|
1231
|
-
toolCallCount++;
|
|
1232
|
-
toolBreakdown[e.name] = (toolBreakdown[e.name] ?? 0) + 1;
|
|
1233
|
-
} else if (e.type === "tool_result" && e.isError) toolErrorCount++;
|
|
1234
|
-
else if (e.type === "file_snapshot") fileChangeCount += e.files.length;
|
|
1235
|
-
else if (e.type === "error" || e.type === "provider_error") hasError = true;
|
|
1308
|
+
} catch {
|
|
1309
|
+
}
|
|
1236
1310
|
}
|
|
1237
|
-
if (
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
outcome = "error";
|
|
1311
|
+
if (openToolUses.size > 0) {
|
|
1312
|
+
this.events?.emit("session.damaged", {
|
|
1313
|
+
sessionId: id,
|
|
1314
|
+
detail: `${openToolUses.size} tool_use blocks without matching results - replay repaired`
|
|
1315
|
+
});
|
|
1243
1316
|
}
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
iterationCount: iterationCount > 0 ? iterationCount : void 0,
|
|
1253
|
-
toolCallCount: toolCallCount > 0 ? toolCallCount : void 0,
|
|
1254
|
-
toolErrorCount: toolErrorCount > 0 ? toolErrorCount : void 0,
|
|
1255
|
-
fileChangeCount: fileChangeCount > 0 ? fileChangeCount : void 0,
|
|
1256
|
-
toolBreakdown: Object.keys(toolBreakdown).length > 0 ? toolBreakdown : {},
|
|
1257
|
-
outcome
|
|
1258
|
-
};
|
|
1259
|
-
} catch {
|
|
1260
|
-
return {
|
|
1317
|
+
const repaired = repairToolUseAdjacency(messages);
|
|
1318
|
+
if (repaired.report.changed) {
|
|
1319
|
+
this.events?.emit("session.damaged", {
|
|
1320
|
+
sessionId: id,
|
|
1321
|
+
detail: `Repaired replay adjacency: removed ${repaired.report.removedToolUses.length} tool_use, ${repaired.report.removedToolResults.length} tool_result, ${repaired.report.removedMessages} empty messages`
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
const meta = {
|
|
1261
1325
|
id,
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
model:
|
|
1265
|
-
provider:
|
|
1266
|
-
|
|
1326
|
+
startedAt: sessionStartEvent?.ts ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1327
|
+
endedAt: sessionEndEvent?.ts,
|
|
1328
|
+
model: sessionModel,
|
|
1329
|
+
provider: sessionProvider,
|
|
1330
|
+
pendingToolUses: sessionPendingToolUses
|
|
1267
1331
|
};
|
|
1332
|
+
const toolCallEnds = extractToolCallEnds(events);
|
|
1333
|
+
const data = { metadata: meta, events, messages: repaired.messages, usage, toolCallEnds };
|
|
1334
|
+
if (this._loadCache.size >= _DefaultSessionStore.LOAD_CACHE_MAX_ENTRIES) {
|
|
1335
|
+
const oldest = this._loadCache.keys().next().value;
|
|
1336
|
+
if (oldest !== void 0) {
|
|
1337
|
+
this._loadCache.delete(oldest);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
this._loadCache.set(id, { mtimeMs: stat10.mtimeMs, size: stat10.size, data });
|
|
1341
|
+
return data;
|
|
1342
|
+
} catch (err) {
|
|
1343
|
+
outcome = "failure";
|
|
1344
|
+
errorMsg = toErrorMessage(err);
|
|
1345
|
+
throw err;
|
|
1346
|
+
} finally {
|
|
1347
|
+
this.emitRead(id, file, "load", outcome, Date.now() - t0, errorMsg);
|
|
1348
|
+
if (cacheHit) {
|
|
1349
|
+
this.events?.emit("storage.cache_hit", {
|
|
1350
|
+
sessionId: id,
|
|
1351
|
+
store: "session",
|
|
1352
|
+
filePath: file,
|
|
1353
|
+
operation: "load",
|
|
1354
|
+
durationMs: Date.now() - t0
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1268
1357
|
}
|
|
1269
1358
|
}
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1359
|
+
/**
|
|
1360
|
+
* Streaming search over a session's JSONL. Walks the file once, parses
|
|
1361
|
+
* each event lazily, and yields only the events that match `predicate`.
|
|
1362
|
+
* Stops as soon as `opts.limit` matches are collected.
|
|
1363
|
+
*
|
|
1364
|
+
* Why this exists: `load()` parses the entire file into memory and
|
|
1365
|
+
* rebuilds `messages`/`toolCallEnds` for every caller. `search()` only
|
|
1366
|
+
* needs to know which events contain matching text — a per-line
|
|
1367
|
+
* predicate is enough. The full parse work (and the `_loadCache` poll)
|
|
1368
|
+
* is wasted in that case.
|
|
1369
|
+
*
|
|
1370
|
+
* Memory: O(hits) regardless of file size. Disk: one linear scan,
|
|
1371
|
+
* terminated at `limit` if the caller asked for one.
|
|
1372
|
+
*
|
|
1373
|
+
* Errors: missing file yields []. Corrupt lines are skipped (same
|
|
1374
|
+
* policy as `load()`). Aborting via `signal` rejects with `AbortError`.
|
|
1375
|
+
*/
|
|
1376
|
+
async searchEvents(id, predicate, opts) {
|
|
1377
|
+
const file = this.sessionPath(id, ".jsonl");
|
|
1378
|
+
const limit = opts?.limit;
|
|
1379
|
+
const signal = opts?.signal;
|
|
1380
|
+
const out = [];
|
|
1381
|
+
let stat10;
|
|
1273
1382
|
try {
|
|
1274
|
-
|
|
1275
|
-
|
|
1383
|
+
stat10 = await fsp2.stat(file);
|
|
1384
|
+
} catch (err) {
|
|
1385
|
+
if (err.code === "ENOENT") return [];
|
|
1386
|
+
throw err;
|
|
1387
|
+
}
|
|
1388
|
+
if (stat10.size === 0) return [];
|
|
1389
|
+
let fh;
|
|
1390
|
+
try {
|
|
1391
|
+
fh = await fsp2.open(file, "r");
|
|
1392
|
+
const CHUNK = 64 * 1024;
|
|
1393
|
+
const buf = Buffer.alloc(CHUNK);
|
|
1394
|
+
let leftover = "";
|
|
1395
|
+
let eventIndex = 0;
|
|
1396
|
+
for (let position = 0; ; position += buf.byteLength) {
|
|
1397
|
+
if (signal?.aborted) {
|
|
1398
|
+
const reason = signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
1399
|
+
throw reason;
|
|
1400
|
+
}
|
|
1401
|
+
const { bytesRead } = await fh.read(buf, 0, CHUNK, position);
|
|
1402
|
+
if (bytesRead === 0) break;
|
|
1403
|
+
const text = leftover + buf.subarray(0, bytesRead).toString("utf8");
|
|
1404
|
+
const parts = text.split("\n");
|
|
1405
|
+
leftover = parts.pop() ?? "";
|
|
1406
|
+
for (const line of parts) {
|
|
1407
|
+
if (!line) continue;
|
|
1408
|
+
let ev;
|
|
1409
|
+
try {
|
|
1410
|
+
const parsed = JSON.parse(line);
|
|
1411
|
+
if (parsed === null || typeof parsed !== "object" || typeof parsed.type !== "string" || typeof parsed.ts !== "string") {
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
ev = parsed;
|
|
1415
|
+
} catch {
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
if (predicate(ev, eventIndex, ev.ts)) {
|
|
1419
|
+
out.push({ event: ev, eventIndex, ts: ev.ts });
|
|
1420
|
+
if (limit !== void 0 && out.length >= limit) {
|
|
1421
|
+
return out;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
eventIndex++;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
if (leftover.trim()) {
|
|
1276
1428
|
try {
|
|
1277
|
-
const parsed = JSON.parse(
|
|
1429
|
+
const parsed = JSON.parse(leftover);
|
|
1278
1430
|
if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
|
|
1279
|
-
|
|
1431
|
+
const ev = parsed;
|
|
1432
|
+
if (predicate(ev, eventIndex, ev.ts)) {
|
|
1433
|
+
out.push({ event: ev, eventIndex, ts: ev.ts });
|
|
1434
|
+
}
|
|
1280
1435
|
}
|
|
1281
1436
|
} catch {
|
|
1282
1437
|
}
|
|
1283
1438
|
}
|
|
1439
|
+
return out;
|
|
1284
1440
|
} finally {
|
|
1285
|
-
|
|
1286
|
-
stream.destroy();
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
};
|
|
1290
|
-
function extractToolCallEnds(events) {
|
|
1291
|
-
const result = [];
|
|
1292
|
-
for (const e of events) {
|
|
1293
|
-
if (e.type === "tool_call_end") {
|
|
1294
|
-
result.push({
|
|
1295
|
-
name: e.name,
|
|
1296
|
-
id: e.id,
|
|
1297
|
-
durationMs: e.durationMs,
|
|
1298
|
-
ok: e.ok ?? false,
|
|
1299
|
-
outputBytes: e.outputBytes,
|
|
1300
|
-
outputTokens: e.outputTokens,
|
|
1301
|
-
outputLines: e.outputLines
|
|
1302
|
-
});
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
return result;
|
|
1306
|
-
}
|
|
1307
|
-
var FileSessionWriter = class _FileSessionWriter {
|
|
1308
|
-
constructor(id, handle, startedAt, meta, events, opts = {}, traceId) {
|
|
1309
|
-
this.id = id;
|
|
1310
|
-
this.handle = handle;
|
|
1311
|
-
this.startedAt = startedAt;
|
|
1312
|
-
this.meta = meta;
|
|
1313
|
-
this.events = events;
|
|
1314
|
-
this.resumed = opts.resumed ?? false;
|
|
1315
|
-
this.manifestFile = opts.dir ? path2.join(opts.dir, `${path2.basename(id)}.summary.json`) : "";
|
|
1316
|
-
this.filePath = opts.filePath ?? "";
|
|
1317
|
-
this.secretScrubber = opts.secretScrubber;
|
|
1318
|
-
this.onCloseCb = opts.onClose;
|
|
1319
|
-
this.summary = {
|
|
1320
|
-
id,
|
|
1321
|
-
title: "(empty session)",
|
|
1322
|
-
startedAt,
|
|
1323
|
-
model: meta.model ?? "unknown",
|
|
1324
|
-
provider: meta.provider ?? "unknown",
|
|
1325
|
-
tokenTotal: 0
|
|
1326
|
-
};
|
|
1327
|
-
this.traceId = traceId;
|
|
1328
|
-
}
|
|
1329
|
-
id;
|
|
1330
|
-
handle;
|
|
1331
|
-
startedAt;
|
|
1332
|
-
meta;
|
|
1333
|
-
events;
|
|
1334
|
-
closed = false;
|
|
1335
|
-
closePromise = null;
|
|
1336
|
-
manifestFile;
|
|
1337
|
-
summary;
|
|
1338
|
-
tokenIn = 0;
|
|
1339
|
-
tokenOut = 0;
|
|
1340
|
-
filePath;
|
|
1341
|
-
get transcriptPath() {
|
|
1342
|
-
return this.filePath || void 0;
|
|
1343
|
-
}
|
|
1344
|
-
/**
|
|
1345
|
-
* Lazy session_start/session_resumed init, shared by all appenders.
|
|
1346
|
-
* A single promise (not a boolean) so a second append racing the first
|
|
1347
|
-
* can't push its event into the buffer BEFORE the first append's event —
|
|
1348
|
-
* every appender awaits the same init and resumes in FIFO call order.
|
|
1349
|
-
*/
|
|
1350
|
-
initPromise = null;
|
|
1351
|
-
ensureInit() {
|
|
1352
|
-
if (!this.initPromise) this.initPromise = this.writeSessionStartLazy();
|
|
1353
|
-
return this.initPromise;
|
|
1354
|
-
}
|
|
1355
|
-
resumed;
|
|
1356
|
-
appendFailCount = 0;
|
|
1357
|
-
lastAppendWarnAt = 0;
|
|
1358
|
-
secretScrubber;
|
|
1359
|
-
onCloseCb;
|
|
1360
|
-
/** Implements SessionWriter.traceId — propagated from ContextInit.traceId. */
|
|
1361
|
-
traceId;
|
|
1362
|
-
// ── Write buffer — batches events to reduce per-event disk I/O ─────────
|
|
1363
|
-
//
|
|
1364
|
-
// Every append() pushes the scrubbed event into an in-memory buffer instead
|
|
1365
|
-
// of calling handle.appendFile() synchronously. The buffer flushes to disk
|
|
1366
|
-
// when it reaches FLUSH_SIZE events OR after FLUSH_INTERVAL_MS of inactivity.
|
|
1367
|
-
// This cuts the number of disk writes by ~95% without changing the on-disk
|
|
1368
|
-
// format — the JSONL is still one JSON object per line.
|
|
1369
|
-
writeBuffer = [];
|
|
1370
|
-
flushTimer = null;
|
|
1371
|
-
static FLUSH_INTERVAL_MS = 500;
|
|
1372
|
-
static FLUSH_SIZE = 50;
|
|
1373
|
-
// ── Write serialization ─────────────────────────────────────────────────
|
|
1374
|
-
//
|
|
1375
|
-
// All disk writes are funneled through a FIFO promise chain. Without it,
|
|
1376
|
-
// a timer-driven flush racing an explicit flush()/close() issues two
|
|
1377
|
-
// concurrent appendFile() calls on the shared O_APPEND handle — the kernel
|
|
1378
|
-
// may complete them out of order (chronology breaks) or, for large
|
|
1379
|
-
// batches, interleave partial writes (torn JSONL lines). The chain keeps
|
|
1380
|
-
// exactly one write in flight; failures don't break the chain.
|
|
1381
|
-
writeChain = Promise.resolve();
|
|
1382
|
-
/** Enqueue a write on the FIFO chain. Resolves/rejects with that write. */
|
|
1383
|
-
enqueueWrite(data) {
|
|
1384
|
-
const write = this.writeChain.then(() => this.handle.appendFile(data, "utf8"));
|
|
1385
|
-
this.writeChain = write.then(
|
|
1386
|
-
() => void 0,
|
|
1387
|
-
() => void 0
|
|
1388
|
-
);
|
|
1389
|
-
return write;
|
|
1390
|
-
}
|
|
1391
|
-
// ── Enriched summary tracking ──────────────────────────────────────────
|
|
1392
|
-
iterationCount = 0;
|
|
1393
|
-
toolCallCount = 0;
|
|
1394
|
-
toolErrorCount = 0;
|
|
1395
|
-
toolBreakdown = {};
|
|
1396
|
-
fileChangeCount = 0;
|
|
1397
|
-
compactionCount = 0;
|
|
1398
|
-
outcome = void 0;
|
|
1399
|
-
/**
|
|
1400
|
-
* Scrub secrets out of conversation-turn events before they are observed
|
|
1401
|
-
* for the summary, written to the JSONL log, or surfaced on resume. Only
|
|
1402
|
-
* `user_input` / `llm_response` carry free-form user/model text; other event
|
|
1403
|
-
* types either have no secret-bearing content or are already scrubbed
|
|
1404
|
-
* upstream (tool results). Returns the event unchanged when no scrubber is
|
|
1405
|
-
* configured.
|
|
1406
|
-
*/
|
|
1407
|
-
scrubEvent(event) {
|
|
1408
|
-
const s = this.secretScrubber;
|
|
1409
|
-
if (!s) return event;
|
|
1410
|
-
if (event.type === "user_input") {
|
|
1411
|
-
return {
|
|
1412
|
-
...event,
|
|
1413
|
-
content: typeof event.content === "string" ? s.scrub(event.content) : s.scrubObject(event.content)
|
|
1414
|
-
};
|
|
1415
|
-
}
|
|
1416
|
-
if (event.type === "llm_response") {
|
|
1417
|
-
return { ...event, content: s.scrubObject(event.content) };
|
|
1441
|
+
if (fh) await fh.close().catch(() => void 0);
|
|
1418
1442
|
}
|
|
1419
|
-
return event;
|
|
1420
|
-
}
|
|
1421
|
-
pendingFileSnapshots = [];
|
|
1422
|
-
/** Tracks open tool_use IDs during the current run to serialize on close for resume. */
|
|
1423
|
-
openToolUses = /* @__PURE__ */ new Set();
|
|
1424
|
-
recordFileChange(input) {
|
|
1425
|
-
this.pendingFileSnapshots.push(input);
|
|
1426
|
-
}
|
|
1427
|
-
get pendingToolUses() {
|
|
1428
|
-
return Array.from(this.openToolUses);
|
|
1429
1443
|
}
|
|
1430
|
-
async
|
|
1431
|
-
const record = `${JSON.stringify({
|
|
1432
|
-
type: this.resumed ? "session_resumed" : "session_start",
|
|
1433
|
-
ts: this.startedAt,
|
|
1434
|
-
id: this.id,
|
|
1435
|
-
model: this.meta.model ?? "unknown",
|
|
1436
|
-
provider: this.meta.provider ?? "unknown"
|
|
1437
|
-
})}
|
|
1438
|
-
`;
|
|
1444
|
+
async list(limit = 20) {
|
|
1439
1445
|
try {
|
|
1440
|
-
await this.
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
this.writeBuffer.push(scrubbed);
|
|
1450
|
-
if (this.writeBuffer.length >= _FileSessionWriter.FLUSH_SIZE) {
|
|
1451
|
-
if (this.flushTimer) {
|
|
1452
|
-
clearTimeout(this.flushTimer);
|
|
1453
|
-
this.flushTimer = null;
|
|
1454
|
-
}
|
|
1455
|
-
await this.flushBuffer();
|
|
1456
|
-
} else {
|
|
1457
|
-
this.scheduleFlush();
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
async appendBatch(events) {
|
|
1461
|
-
if (this.closed || events.length === 0) return;
|
|
1462
|
-
await this.ensureInit();
|
|
1463
|
-
for (const event of events) {
|
|
1464
|
-
const scrubbed = this.scrubEvent(event);
|
|
1465
|
-
this.observeForSummary(scrubbed);
|
|
1466
|
-
this.writeBuffer.push(scrubbed);
|
|
1467
|
-
}
|
|
1468
|
-
if (this.writeBuffer.length >= _FileSessionWriter.FLUSH_SIZE) {
|
|
1469
|
-
if (this.flushTimer) {
|
|
1470
|
-
clearTimeout(this.flushTimer);
|
|
1471
|
-
this.flushTimer = null;
|
|
1446
|
+
await ensureDir(this.dir);
|
|
1447
|
+
const indexed = await this.readIndex();
|
|
1448
|
+
if (indexed.length > 0) {
|
|
1449
|
+
indexed.sort((a, b) => {
|
|
1450
|
+
if (a.startedAt < b.startedAt) return 1;
|
|
1451
|
+
if (a.startedAt > b.startedAt) return -1;
|
|
1452
|
+
return a.id.localeCompare(b.id);
|
|
1453
|
+
});
|
|
1454
|
+
return indexed.slice(0, limit);
|
|
1472
1455
|
}
|
|
1473
|
-
await this.
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1456
|
+
return await this.listFromDirectoryScan(limit);
|
|
1457
|
+
} catch {
|
|
1458
|
+
return [];
|
|
1476
1459
|
}
|
|
1477
1460
|
}
|
|
1478
1461
|
/**
|
|
1479
|
-
*
|
|
1480
|
-
*
|
|
1481
|
-
*
|
|
1462
|
+
* List sessions matching filter criteria, using the cached index.
|
|
1463
|
+
* Filters are applied BEFORE sorting and slicing, so the caller gets
|
|
1464
|
+
* exactly `limit` matching sessions — not a slice of a larger fetch.
|
|
1482
1465
|
*
|
|
1483
|
-
*
|
|
1484
|
-
*
|
|
1485
|
-
*
|
|
1466
|
+
* This avoids the DefaultSessionReader pattern of fetching 1000 sessions
|
|
1467
|
+
* then linear-filtering: the index is already in memory (readIndex
|
|
1468
|
+
* caches it), and the filter runs over the cached array without any
|
|
1469
|
+
* additional disk I/O.
|
|
1486
1470
|
*/
|
|
1487
|
-
async
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
this.
|
|
1471
|
+
async listFiltered(criteria) {
|
|
1472
|
+
const limit = criteria.limit ?? 100;
|
|
1473
|
+
try {
|
|
1474
|
+
await ensureDir(this.dir);
|
|
1475
|
+
const indexed = await this.readIndex();
|
|
1476
|
+
if (indexed.length === 0) {
|
|
1477
|
+
const raw = await this.list(Math.max(limit, 100));
|
|
1478
|
+
return raw.filter((s) => matchesSessionFilter(s, criteria)).slice(0, limit);
|
|
1479
|
+
}
|
|
1480
|
+
const filtered = indexed.filter((s) => matchesSessionFilter(s, criteria));
|
|
1481
|
+
filtered.sort((a, b) => {
|
|
1482
|
+
if (a.startedAt < b.startedAt) return 1;
|
|
1483
|
+
if (a.startedAt > b.startedAt) return -1;
|
|
1484
|
+
return a.id.localeCompare(b.id);
|
|
1485
|
+
});
|
|
1486
|
+
return filtered.slice(0, limit);
|
|
1487
|
+
} catch {
|
|
1488
|
+
return [];
|
|
1491
1489
|
}
|
|
1492
|
-
await this.flushBuffer();
|
|
1493
1490
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1491
|
+
// ── Session index (_index.jsonl) ─────────────────────────────────────────
|
|
1492
|
+
//
|
|
1493
|
+
// One JSON line per closed session, appended atomically on close().
|
|
1494
|
+
// When a session is deleted, a tombstone {action:"delete",id:"..."} is
|
|
1495
|
+
// appended. On read, tombstones filter out matching session entries.
|
|
1496
|
+
// This keeps listing O(lines-in-index) instead of O(files-on-disk).
|
|
1497
|
+
//
|
|
1498
|
+
// The index auto-compacts every N appends to prevent unbounded growth
|
|
1499
|
+
// from tombstones and duplicate entries (resume cycles).
|
|
1500
|
+
indexAppendCount = 0;
|
|
1501
|
+
static COMPACT_EVERY = 30;
|
|
1502
|
+
/** Append a session summary to the index. */
|
|
1503
|
+
async appendToIndex(summary) {
|
|
1504
|
+
try {
|
|
1505
|
+
await ensureDir(this.dir);
|
|
1506
|
+
const line = JSON.stringify(summary) + "\n";
|
|
1507
|
+
await fsp2.appendFile(this.indexFile, line, "utf8");
|
|
1508
|
+
this._indexCache = null;
|
|
1509
|
+
this.invalidateShardManifestBySessionId(summary.id);
|
|
1510
|
+
this.indexAppendCount++;
|
|
1511
|
+
if (this.indexAppendCount >= _DefaultSessionStore.COMPACT_EVERY) {
|
|
1512
|
+
await this.compactIndex();
|
|
1513
|
+
this.indexAppendCount = 0;
|
|
1514
|
+
}
|
|
1515
|
+
} catch {
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
/** Append a tombstone entry for a deleted session. */
|
|
1519
|
+
async writeTombstone(id) {
|
|
1520
|
+
try {
|
|
1521
|
+
await ensureDir(this.dir);
|
|
1522
|
+
const line = JSON.stringify({ action: "delete", id }) + "\n";
|
|
1523
|
+
await fsp2.appendFile(this.indexFile, line, "utf8");
|
|
1524
|
+
this._indexCache = null;
|
|
1525
|
+
this.invalidateShardManifestBySessionId(id);
|
|
1526
|
+
this.indexAppendCount++;
|
|
1527
|
+
} catch {
|
|
1528
|
+
}
|
|
1502
1529
|
}
|
|
1503
1530
|
/**
|
|
1504
|
-
*
|
|
1505
|
-
*
|
|
1506
|
-
* append path used — one warning every 5s with a suppressed count.
|
|
1507
|
-
* On failure the buffer is cleared (events are best-effort, same as
|
|
1508
|
-
* the old per-event path where a failed write was silently dropped).
|
|
1531
|
+
* Compact the index: read all entries, drop tombstones, deduplicate
|
|
1532
|
+
* (keep latest per session), and rewrite. Atomic via temp+rename.
|
|
1509
1533
|
*/
|
|
1510
|
-
async
|
|
1511
|
-
if (this.writeBuffer.length === 0) return;
|
|
1512
|
-
const eventCount = this.writeBuffer.length;
|
|
1513
|
-
const batch = this.writeBuffer.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
1514
|
-
this.writeBuffer = [];
|
|
1534
|
+
async compactIndex() {
|
|
1515
1535
|
const t0 = Date.now();
|
|
1516
1536
|
let outcome = "success";
|
|
1517
1537
|
let errorMsg;
|
|
1518
1538
|
try {
|
|
1519
|
-
await this.
|
|
1539
|
+
const entries = await this.readIndex();
|
|
1540
|
+
if (entries.length === 0) return;
|
|
1541
|
+
const tmp = `${this.indexFile}.compact.tmp`;
|
|
1542
|
+
const lines = entries.map((s) => JSON.stringify(s)).join("\n") + "\n";
|
|
1543
|
+
await fsp2.writeFile(tmp, lines, "utf8");
|
|
1544
|
+
await fsp2.rename(tmp, this.indexFile);
|
|
1545
|
+
this._indexCache = null;
|
|
1520
1546
|
} catch (err) {
|
|
1521
1547
|
outcome = "failure";
|
|
1522
1548
|
errorMsg = toErrorMessage(err);
|
|
1523
|
-
this.appendFailCount += eventCount;
|
|
1524
|
-
const now = Date.now();
|
|
1525
|
-
if (now - this.lastAppendWarnAt > 5e3) {
|
|
1526
|
-
const suppressed = this.appendFailCount - 1;
|
|
1527
|
-
const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
|
|
1528
|
-
console.warn(
|
|
1529
|
-
"[session] flush failed:",
|
|
1530
|
-
toErrorMessage(err),
|
|
1531
|
-
tail
|
|
1532
|
-
);
|
|
1533
|
-
this.lastAppendWarnAt = now;
|
|
1534
|
-
this.appendFailCount = 0;
|
|
1535
|
-
}
|
|
1536
1549
|
} finally {
|
|
1537
|
-
this.
|
|
1538
|
-
sessionId: this.id,
|
|
1539
|
-
store: "session",
|
|
1540
|
-
filePath: this.filePath,
|
|
1541
|
-
operation: "flush",
|
|
1542
|
-
outcome,
|
|
1543
|
-
durationMs: Date.now() - t0,
|
|
1544
|
-
...errorMsg !== void 0 ? { error: errorMsg } : {},
|
|
1545
|
-
...eventCount !== void 0 ? { eventCount } : {},
|
|
1546
|
-
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
1547
|
-
});
|
|
1550
|
+
this.emitWrite("~compact~", this.indexFile, "compact", outcome, Date.now() - t0, void 0, errorMsg);
|
|
1548
1551
|
}
|
|
1549
1552
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1553
|
+
/**
|
|
1554
|
+
* Read the index file and return deduplicated session summaries.
|
|
1555
|
+
* Entries with a matching tombstone are filtered out.
|
|
1556
|
+
* Returns empty array when the index doesn't exist or is corrupt.
|
|
1557
|
+
*/
|
|
1558
|
+
async readIndex() {
|
|
1559
|
+
let stat10;
|
|
1560
|
+
try {
|
|
1561
|
+
const s = await fsp2.stat(this.indexFile);
|
|
1562
|
+
stat10 = { mtimeMs: s.mtimeMs, size: s.size };
|
|
1563
|
+
} catch {
|
|
1564
|
+
this._indexCache = null;
|
|
1565
|
+
return [];
|
|
1566
|
+
}
|
|
1567
|
+
if (this._indexCache !== null && this._indexCache.mtimeMs === stat10.mtimeMs && this._indexCache.size === stat10.size) {
|
|
1568
|
+
return [...this._indexCache.summaries];
|
|
1569
|
+
}
|
|
1570
|
+
let raw;
|
|
1571
|
+
try {
|
|
1572
|
+
raw = await fsp2.readFile(this.indexFile, "utf8");
|
|
1573
|
+
} catch {
|
|
1574
|
+
this._indexCache = null;
|
|
1575
|
+
return [];
|
|
1576
|
+
}
|
|
1577
|
+
const deleted = /* @__PURE__ */ new Set();
|
|
1578
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1579
|
+
for (const line of raw.split("\n")) {
|
|
1580
|
+
if (!line.trim()) continue;
|
|
1581
|
+
try {
|
|
1582
|
+
const entry = JSON.parse(line);
|
|
1583
|
+
if (entry.action === "delete" && entry.id) {
|
|
1584
|
+
deleted.add(entry.id);
|
|
1585
|
+
seen.delete(entry.id);
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
if (entry.id && !deleted.has(entry.id)) {
|
|
1589
|
+
seen.set(entry.id, entry);
|
|
1590
|
+
}
|
|
1591
|
+
} catch {
|
|
1554
1592
|
}
|
|
1555
1593
|
}
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1594
|
+
const summaries = Array.from(seen.values());
|
|
1595
|
+
this._indexCache = { ...stat10, summaries };
|
|
1596
|
+
return [...summaries];
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Rebuild the index from disk by scanning all sessions and writing a
|
|
1600
|
+
* fresh _index.jsonl. Useful after manual cleanup or index corruption.
|
|
1601
|
+
*/
|
|
1602
|
+
async rebuildIndex() {
|
|
1603
|
+
const ids = await this.collectSessionIds(this.dir);
|
|
1604
|
+
const summaries = await Promise.all(ids.map((id) => this.summaryFor(id).catch(() => null)));
|
|
1605
|
+
const valid = summaries.filter((s) => s !== null);
|
|
1606
|
+
const tmp = `${this.indexFile}.tmp`;
|
|
1607
|
+
const lines = valid.map((s) => JSON.stringify(s)).join("\n") + "\n";
|
|
1608
|
+
await fsp2.writeFile(tmp, lines, "utf8");
|
|
1609
|
+
await fsp2.rename(tmp, this.indexFile);
|
|
1610
|
+
this._indexCache = null;
|
|
1611
|
+
return valid.length;
|
|
1612
|
+
}
|
|
1613
|
+
async listFromDirectoryScan(limit) {
|
|
1614
|
+
const shardKeys = await this.collectShardKeys();
|
|
1615
|
+
const shardEntries = await mapWithConcurrency(
|
|
1616
|
+
shardKeys,
|
|
1617
|
+
_DefaultSessionStore.LIST_SCAN_CONCURRENCY,
|
|
1618
|
+
async (shardKey) => await this.readOrBuildShardManifest(shardKey)
|
|
1619
|
+
);
|
|
1620
|
+
const out = [];
|
|
1621
|
+
for (const entry of shardEntries) {
|
|
1622
|
+
for (const summary of entry.summaries) {
|
|
1623
|
+
out.push({ summary, needsBackfill: false });
|
|
1566
1624
|
}
|
|
1567
|
-
} else if (event.type === "file_snapshot") {
|
|
1568
|
-
this.fileChangeCount += event.files.length;
|
|
1569
|
-
} else if (event.type === "compaction") {
|
|
1570
|
-
this.compactionCount++;
|
|
1571
1625
|
}
|
|
1572
|
-
|
|
1573
|
-
|
|
1626
|
+
out.sort((a, b) => compareSessionSummaries(a.summary, b.summary));
|
|
1627
|
+
const selected = out.slice(0, limit);
|
|
1628
|
+
const summaries = await mapWithConcurrency(
|
|
1629
|
+
selected,
|
|
1630
|
+
Math.min(_DefaultSessionStore.LIST_SCAN_CONCURRENCY, Math.max(1, limit)),
|
|
1631
|
+
async (candidate) => candidate.summary
|
|
1632
|
+
);
|
|
1633
|
+
return summaries.filter((s) => s !== null);
|
|
1634
|
+
}
|
|
1635
|
+
async collectShardKeys() {
|
|
1636
|
+
let entries;
|
|
1637
|
+
try {
|
|
1638
|
+
entries = await fsp2.readdir(this.dir, { withFileTypes: true });
|
|
1639
|
+
} catch {
|
|
1640
|
+
return [""];
|
|
1574
1641
|
}
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
this.summary = { ...this.summary, tokenTotal: this.tokenIn + this.tokenOut };
|
|
1581
|
-
} else if (event.type === "session_end") {
|
|
1582
|
-
const total = event.usage.input + event.usage.output;
|
|
1583
|
-
if (total > 0) this.summary = { ...this.summary, tokenTotal: total };
|
|
1584
|
-
} else if (event.type === "in_flight_start") {
|
|
1585
|
-
this.iterationCount++;
|
|
1642
|
+
const shardKeys = [""];
|
|
1643
|
+
for (const entry of entries) {
|
|
1644
|
+
if (entry.name.startsWith(".") && entry.name !== ".wrongstack") continue;
|
|
1645
|
+
if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments") continue;
|
|
1646
|
+
if (entry.isDirectory()) shardKeys.push(entry.name);
|
|
1586
1647
|
}
|
|
1648
|
+
return shardKeys;
|
|
1587
1649
|
}
|
|
1588
|
-
async
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1650
|
+
async readOrBuildShardManifest(shardKey) {
|
|
1651
|
+
const cached = this.shardManifestCache.get(shardKey);
|
|
1652
|
+
if (cached) return cached;
|
|
1653
|
+
const manifestPath = this.shardManifestPath(shardKey);
|
|
1654
|
+
try {
|
|
1655
|
+
const raw = await fsp2.readFile(manifestPath, "utf8");
|
|
1656
|
+
const parsed = JSON.parse(raw);
|
|
1657
|
+
const entry2 = {
|
|
1658
|
+
summaries: Array.isArray(parsed.summaries) ? parsed.summaries : [],
|
|
1659
|
+
ids: Array.isArray(parsed.ids) ? parsed.ids : []
|
|
1660
|
+
};
|
|
1661
|
+
this.shardManifestCache.set(shardKey, entry2);
|
|
1662
|
+
return entry2;
|
|
1663
|
+
} catch {
|
|
1664
|
+
}
|
|
1665
|
+
const refs = await this.collectSessionFilesInShard(shardKey);
|
|
1666
|
+
const candidates = await mapWithConcurrency(
|
|
1667
|
+
refs,
|
|
1668
|
+
_DefaultSessionStore.LIST_SCAN_CONCURRENCY,
|
|
1669
|
+
async (ref) => {
|
|
1670
|
+
const manifest = await this.readSummaryManifest(ref.id);
|
|
1671
|
+
if (manifest) return { summary: manifest, needsBackfill: false };
|
|
1672
|
+
const summary = await this.summaryHeaderFor(ref);
|
|
1673
|
+
if (!summary) return null;
|
|
1674
|
+
const hydrated = await this.summaryFor(summary.id).catch(() => summary);
|
|
1675
|
+
return { summary: hydrated, needsBackfill: false };
|
|
1676
|
+
}
|
|
1677
|
+
);
|
|
1678
|
+
const summaries = candidates.filter((candidate) => candidate !== null).map((candidate) => candidate.summary);
|
|
1679
|
+
summaries.sort(compareSessionSummaries);
|
|
1680
|
+
const entry = { summaries, ids: summaries.map((summary) => summary.id) };
|
|
1681
|
+
this.shardManifestCache.set(shardKey, entry);
|
|
1682
|
+
await atomicWrite(manifestPath, JSON.stringify(entry), { mode: 384 }).catch(() => void 0);
|
|
1683
|
+
return entry;
|
|
1684
|
+
}
|
|
1685
|
+
async collectSessionFilesInShard(shardKey) {
|
|
1686
|
+
const dir = shardKey ? path2.join(this.dir, shardKey) : this.dir;
|
|
1687
|
+
const entries = await this.collectSessionFiles(dir, shardKey);
|
|
1688
|
+
return shardKey ? entries.filter((entry) => entry.id.startsWith(`${shardKey}/`)) : entries.filter((entry) => !entry.id.includes("/"));
|
|
1689
|
+
}
|
|
1690
|
+
async collectSessionFiles(dir, prefix = "", depth = 0) {
|
|
1691
|
+
let entries;
|
|
1692
|
+
try {
|
|
1693
|
+
entries = await fsp2.readdir(dir, { withFileTypes: true });
|
|
1694
|
+
} catch {
|
|
1695
|
+
return [];
|
|
1696
|
+
}
|
|
1697
|
+
const dirEntries = [];
|
|
1698
|
+
const files = [];
|
|
1699
|
+
for (const entry of entries) {
|
|
1700
|
+
if (entry.name.startsWith(".") && entry.name !== ".wrongstack") continue;
|
|
1701
|
+
if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments")
|
|
1702
|
+
continue;
|
|
1703
|
+
if (entry.isDirectory()) {
|
|
1704
|
+
dirEntries.push(entry);
|
|
1705
|
+
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
1706
|
+
if (entry.name === "_index.jsonl") continue;
|
|
1707
|
+
const base = entry.name.replace(/\.jsonl$/, "");
|
|
1708
|
+
const id = prefix ? `${prefix}/${base}` : base;
|
|
1709
|
+
files.push({ id, filePath: path2.join(dir, entry.name) });
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
const childFileArrays = await Promise.all(
|
|
1713
|
+
dirEntries.map((entry) => {
|
|
1714
|
+
const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
|
|
1715
|
+
return this.collectSessionFiles(path2.join(dir, entry.name), childPrefix, depth + 1);
|
|
1716
|
+
})
|
|
1717
|
+
);
|
|
1718
|
+
return [...childFileArrays.flat(), ...files];
|
|
1592
1719
|
}
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1720
|
+
/** Recursively collect session IDs from date-shard subdirectories.
|
|
1721
|
+
* IDs include the date-prefix path (e.g. "2026-06-06/17-46-57Z_…").
|
|
1722
|
+
* Skips `.jsonl`/`.summary.json` root files, dot-files, and
|
|
1723
|
+
* sub-directories that belong to fleet/subagent sessions. */
|
|
1724
|
+
async collectSessionIds(dir, prefix = "", depth = 0) {
|
|
1725
|
+
let entries;
|
|
1726
|
+
try {
|
|
1727
|
+
entries = await fsp2.readdir(dir, { withFileTypes: true });
|
|
1728
|
+
} catch {
|
|
1729
|
+
return [];
|
|
1598
1730
|
}
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
};
|
|
1612
|
-
if (this.manifestFile) {
|
|
1613
|
-
const t0 = Date.now();
|
|
1614
|
-
let outcome = "success";
|
|
1615
|
-
let errorMsg;
|
|
1616
|
-
try {
|
|
1617
|
-
await atomicWrite(this.manifestFile, JSON.stringify(this.summary), { mode: 384 });
|
|
1618
|
-
} catch (err) {
|
|
1619
|
-
outcome = "failure";
|
|
1620
|
-
errorMsg = toErrorMessage(err);
|
|
1621
|
-
} finally {
|
|
1622
|
-
this.events?.emit("storage.write", {
|
|
1623
|
-
sessionId: this.id,
|
|
1624
|
-
store: "session",
|
|
1625
|
-
filePath: this.manifestFile,
|
|
1626
|
-
operation: "close",
|
|
1627
|
-
outcome,
|
|
1628
|
-
durationMs: Date.now() - t0,
|
|
1629
|
-
...errorMsg !== void 0 ? { error: errorMsg } : {},
|
|
1630
|
-
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
1631
|
-
});
|
|
1731
|
+
const dirEntries = [];
|
|
1732
|
+
const fileIds = [];
|
|
1733
|
+
for (const entry of entries) {
|
|
1734
|
+
if (entry.name.startsWith(".") && entry.name !== ".wrongstack") continue;
|
|
1735
|
+
if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments")
|
|
1736
|
+
continue;
|
|
1737
|
+
if (entry.isDirectory()) {
|
|
1738
|
+
dirEntries.push(entry);
|
|
1739
|
+
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
1740
|
+
if (entry.name === "_index.jsonl") continue;
|
|
1741
|
+
const base = entry.name.replace(/\.jsonl$/, "");
|
|
1742
|
+
fileIds.push(prefix ? `${prefix}/${base}` : base);
|
|
1632
1743
|
}
|
|
1633
1744
|
}
|
|
1634
|
-
const
|
|
1635
|
-
|
|
1636
|
-
|
|
1745
|
+
const childIdArrays = await Promise.all(
|
|
1746
|
+
dirEntries.map((entry) => {
|
|
1747
|
+
const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
|
|
1748
|
+
return this.collectSessionIds(path2.join(dir, entry.name), childPrefix, depth + 1);
|
|
1749
|
+
})
|
|
1750
|
+
);
|
|
1751
|
+
return [...childIdArrays.flat(), ...fileIds];
|
|
1752
|
+
}
|
|
1753
|
+
async summaryFor(id) {
|
|
1754
|
+
const manifest = this.sessionPath(id, ".summary.json");
|
|
1755
|
+
const t0 = Date.now();
|
|
1756
|
+
let outcome = "success";
|
|
1757
|
+
let errorMsg;
|
|
1758
|
+
const fromManifest = await this.readSummaryManifest(id, t0);
|
|
1759
|
+
if (fromManifest) return fromManifest;
|
|
1637
1760
|
try {
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
1761
|
+
const full = this.sessionPath(id, ".jsonl");
|
|
1762
|
+
const stat10 = await fsp2.stat(full);
|
|
1763
|
+
const summary = await this.summarize(id, stat10.mtime.toISOString());
|
|
1764
|
+
await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
|
|
1765
|
+
const msg = toErrorMessage(err);
|
|
1766
|
+
this.emitError(id, manifest, "summary_fallback", msg, true);
|
|
1767
|
+
console.warn(JSON.stringify({
|
|
1768
|
+
level: "warn",
|
|
1769
|
+
event: "session_store.manifest_write_failed",
|
|
1770
|
+
sessionId: id,
|
|
1771
|
+
message: msg,
|
|
1772
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1773
|
+
}));
|
|
1652
1774
|
});
|
|
1775
|
+
outcome = "failure";
|
|
1776
|
+
errorMsg = "summary fallback \xE2\u20AC\u201D manifest rebuilt";
|
|
1777
|
+
this.emitRead(id, manifest, "summary", outcome, Date.now() - t0, errorMsg);
|
|
1778
|
+
return summary;
|
|
1779
|
+
} catch (err) {
|
|
1780
|
+
outcome = "failure";
|
|
1781
|
+
errorMsg = toErrorMessage(err);
|
|
1782
|
+
this.emitRead(id, manifest, "summary", outcome, Date.now() - t0, errorMsg);
|
|
1783
|
+
return {
|
|
1784
|
+
id,
|
|
1785
|
+
title: "(damaged)",
|
|
1786
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1787
|
+
model: "unknown",
|
|
1788
|
+
provider: "unknown",
|
|
1789
|
+
tokenTotal: 0
|
|
1790
|
+
};
|
|
1653
1791
|
}
|
|
1792
|
+
}
|
|
1793
|
+
async readSummaryManifest(id, startTime = Date.now()) {
|
|
1794
|
+
const manifest = this.sessionPath(id, ".summary.json");
|
|
1654
1795
|
try {
|
|
1655
|
-
await
|
|
1796
|
+
const raw = await fsp2.readFile(manifest, "utf8");
|
|
1797
|
+
this.emitRead(id, manifest, "summary", "success", Date.now() - startTime);
|
|
1798
|
+
return JSON.parse(raw);
|
|
1656
1799
|
} catch {
|
|
1800
|
+
return null;
|
|
1657
1801
|
}
|
|
1658
1802
|
}
|
|
1659
|
-
async
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
await
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
});
|
|
1677
|
-
}
|
|
1678
|
-
async writeFileSnapshot(promptIndex, files) {
|
|
1679
|
-
await this.append({
|
|
1680
|
-
type: "file_snapshot",
|
|
1681
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1682
|
-
promptIndex,
|
|
1683
|
-
files
|
|
1684
|
-
});
|
|
1685
|
-
}
|
|
1686
|
-
/**
|
|
1687
|
-
* Truncate the session file to the checkpoint with the given promptIndex,
|
|
1688
|
-
* removing all events that follow it. Uses a single-pass byte-offset scan
|
|
1689
|
-
* so post-checkpoint content is never read or parsed — O(1) memory instead
|
|
1690
|
-
* of O(N) JSON.parse calls over the full file.
|
|
1691
|
-
*/
|
|
1692
|
-
async truncateToCheckpoint(targetPromptIndex) {
|
|
1693
|
-
if (!this.filePath) return 0;
|
|
1694
|
-
if (this.flushTimer) {
|
|
1695
|
-
clearTimeout(this.flushTimer);
|
|
1696
|
-
this.flushTimer = null;
|
|
1803
|
+
async summaryHeaderFor(ref) {
|
|
1804
|
+
let mtime = (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1805
|
+
try {
|
|
1806
|
+
const stat10 = await fsp2.stat(ref.filePath);
|
|
1807
|
+
if (!stat10.isFile()) {
|
|
1808
|
+
return {
|
|
1809
|
+
id: ref.id,
|
|
1810
|
+
title: "(damaged)",
|
|
1811
|
+
startedAt: stat10.mtime.toISOString(),
|
|
1812
|
+
model: "unknown",
|
|
1813
|
+
provider: "unknown",
|
|
1814
|
+
tokenTotal: 0
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
mtime = stat10.mtime.toISOString();
|
|
1818
|
+
} catch {
|
|
1819
|
+
return null;
|
|
1697
1820
|
}
|
|
1698
|
-
await this.flushBuffer();
|
|
1699
|
-
await this.writeChain;
|
|
1700
|
-
const CHUNK_SIZE = 65536;
|
|
1701
|
-
let fd;
|
|
1702
|
-
let fileOffset = 0;
|
|
1703
|
-
let lineStartOffset = 0;
|
|
1704
|
-
let checkpointByteOffset = -1;
|
|
1705
|
-
let removedCount = 0;
|
|
1706
|
-
let targetCheckpointSeen = false;
|
|
1707
1821
|
try {
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
break;
|
|
1719
|
-
}
|
|
1720
|
-
if (checkpointByteOffset !== -1) {
|
|
1721
|
-
removedCount++;
|
|
1722
|
-
} else {
|
|
1723
|
-
const lineBytes = buf.subarray(chunkPos, idx);
|
|
1724
|
-
const line = new TextDecoder("utf-8", { fatal: false }).decode(lineBytes);
|
|
1725
|
-
if (line.trim()) {
|
|
1726
|
-
try {
|
|
1727
|
-
const event = JSON.parse(line);
|
|
1728
|
-
if (event.type === "checkpoint") {
|
|
1729
|
-
if (event.promptIndex === targetPromptIndex) {
|
|
1730
|
-
checkpointByteOffset = lineStartOffset;
|
|
1731
|
-
targetCheckpointSeen = true;
|
|
1732
|
-
} else if (event.promptIndex !== void 0 && event.promptIndex > targetPromptIndex) {
|
|
1733
|
-
checkpointByteOffset = lineStartOffset;
|
|
1734
|
-
}
|
|
1735
|
-
} else if (targetCheckpointSeen && event.promptIndex !== void 0 && event.promptIndex > targetPromptIndex) {
|
|
1736
|
-
removedCount++;
|
|
1737
|
-
} else if (targetCheckpointSeen && event.promptIndex === void 0) {
|
|
1738
|
-
removedCount++;
|
|
1739
|
-
} else if (!targetCheckpointSeen && event.promptIndex === void 0) {
|
|
1740
|
-
removedCount++;
|
|
1741
|
-
} else if (!targetCheckpointSeen && event.promptIndex !== void 0 && event.promptIndex > targetPromptIndex) {
|
|
1742
|
-
removedCount++;
|
|
1743
|
-
}
|
|
1744
|
-
} catch {
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
chunkPos = idx + 1;
|
|
1749
|
-
lineStartOffset = fileOffset + chunkPos;
|
|
1822
|
+
for await (const event of this.iterSessionEvents(ref.filePath)) {
|
|
1823
|
+
if (event.type === "session_start") {
|
|
1824
|
+
return {
|
|
1825
|
+
id: ref.id,
|
|
1826
|
+
title: "(empty session)",
|
|
1827
|
+
startedAt: event.ts,
|
|
1828
|
+
model: event.model ?? "unknown",
|
|
1829
|
+
provider: event.provider ?? "unknown",
|
|
1830
|
+
tokenTotal: 0
|
|
1831
|
+
};
|
|
1750
1832
|
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1833
|
+
}
|
|
1834
|
+
return {
|
|
1835
|
+
id: ref.id,
|
|
1836
|
+
title: "(empty session)",
|
|
1837
|
+
startedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1838
|
+
model: "unknown",
|
|
1839
|
+
provider: "unknown",
|
|
1840
|
+
tokenTotal: 0
|
|
1841
|
+
};
|
|
1842
|
+
} catch {
|
|
1843
|
+
return {
|
|
1844
|
+
id: ref.id,
|
|
1845
|
+
title: "(damaged)",
|
|
1846
|
+
startedAt: mtime,
|
|
1847
|
+
model: "unknown",
|
|
1848
|
+
provider: "unknown",
|
|
1849
|
+
tokenTotal: 0
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Delete a session and all associated files: JSONL, summary, plan/todos
|
|
1855
|
+
* sidecars, and the session directory (fleet.json, shared/, subagents/).
|
|
1856
|
+
*
|
|
1857
|
+
* Individual file deletions are best-effort (logged as structured warnings),
|
|
1858
|
+
* but a tombstone is always written so readIndex() filters this session out.
|
|
1859
|
+
* If the session directory itself can't be removed, the error is surfaced
|
|
1860
|
+
* to the caller so prune() can report it.
|
|
1861
|
+
*/
|
|
1862
|
+
async deleteSession(id) {
|
|
1863
|
+
const jsonlPath = this.sessionPath(id, ".jsonl");
|
|
1864
|
+
const summaryPath = this.sessionPath(id, ".summary.json");
|
|
1865
|
+
const shardDir = path2.dirname(path2.join(this.dir, id));
|
|
1866
|
+
const base = path2.basename(id);
|
|
1867
|
+
const sessDir = path2.join(shardDir, base);
|
|
1868
|
+
const deletions = [
|
|
1869
|
+
fsp2.unlink(jsonlPath),
|
|
1870
|
+
fsp2.unlink(summaryPath),
|
|
1871
|
+
fsp2.unlink(path2.join(shardDir, `${base}.plan.json`)),
|
|
1872
|
+
fsp2.unlink(path2.join(shardDir, `${base}.todos.json`))
|
|
1873
|
+
];
|
|
1874
|
+
const results = await Promise.allSettled(deletions);
|
|
1875
|
+
for (const r of results) {
|
|
1876
|
+
if (r.status === "rejected") {
|
|
1877
|
+
const msg = r.reason instanceof Error ? r.reason.message : String(r.reason);
|
|
1878
|
+
if (r.reason?.code !== "ENOENT") {
|
|
1879
|
+
console.warn(JSON.stringify({
|
|
1880
|
+
level: "warn",
|
|
1881
|
+
event: "session_store.delete_failed",
|
|
1882
|
+
sessionId: id,
|
|
1883
|
+
message: msg,
|
|
1884
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1885
|
+
}));
|
|
1754
1886
|
}
|
|
1755
1887
|
}
|
|
1756
|
-
} finally {
|
|
1757
|
-
await fd?.close();
|
|
1758
1888
|
}
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1889
|
+
await fsp2.rm(sessDir, { recursive: true, force: true }).catch((err) => {
|
|
1890
|
+
console.warn(JSON.stringify({
|
|
1891
|
+
level: "warn",
|
|
1892
|
+
event: "session_store.rmdir_failed",
|
|
1893
|
+
sessionId: id,
|
|
1894
|
+
message: toErrorMessage(err),
|
|
1895
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1896
|
+
}));
|
|
1897
|
+
});
|
|
1898
|
+
await this.writeTombstone(id);
|
|
1899
|
+
}
|
|
1900
|
+
async delete(id) {
|
|
1901
|
+
await this.deleteSession(id);
|
|
1902
|
+
}
|
|
1903
|
+
async prune(maxAgeDays = 30) {
|
|
1904
|
+
const cutoff = Date.now() - maxAgeDays * 864e5;
|
|
1905
|
+
let deleted = 0;
|
|
1906
|
+
let activeSessionId = null;
|
|
1764
1907
|
try {
|
|
1765
|
-
const
|
|
1766
|
-
const
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1908
|
+
const raw = await fsp2.readFile(path2.join(this.dir, "active.json"), "utf8");
|
|
1909
|
+
const active = JSON.parse(raw);
|
|
1910
|
+
activeSessionId = active.sessionId ?? null;
|
|
1911
|
+
} catch {
|
|
1912
|
+
}
|
|
1913
|
+
const isPrunableJsonl = (name) => name.endsWith(".jsonl") && name !== "_index.jsonl" && name !== "_mailbox.jsonl" && !name.endsWith(".replay.jsonl") && !name.endsWith(".audit.jsonl");
|
|
1914
|
+
const pruneFile = async (dir, name, prefix) => {
|
|
1915
|
+
const jsonlPath = path2.join(dir, name);
|
|
1916
|
+
try {
|
|
1917
|
+
const stat10 = await fsp2.stat(jsonlPath);
|
|
1918
|
+
if (stat10.mtimeMs >= cutoff) return;
|
|
1919
|
+
} catch {
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
const base = name.replace(/\.jsonl$/, "");
|
|
1923
|
+
const id = prefix ? `${prefix}/${base}` : base;
|
|
1924
|
+
if (activeSessionId && id === activeSessionId) return;
|
|
1925
|
+
await this.deleteSession(id);
|
|
1926
|
+
deleted++;
|
|
1927
|
+
};
|
|
1928
|
+
const entries = await fsp2.readdir(this.dir, { withFileTypes: true }).catch(() => []);
|
|
1929
|
+
for (const entry of entries) {
|
|
1930
|
+
if (entry.isFile()) {
|
|
1931
|
+
if (isPrunableJsonl(entry.name)) await pruneFile(this.dir, entry.name, "");
|
|
1932
|
+
continue;
|
|
1933
|
+
}
|
|
1934
|
+
if (!entry.isDirectory()) continue;
|
|
1935
|
+
const dateDir = path2.join(this.dir, entry.name);
|
|
1936
|
+
const files = await fsp2.readdir(dateDir, { withFileTypes: true }).catch(() => []);
|
|
1937
|
+
for (const file of files) {
|
|
1938
|
+
if (!file.isFile() || !isPrunableJsonl(file.name)) continue;
|
|
1939
|
+
await pruneFile(dateDir, file.name, entry.name);
|
|
1778
1940
|
}
|
|
1779
|
-
|
|
1941
|
+
}
|
|
1942
|
+
if (deleted > 0) {
|
|
1943
|
+
await this.compactIndex().catch(() => void 0);
|
|
1944
|
+
}
|
|
1945
|
+
for (const entry of entries) {
|
|
1946
|
+
if (!entry.isDirectory()) continue;
|
|
1947
|
+
const dateDir = path2.join(this.dir, entry.name);
|
|
1780
1948
|
try {
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
const copyBuf = Buffer.alloc(toCopy);
|
|
1785
|
-
const { bytesRead: r } = await src.read(copyBuf, 0, toCopy, readOffset);
|
|
1786
|
-
if (r === 0) break;
|
|
1787
|
-
await writeFd.write(copyBuf, 0, r);
|
|
1788
|
-
readOffset += r;
|
|
1789
|
-
}
|
|
1790
|
-
const raw = await fsp.readFile(this.filePath);
|
|
1791
|
-
const tail = raw.subarray(newlineAfterCheckpoint).toString("utf8");
|
|
1792
|
-
for (const line of tail.split("\n")) {
|
|
1793
|
-
if (!line.trim()) continue;
|
|
1794
|
-
try {
|
|
1795
|
-
JSON.parse(line);
|
|
1796
|
-
} catch {
|
|
1797
|
-
await writeFd.write(`${line}
|
|
1798
|
-
`, void 0, "utf8");
|
|
1799
|
-
}
|
|
1949
|
+
const remaining = await fsp2.readdir(dateDir);
|
|
1950
|
+
if (remaining.length === 0) {
|
|
1951
|
+
await fsp2.rmdir(dateDir).catch(() => void 0);
|
|
1800
1952
|
}
|
|
1801
|
-
}
|
|
1802
|
-
await writeFd.close();
|
|
1953
|
+
} catch {
|
|
1803
1954
|
}
|
|
1804
|
-
await src.close();
|
|
1805
|
-
await fsp.rename(tmpPath, this.filePath);
|
|
1806
|
-
this.handle = await fsp.open(this.filePath, "a", 384);
|
|
1807
|
-
} catch (err) {
|
|
1808
|
-
await fsp.unlink(tmpPath).catch(() => void 0);
|
|
1809
|
-
this.handle = await fsp.open(this.filePath, "a", 384).catch(() => this.handle);
|
|
1810
|
-
throw err;
|
|
1811
1955
|
}
|
|
1812
|
-
|
|
1813
|
-
type: "rewound",
|
|
1814
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1815
|
-
toPromptIndex: targetPromptIndex,
|
|
1816
|
-
revertedFiles: []
|
|
1817
|
-
});
|
|
1818
|
-
this.events?.emit("session.rewound", {
|
|
1819
|
-
toPromptIndex: targetPromptIndex,
|
|
1820
|
-
revertedFiles: [],
|
|
1821
|
-
removedEvents: removedCount
|
|
1822
|
-
});
|
|
1823
|
-
return removedCount;
|
|
1956
|
+
return deleted;
|
|
1824
1957
|
}
|
|
1825
|
-
async
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
this.flushTimer = null;
|
|
1830
|
-
}
|
|
1831
|
-
this.writeBuffer = [];
|
|
1832
|
-
await this.writeChain;
|
|
1958
|
+
async clearHistory(id) {
|
|
1959
|
+
await this.ensureShardDir(id);
|
|
1960
|
+
const file = this.sessionPath(id, ".jsonl");
|
|
1961
|
+
const meta = this.sessionPath(id, ".summary.json");
|
|
1833
1962
|
const record = `${JSON.stringify({
|
|
1834
1963
|
type: "session_start",
|
|
1835
1964
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1836
|
-
id
|
|
1837
|
-
model:
|
|
1838
|
-
provider:
|
|
1965
|
+
id,
|
|
1966
|
+
model: "unknown",
|
|
1967
|
+
provider: "unknown"
|
|
1839
1968
|
})}
|
|
1840
1969
|
`;
|
|
1841
|
-
await
|
|
1970
|
+
await fsp2.writeFile(file, record, "utf8");
|
|
1971
|
+
await fsp2.unlink(meta).catch(() => void 0);
|
|
1842
1972
|
}
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1973
|
+
async summarize(id, mtime) {
|
|
1974
|
+
try {
|
|
1975
|
+
const file = this.sessionPath(id, ".jsonl");
|
|
1976
|
+
let title = "(empty session)";
|
|
1977
|
+
let startedAt = (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1978
|
+
let endedAt;
|
|
1979
|
+
let model = "unknown";
|
|
1980
|
+
let provider = "unknown";
|
|
1981
|
+
let tokenIn = 0;
|
|
1982
|
+
let tokenOut = 0;
|
|
1983
|
+
let iterationCount = 0;
|
|
1984
|
+
let toolCallCount = 0;
|
|
1985
|
+
let toolErrorCount = 0;
|
|
1986
|
+
let fileChangeCount = 0;
|
|
1987
|
+
const toolBreakdown = {};
|
|
1988
|
+
let outcome;
|
|
1989
|
+
let lastEventType;
|
|
1990
|
+
let hasError = false;
|
|
1991
|
+
let sawStart = false;
|
|
1992
|
+
for await (const e of this.iterSessionEvents(file)) {
|
|
1993
|
+
lastEventType = e.type;
|
|
1994
|
+
if (e.type === "session_start") {
|
|
1995
|
+
if (!sawStart) {
|
|
1996
|
+
sawStart = true;
|
|
1997
|
+
startedAt = e.ts;
|
|
1998
|
+
model = e.model ?? "unknown";
|
|
1999
|
+
provider = e.provider ?? "unknown";
|
|
2000
|
+
}
|
|
2001
|
+
} else if (e.type === "session_end") {
|
|
2002
|
+
endedAt = e.ts;
|
|
2003
|
+
} else if (e.type === "user_input") {
|
|
2004
|
+
if (title === "(empty session)") title = userInputTitle(e.content);
|
|
2005
|
+
} else if (e.type === "llm_response") {
|
|
2006
|
+
tokenIn += e.usage.input ?? 0;
|
|
2007
|
+
tokenOut += e.usage.output ?? 0;
|
|
2008
|
+
} else if (e.type === "in_flight_start") iterationCount++;
|
|
2009
|
+
else if (e.type === "tool_call_start") {
|
|
2010
|
+
toolCallCount++;
|
|
2011
|
+
toolBreakdown[e.name] = (toolBreakdown[e.name] ?? 0) + 1;
|
|
2012
|
+
} else if (e.type === "tool_result" && e.isError) toolErrorCount++;
|
|
2013
|
+
else if (e.type === "file_snapshot") fileChangeCount += e.files.length;
|
|
2014
|
+
else if (e.type === "error" || e.type === "provider_error") hasError = true;
|
|
2015
|
+
}
|
|
2016
|
+
if (lastEventType === "session_end") {
|
|
2017
|
+
outcome = "completed";
|
|
2018
|
+
} else if (lastEventType === "in_flight_start") {
|
|
2019
|
+
outcome = "aborted";
|
|
2020
|
+
} else if (hasError) {
|
|
2021
|
+
outcome = "error";
|
|
2022
|
+
}
|
|
2023
|
+
return {
|
|
2024
|
+
id,
|
|
2025
|
+
title,
|
|
2026
|
+
startedAt,
|
|
2027
|
+
endedAt,
|
|
2028
|
+
model,
|
|
2029
|
+
provider,
|
|
2030
|
+
tokenTotal: tokenIn + tokenOut,
|
|
2031
|
+
iterationCount: iterationCount > 0 ? iterationCount : void 0,
|
|
2032
|
+
toolCallCount: toolCallCount > 0 ? toolCallCount : void 0,
|
|
2033
|
+
toolErrorCount: toolErrorCount > 0 ? toolErrorCount : void 0,
|
|
2034
|
+
fileChangeCount: fileChangeCount > 0 ? fileChangeCount : void 0,
|
|
2035
|
+
toolBreakdown: Object.keys(toolBreakdown).length > 0 ? toolBreakdown : {},
|
|
2036
|
+
outcome
|
|
2037
|
+
};
|
|
2038
|
+
} catch {
|
|
2039
|
+
return {
|
|
2040
|
+
id,
|
|
2041
|
+
title: "(damaged)",
|
|
2042
|
+
startedAt: mtime,
|
|
2043
|
+
model: "unknown",
|
|
2044
|
+
provider: "unknown",
|
|
2045
|
+
tokenTotal: 0
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
async *iterSessionEvents(file) {
|
|
2050
|
+
const stream = createReadStream(file, { encoding: "utf8" });
|
|
2051
|
+
const lines = createInterface({ input: stream, crlfDelay: Infinity });
|
|
2052
|
+
try {
|
|
2053
|
+
for await (const line of lines) {
|
|
2054
|
+
if (!line.trim()) continue;
|
|
2055
|
+
try {
|
|
2056
|
+
const parsed = JSON.parse(line);
|
|
2057
|
+
if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
|
|
2058
|
+
yield parsed;
|
|
2059
|
+
}
|
|
2060
|
+
} catch {
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
} finally {
|
|
2064
|
+
lines.close();
|
|
2065
|
+
stream.destroy();
|
|
1852
2066
|
}
|
|
1853
|
-
await this.append({
|
|
1854
|
-
type: "in_flight_start",
|
|
1855
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1856
|
-
context
|
|
1857
|
-
});
|
|
1858
|
-
this.events?.emit("in_flight.started", { context, ts: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1859
|
-
}
|
|
1860
|
-
/**
|
|
1861
|
-
* Idea #1 — close the in-flight marker. Idempotent in spirit
|
|
1862
|
-
* (you can call it after a successful iteration even if you
|
|
1863
|
-
* didn't open one this round) — but the session log records
|
|
1864
|
-
* every call so postmortem tooling can see "the agent finished
|
|
1865
|
-
* cleanly X times, then died without finishing Y".
|
|
1866
|
-
*/
|
|
1867
|
-
async clearInFlightMarker(reason) {
|
|
1868
|
-
await this.append({
|
|
1869
|
-
type: "in_flight_end",
|
|
1870
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1871
|
-
reason
|
|
1872
|
-
});
|
|
1873
|
-
this.events?.emit("in_flight.ended", { reason, ts: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1874
2067
|
}
|
|
1875
2068
|
};
|
|
1876
|
-
function
|
|
1877
|
-
const
|
|
1878
|
-
|
|
2069
|
+
function extractToolCallEnds(events) {
|
|
2070
|
+
const result = [];
|
|
2071
|
+
for (const e of events) {
|
|
2072
|
+
if (e.type === "tool_call_end") {
|
|
2073
|
+
result.push({
|
|
2074
|
+
name: e.name,
|
|
2075
|
+
id: e.id,
|
|
2076
|
+
durationMs: e.durationMs,
|
|
2077
|
+
ok: e.ok ?? false,
|
|
2078
|
+
outputBytes: e.outputBytes,
|
|
2079
|
+
outputTokens: e.outputTokens,
|
|
2080
|
+
outputLines: e.outputLines
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
return result;
|
|
1879
2085
|
}
|
|
1880
2086
|
function compareSessionSummaries(a, b) {
|
|
1881
2087
|
if (a.startedAt < b.startedAt) return 1;
|
|
1882
2088
|
if (a.startedAt > b.startedAt) return -1;
|
|
1883
2089
|
return a.id.localeCompare(b.id);
|
|
1884
2090
|
}
|
|
2091
|
+
function matchesSessionFilter(s, criteria) {
|
|
2092
|
+
if (criteria.since && s.startedAt < criteria.since) return false;
|
|
2093
|
+
if (criteria.until && s.startedAt > criteria.until) return false;
|
|
2094
|
+
if (criteria.provider && s.provider !== criteria.provider) return false;
|
|
2095
|
+
if (criteria.model && s.model !== criteria.model) return false;
|
|
2096
|
+
if (criteria.minTokens !== void 0 && s.tokenTotal < criteria.minTokens) return false;
|
|
2097
|
+
if (criteria.titleContains) {
|
|
2098
|
+
const needle = criteria.titleContains.toLowerCase();
|
|
2099
|
+
if (!s.title.toLowerCase().includes(needle)) return false;
|
|
2100
|
+
}
|
|
2101
|
+
return true;
|
|
2102
|
+
}
|
|
1885
2103
|
async function mapWithConcurrency(items, concurrency, fn) {
|
|
1886
2104
|
if (items.length === 0) return [];
|
|
1887
2105
|
const out = new Array(items.length);
|
|
@@ -1950,7 +2168,7 @@ var QueueStore = class {
|
|
|
1950
2168
|
const t0 = Date.now();
|
|
1951
2169
|
let raw;
|
|
1952
2170
|
try {
|
|
1953
|
-
raw = await
|
|
2171
|
+
raw = await fsp2.readFile(this.file, "utf8");
|
|
1954
2172
|
} catch (err) {
|
|
1955
2173
|
const code = err.code;
|
|
1956
2174
|
if (code === "ENOENT") {
|
|
@@ -2031,7 +2249,7 @@ var QueueStore = class {
|
|
|
2031
2249
|
async clear() {
|
|
2032
2250
|
const t0 = Date.now();
|
|
2033
2251
|
try {
|
|
2034
|
-
await
|
|
2252
|
+
await fsp2.unlink(this.file);
|
|
2035
2253
|
this.events?.emit("storage.write", {
|
|
2036
2254
|
sessionId: this.traceId ?? "~boot~",
|
|
2037
2255
|
store: "queue",
|
|
@@ -2088,7 +2306,7 @@ var DefaultAttachmentStore = class {
|
|
|
2088
2306
|
let spooledPath;
|
|
2089
2307
|
let data = input.data;
|
|
2090
2308
|
if (this.spoolDir && bytes >= this.spoolThreshold) {
|
|
2091
|
-
await
|
|
2309
|
+
await fsp2.mkdir(this.spoolDir, { recursive: true });
|
|
2092
2310
|
spooledPath = path2.join(this.spoolDir, `${id}.bin`);
|
|
2093
2311
|
await atomicWrite(spooledPath, input.data, {
|
|
2094
2312
|
encoding: input.kind === "image" ? "base64" : "utf8"
|
|
@@ -2151,7 +2369,7 @@ var DefaultAttachmentStore = class {
|
|
|
2151
2369
|
for (const att of this.items.values()) {
|
|
2152
2370
|
if (att.path) toDelete.push(att.path);
|
|
2153
2371
|
}
|
|
2154
|
-
await Promise.all(toDelete.map((p) =>
|
|
2372
|
+
await Promise.all(toDelete.map((p) => fsp2.unlink(p).catch(() => void 0)));
|
|
2155
2373
|
}
|
|
2156
2374
|
this.items.clear();
|
|
2157
2375
|
this.refs.length = 0;
|
|
@@ -2159,7 +2377,7 @@ var DefaultAttachmentStore = class {
|
|
|
2159
2377
|
}
|
|
2160
2378
|
async toBlock(att) {
|
|
2161
2379
|
if (att.kind === "image") {
|
|
2162
|
-
const data = att.data ?? (att.path ? await
|
|
2380
|
+
const data = att.data ?? (att.path ? await fsp2.readFile(att.path, { encoding: "base64" }) : "");
|
|
2163
2381
|
return {
|
|
2164
2382
|
type: "image",
|
|
2165
2383
|
source: {
|
|
@@ -2169,7 +2387,7 @@ var DefaultAttachmentStore = class {
|
|
|
2169
2387
|
}
|
|
2170
2388
|
};
|
|
2171
2389
|
}
|
|
2172
|
-
const raw = att.data ?? (att.path ? await
|
|
2390
|
+
const raw = att.data ?? (att.path ? await fsp2.readFile(att.path, "utf8") : "");
|
|
2173
2391
|
const label = att.meta.filename ? `<file path="${att.meta.filename}">` : "<pasted>";
|
|
2174
2392
|
const close = att.meta.filename ? "</file>" : "</pasted>";
|
|
2175
2393
|
return { type: "text", text: `${label}
|
|
@@ -2304,7 +2522,7 @@ var FileMemoryBackend = class {
|
|
|
2304
2522
|
await ensureDir(path2.dirname(file));
|
|
2305
2523
|
let existing = "";
|
|
2306
2524
|
try {
|
|
2307
|
-
existing = await
|
|
2525
|
+
existing = await fsp2.readFile(file, "utf8");
|
|
2308
2526
|
} catch {
|
|
2309
2527
|
}
|
|
2310
2528
|
const id = `mem_${Date.now()}_${randomUUID().slice(0, 8)}`;
|
|
@@ -2321,7 +2539,7 @@ ${line}`;
|
|
|
2321
2539
|
return withFileLock(file, async () => {
|
|
2322
2540
|
let existing;
|
|
2323
2541
|
try {
|
|
2324
|
-
existing = await
|
|
2542
|
+
existing = await fsp2.readFile(file, "utf8");
|
|
2325
2543
|
} catch {
|
|
2326
2544
|
return 0;
|
|
2327
2545
|
}
|
|
@@ -2357,7 +2575,7 @@ ${line}`;
|
|
|
2357
2575
|
async readAll(scope, filePath) {
|
|
2358
2576
|
const file = this.resolveFile(filePath, scope);
|
|
2359
2577
|
try {
|
|
2360
|
-
return await
|
|
2578
|
+
return await fsp2.readFile(file, "utf8");
|
|
2361
2579
|
} catch {
|
|
2362
2580
|
return "";
|
|
2363
2581
|
}
|
|
@@ -2392,7 +2610,7 @@ ${line}`;
|
|
|
2392
2610
|
const file = this.resolveFile(filePath, scope);
|
|
2393
2611
|
let existing;
|
|
2394
2612
|
try {
|
|
2395
|
-
existing = await
|
|
2613
|
+
existing = await fsp2.readFile(file, "utf8");
|
|
2396
2614
|
} catch {
|
|
2397
2615
|
return 0;
|
|
2398
2616
|
}
|
|
@@ -2412,7 +2630,7 @@ ${line}`;
|
|
|
2412
2630
|
const next = lines.join("\n");
|
|
2413
2631
|
const backup = `${file}.bak.${Date.now()}`;
|
|
2414
2632
|
try {
|
|
2415
|
-
await
|
|
2633
|
+
await fsp2.copyFile(file, backup);
|
|
2416
2634
|
await pruneConsolidateBackups(file);
|
|
2417
2635
|
} catch {
|
|
2418
2636
|
}
|
|
@@ -2428,11 +2646,11 @@ async function pruneConsolidateBackups(file) {
|
|
|
2428
2646
|
const dir = path2.dirname(file);
|
|
2429
2647
|
const base = path2.basename(file);
|
|
2430
2648
|
const prefix = `${base}.bak.`;
|
|
2431
|
-
const backups = (await
|
|
2649
|
+
const backups = (await fsp2.readdir(dir)).filter((name) => name.startsWith(prefix)).sort().reverse();
|
|
2432
2650
|
await Promise.all(
|
|
2433
2651
|
backups.slice(MAX_MEMORY_CONSOLIDATE_BACKUPS).map(async (name) => {
|
|
2434
2652
|
try {
|
|
2435
|
-
await
|
|
2653
|
+
await fsp2.unlink(path2.join(dir, name));
|
|
2436
2654
|
} catch {
|
|
2437
2655
|
}
|
|
2438
2656
|
})
|
|
@@ -2987,9 +3205,9 @@ ${body.trim()}`);
|
|
|
2987
3205
|
if (!this.persistBackup || scope === "project-agents") return;
|
|
2988
3206
|
try {
|
|
2989
3207
|
const content = await this.backend.readAll(scope, this.files[scope]);
|
|
2990
|
-
const { writeFile:
|
|
3208
|
+
const { writeFile: writeFile8, mkdir: mkdir7 } = await import('fs/promises');
|
|
2991
3209
|
await mkdir7(this.backupDir, { recursive: true });
|
|
2992
|
-
await
|
|
3210
|
+
await writeFile8(`${this.backupDir}/${scope}.md`, content, "utf8");
|
|
2993
3211
|
} catch {
|
|
2994
3212
|
}
|
|
2995
3213
|
}
|
|
@@ -3004,7 +3222,7 @@ function labelOf(scope) {
|
|
|
3004
3222
|
return "User memory";
|
|
3005
3223
|
}
|
|
3006
3224
|
}
|
|
3007
|
-
var GraphMemoryBackend = class {
|
|
3225
|
+
var GraphMemoryBackend = class _GraphMemoryBackend {
|
|
3008
3226
|
kind = "graph";
|
|
3009
3227
|
file;
|
|
3010
3228
|
graphFile;
|
|
@@ -3013,6 +3231,14 @@ var GraphMemoryBackend = class {
|
|
|
3013
3231
|
edges = [];
|
|
3014
3232
|
loadedScope = null;
|
|
3015
3233
|
loaded = false;
|
|
3234
|
+
/**
|
|
3235
|
+
* Inverted index for O(1) term → node lookup during search.
|
|
3236
|
+
* Built incrementally on remember/forget, rebuilt on loadGraph if absent.
|
|
3237
|
+
* Maps lowercase term (min 3 chars) → Set of node IDs containing that term.
|
|
3238
|
+
*/
|
|
3239
|
+
invertedIndex = /* @__PURE__ */ new Map();
|
|
3240
|
+
/** Minimum term length to index — avoids noise from 1-2 char fragments. */
|
|
3241
|
+
static MIN_TERM_LEN = 3;
|
|
3016
3242
|
/**
|
|
3017
3243
|
* Promise that resolves when the current in-flight _saveGraph completes.
|
|
3018
3244
|
* Tests call flush() to await this before deleting the backend or its temp dir.
|
|
@@ -3035,6 +3261,7 @@ var GraphMemoryBackend = class {
|
|
|
3035
3261
|
existing.type = entry.type;
|
|
3036
3262
|
existing.tags = entry.tags;
|
|
3037
3263
|
existing.priority = entry.priority;
|
|
3264
|
+
this.updateNodeIndex(nodeId, entry);
|
|
3038
3265
|
} else {
|
|
3039
3266
|
this.nodes.set(nodeId, {
|
|
3040
3267
|
id: nodeId,
|
|
@@ -3045,6 +3272,7 @@ var GraphMemoryBackend = class {
|
|
|
3045
3272
|
tags: entry.tags,
|
|
3046
3273
|
priority: entry.priority
|
|
3047
3274
|
});
|
|
3275
|
+
this.indexNode(nodeId, entry);
|
|
3048
3276
|
const recentNodes = [...this.nodes.values()].slice(-100);
|
|
3049
3277
|
for (const other of recentNodes) {
|
|
3050
3278
|
if (other.id === nodeId) continue;
|
|
@@ -3076,7 +3304,10 @@ var GraphMemoryBackend = class {
|
|
|
3076
3304
|
toRemove.push(id);
|
|
3077
3305
|
}
|
|
3078
3306
|
}
|
|
3079
|
-
for (const id of toRemove)
|
|
3307
|
+
for (const id of toRemove) {
|
|
3308
|
+
this.removeNodeFromIndex(id, this.nodes.get(id)?.entry);
|
|
3309
|
+
this.nodes.delete(id);
|
|
3310
|
+
}
|
|
3080
3311
|
this.edges = this.edges.filter((e) => !toRemove.includes(e.from) && !toRemove.includes(e.to));
|
|
3081
3312
|
this._saveDone = this._saveGraph(scope);
|
|
3082
3313
|
await this._saveDone;
|
|
@@ -3114,20 +3345,36 @@ var GraphMemoryBackend = class {
|
|
|
3114
3345
|
}
|
|
3115
3346
|
async search(scope, query, _filePath, limit) {
|
|
3116
3347
|
await this.loadGraph(scope);
|
|
3117
|
-
const needle = query.toLowerCase().split(/\s+/);
|
|
3118
|
-
const
|
|
3119
|
-
for (const
|
|
3348
|
+
const needle = query.toLowerCase().split(/\s+/).filter((t) => t.length >= _GraphMemoryBackend.MIN_TERM_LEN);
|
|
3349
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
3350
|
+
for (const term of needle) {
|
|
3351
|
+
const nodeIds = this.invertedIndex.get(term);
|
|
3352
|
+
if (!nodeIds) continue;
|
|
3353
|
+
for (const nodeId of nodeIds) {
|
|
3354
|
+
const node = this.nodes.get(nodeId);
|
|
3355
|
+
if (!node || node.entry.scope !== scope) continue;
|
|
3356
|
+
candidates.set(nodeId, (candidates.get(nodeId) ?? 0) + 1);
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
for (const [nodeId, node] of this.nodes) {
|
|
3120
3360
|
if (node.entry.scope !== scope) continue;
|
|
3121
|
-
|
|
3361
|
+
if (candidates.has(nodeId)) continue;
|
|
3362
|
+
if (node.priority === "critical" || node.priority === "high") {
|
|
3363
|
+
candidates.set(nodeId, 0);
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
const scored = [];
|
|
3367
|
+
for (const [nodeId] of candidates) {
|
|
3368
|
+
const node = this.nodes.get(nodeId);
|
|
3122
3369
|
let score = 0;
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
if (node.entry.tags?.some((t) => t.toLowerCase().includes(
|
|
3370
|
+
score += (candidates.get(nodeId) ?? 0) * 1;
|
|
3371
|
+
for (const term of needle) {
|
|
3372
|
+
if (node.entry.tags?.some((t) => t.toLowerCase().includes(term))) score += 2;
|
|
3126
3373
|
}
|
|
3127
3374
|
if (node.priority === "critical") score += 3;
|
|
3128
3375
|
else if (node.priority === "high") score += 2;
|
|
3129
3376
|
score += node.count * 0.5;
|
|
3130
|
-
|
|
3377
|
+
scored.push({ entry: node.entry, score });
|
|
3131
3378
|
}
|
|
3132
3379
|
scored.sort((a, b) => b.score - a.score);
|
|
3133
3380
|
const matched = scored.map((s) => s.entry);
|
|
@@ -3137,10 +3384,11 @@ var GraphMemoryBackend = class {
|
|
|
3137
3384
|
await this.file.clear(scope, filePath);
|
|
3138
3385
|
this.nodes.clear();
|
|
3139
3386
|
this.edges = [];
|
|
3387
|
+
this.invertedIndex = /* @__PURE__ */ new Map();
|
|
3140
3388
|
this.loadedScope = scope;
|
|
3141
3389
|
this.loaded = true;
|
|
3142
3390
|
try {
|
|
3143
|
-
await
|
|
3391
|
+
await fsp2.unlink(this.graphFile);
|
|
3144
3392
|
} catch {
|
|
3145
3393
|
}
|
|
3146
3394
|
}
|
|
@@ -3180,7 +3428,7 @@ var GraphMemoryBackend = class {
|
|
|
3180
3428
|
async loadGraph(scope) {
|
|
3181
3429
|
if (this.loaded && this.loadedScope === scope) return;
|
|
3182
3430
|
try {
|
|
3183
|
-
const raw = await
|
|
3431
|
+
const raw = await fsp2.readFile(this.graphFile, "utf8");
|
|
3184
3432
|
const data = JSON.parse(raw);
|
|
3185
3433
|
this.nodes = new Map(data.nodes);
|
|
3186
3434
|
this.edges = data.edges;
|
|
@@ -3188,6 +3436,7 @@ var GraphMemoryBackend = class {
|
|
|
3188
3436
|
this.nodes = /* @__PURE__ */ new Map();
|
|
3189
3437
|
this.edges = [];
|
|
3190
3438
|
}
|
|
3439
|
+
this.buildInvertedIndex();
|
|
3191
3440
|
this.loadedScope = scope;
|
|
3192
3441
|
this.loaded = true;
|
|
3193
3442
|
}
|
|
@@ -3201,10 +3450,10 @@ var GraphMemoryBackend = class {
|
|
|
3201
3450
|
edges: this.edges
|
|
3202
3451
|
};
|
|
3203
3452
|
const dir = this.graphFile.substring(0, this.graphFile.lastIndexOf("/"));
|
|
3204
|
-
await
|
|
3453
|
+
await fsp2.mkdir(dir, { recursive: true });
|
|
3205
3454
|
const tmp = `${this.graphFile}.tmp`;
|
|
3206
|
-
await
|
|
3207
|
-
await
|
|
3455
|
+
await fsp2.writeFile(tmp, JSON.stringify(data));
|
|
3456
|
+
await fsp2.rename(tmp, this.graphFile);
|
|
3208
3457
|
} catch {
|
|
3209
3458
|
}
|
|
3210
3459
|
}
|
|
@@ -3215,6 +3464,86 @@ var GraphMemoryBackend = class {
|
|
|
3215
3464
|
async flush() {
|
|
3216
3465
|
await this._saveDone;
|
|
3217
3466
|
}
|
|
3467
|
+
// ── Inverted Index Helpers ───────────────────────────────────────────
|
|
3468
|
+
/**
|
|
3469
|
+
* Build the inverted index from all currently loaded nodes.
|
|
3470
|
+
* Called on loadGraph() to reconstruct the index after loading from disk.
|
|
3471
|
+
*/
|
|
3472
|
+
buildInvertedIndex() {
|
|
3473
|
+
this.invertedIndex = /* @__PURE__ */ new Map();
|
|
3474
|
+
for (const [nodeId, node] of this.nodes) {
|
|
3475
|
+
for (const term of this.extractTerms(node.entry)) {
|
|
3476
|
+
let nodeIds = this.invertedIndex.get(term);
|
|
3477
|
+
if (!nodeIds) {
|
|
3478
|
+
nodeIds = /* @__PURE__ */ new Set();
|
|
3479
|
+
this.invertedIndex.set(term, nodeIds);
|
|
3480
|
+
}
|
|
3481
|
+
nodeIds.add(nodeId);
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
/**
|
|
3486
|
+
* Index a node in the inverted index. Adds the node's ID to all term entries.
|
|
3487
|
+
*/
|
|
3488
|
+
indexNode(nodeId, entry) {
|
|
3489
|
+
for (const term of this.extractTerms(entry)) {
|
|
3490
|
+
let nodeIds = this.invertedIndex.get(term);
|
|
3491
|
+
if (!nodeIds) {
|
|
3492
|
+
nodeIds = /* @__PURE__ */ new Set();
|
|
3493
|
+
this.invertedIndex.set(term, nodeIds);
|
|
3494
|
+
}
|
|
3495
|
+
nodeIds.add(nodeId);
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
/**
|
|
3499
|
+
* Remove a node's terms from the inverted index before deleting it.
|
|
3500
|
+
* Used in forget() and when updating existing nodes.
|
|
3501
|
+
*/
|
|
3502
|
+
removeNodeFromIndex(nodeId, entry) {
|
|
3503
|
+
if (!entry) return;
|
|
3504
|
+
for (const term of this.extractTerms(entry)) {
|
|
3505
|
+
const nodeIds = this.invertedIndex.get(term);
|
|
3506
|
+
if (nodeIds) {
|
|
3507
|
+
nodeIds.delete(nodeId);
|
|
3508
|
+
if (nodeIds.size === 0) {
|
|
3509
|
+
this.invertedIndex.delete(term);
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
/**
|
|
3515
|
+
* Update an existing node's entries in the inverted index.
|
|
3516
|
+
* Removes old terms and adds new ones.
|
|
3517
|
+
*/
|
|
3518
|
+
updateNodeIndex(nodeId, entry) {
|
|
3519
|
+
const existing = this.nodes.get(nodeId);
|
|
3520
|
+
if (existing) {
|
|
3521
|
+
this.removeNodeFromIndex(nodeId, existing.entry);
|
|
3522
|
+
}
|
|
3523
|
+
this.indexNode(nodeId, entry);
|
|
3524
|
+
}
|
|
3525
|
+
/**
|
|
3526
|
+
* Extract searchable terms from a memory entry.
|
|
3527
|
+
* Returns lowercase words from text (min length) and full tag strings.
|
|
3528
|
+
*/
|
|
3529
|
+
extractTerms(entry) {
|
|
3530
|
+
const terms = [];
|
|
3531
|
+
const words = entry.text.toLowerCase().split(/\s+/);
|
|
3532
|
+
for (const word of words) {
|
|
3533
|
+
if (word.length >= _GraphMemoryBackend.MIN_TERM_LEN) {
|
|
3534
|
+
terms.push(word);
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
if (entry.tags) {
|
|
3538
|
+
for (const tag of entry.tags) {
|
|
3539
|
+
const lower = tag.toLowerCase();
|
|
3540
|
+
if (lower.length >= _GraphMemoryBackend.MIN_TERM_LEN && !terms.includes(lower)) {
|
|
3541
|
+
terms.push(lower);
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
return terms;
|
|
3546
|
+
}
|
|
3218
3547
|
};
|
|
3219
3548
|
function wordOverlap(a, b) {
|
|
3220
3549
|
const wordsA = new Set(a.toLowerCase().split(/\s+/).filter((w) => w.length > 2));
|
|
@@ -3660,6 +3989,9 @@ function isContextWindowModeId(id) {
|
|
|
3660
3989
|
return CONTEXT_WINDOW_MODES.some((m) => m.id === id);
|
|
3661
3990
|
}
|
|
3662
3991
|
|
|
3992
|
+
// src/types/config.ts
|
|
3993
|
+
var DEFAULT_TUI_THINKING_WORD = "thinking";
|
|
3994
|
+
|
|
3663
3995
|
// src/types/default-config.ts
|
|
3664
3996
|
var DEFAULT_TOOLS_CONFIG = Object.freeze({
|
|
3665
3997
|
defaultExecutionStrategy: "smart",
|
|
@@ -3678,6 +4010,10 @@ var DEFAULT_CONTEXT_CONFIG = Object.freeze({
|
|
|
3678
4010
|
var DEFAULT_AUTONOMY_CONFIG = Object.freeze({
|
|
3679
4011
|
autoProceedDelayMs: 45e3
|
|
3680
4012
|
});
|
|
4013
|
+
var DEFAULT_CIRCUIT_BREAKER_CONFIG = Object.freeze({
|
|
4014
|
+
enabled: false,
|
|
4015
|
+
autoKillResetMs: 6e4
|
|
4016
|
+
});
|
|
3681
4017
|
var DEFAULT_SESSION_LOGGING_CONFIG = Object.freeze({
|
|
3682
4018
|
auditLevel: "standard",
|
|
3683
4019
|
sampling: {
|
|
@@ -3704,7 +4040,8 @@ var BEHAVIOR_DEFAULTS = {
|
|
|
3704
4040
|
hardThreshold: 0.9,
|
|
3705
4041
|
autoCompact: true,
|
|
3706
4042
|
preserveK: DEFAULT_CONTEXT_CONFIG.preserveK,
|
|
3707
|
-
eliseThreshold: DEFAULT_CONTEXT_CONFIG.eliseThreshold
|
|
4043
|
+
eliseThreshold: DEFAULT_CONTEXT_CONFIG.eliseThreshold,
|
|
4044
|
+
strategy: "hybrid"
|
|
3708
4045
|
},
|
|
3709
4046
|
tools: {
|
|
3710
4047
|
defaultExecutionStrategy: DEFAULT_TOOLS_CONFIG.defaultExecutionStrategy,
|
|
@@ -3727,6 +4064,13 @@ var BEHAVIOR_DEFAULTS = {
|
|
|
3727
4064
|
allowOutsideProjectRoot: true
|
|
3728
4065
|
},
|
|
3729
4066
|
mcpServers: {},
|
|
4067
|
+
fallbackAuto: true,
|
|
4068
|
+
maxConcurrent: 4,
|
|
4069
|
+
yolo: false,
|
|
4070
|
+
nextPrediction: false,
|
|
4071
|
+
hints: true,
|
|
4072
|
+
debugStream: false,
|
|
4073
|
+
configScope: "global",
|
|
3730
4074
|
indexing: {
|
|
3731
4075
|
onSessionStart: true,
|
|
3732
4076
|
onEdit: true,
|
|
@@ -3734,8 +4078,60 @@ var BEHAVIOR_DEFAULTS = {
|
|
|
3734
4078
|
debounceMs: 400
|
|
3735
4079
|
},
|
|
3736
4080
|
session: { ...DEFAULT_SESSION_LOGGING_CONFIG },
|
|
3737
|
-
autonomy: {
|
|
4081
|
+
autonomy: {
|
|
4082
|
+
defaultMode: "off",
|
|
4083
|
+
autoProceedDelayMs: DEFAULT_AUTONOMY_CONFIG.autoProceedDelayMs,
|
|
4084
|
+
autoProceedMaxIterations: 50,
|
|
4085
|
+
autonomyNextPrompt: "auto {{suggestion}}",
|
|
4086
|
+
terminalTitleAnimation: true,
|
|
4087
|
+
yolo: false,
|
|
4088
|
+
streamFleet: true,
|
|
4089
|
+
chime: false,
|
|
4090
|
+
confirmExit: true,
|
|
4091
|
+
mouseMode: false,
|
|
4092
|
+
enhance: true,
|
|
4093
|
+
enhanceDelayMs: 6e4,
|
|
4094
|
+
enhanceLanguage: "original",
|
|
4095
|
+
statuslineMode: "detailed",
|
|
4096
|
+
thinkingWord: DEFAULT_TUI_THINKING_WORD
|
|
4097
|
+
},
|
|
4098
|
+
circuitBreaker: { ...DEFAULT_CIRCUIT_BREAKER_CONFIG },
|
|
4099
|
+
modelRuntime: {
|
|
4100
|
+
// `effort` is intentionally undefined by default. Leaving it unset lets
|
|
4101
|
+
// each model use its provider-recommended reasoning effort (or none at
|
|
4102
|
+
// all) instead of forcing an opinionated value that may be unsupported,
|
|
4103
|
+
// silently omitted, and surfaced as a per-request warning. Users who
|
|
4104
|
+
// want a specific effort can opt in via `/settings` or the WebUI panel.
|
|
4105
|
+
reasoning: { mode: "auto" },
|
|
4106
|
+
cache: {}
|
|
4107
|
+
}
|
|
3738
4108
|
};
|
|
4109
|
+
function isPlainRecord(value) {
|
|
4110
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4111
|
+
}
|
|
4112
|
+
function cloneJsonValue(value) {
|
|
4113
|
+
return structuredClone(value);
|
|
4114
|
+
}
|
|
4115
|
+
function fillMissingDefaults(target, defaults) {
|
|
4116
|
+
const value = cloneJsonValue(target);
|
|
4117
|
+
const changed = fillMissingDefaultsInPlace(value, defaults);
|
|
4118
|
+
return { value, changed };
|
|
4119
|
+
}
|
|
4120
|
+
function fillMissingDefaultsInPlace(target, defaults) {
|
|
4121
|
+
let changed = false;
|
|
4122
|
+
for (const [key, defaultValue] of Object.entries(defaults)) {
|
|
4123
|
+
if (!Object.prototype.hasOwnProperty.call(target, key)) {
|
|
4124
|
+
target[key] = cloneJsonValue(defaultValue);
|
|
4125
|
+
changed = true;
|
|
4126
|
+
continue;
|
|
4127
|
+
}
|
|
4128
|
+
const current = target[key];
|
|
4129
|
+
if (isPlainRecord(current) && isPlainRecord(defaultValue)) {
|
|
4130
|
+
changed = fillMissingDefaultsInPlace(current, defaultValue) || changed;
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
return changed;
|
|
4134
|
+
}
|
|
3739
4135
|
function envBool(v) {
|
|
3740
4136
|
return !/^(0|false|no|off)$/i.test(v.trim());
|
|
3741
4137
|
}
|
|
@@ -3787,27 +4183,139 @@ var defaultIndexing = {
|
|
|
3787
4183
|
watchExternal: true,
|
|
3788
4184
|
debounceMs: 400
|
|
3789
4185
|
};
|
|
3790
|
-
var
|
|
4186
|
+
var IN_PROJECT_ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
4187
|
+
"version",
|
|
4188
|
+
"model",
|
|
4189
|
+
"cwd",
|
|
4190
|
+
"context",
|
|
4191
|
+
"tools",
|
|
4192
|
+
"features",
|
|
4193
|
+
"autonomy",
|
|
4194
|
+
"indexing",
|
|
4195
|
+
"session",
|
|
4196
|
+
"log",
|
|
4197
|
+
"launch",
|
|
4198
|
+
"nextPrediction",
|
|
4199
|
+
"hints",
|
|
4200
|
+
"debugStream",
|
|
4201
|
+
"configScope",
|
|
4202
|
+
"maxConcurrent",
|
|
4203
|
+
"fallbackModels",
|
|
4204
|
+
"fallbackAuto",
|
|
4205
|
+
"models",
|
|
4206
|
+
"modelMatrix",
|
|
4207
|
+
"circuitBreaker",
|
|
4208
|
+
"adaptiveConcurrency",
|
|
4209
|
+
"modelRuntime"
|
|
4210
|
+
]);
|
|
4211
|
+
var KNOWN_DENIED_IN_PROJECT = [
|
|
4212
|
+
{ key: "provider", reason: "Provider id override; can intercept prompts/responses." },
|
|
4213
|
+
{ key: "apiKey", reason: "Overrides user API key; exfiltrates prompts." },
|
|
4214
|
+
{ key: "baseUrl", reason: "Redirects provider endpoint; leaks real API key." },
|
|
4215
|
+
{ key: "providers", reason: "Per-provider apiKey/baseUrl/oauthConfig; same redirect/exfil." },
|
|
4216
|
+
{ key: "mcpServers", reason: "Arbitrary command/args/env spawned at boot (RCE)." },
|
|
4217
|
+
{ key: "hooks", reason: "Shell command arrays on lifecycle events (RCE)." },
|
|
4218
|
+
{ key: "plugins", reason: "Dynamic npm package load at boot (RCE)." },
|
|
4219
|
+
{ key: "sync", reason: "Carries githubToken credential and target repo." },
|
|
4220
|
+
{ key: "yolo", reason: "Disables all permission confirmation prompts." },
|
|
4221
|
+
{ key: "extensions", reason: "Per-plugin config can carry command/credential fields." },
|
|
4222
|
+
{ key: "hq", reason: "Carries HQ client token credential and endpoint URL." }
|
|
4223
|
+
];
|
|
4224
|
+
var KNOWN_CONFIG_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
4225
|
+
"version",
|
|
3791
4226
|
"provider",
|
|
4227
|
+
"model",
|
|
3792
4228
|
"apiKey",
|
|
3793
4229
|
"baseUrl",
|
|
4230
|
+
"maxConcurrent",
|
|
3794
4231
|
"providers",
|
|
4232
|
+
"models",
|
|
4233
|
+
"modelMatrix",
|
|
4234
|
+
"context",
|
|
4235
|
+
"tools",
|
|
3795
4236
|
"mcpServers",
|
|
4237
|
+
"fallbackModels",
|
|
4238
|
+
"fallbackAuto",
|
|
3796
4239
|
"hooks",
|
|
3797
4240
|
"plugins",
|
|
3798
|
-
"
|
|
4241
|
+
"log",
|
|
4242
|
+
"features",
|
|
3799
4243
|
"yolo",
|
|
4244
|
+
"nextPrediction",
|
|
4245
|
+
"cwd",
|
|
4246
|
+
"autonomy",
|
|
4247
|
+
"hints",
|
|
4248
|
+
"debugStream",
|
|
4249
|
+
"configScope",
|
|
4250
|
+
"indexing",
|
|
4251
|
+
"circuitBreaker",
|
|
4252
|
+
"adaptiveConcurrency",
|
|
4253
|
+
"launch",
|
|
4254
|
+
"session",
|
|
4255
|
+
"modelRuntime",
|
|
4256
|
+
"hq",
|
|
4257
|
+
"sync",
|
|
3800
4258
|
"extensions"
|
|
3801
4259
|
]);
|
|
4260
|
+
function assertInProjectAllowListComplete() {
|
|
4261
|
+
const missingFromBoth = [];
|
|
4262
|
+
for (const key of KNOWN_CONFIG_TOP_LEVEL_KEYS) {
|
|
4263
|
+
if (IN_PROJECT_ALLOWED_KEYS.has(key)) continue;
|
|
4264
|
+
const denied = KNOWN_DENIED_IN_PROJECT.find((d) => d.key === key);
|
|
4265
|
+
if (!denied) missingFromBoth.push(key);
|
|
4266
|
+
}
|
|
4267
|
+
const staleDenials = KNOWN_DENIED_IN_PROJECT.filter((d) => !KNOWN_CONFIG_TOP_LEVEL_KEYS.has(d.key)).map((d) => d.key);
|
|
4268
|
+
const duplicate = KNOWN_DENIED_IN_PROJECT.filter((d) => IN_PROJECT_ALLOWED_KEYS.has(d.key)).map((d) => d.key);
|
|
4269
|
+
const problems = [];
|
|
4270
|
+
if (missingFromBoth.length > 0) {
|
|
4271
|
+
problems.push(
|
|
4272
|
+
`new Config field(s) not classified as allowed or denied for in-project config: ` + missingFromBoth.join(", ") + ". Add each to IN_PROJECT_ALLOWED_KEYS (if safe) or KNOWN_DENIED_IN_PROJECT (with a reason)."
|
|
4273
|
+
);
|
|
4274
|
+
}
|
|
4275
|
+
if (staleDenials.length > 0) {
|
|
4276
|
+
problems.push(
|
|
4277
|
+
`KNOWN_DENIED_IN_PROJECT references keys that no longer exist on Config: ` + staleDenials.join(", ") + ". Remove them or restore the field on Config."
|
|
4278
|
+
);
|
|
4279
|
+
}
|
|
4280
|
+
if (duplicate.length > 0) {
|
|
4281
|
+
problems.push(
|
|
4282
|
+
`field(s) appear in BOTH IN_PROJECT_ALLOWED_KEYS and KNOWN_DENIED_IN_PROJECT: ` + duplicate.join(", ") + ". The allow-list wins at runtime; remove from one of the two."
|
|
4283
|
+
);
|
|
4284
|
+
}
|
|
4285
|
+
if (problems.length > 0) {
|
|
4286
|
+
throw new Error(
|
|
4287
|
+
`stripUnsafeInProjectFields drift check failed:
|
|
4288
|
+
- ${problems.join("\n - ")}`
|
|
4289
|
+
);
|
|
4290
|
+
}
|
|
4291
|
+
}
|
|
4292
|
+
var driftChecked = false;
|
|
3802
4293
|
function stripUnsafeInProjectFields(inProject, sourcePath, warn = (msg) => console.warn(msg)) {
|
|
4294
|
+
if (!driftChecked) {
|
|
4295
|
+
assertInProjectAllowListComplete();
|
|
4296
|
+
driftChecked = true;
|
|
4297
|
+
}
|
|
3803
4298
|
const stripped = [];
|
|
3804
4299
|
const out = {};
|
|
3805
4300
|
for (const [k, v] of Object.entries(inProject)) {
|
|
3806
|
-
if (
|
|
3807
|
-
|
|
4301
|
+
if (IN_PROJECT_ALLOWED_KEYS.has(k)) {
|
|
4302
|
+
out[k] = v;
|
|
3808
4303
|
continue;
|
|
3809
4304
|
}
|
|
3810
|
-
|
|
4305
|
+
stripped.push(k);
|
|
4306
|
+
}
|
|
4307
|
+
const outTools = out["tools"];
|
|
4308
|
+
if (outTools && typeof outTools === "object") {
|
|
4309
|
+
const execCfg = outTools["exec"];
|
|
4310
|
+
if (execCfg && typeof execCfg === "object" && "allow" in execCfg) {
|
|
4311
|
+
const clonedExec = { ...execCfg };
|
|
4312
|
+
delete clonedExec["allow"];
|
|
4313
|
+
out["tools"] = {
|
|
4314
|
+
...outTools,
|
|
4315
|
+
exec: clonedExec
|
|
4316
|
+
};
|
|
4317
|
+
stripped.push("tools.exec.allow");
|
|
4318
|
+
}
|
|
3811
4319
|
}
|
|
3812
4320
|
if (stripped.length > 0) {
|
|
3813
4321
|
warn(
|
|
@@ -3816,7 +4324,7 @@ function stripUnsafeInProjectFields(inProject, sourcePath, warn = (msg) => conso
|
|
|
3816
4324
|
event: "config.in_project_unsafe_fields_ignored",
|
|
3817
4325
|
path: sourcePath,
|
|
3818
4326
|
ignoredKeys: stripped,
|
|
3819
|
-
message: `Ignored ${stripped.length}
|
|
4327
|
+
message: `Ignored ${stripped.length} field(s) from the repo-committed config "${sourcePath}": ${stripped.join(", ")}. Only a small allow-list of benign preferences (model, context, tools limits, features, \u2026) may be set by <project>/.wrongstack/config.json. Everything else must live in your personal ~/.wrongstack/config.json.`,
|
|
3820
4328
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3821
4329
|
})
|
|
3822
4330
|
);
|
|
@@ -3850,6 +4358,7 @@ var DefaultConfigLoader = class {
|
|
|
3850
4358
|
extraSources;
|
|
3851
4359
|
events;
|
|
3852
4360
|
traceId;
|
|
4361
|
+
jsonCache = /* @__PURE__ */ new Map();
|
|
3853
4362
|
constructor(opts) {
|
|
3854
4363
|
this.paths = opts.paths;
|
|
3855
4364
|
this.strict = opts.strict ?? false;
|
|
@@ -3860,6 +4369,7 @@ var DefaultConfigLoader = class {
|
|
|
3860
4369
|
}
|
|
3861
4370
|
async load(opts = {}) {
|
|
3862
4371
|
let cfg = { ...BEHAVIOR_DEFAULTS };
|
|
4372
|
+
await this.ensureGlobalDefaults();
|
|
3863
4373
|
const inProjectCollides = samePath(this.paths.inProjectConfig, this.paths.globalConfig) || samePath(this.paths.inProjectConfig, this.paths.projectLocalConfig);
|
|
3864
4374
|
const [global, local, inProject] = await Promise.all([
|
|
3865
4375
|
this.readJson(this.paths.globalConfig),
|
|
@@ -3924,6 +4434,80 @@ var DefaultConfigLoader = class {
|
|
|
3924
4434
|
}
|
|
3925
4435
|
return Object.freeze(cfg);
|
|
3926
4436
|
}
|
|
4437
|
+
async ensureGlobalDefaults() {
|
|
4438
|
+
const fp = this.paths.globalConfig;
|
|
4439
|
+
const t0 = Date.now();
|
|
4440
|
+
try {
|
|
4441
|
+
await withFileLock(fp, async () => {
|
|
4442
|
+
let parsed;
|
|
4443
|
+
try {
|
|
4444
|
+
const raw = await fsp2.readFile(fp, "utf8");
|
|
4445
|
+
const result = safeParse(raw);
|
|
4446
|
+
if (!result.ok || !isPlainRecord(result.value)) {
|
|
4447
|
+
return;
|
|
4448
|
+
}
|
|
4449
|
+
parsed = result.value;
|
|
4450
|
+
} catch (err) {
|
|
4451
|
+
if (err.code !== "ENOENT") {
|
|
4452
|
+
this.events?.emit("storage.error", {
|
|
4453
|
+
sessionId: "~config~",
|
|
4454
|
+
store: "config",
|
|
4455
|
+
filePath: fp,
|
|
4456
|
+
operation: "ensure_defaults",
|
|
4457
|
+
outcome: "failure",
|
|
4458
|
+
error: storageErrorString(err),
|
|
4459
|
+
recoverable: false,
|
|
4460
|
+
durationMs: Date.now() - t0,
|
|
4461
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
4462
|
+
});
|
|
4463
|
+
console.warn(JSON.stringify({
|
|
4464
|
+
level: "warn",
|
|
4465
|
+
event: "config.defaults_read_failed",
|
|
4466
|
+
path: fp,
|
|
4467
|
+
message: toErrorMessage(err),
|
|
4468
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4469
|
+
}));
|
|
4470
|
+
return;
|
|
4471
|
+
}
|
|
4472
|
+
parsed = {};
|
|
4473
|
+
}
|
|
4474
|
+
const { value, changed } = fillMissingDefaults(
|
|
4475
|
+
parsed,
|
|
4476
|
+
BEHAVIOR_DEFAULTS
|
|
4477
|
+
);
|
|
4478
|
+
if (!changed) return;
|
|
4479
|
+
await atomicWrite(fp, JSON.stringify(value, null, 2), { mode: 384 });
|
|
4480
|
+
this.events?.emit("storage.write", {
|
|
4481
|
+
sessionId: "~config~",
|
|
4482
|
+
store: "config",
|
|
4483
|
+
filePath: fp,
|
|
4484
|
+
operation: "ensure_defaults",
|
|
4485
|
+
outcome: "success",
|
|
4486
|
+
durationMs: Date.now() - t0,
|
|
4487
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
4488
|
+
});
|
|
4489
|
+
});
|
|
4490
|
+
} catch (err) {
|
|
4491
|
+
this.events?.emit("storage.error", {
|
|
4492
|
+
sessionId: "~config~",
|
|
4493
|
+
store: "config",
|
|
4494
|
+
filePath: fp,
|
|
4495
|
+
operation: "ensure_defaults",
|
|
4496
|
+
outcome: "failure",
|
|
4497
|
+
error: storageErrorString(err),
|
|
4498
|
+
recoverable: false,
|
|
4499
|
+
durationMs: Date.now() - t0,
|
|
4500
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
4501
|
+
});
|
|
4502
|
+
console.warn(JSON.stringify({
|
|
4503
|
+
level: "warn",
|
|
4504
|
+
event: "config.defaults_write_failed",
|
|
4505
|
+
path: fp,
|
|
4506
|
+
message: toErrorMessage(err),
|
|
4507
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4508
|
+
}));
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
3927
4511
|
/**
|
|
3928
4512
|
* Persist a sync config to ~/.wrongstack/sync.json, with the token encrypted
|
|
3929
4513
|
* by the vault (if provided). The file is isolated from the main config
|
|
@@ -3972,7 +4556,7 @@ var DefaultConfigLoader = class {
|
|
|
3972
4556
|
const fp = this.paths.syncConfig;
|
|
3973
4557
|
const t0 = Date.now();
|
|
3974
4558
|
try {
|
|
3975
|
-
const raw = await
|
|
4559
|
+
const raw = await fsp2.readFile(fp, "utf8");
|
|
3976
4560
|
const parsed = safeParse(raw);
|
|
3977
4561
|
if (!parsed.ok || !parsed.value) {
|
|
3978
4562
|
this.events?.emit("storage.read", {
|
|
@@ -4033,10 +4617,42 @@ var DefaultConfigLoader = class {
|
|
|
4033
4617
|
}
|
|
4034
4618
|
}
|
|
4035
4619
|
async readJson(file) {
|
|
4036
|
-
let raw;
|
|
4037
4620
|
const t0 = Date.now();
|
|
4621
|
+
let mtimeMs = null;
|
|
4622
|
+
try {
|
|
4623
|
+
const stat10 = await fsp2.stat(file);
|
|
4624
|
+
mtimeMs = stat10.mtimeMs;
|
|
4625
|
+
const cached = this.jsonCache.get(file);
|
|
4626
|
+
if (cached && cached.mtimeMs === mtimeMs) {
|
|
4627
|
+
return structuredClone(cached.value);
|
|
4628
|
+
}
|
|
4629
|
+
} catch (err) {
|
|
4630
|
+
if (err.code === "ENOENT") {
|
|
4631
|
+
this.jsonCache.set(file, { mtimeMs: null, value: {} });
|
|
4632
|
+
return {};
|
|
4633
|
+
}
|
|
4634
|
+
this.events?.emit("storage.read", {
|
|
4635
|
+
sessionId: "~config~",
|
|
4636
|
+
store: "config",
|
|
4637
|
+
filePath: file,
|
|
4638
|
+
operation: "read_json",
|
|
4639
|
+
outcome: "failure",
|
|
4640
|
+
durationMs: Date.now() - t0,
|
|
4641
|
+
error: storageErrorString(err),
|
|
4642
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
4643
|
+
});
|
|
4644
|
+
console.warn(JSON.stringify({
|
|
4645
|
+
level: "warn",
|
|
4646
|
+
event: "config.read_failed",
|
|
4647
|
+
path: file,
|
|
4648
|
+
message: toErrorMessage(err),
|
|
4649
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4650
|
+
}));
|
|
4651
|
+
return {};
|
|
4652
|
+
}
|
|
4653
|
+
let raw;
|
|
4038
4654
|
try {
|
|
4039
|
-
raw = await
|
|
4655
|
+
raw = await fsp2.readFile(file, "utf8");
|
|
4040
4656
|
} catch (err) {
|
|
4041
4657
|
if (err.code !== "ENOENT") {
|
|
4042
4658
|
this.events?.emit("storage.read", {
|
|
@@ -4057,6 +4673,7 @@ var DefaultConfigLoader = class {
|
|
|
4057
4673
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4058
4674
|
}));
|
|
4059
4675
|
}
|
|
4676
|
+
this.jsonCache.set(file, { mtimeMs: null, value: {} });
|
|
4060
4677
|
return {};
|
|
4061
4678
|
}
|
|
4062
4679
|
const parsed = safeParse(raw);
|
|
@@ -4080,6 +4697,7 @@ var DefaultConfigLoader = class {
|
|
|
4080
4697
|
}));
|
|
4081
4698
|
return {};
|
|
4082
4699
|
}
|
|
4700
|
+
this.jsonCache.set(file, { mtimeMs, value: structuredClone(parsed.value) });
|
|
4083
4701
|
return parsed.value;
|
|
4084
4702
|
}
|
|
4085
4703
|
validateBehavior(cfg) {
|
|
@@ -4274,7 +4892,7 @@ var RecoveryLock = class {
|
|
|
4274
4892
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4275
4893
|
};
|
|
4276
4894
|
try {
|
|
4277
|
-
await
|
|
4895
|
+
await fsp2.writeFile(this.file, JSON.stringify(lock), { flag: "wx", mode: 384 });
|
|
4278
4896
|
} catch (err) {
|
|
4279
4897
|
const code = err.code;
|
|
4280
4898
|
if (code === "EEXIST") {
|
|
@@ -4290,7 +4908,7 @@ var RecoveryLock = class {
|
|
|
4290
4908
|
*/
|
|
4291
4909
|
async clear() {
|
|
4292
4910
|
try {
|
|
4293
|
-
await
|
|
4911
|
+
await fsp2.unlink(this.file);
|
|
4294
4912
|
} catch (err) {
|
|
4295
4913
|
const code = err.code;
|
|
4296
4914
|
if (code === "ENOENT") return;
|
|
@@ -4300,7 +4918,7 @@ var RecoveryLock = class {
|
|
|
4300
4918
|
async readLock() {
|
|
4301
4919
|
let raw;
|
|
4302
4920
|
try {
|
|
4303
|
-
raw = await
|
|
4921
|
+
raw = await fsp2.readFile(this.file, "utf8");
|
|
4304
4922
|
} catch (err) {
|
|
4305
4923
|
const code = err.code;
|
|
4306
4924
|
if (code === "ENOENT") return null;
|
|
@@ -4331,26 +4949,82 @@ function defaultIsPidAlive(pid) {
|
|
|
4331
4949
|
return false;
|
|
4332
4950
|
}
|
|
4333
4951
|
}
|
|
4334
|
-
|
|
4335
|
-
// src/storage/session-reader.ts
|
|
4336
|
-
var DefaultSessionReader = class {
|
|
4952
|
+
var DefaultSessionReader = class _DefaultSessionReader {
|
|
4337
4953
|
store;
|
|
4954
|
+
eventCache = /* @__PURE__ */ new Map();
|
|
4955
|
+
eventCacheMtimes = /* @__PURE__ */ new Map();
|
|
4956
|
+
static EVENT_CACHE_MAX_ENTRIES = 32;
|
|
4338
4957
|
constructor(opts) {
|
|
4339
4958
|
this.store = opts.store;
|
|
4340
4959
|
}
|
|
4960
|
+
async loadCachedSessionData(sessionId) {
|
|
4961
|
+
const storeWithPath = this.store;
|
|
4962
|
+
const rootDir = storeWithPath.dir;
|
|
4963
|
+
if (!rootDir) {
|
|
4964
|
+
return await this.store.load(sessionId);
|
|
4965
|
+
}
|
|
4966
|
+
const sessionPath = path2.join(rootDir, `${sessionId}.jsonl`);
|
|
4967
|
+
let mtimeMs = null;
|
|
4968
|
+
try {
|
|
4969
|
+
const stat10 = await fsp2.stat(sessionPath);
|
|
4970
|
+
mtimeMs = stat10.mtimeMs;
|
|
4971
|
+
} catch {
|
|
4972
|
+
this.eventCache.delete(sessionId);
|
|
4973
|
+
this.eventCacheMtimes.delete(sessionId);
|
|
4974
|
+
return await this.store.load(sessionId);
|
|
4975
|
+
}
|
|
4976
|
+
const cachedMtime = this.eventCacheMtimes.get(sessionId);
|
|
4977
|
+
const cachedData = this.eventCache.get(sessionId);
|
|
4978
|
+
if (cachedData && cachedMtime === mtimeMs) {
|
|
4979
|
+
this.eventCache.delete(sessionId);
|
|
4980
|
+
this.eventCacheMtimes.delete(sessionId);
|
|
4981
|
+
this.eventCache.set(sessionId, cachedData);
|
|
4982
|
+
this.eventCacheMtimes.set(sessionId, mtimeMs);
|
|
4983
|
+
return cachedData;
|
|
4984
|
+
}
|
|
4985
|
+
const data = await this.store.load(sessionId);
|
|
4986
|
+
this.eventCache.delete(sessionId);
|
|
4987
|
+
this.eventCacheMtimes.delete(sessionId);
|
|
4988
|
+
this.eventCache.set(sessionId, data);
|
|
4989
|
+
this.eventCacheMtimes.set(sessionId, mtimeMs);
|
|
4990
|
+
while (this.eventCache.size > _DefaultSessionReader.EVENT_CACHE_MAX_ENTRIES) {
|
|
4991
|
+
const oldest = this.eventCache.keys().next().value;
|
|
4992
|
+
if (oldest === void 0) break;
|
|
4993
|
+
this.eventCache.delete(oldest);
|
|
4994
|
+
this.eventCacheMtimes.delete(oldest);
|
|
4995
|
+
}
|
|
4996
|
+
if (data.metadata.endedAt) {
|
|
4997
|
+
storeWithPath.clearLoadCache?.(sessionId);
|
|
4998
|
+
}
|
|
4999
|
+
return data;
|
|
5000
|
+
}
|
|
4341
5001
|
async query(q = {}) {
|
|
4342
|
-
const
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
5002
|
+
const storeWithFilter = this.store;
|
|
5003
|
+
let raw;
|
|
5004
|
+
if (typeof storeWithFilter.listFiltered === "function") {
|
|
5005
|
+
raw = await storeWithFilter.listFiltered({
|
|
5006
|
+
since: q.since,
|
|
5007
|
+
until: q.until,
|
|
5008
|
+
provider: q.provider,
|
|
5009
|
+
model: q.model,
|
|
5010
|
+
minTokens: q.minTokens,
|
|
5011
|
+
titleContains: q.titleContains,
|
|
5012
|
+
limit: q.limit
|
|
5013
|
+
});
|
|
5014
|
+
} else {
|
|
5015
|
+
const fetched = await this.store.list(q.limit ? Math.max(q.limit, 100) : 1e3);
|
|
5016
|
+
const titleNeedle = q.titleContains?.toLowerCase();
|
|
5017
|
+
raw = fetched.filter((s) => {
|
|
5018
|
+
if (q.since && s.startedAt < q.since) return false;
|
|
5019
|
+
if (q.until && s.startedAt > q.until) return false;
|
|
5020
|
+
if (q.provider && s.provider !== q.provider) return false;
|
|
5021
|
+
if (q.model && s.model !== q.model) return false;
|
|
5022
|
+
if (q.minTokens !== void 0 && s.tokenTotal < q.minTokens) return false;
|
|
5023
|
+
if (titleNeedle && !s.title.toLowerCase().includes(titleNeedle)) return false;
|
|
5024
|
+
return true;
|
|
5025
|
+
});
|
|
5026
|
+
}
|
|
5027
|
+
const out = raw.map((s) => ({
|
|
4354
5028
|
id: s.id,
|
|
4355
5029
|
title: s.title,
|
|
4356
5030
|
startedAt: s.startedAt,
|
|
@@ -4361,7 +5035,7 @@ var DefaultSessionReader = class {
|
|
|
4361
5035
|
return q.limit ? out.slice(0, q.limit) : out;
|
|
4362
5036
|
}
|
|
4363
5037
|
async *replay(sessionId) {
|
|
4364
|
-
const data = await this.
|
|
5038
|
+
const data = await this.loadCachedSessionData(sessionId);
|
|
4365
5039
|
for (const e of data.events) yield e;
|
|
4366
5040
|
}
|
|
4367
5041
|
async search(q, sessionId, sessionQuery) {
|
|
@@ -4372,24 +5046,66 @@ var DefaultSessionReader = class {
|
|
|
4372
5046
|
if (sessionId) {
|
|
4373
5047
|
ids = [sessionId];
|
|
4374
5048
|
} else {
|
|
4375
|
-
const
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
5049
|
+
const storeWithFilter = this.store;
|
|
5050
|
+
let sessions;
|
|
5051
|
+
if (typeof storeWithFilter.listFiltered === "function") {
|
|
5052
|
+
sessions = await storeWithFilter.listFiltered({
|
|
5053
|
+
since: sessionQuery?.since,
|
|
5054
|
+
until: sessionQuery?.until,
|
|
5055
|
+
provider: sessionQuery?.provider,
|
|
5056
|
+
model: sessionQuery?.model,
|
|
5057
|
+
minTokens: sessionQuery?.minTokens,
|
|
5058
|
+
titleContains: sessionQuery?.titleContains,
|
|
5059
|
+
limit: 1e3
|
|
5060
|
+
});
|
|
5061
|
+
} else {
|
|
5062
|
+
sessions = await this.store.list(1e3);
|
|
5063
|
+
const titleNeedle = sessionQuery?.titleContains?.toLowerCase();
|
|
5064
|
+
sessions = sessions.filter((s) => {
|
|
5065
|
+
if (sessionQuery?.since && s.startedAt < sessionQuery.since) return false;
|
|
5066
|
+
if (sessionQuery?.until && s.startedAt > sessionQuery.until) return false;
|
|
5067
|
+
if (sessionQuery?.provider && s.provider !== sessionQuery.provider) return false;
|
|
5068
|
+
if (sessionQuery?.model && s.model !== sessionQuery.model) return false;
|
|
5069
|
+
if (sessionQuery?.minTokens !== void 0 && s.tokenTotal < sessionQuery.minTokens) return false;
|
|
5070
|
+
if (titleNeedle && !s.title.toLowerCase().includes(titleNeedle)) return false;
|
|
5071
|
+
return true;
|
|
5072
|
+
});
|
|
5073
|
+
}
|
|
5074
|
+
ids = sessions.map((s) => s.id);
|
|
4387
5075
|
}
|
|
4388
5076
|
const hits = [];
|
|
5077
|
+
const streaming = this.store.searchEvents?.bind(this.store);
|
|
5078
|
+
if (streaming) {
|
|
5079
|
+
for (const id of ids) {
|
|
5080
|
+
const matched = await streaming(
|
|
5081
|
+
id,
|
|
5082
|
+
(ev) => {
|
|
5083
|
+
if (allowedTypes && !allowedTypes.has(ev.type)) return false;
|
|
5084
|
+
const text = eventText(ev);
|
|
5085
|
+
if (text === null) return false;
|
|
5086
|
+
return matcher(text) !== null;
|
|
5087
|
+
},
|
|
5088
|
+
{ limit: limit - hits.length }
|
|
5089
|
+
);
|
|
5090
|
+
for (const m of matched) {
|
|
5091
|
+
const text = expectDefined(eventText(m.event));
|
|
5092
|
+
const hit = expectDefined(matcher(text));
|
|
5093
|
+
hits.push({
|
|
5094
|
+
sessionId: id,
|
|
5095
|
+
eventIndex: m.eventIndex,
|
|
5096
|
+
ts: m.ts,
|
|
5097
|
+
type: m.event.type,
|
|
5098
|
+
snippet: snippetOf(text, hit.start, hit.end)
|
|
5099
|
+
});
|
|
5100
|
+
if (hits.length >= limit) return hits;
|
|
5101
|
+
}
|
|
5102
|
+
}
|
|
5103
|
+
return hits;
|
|
5104
|
+
}
|
|
4389
5105
|
for (const id of ids) {
|
|
4390
5106
|
let data;
|
|
4391
5107
|
try {
|
|
4392
|
-
data = await this.
|
|
5108
|
+
data = await this.loadCachedSessionData(id);
|
|
4393
5109
|
} catch {
|
|
4394
5110
|
continue;
|
|
4395
5111
|
}
|
|
@@ -4413,7 +5129,7 @@ var DefaultSessionReader = class {
|
|
|
4413
5129
|
return hits;
|
|
4414
5130
|
}
|
|
4415
5131
|
async export(sessionId, opts) {
|
|
4416
|
-
const data = await this.
|
|
5132
|
+
const data = await this.loadCachedSessionData(sessionId);
|
|
4417
5133
|
const includeTools = opts.includeTools ?? true;
|
|
4418
5134
|
const includeDiagnostics = opts.includeDiagnostics ?? true;
|
|
4419
5135
|
const filtered = data.events.filter((e) => {
|
|
@@ -4434,7 +5150,7 @@ var DefaultSessionReader = class {
|
|
|
4434
5150
|
return renderMarkdown(data.metadata, filtered);
|
|
4435
5151
|
}
|
|
4436
5152
|
async metadata(sessionId) {
|
|
4437
|
-
const data = await this.
|
|
5153
|
+
const data = await this.loadCachedSessionData(sessionId);
|
|
4438
5154
|
return data.metadata;
|
|
4439
5155
|
}
|
|
4440
5156
|
};
|
|
@@ -4852,7 +5568,7 @@ var AnnotationsStore = class {
|
|
|
4852
5568
|
const fp = this.filePath(sessionId);
|
|
4853
5569
|
let raw;
|
|
4854
5570
|
try {
|
|
4855
|
-
raw = await
|
|
5571
|
+
raw = await fsp2.readFile(fp, "utf8");
|
|
4856
5572
|
} catch (err) {
|
|
4857
5573
|
if (err.code === "ENOENT") return null;
|
|
4858
5574
|
throw err;
|
|
@@ -4933,7 +5649,7 @@ var ReplayLogStore = class {
|
|
|
4933
5649
|
events;
|
|
4934
5650
|
traceId;
|
|
4935
5651
|
writeChains = /* @__PURE__ */ new Map();
|
|
4936
|
-
/** Per-session hash →
|
|
5652
|
+
/** Per-session hash → on-disk location, with lazy entry hydration. */
|
|
4937
5653
|
cache = /* @__PURE__ */ new Map();
|
|
4938
5654
|
/** Per-session entry count on disk, to detect when compaction is needed. */
|
|
4939
5655
|
diskCount = /* @__PURE__ */ new Map();
|
|
@@ -4968,8 +5684,16 @@ var ReplayLogStore = class {
|
|
|
4968
5684
|
const currentCount = this.diskCount.get(input.sessionId) ?? 0;
|
|
4969
5685
|
const willEvict = currentCount + 1 > this.maxEntries;
|
|
4970
5686
|
if (!willEvict) {
|
|
4971
|
-
|
|
4972
|
-
|
|
5687
|
+
const line = JSON.stringify(entry) + "\n";
|
|
5688
|
+
let offset2 = 0;
|
|
5689
|
+
try {
|
|
5690
|
+
const stat10 = await fsp2.stat(fp);
|
|
5691
|
+
offset2 = stat10.size;
|
|
5692
|
+
} catch (err) {
|
|
5693
|
+
if (err.code !== "ENOENT") throw err;
|
|
5694
|
+
}
|
|
5695
|
+
await fsp2.appendFile(fp, line, "utf8");
|
|
5696
|
+
cache.set(hash, { entry, offset: offset2, length: Buffer.byteLength(line, "utf8") });
|
|
4973
5697
|
this.diskCount.set(input.sessionId, currentCount + 1);
|
|
4974
5698
|
this.events?.emit("storage.write", {
|
|
4975
5699
|
sessionId: input.sessionId,
|
|
@@ -4986,7 +5710,12 @@ var ReplayLogStore = class {
|
|
|
4986
5710
|
all.push(entry);
|
|
4987
5711
|
const keep = all.slice(-this.maxEntries);
|
|
4988
5712
|
const refreshed = /* @__PURE__ */ new Map();
|
|
4989
|
-
|
|
5713
|
+
let offset = 0;
|
|
5714
|
+
for (const e of keep) {
|
|
5715
|
+
const line = JSON.stringify(e) + "\n";
|
|
5716
|
+
refreshed.set(e.hash, { entry: e, offset, length: Buffer.byteLength(line, "utf8") });
|
|
5717
|
+
offset += Buffer.byteLength(line, "utf8");
|
|
5718
|
+
}
|
|
4990
5719
|
this.cache.set(input.sessionId, refreshed);
|
|
4991
5720
|
this.diskCount.set(input.sessionId, keep.length);
|
|
4992
5721
|
await this.writeAll(input.sessionId, keep, "compact");
|
|
@@ -5019,6 +5748,8 @@ var ReplayLogStore = class {
|
|
|
5019
5748
|
const t0 = Date.now();
|
|
5020
5749
|
try {
|
|
5021
5750
|
const cache = await this.ensureCache(sessionId);
|
|
5751
|
+
const location = cache.get(hash);
|
|
5752
|
+
const entry = location ? await this.hydrateEntry(sessionId, hash, location) : null;
|
|
5022
5753
|
this.events?.emit("storage.read", {
|
|
5023
5754
|
sessionId,
|
|
5024
5755
|
store: "replay",
|
|
@@ -5028,7 +5759,7 @@ var ReplayLogStore = class {
|
|
|
5028
5759
|
durationMs: Date.now() - t0,
|
|
5029
5760
|
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
5030
5761
|
});
|
|
5031
|
-
return
|
|
5762
|
+
return entry;
|
|
5032
5763
|
} catch (err) {
|
|
5033
5764
|
this.events?.emit("storage.read", {
|
|
5034
5765
|
sessionId,
|
|
@@ -5049,6 +5780,10 @@ var ReplayLogStore = class {
|
|
|
5049
5780
|
const t0 = Date.now();
|
|
5050
5781
|
try {
|
|
5051
5782
|
const cache = await this.ensureCache(sessionId);
|
|
5783
|
+
const entries = [];
|
|
5784
|
+
for (const [hash, location] of cache) {
|
|
5785
|
+
entries.push(await this.hydrateEntry(sessionId, hash, location));
|
|
5786
|
+
}
|
|
5052
5787
|
const durationMs = Date.now() - t0;
|
|
5053
5788
|
this.events?.emit("storage.read", {
|
|
5054
5789
|
sessionId,
|
|
@@ -5059,7 +5794,7 @@ var ReplayLogStore = class {
|
|
|
5059
5794
|
durationMs,
|
|
5060
5795
|
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
5061
5796
|
});
|
|
5062
|
-
return
|
|
5797
|
+
return entries;
|
|
5063
5798
|
} catch (err) {
|
|
5064
5799
|
const durationMs = Date.now() - t0;
|
|
5065
5800
|
this.events?.emit("storage.read", {
|
|
@@ -5085,7 +5820,7 @@ var ReplayLogStore = class {
|
|
|
5085
5820
|
const scan = async (dir, prefix, depth) => {
|
|
5086
5821
|
let entries;
|
|
5087
5822
|
try {
|
|
5088
|
-
entries = await
|
|
5823
|
+
entries = await fsp2.readdir(dir, { withFileTypes: true });
|
|
5089
5824
|
} catch (err) {
|
|
5090
5825
|
if (depth === 0 && err.code !== "ENOENT") {
|
|
5091
5826
|
console.warn(JSON.stringify({
|
|
@@ -5123,7 +5858,7 @@ var ReplayLogStore = class {
|
|
|
5123
5858
|
return sessionScopedPath(this.dir, sessionId, ".replay.jsonl");
|
|
5124
5859
|
}
|
|
5125
5860
|
async countEntries(filePath) {
|
|
5126
|
-
const handle = await
|
|
5861
|
+
const handle = await fsp2.open(filePath, "r");
|
|
5127
5862
|
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
5128
5863
|
let count = 0;
|
|
5129
5864
|
let hasNonWhitespace = false;
|
|
@@ -5149,23 +5884,27 @@ var ReplayLogStore = class {
|
|
|
5149
5884
|
if (hasNonWhitespace) count++;
|
|
5150
5885
|
return count;
|
|
5151
5886
|
}
|
|
5887
|
+
parseReplayLine(line) {
|
|
5888
|
+
try {
|
|
5889
|
+
const parsed = safeParse(line);
|
|
5890
|
+
if (!parsed.ok || !parsed.value) return null;
|
|
5891
|
+
if ("entry" in parsed.value && parsed.value.entry) {
|
|
5892
|
+
return parsed.value.entry;
|
|
5893
|
+
}
|
|
5894
|
+
return parsed.value;
|
|
5895
|
+
} catch {
|
|
5896
|
+
return null;
|
|
5897
|
+
}
|
|
5898
|
+
}
|
|
5152
5899
|
async readAll(sessionId) {
|
|
5153
5900
|
const fp = this.filePath(sessionId);
|
|
5154
5901
|
try {
|
|
5155
|
-
const raw = await
|
|
5902
|
+
const raw = await fsp2.readFile(fp, "utf8");
|
|
5156
5903
|
const out = [];
|
|
5157
5904
|
for (const line of raw.split("\n")) {
|
|
5158
5905
|
if (!line.trim()) continue;
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
if (!parsed.ok || !parsed.value) continue;
|
|
5162
|
-
if ("entry" in parsed.value && parsed.value.entry) {
|
|
5163
|
-
out.push(parsed.value.entry);
|
|
5164
|
-
} else {
|
|
5165
|
-
out.push(parsed.value);
|
|
5166
|
-
}
|
|
5167
|
-
} catch {
|
|
5168
|
-
}
|
|
5906
|
+
const parsed = this.parseReplayLine(line);
|
|
5907
|
+
if (parsed) out.push(parsed);
|
|
5169
5908
|
}
|
|
5170
5909
|
return out;
|
|
5171
5910
|
} catch (err) {
|
|
@@ -5192,13 +5931,75 @@ var ReplayLogStore = class {
|
|
|
5192
5931
|
async ensureCache(sessionId) {
|
|
5193
5932
|
let cache = this.cache.get(sessionId);
|
|
5194
5933
|
if (cache) return cache;
|
|
5195
|
-
const
|
|
5934
|
+
const fp = this.filePath(sessionId);
|
|
5196
5935
|
cache = /* @__PURE__ */ new Map();
|
|
5197
|
-
|
|
5936
|
+
try {
|
|
5937
|
+
const handle = await fsp2.open(fp, "r");
|
|
5938
|
+
const CHUNK = 64 * 1024;
|
|
5939
|
+
const buffer = Buffer.alloc(CHUNK);
|
|
5940
|
+
let leftover = "";
|
|
5941
|
+
let leftoverOffset = 0;
|
|
5942
|
+
let fileOffset = 0;
|
|
5943
|
+
try {
|
|
5944
|
+
while (true) {
|
|
5945
|
+
const { bytesRead } = await handle.read(buffer, 0, CHUNK, fileOffset);
|
|
5946
|
+
if (bytesRead === 0) break;
|
|
5947
|
+
const chunk = buffer.subarray(0, bytesRead).toString("utf8");
|
|
5948
|
+
const text = leftover + chunk;
|
|
5949
|
+
let lineStart = leftoverOffset;
|
|
5950
|
+
let searchFrom = 0;
|
|
5951
|
+
while (true) {
|
|
5952
|
+
const newlineIndex = text.indexOf("\n", searchFrom);
|
|
5953
|
+
if (newlineIndex === -1) break;
|
|
5954
|
+
const line = text.slice(searchFrom, newlineIndex);
|
|
5955
|
+
const lineLength = Buffer.byteLength(text.slice(searchFrom, newlineIndex + 1), "utf8");
|
|
5956
|
+
if (line.trim()) {
|
|
5957
|
+
const entry = this.parseReplayLine(line);
|
|
5958
|
+
if (entry) {
|
|
5959
|
+
cache.set(entry.hash, { offset: lineStart, length: lineLength });
|
|
5960
|
+
}
|
|
5961
|
+
}
|
|
5962
|
+
lineStart += lineLength;
|
|
5963
|
+
searchFrom = newlineIndex + 1;
|
|
5964
|
+
}
|
|
5965
|
+
leftover = text.slice(searchFrom);
|
|
5966
|
+
leftoverOffset = lineStart;
|
|
5967
|
+
fileOffset += bytesRead;
|
|
5968
|
+
}
|
|
5969
|
+
if (leftover.trim()) {
|
|
5970
|
+
const entry = this.parseReplayLine(leftover);
|
|
5971
|
+
if (entry) {
|
|
5972
|
+
cache.set(entry.hash, { offset: leftoverOffset, length: Buffer.byteLength(leftover, "utf8") });
|
|
5973
|
+
}
|
|
5974
|
+
}
|
|
5975
|
+
} finally {
|
|
5976
|
+
await handle.close();
|
|
5977
|
+
}
|
|
5978
|
+
} catch (err) {
|
|
5979
|
+
if (err.code !== "ENOENT") throw err;
|
|
5980
|
+
}
|
|
5198
5981
|
this.cache.set(sessionId, cache);
|
|
5199
|
-
this.diskCount.set(sessionId,
|
|
5982
|
+
this.diskCount.set(sessionId, cache.size);
|
|
5200
5983
|
return cache;
|
|
5201
5984
|
}
|
|
5985
|
+
async hydrateEntry(sessionId, hash, location) {
|
|
5986
|
+
if (location.entry) return location.entry;
|
|
5987
|
+
const fp = this.filePath(sessionId);
|
|
5988
|
+
const handle = await fsp2.open(fp, "r");
|
|
5989
|
+
try {
|
|
5990
|
+
const buffer = Buffer.alloc(location.length);
|
|
5991
|
+
const { bytesRead } = await handle.read(buffer, 0, location.length, location.offset);
|
|
5992
|
+
const line = buffer.subarray(0, bytesRead).toString("utf8").trimEnd();
|
|
5993
|
+
const entry = this.parseReplayLine(line);
|
|
5994
|
+
if (!entry) {
|
|
5995
|
+
throw new Error(`Replay entry ${hash} is unreadable`);
|
|
5996
|
+
}
|
|
5997
|
+
location.entry = entry;
|
|
5998
|
+
return entry;
|
|
5999
|
+
} finally {
|
|
6000
|
+
await handle.close();
|
|
6001
|
+
}
|
|
6002
|
+
}
|
|
5202
6003
|
enqueue(sessionId, fn) {
|
|
5203
6004
|
const prev = this.writeChains.get(sessionId) ?? Promise.resolve();
|
|
5204
6005
|
const next = prev.then(fn, fn);
|
|
@@ -5227,19 +6028,19 @@ var SessionRecovery = class {
|
|
|
5227
6028
|
async detectStale(sessionId) {
|
|
5228
6029
|
const fp = this.filePath(sessionId);
|
|
5229
6030
|
const TAIL_SIZE = 8192;
|
|
5230
|
-
let
|
|
6031
|
+
let stat10;
|
|
5231
6032
|
try {
|
|
5232
|
-
|
|
6033
|
+
stat10 = await fsp2.stat(fp);
|
|
5233
6034
|
} catch (err) {
|
|
5234
6035
|
if (err.code === "ENOENT") return null;
|
|
5235
6036
|
return null;
|
|
5236
6037
|
}
|
|
5237
|
-
if (
|
|
5238
|
-
const position = Math.max(0,
|
|
6038
|
+
if (stat10.size === 0) return null;
|
|
6039
|
+
const position = Math.max(0, stat10.size - TAIL_SIZE);
|
|
5239
6040
|
const buf = Buffer.alloc(TAIL_SIZE);
|
|
5240
6041
|
let fh;
|
|
5241
6042
|
try {
|
|
5242
|
-
fh = await
|
|
6043
|
+
fh = await fsp2.open(fp, "r");
|
|
5243
6044
|
const { bytesRead } = await fh.read(buf, 0, TAIL_SIZE, position);
|
|
5244
6045
|
let eventCount = 0;
|
|
5245
6046
|
const raw = buf.subarray(0, bytesRead).toString("utf8");
|
|
@@ -5284,7 +6085,7 @@ var SessionRecovery = class {
|
|
|
5284
6085
|
const fp = this.filePath(sessionId);
|
|
5285
6086
|
let raw;
|
|
5286
6087
|
try {
|
|
5287
|
-
raw = await
|
|
6088
|
+
raw = await fsp2.readFile(fp, "utf8");
|
|
5288
6089
|
} catch (err) {
|
|
5289
6090
|
if (err.code === "ENOENT") return null;
|
|
5290
6091
|
return null;
|
|
@@ -5329,7 +6130,7 @@ var SessionRecovery = class {
|
|
|
5329
6130
|
const collect = async (dir, prefix, depth) => {
|
|
5330
6131
|
let entries;
|
|
5331
6132
|
try {
|
|
5332
|
-
entries = await
|
|
6133
|
+
entries = await fsp2.readdir(dir, { withFileTypes: true });
|
|
5333
6134
|
} catch {
|
|
5334
6135
|
return;
|
|
5335
6136
|
}
|
|
@@ -5430,9 +6231,9 @@ var ToolAuditLog = class {
|
|
|
5430
6231
|
isError: input.isError,
|
|
5431
6232
|
index
|
|
5432
6233
|
};
|
|
5433
|
-
await
|
|
6234
|
+
await fsp2.appendFile(fp, JSON.stringify(entry) + "\n", "utf8");
|
|
5434
6235
|
try {
|
|
5435
|
-
const st = await
|
|
6236
|
+
const st = await fsp2.stat(fp);
|
|
5436
6237
|
this.tailStat.set(input.sessionId, { mtimeMs: st.mtimeMs, size: st.size });
|
|
5437
6238
|
} catch {
|
|
5438
6239
|
}
|
|
@@ -5479,7 +6280,7 @@ var ToolAuditLog = class {
|
|
|
5479
6280
|
const cachedStat = this.tailStat.get(sessionId);
|
|
5480
6281
|
if (cachedHash !== void 0 && cachedIndex !== void 0 && cachedStat) {
|
|
5481
6282
|
try {
|
|
5482
|
-
const st = await
|
|
6283
|
+
const st = await fsp2.stat(fp);
|
|
5483
6284
|
if (st.mtimeMs === cachedStat.mtimeMs && st.size === cachedStat.size) {
|
|
5484
6285
|
return { prevHash: cachedHash, nextIndex: cachedIndex };
|
|
5485
6286
|
}
|
|
@@ -5500,7 +6301,7 @@ var ToolAuditLog = class {
|
|
|
5500
6301
|
this.tailHash.set(sessionId, prevHash);
|
|
5501
6302
|
this.tailIndex.set(sessionId, nextIndex);
|
|
5502
6303
|
try {
|
|
5503
|
-
const st = await
|
|
6304
|
+
const st = await fsp2.stat(fp);
|
|
5504
6305
|
this.tailStat.set(sessionId, { mtimeMs: st.mtimeMs, size: st.size });
|
|
5505
6306
|
} catch {
|
|
5506
6307
|
}
|
|
@@ -5621,7 +6422,7 @@ var ToolAuditLog = class {
|
|
|
5621
6422
|
async readAll(sessionId) {
|
|
5622
6423
|
const fp = this.filePath(sessionId);
|
|
5623
6424
|
try {
|
|
5624
|
-
const raw = await
|
|
6425
|
+
const raw = await fsp2.readFile(fp, "utf8");
|
|
5625
6426
|
const out = [];
|
|
5626
6427
|
for (const line of raw.split("\n")) {
|
|
5627
6428
|
if (!line.trim()) continue;
|
|
@@ -5659,7 +6460,7 @@ var ToolAuditLog = class {
|
|
|
5659
6460
|
}
|
|
5660
6461
|
async sync(sessionId, fp) {
|
|
5661
6462
|
try {
|
|
5662
|
-
const fh = await
|
|
6463
|
+
const fh = await fsp2.open(fp, "r+");
|
|
5663
6464
|
try {
|
|
5664
6465
|
await fh.sync();
|
|
5665
6466
|
} finally {
|
|
@@ -5977,7 +6778,7 @@ var SessionRegistry = class {
|
|
|
5977
6778
|
}
|
|
5978
6779
|
async readAndPrune() {
|
|
5979
6780
|
try {
|
|
5980
|
-
const raw = await
|
|
6781
|
+
const raw = await fsp2.readFile(this.filePath, "utf8");
|
|
5981
6782
|
const registry = JSON.parse(raw);
|
|
5982
6783
|
const now = Date.now();
|
|
5983
6784
|
let pruned = false;
|
|
@@ -6011,11 +6812,11 @@ var SessionRegistry = class {
|
|
|
6011
6812
|
const retryDelayMs = 20;
|
|
6012
6813
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
6013
6814
|
try {
|
|
6014
|
-
await
|
|
6015
|
-
let lockHandle = await
|
|
6815
|
+
await fsp2.mkdir(path2.dirname(this.filePath), { recursive: true });
|
|
6816
|
+
let lockHandle = await fsp2.open(lockPath, "wx").catch(() => null);
|
|
6016
6817
|
if (!lockHandle) {
|
|
6017
6818
|
if (await this.breakStaleLock(lockPath)) {
|
|
6018
|
-
lockHandle = await
|
|
6819
|
+
lockHandle = await fsp2.open(lockPath, "wx").catch(() => null);
|
|
6019
6820
|
}
|
|
6020
6821
|
if (!lockHandle) {
|
|
6021
6822
|
await new Promise((r) => setTimeout(r, retryDelayMs * (attempt + 1)));
|
|
@@ -6024,14 +6825,14 @@ var SessionRegistry = class {
|
|
|
6024
6825
|
}
|
|
6025
6826
|
try {
|
|
6026
6827
|
await lockHandle.writeFile(String(process.pid)).catch(() => void 0);
|
|
6027
|
-
const raw = await
|
|
6828
|
+
const raw = await fsp2.readFile(this.filePath, "utf8").catch(() => "{}");
|
|
6028
6829
|
const registry = JSON.parse(raw);
|
|
6029
6830
|
fn(registry);
|
|
6030
6831
|
await this.writeAtomicLocked(registry);
|
|
6031
6832
|
return;
|
|
6032
6833
|
} finally {
|
|
6033
6834
|
await lockHandle.close();
|
|
6034
|
-
await
|
|
6835
|
+
await fsp2.unlink(lockPath).catch(() => void 0);
|
|
6035
6836
|
}
|
|
6036
6837
|
} catch {
|
|
6037
6838
|
return;
|
|
@@ -6047,15 +6848,15 @@ var SessionRegistry = class {
|
|
|
6047
6848
|
*/
|
|
6048
6849
|
async breakStaleLock(lockPath) {
|
|
6049
6850
|
try {
|
|
6050
|
-
const [
|
|
6051
|
-
|
|
6052
|
-
|
|
6851
|
+
const [stat10, content] = await Promise.all([
|
|
6852
|
+
fsp2.stat(lockPath),
|
|
6853
|
+
fsp2.readFile(lockPath, "utf8").catch(() => "")
|
|
6053
6854
|
]);
|
|
6054
|
-
const ageMs = Date.now() -
|
|
6855
|
+
const ageMs = Date.now() - stat10.mtimeMs;
|
|
6055
6856
|
const ownerPid = Number.parseInt(content.trim(), 10);
|
|
6056
6857
|
const ownerDead = Number.isInteger(ownerPid) && ownerPid > 0 && ownerPid !== process.pid && !pidAlive(ownerPid);
|
|
6057
6858
|
if (ownerDead || ageMs > STALE_LOCK_MS) {
|
|
6058
|
-
await
|
|
6859
|
+
await fsp2.unlink(lockPath).catch(() => void 0);
|
|
6059
6860
|
return true;
|
|
6060
6861
|
}
|
|
6061
6862
|
return false;
|
|
@@ -6078,10 +6879,10 @@ var SessionRegistry = class {
|
|
|
6078
6879
|
`.${path2.basename(this.filePath)}.${randomUUID().slice(0, 8)}.tmp`
|
|
6079
6880
|
);
|
|
6080
6881
|
try {
|
|
6081
|
-
await
|
|
6082
|
-
await
|
|
6882
|
+
await fsp2.writeFile(tmp, JSON.stringify(registry, null, 2), "utf8");
|
|
6883
|
+
await fsp2.rename(tmp, this.filePath);
|
|
6083
6884
|
} catch (err) {
|
|
6084
|
-
await
|
|
6885
|
+
await fsp2.unlink(tmp).catch(() => void 0);
|
|
6085
6886
|
throw err;
|
|
6086
6887
|
}
|
|
6087
6888
|
}
|
|
@@ -6091,17 +6892,17 @@ var SessionRegistry = class {
|
|
|
6091
6892
|
const base = path2.basename(this.filePath);
|
|
6092
6893
|
const now = Date.now();
|
|
6093
6894
|
const stale = [];
|
|
6094
|
-
for (const name of await
|
|
6895
|
+
for (const name of await fsp2.readdir(dir)) {
|
|
6095
6896
|
const isTemp = (name.startsWith(`${base}.`) || name.startsWith(`.${base}.`)) && name.endsWith(".tmp");
|
|
6096
6897
|
if (!isTemp) continue;
|
|
6097
|
-
const
|
|
6098
|
-
if (!
|
|
6099
|
-
if (now -
|
|
6898
|
+
const stat10 = await fsp2.stat(path2.join(dir, name)).catch(() => null);
|
|
6899
|
+
if (!stat10) continue;
|
|
6900
|
+
if (now - stat10.mtimeMs > STALE_TMP_MS) stale.push({ name, mtimeMs: stat10.mtimeMs });
|
|
6100
6901
|
}
|
|
6101
6902
|
stale.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
6102
6903
|
await Promise.all(
|
|
6103
6904
|
stale.slice(MAX_STALE_TMP_FILES).map(async ({ name }) => {
|
|
6104
|
-
await
|
|
6905
|
+
await fsp2.unlink(path2.join(dir, name)).catch(() => void 0);
|
|
6105
6906
|
})
|
|
6106
6907
|
);
|
|
6107
6908
|
} catch {
|
|
@@ -6127,6 +6928,10 @@ var AGENT_REAP_MS = 3e4;
|
|
|
6127
6928
|
var AGENT_SWEEP_INTERVAL_MS = 1e4;
|
|
6128
6929
|
var PARTIAL_TEXT_CAP = 1200;
|
|
6129
6930
|
var PARTIAL_FLUSH_THROTTLE_MS = 300;
|
|
6931
|
+
function clampPct(pct) {
|
|
6932
|
+
if (!Number.isFinite(pct)) return 0;
|
|
6933
|
+
return Math.max(0, Math.min(100, pct));
|
|
6934
|
+
}
|
|
6130
6935
|
var AgentStatusTracker = class {
|
|
6131
6936
|
events;
|
|
6132
6937
|
registry;
|
|
@@ -6278,7 +7083,7 @@ var AgentStatusTracker = class {
|
|
|
6278
7083
|
this.events.onPattern("ctx.pct", (_e, payload) => {
|
|
6279
7084
|
const p = payload;
|
|
6280
7085
|
if (typeof p?.load === "number" && Number.isFinite(p.load)) {
|
|
6281
|
-
this.leaderCtxPct = Math.round(p.load * 100);
|
|
7086
|
+
this.leaderCtxPct = clampPct(Math.round(p.load * 100));
|
|
6282
7087
|
this.flush();
|
|
6283
7088
|
}
|
|
6284
7089
|
})
|
|
@@ -6320,7 +7125,7 @@ var AgentStatusTracker = class {
|
|
|
6320
7125
|
const p = payload;
|
|
6321
7126
|
if (!p?.subagentId) return;
|
|
6322
7127
|
const entry = touch(p.subagentId);
|
|
6323
|
-
if (typeof p.load === "number") entry.ctxPct = Math.round(p.load * 100);
|
|
7128
|
+
if (typeof p.load === "number") entry.ctxPct = clampPct(Math.round(p.load * 100));
|
|
6324
7129
|
this.flush();
|
|
6325
7130
|
})
|
|
6326
7131
|
);
|
|
@@ -6481,7 +7286,7 @@ var AgentStatusTracker = class {
|
|
|
6481
7286
|
const providerMax = c.provider?.capabilities?.maxContext;
|
|
6482
7287
|
const maxContext = typeof metaLimit === "number" && metaLimit > 0 ? metaLimit : typeof providerMax === "number" && providerMax > 0 ? providerMax : void 0;
|
|
6483
7288
|
if (typeof c.lastRequestTokens === "number" && c.lastRequestTokens > 0 && maxContext !== void 0) {
|
|
6484
|
-
this.leaderCtxPct = Math.round(c.lastRequestTokens / maxContext * 100);
|
|
7289
|
+
this.leaderCtxPct = clampPct(Math.round(c.lastRequestTokens / maxContext * 100));
|
|
6485
7290
|
}
|
|
6486
7291
|
}
|
|
6487
7292
|
};
|
|
@@ -6546,7 +7351,7 @@ var FleetNotifier = class {
|
|
|
6546
7351
|
}
|
|
6547
7352
|
async discover() {
|
|
6548
7353
|
try {
|
|
6549
|
-
const raw = await
|
|
7354
|
+
const raw = await fsp2.readFile(path2.join(this.baseDir, INSTANCES_FILE), "utf8");
|
|
6550
7355
|
const data = JSON.parse(raw);
|
|
6551
7356
|
const list = Array.isArray(data?.instances) ? data.instances : [];
|
|
6552
7357
|
return list.filter((i) => i && typeof i.httpPort === "number").filter((i) => i.pid !== this.selfPid).filter((i) => normRoot(i.projectRoot) === this.projectRoot).filter((i) => pidAlive2(i.pid)).map((i) => {
|
|
@@ -6573,7 +7378,7 @@ var DefaultSessionRewinder = class {
|
|
|
6573
7378
|
projectRoot;
|
|
6574
7379
|
async listCheckpoints(sessionId) {
|
|
6575
7380
|
const file = path2.join(this.sessionsDir, `${sessionId}.jsonl`);
|
|
6576
|
-
const raw = await
|
|
7381
|
+
const raw = await fsp2.readFile(file, "utf8");
|
|
6577
7382
|
const events = parseEvents(raw);
|
|
6578
7383
|
const fileCountMap = /* @__PURE__ */ new Map();
|
|
6579
7384
|
for (const event of events) {
|
|
@@ -6598,7 +7403,7 @@ var DefaultSessionRewinder = class {
|
|
|
6598
7403
|
}
|
|
6599
7404
|
async rewindToCheckpoint(sessionId, checkpointIndex) {
|
|
6600
7405
|
const file = path2.join(this.sessionsDir, `${sessionId}.jsonl`);
|
|
6601
|
-
const raw = await
|
|
7406
|
+
const raw = await fsp2.readFile(file, "utf8");
|
|
6602
7407
|
const events = parseEvents(raw);
|
|
6603
7408
|
let targetIdx = -1;
|
|
6604
7409
|
for (let i = 0; i < events.length; i++) {
|
|
@@ -6637,7 +7442,7 @@ var DefaultSessionRewinder = class {
|
|
|
6637
7442
|
}
|
|
6638
7443
|
async rewindLastN(sessionId, n) {
|
|
6639
7444
|
const file = path2.join(this.sessionsDir, `${sessionId}.jsonl`);
|
|
6640
|
-
const raw = await
|
|
7445
|
+
const raw = await fsp2.readFile(file, "utf8");
|
|
6641
7446
|
const events = parseEvents(raw);
|
|
6642
7447
|
const checkpoints = [];
|
|
6643
7448
|
for (const event of events) {
|
|
@@ -6666,7 +7471,7 @@ var DefaultSessionRewinder = class {
|
|
|
6666
7471
|
}
|
|
6667
7472
|
async rewindToStart(sessionId) {
|
|
6668
7473
|
const file = path2.join(this.sessionsDir, `${sessionId}.jsonl`);
|
|
6669
|
-
const raw = await
|
|
7474
|
+
const raw = await fsp2.readFile(file, "utf8");
|
|
6670
7475
|
const events = parseEvents(raw);
|
|
6671
7476
|
const allSnapshots = [];
|
|
6672
7477
|
for (const event of events) {
|
|
@@ -6714,7 +7519,7 @@ async function revertSnapshots(snapshots, projectRoot) {
|
|
|
6714
7519
|
revertedFiles.push(file.path);
|
|
6715
7520
|
}
|
|
6716
7521
|
} else if (file.action === "created") {
|
|
6717
|
-
await
|
|
7522
|
+
await fsp2.unlink(file.path);
|
|
6718
7523
|
revertedFiles.push(file.path);
|
|
6719
7524
|
} else if (file.action === "modified") {
|
|
6720
7525
|
if (file.before !== null) {
|
|
@@ -6733,7 +7538,7 @@ async function loadTodosCheckpoint(filePath, events, traceId) {
|
|
|
6733
7538
|
const t0 = Date.now();
|
|
6734
7539
|
let raw;
|
|
6735
7540
|
try {
|
|
6736
|
-
raw = await
|
|
7541
|
+
raw = await fsp2.readFile(filePath, "utf8");
|
|
6737
7542
|
} catch (err) {
|
|
6738
7543
|
events?.emit("storage.error", {
|
|
6739
7544
|
sessionId: traceId ?? "~boot~",
|
|
@@ -6872,7 +7677,7 @@ async function loadPlan(filePath, events) {
|
|
|
6872
7677
|
const t0 = Date.now();
|
|
6873
7678
|
let raw;
|
|
6874
7679
|
try {
|
|
6875
|
-
raw = await
|
|
7680
|
+
raw = await fsp2.readFile(filePath, "utf8");
|
|
6876
7681
|
} catch (err) {
|
|
6877
7682
|
events?.emit("storage.error", {
|
|
6878
7683
|
sessionId: "~boot~",
|
|
@@ -7183,7 +7988,7 @@ async function loadTasks(filePath, events, traceId) {
|
|
|
7183
7988
|
const t0 = Date.now();
|
|
7184
7989
|
let raw;
|
|
7185
7990
|
try {
|
|
7186
|
-
raw = await
|
|
7991
|
+
raw = await fsp2.readFile(filePath, "utf8");
|
|
7187
7992
|
} catch (err) {
|
|
7188
7993
|
events?.emit("storage.error", {
|
|
7189
7994
|
sessionId: traceId ?? "~boot~",
|
|
@@ -7282,7 +8087,7 @@ async function mutateTasks(filePath, sessionId, fn, events, traceId) {
|
|
|
7282
8087
|
async function loadDirectorState(filePath) {
|
|
7283
8088
|
let raw;
|
|
7284
8089
|
try {
|
|
7285
|
-
raw = await
|
|
8090
|
+
raw = await fsp2.readFile(filePath, "utf8");
|
|
7286
8091
|
} catch {
|
|
7287
8092
|
return null;
|
|
7288
8093
|
}
|
|
@@ -7297,7 +8102,7 @@ async function loadDirectorState(filePath) {
|
|
|
7297
8102
|
async function acquireDirectorStateLock(lockPath, processId = process.pid) {
|
|
7298
8103
|
let existing;
|
|
7299
8104
|
try {
|
|
7300
|
-
existing = await
|
|
8105
|
+
existing = await fsp2.readFile(lockPath, "utf8");
|
|
7301
8106
|
} catch {
|
|
7302
8107
|
}
|
|
7303
8108
|
if (existing) {
|
|
@@ -7321,7 +8126,7 @@ async function acquireDirectorStateLock(lockPath, processId = process.pid) {
|
|
|
7321
8126
|
}
|
|
7322
8127
|
async function releaseDirectorStateLock(lockPath) {
|
|
7323
8128
|
try {
|
|
7324
|
-
await
|
|
8129
|
+
await fsp2.unlink(lockPath);
|
|
7325
8130
|
} catch {
|
|
7326
8131
|
}
|
|
7327
8132
|
}
|
|
@@ -7465,7 +8270,7 @@ async function loadGoal(filePath, events) {
|
|
|
7465
8270
|
const t0 = Date.now();
|
|
7466
8271
|
let raw;
|
|
7467
8272
|
try {
|
|
7468
|
-
raw = await
|
|
8273
|
+
raw = await fsp2.readFile(filePath, "utf8");
|
|
7469
8274
|
} catch (err) {
|
|
7470
8275
|
const code = err.code;
|
|
7471
8276
|
if (code === "ENOENT") {
|
|
@@ -7718,12 +8523,12 @@ var DefaultPromptStore = class {
|
|
|
7718
8523
|
await ensureDir(this.dir);
|
|
7719
8524
|
const entries = [];
|
|
7720
8525
|
try {
|
|
7721
|
-
const files = await
|
|
8526
|
+
const files = await fsp2.readdir(this.dir);
|
|
7722
8527
|
for (const file of files) {
|
|
7723
8528
|
if (!file.endsWith(".json")) continue;
|
|
7724
8529
|
try {
|
|
7725
8530
|
const raw = JSON.parse(
|
|
7726
|
-
await
|
|
8531
|
+
await fsp2.readFile(path2.join(this.dir, file), "utf8")
|
|
7727
8532
|
);
|
|
7728
8533
|
entries.push(raw.entry);
|
|
7729
8534
|
} catch {
|
|
@@ -7738,7 +8543,7 @@ var DefaultPromptStore = class {
|
|
|
7738
8543
|
async get(id) {
|
|
7739
8544
|
const file = path2.join(this.dir, `${id}.json`);
|
|
7740
8545
|
try {
|
|
7741
|
-
const raw = JSON.parse(await
|
|
8546
|
+
const raw = JSON.parse(await fsp2.readFile(file, "utf8"));
|
|
7742
8547
|
return raw.entry;
|
|
7743
8548
|
} catch {
|
|
7744
8549
|
return null;
|
|
@@ -7753,7 +8558,7 @@ var DefaultPromptStore = class {
|
|
|
7753
8558
|
async delete(id) {
|
|
7754
8559
|
const file = path2.join(this.dir, `${id}.json`);
|
|
7755
8560
|
try {
|
|
7756
|
-
await
|
|
8561
|
+
await fsp2.unlink(file);
|
|
7757
8562
|
return true;
|
|
7758
8563
|
} catch {
|
|
7759
8564
|
return false;
|
|
@@ -7860,7 +8665,7 @@ var CloudSync = class {
|
|
|
7860
8665
|
lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7861
8666
|
localRev: rev
|
|
7862
8667
|
};
|
|
7863
|
-
await
|
|
8668
|
+
await fsp2.writeFile(this.statePath, JSON.stringify(syncState, null, 2), "utf8");
|
|
7864
8669
|
this.state = syncState;
|
|
7865
8670
|
return {
|
|
7866
8671
|
ok: true,
|
|
@@ -7892,8 +8697,8 @@ var CloudSync = class {
|
|
|
7892
8697
|
const rel = segments.slice(2).join("/");
|
|
7893
8698
|
const destPath = resolvePulledCategoryPath(cat, localPath, rel, entry.path);
|
|
7894
8699
|
const blobData = await this.getBlob(token, owner, repoName, entry.sha);
|
|
7895
|
-
await
|
|
7896
|
-
await
|
|
8700
|
+
await fsp2.mkdir(path2.dirname(destPath), { recursive: true });
|
|
8701
|
+
await fsp2.writeFile(destPath, Buffer.from(blobData, "base64"));
|
|
7897
8702
|
}
|
|
7898
8703
|
const localRev = await this.hashLocalCategories(cfg.categories);
|
|
7899
8704
|
const syncState = {
|
|
@@ -7902,7 +8707,7 @@ var CloudSync = class {
|
|
|
7902
8707
|
lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7903
8708
|
localRev
|
|
7904
8709
|
};
|
|
7905
|
-
await
|
|
8710
|
+
await fsp2.writeFile(this.statePath, JSON.stringify(syncState, null, 2), "utf8");
|
|
7906
8711
|
this.state = syncState;
|
|
7907
8712
|
return {
|
|
7908
8713
|
ok: true,
|
|
@@ -7921,7 +8726,7 @@ var CloudSync = class {
|
|
|
7921
8726
|
}
|
|
7922
8727
|
async loadState() {
|
|
7923
8728
|
try {
|
|
7924
|
-
const raw = await
|
|
8729
|
+
const raw = await fsp2.readFile(this.statePath, "utf8");
|
|
7925
8730
|
this.state = JSON.parse(raw);
|
|
7926
8731
|
} catch {
|
|
7927
8732
|
this.state = null;
|
|
@@ -7999,17 +8804,17 @@ var CloudSync = class {
|
|
|
7999
8804
|
const localPath = this.categoryToPath(cat);
|
|
8000
8805
|
if (!localPath) continue;
|
|
8001
8806
|
try {
|
|
8002
|
-
const
|
|
8003
|
-
if (
|
|
8807
|
+
const stat10 = await fsp2.stat(localPath);
|
|
8808
|
+
if (stat10.isDirectory()) {
|
|
8004
8809
|
const files = await this.walkDir(localPath, localPath);
|
|
8005
8810
|
for (const file of files) {
|
|
8006
|
-
const content = await
|
|
8811
|
+
const content = await fsp2.readFile(file, "utf8");
|
|
8007
8812
|
const rel = path2.relative(localPath, file).replace(/\\/g, "/");
|
|
8008
8813
|
entries.push({ path: `data/${cat}/${rel}`, content, mode: "100644" });
|
|
8009
8814
|
hashes.push(content);
|
|
8010
8815
|
}
|
|
8011
8816
|
} else {
|
|
8012
|
-
const content = await
|
|
8817
|
+
const content = await fsp2.readFile(localPath, "utf8");
|
|
8013
8818
|
entries.push({ path: `data/${cat}`, content, mode: "100644" });
|
|
8014
8819
|
hashes.push(content);
|
|
8015
8820
|
}
|
|
@@ -8025,15 +8830,15 @@ var CloudSync = class {
|
|
|
8025
8830
|
const localPath = this.categoryToPath(cat);
|
|
8026
8831
|
if (!localPath) continue;
|
|
8027
8832
|
try {
|
|
8028
|
-
const
|
|
8029
|
-
if (
|
|
8833
|
+
const stat10 = await fsp2.stat(localPath);
|
|
8834
|
+
if (stat10.isDirectory()) {
|
|
8030
8835
|
const files = await this.walkDir(localPath, localPath);
|
|
8031
8836
|
for (const file of files) {
|
|
8032
|
-
const content = await
|
|
8837
|
+
const content = await fsp2.readFile(file);
|
|
8033
8838
|
hashes.push(content.toString("base64") + file);
|
|
8034
8839
|
}
|
|
8035
8840
|
} else {
|
|
8036
|
-
const content = await
|
|
8841
|
+
const content = await fsp2.readFile(localPath);
|
|
8037
8842
|
hashes.push(content.toString("base64") + localPath);
|
|
8038
8843
|
}
|
|
8039
8844
|
} catch {
|
|
@@ -8060,7 +8865,7 @@ var CloudSync = class {
|
|
|
8060
8865
|
}
|
|
8061
8866
|
async walkDir(dir, base) {
|
|
8062
8867
|
const results = [];
|
|
8063
|
-
const entries = await
|
|
8868
|
+
const entries = await fsp2.readdir(dir, { withFileTypes: true });
|
|
8064
8869
|
for (const entry of entries) {
|
|
8065
8870
|
const full = path2.join(dir, entry.name);
|
|
8066
8871
|
if (entry.isDirectory()) {
|