@wrongstack/core 0.9.7 → 0.9.20
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-DMVOX0cF.d.ts → agent-bridge-Dti3KXGk.d.ts} +1 -1
- package/dist/{agent-subagent-runner-DaF_EgRG.d.ts → agent-subagent-runner-U-rs7kk7.d.ts} +3 -4
- package/dist/compactor-D7X96RLZ.d.ts +41 -0
- package/dist/{config-SkMIDN9L.d.ts → config-CLXMDOSs.d.ts} +5 -1
- package/dist/{context-BRNbHmRM.d.ts → context-zkZeILpr.d.ts} +46 -0
- package/dist/coordination/index.d.ts +14 -13
- package/dist/coordination/index.js +1263 -123
- package/dist/coordination/index.js.map +1 -1
- package/dist/defaults/index.d.ts +22 -21
- package/dist/defaults/index.js +1477 -152
- package/dist/defaults/index.js.map +1 -1
- package/dist/{events-CiG9qUM_.d.ts → events-DH-9r-_C.d.ts} +42 -1
- package/dist/execution/index.d.ts +42 -30
- package/dist/execution/index.js +359 -112
- package/dist/execution/index.js.map +1 -1
- package/dist/extension/index.d.ts +7 -6
- package/dist/extension/index.js.map +1 -1
- package/dist/{index-CP8638Wm.d.ts → index-BIHJ4uII.d.ts} +12 -8
- package/dist/{index-Bsha5K4D.d.ts → index-CFO9QmJo.d.ts} +9 -8
- package/dist/index.d.ts +319 -37
- package/dist/index.js +2571 -324
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/index.d.ts +6 -6
- package/dist/infrastructure/index.js +36 -0
- package/dist/infrastructure/index.js.map +1 -1
- package/dist/kernel/index.d.ts +10 -9
- package/dist/kernel/index.js.map +1 -1
- package/dist/{mcp-servers-BouUWYW6.d.ts → mcp-servers-DkESgh0G.d.ts} +25 -3
- package/dist/models/index.d.ts +2 -2
- package/dist/models/index.js +1 -1
- package/dist/models/index.js.map +1 -1
- package/dist/{multi-agent-3ZnTB1aT.d.ts → multi-agent-DNp6lAzg.d.ts} +35 -23
- package/dist/{multi-agent-coordinator-DTXF2aAl.d.ts → multi-agent-coordinator-CAhsegPz.d.ts} +20 -2
- package/dist/{null-fleet-bus-Chrc_3Pp.d.ts → null-fleet-bus-Dnl19vmf.d.ts} +496 -15
- package/dist/observability/index.d.ts +2 -2
- package/dist/{path-resolver-TcJfc29Y.d.ts → path-resolver-CHiBL0DD.d.ts} +2 -2
- package/dist/{secret-scrubber-DttNiGYA.d.ts → permission-H35s9Evv.d.ts} +2 -7
- package/dist/{permission-policy-BpCGYBud.d.ts → permission-policy-CT-nRmTn.d.ts} +9 -2
- package/dist/{plan-templates-envSmNlZ.d.ts → plan-templates-Bs8iRwi6.d.ts} +28 -4
- package/dist/{provider-runner-BEpikbbN.d.ts → provider-runner-BZdDrWrS.d.ts} +3 -3
- package/dist/{retry-policy-BYkq0ugs.d.ts → retry-policy-J9N_PM40.d.ts} +1 -1
- package/dist/sdd/index.d.ts +10 -10
- package/dist/sdd/index.js +225 -68
- package/dist/sdd/index.js.map +1 -1
- package/dist/secret-scrubber-3MHDDAtm.d.ts +6 -0
- package/dist/{secret-scrubber-QSeI0ADi.d.ts → secret-scrubber-7rSC_emZ.d.ts} +1 -1
- package/dist/security/index.d.ts +5 -4
- package/dist/security/index.js +37 -6
- package/dist/security/index.js.map +1 -1
- package/dist/{selector-pox8abg0.d.ts → selector-CFTh3Z6p.d.ts} +1 -1
- package/dist/{session-reader-CSWcb5Ga.d.ts → session-reader-C7JJlxOw.d.ts} +2 -2
- package/dist/skills/index.d.ts +1 -1
- package/dist/skills/index.js +1 -1
- package/dist/skills/index.js.map +1 -1
- package/dist/storage/index.d.ts +391 -6
- package/dist/storage/index.js +760 -40
- package/dist/storage/index.js.map +1 -1
- package/dist/{system-prompt-Bs-Wliab.d.ts → system-prompt-CneIxVip.d.ts} +1 -1
- package/dist/{tool-executor-CsktM3h9.d.ts → tool-executor-flTuxsqO.d.ts} +9 -4
- package/dist/types/index.d.ts +16 -15
- package/dist/types/index.js +60 -0
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.d.ts +25 -2
- package/dist/utils/index.js +90 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/compactor-DVTKL7XD.d.ts +0 -23
package/dist/storage/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomBytes, randomUUID, createHash } from 'crypto';
|
|
2
2
|
import * as fsp from 'fs/promises';
|
|
3
|
-
import * as
|
|
3
|
+
import * as path11 from 'path';
|
|
4
4
|
import * as os from 'os';
|
|
5
5
|
|
|
6
6
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
@@ -10,9 +10,9 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
10
10
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
11
11
|
});
|
|
12
12
|
async function atomicWrite(targetPath, content, opts = {}) {
|
|
13
|
-
const dir =
|
|
13
|
+
const dir = path11.dirname(targetPath);
|
|
14
14
|
await fsp.mkdir(dir, { recursive: true });
|
|
15
|
-
const tmp =
|
|
15
|
+
const tmp = path11.join(dir, `.${path11.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
|
|
16
16
|
try {
|
|
17
17
|
if (typeof content === "string") {
|
|
18
18
|
await fsp.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
|
|
@@ -170,13 +170,15 @@ function isEmptyMessage(msg) {
|
|
|
170
170
|
var DefaultSessionStore = class {
|
|
171
171
|
dir;
|
|
172
172
|
events;
|
|
173
|
+
secretScrubber;
|
|
173
174
|
constructor(opts) {
|
|
174
175
|
this.dir = opts.dir;
|
|
175
176
|
this.events = opts.events;
|
|
177
|
+
this.secretScrubber = opts.secretScrubber;
|
|
176
178
|
}
|
|
177
179
|
/** Join session ID to its absolute path within the store directory. */
|
|
178
180
|
sessionPath(id, ext) {
|
|
179
|
-
return
|
|
181
|
+
return path11.join(this.dir, `${id}${ext}`);
|
|
180
182
|
}
|
|
181
183
|
async ensureShardDir(_id) {
|
|
182
184
|
await ensureDir(this.dir);
|
|
@@ -186,7 +188,7 @@ var DefaultSessionStore = class {
|
|
|
186
188
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
187
189
|
const id = meta.id ?? `${startedAt.replace(/[:.]/g, "-")}-${randomBytes(2).toString("hex")}`;
|
|
188
190
|
const shardDir = await this.ensureShardDir(id);
|
|
189
|
-
const file =
|
|
191
|
+
const file = path11.join(shardDir, `${id}.jsonl`);
|
|
190
192
|
let handle;
|
|
191
193
|
try {
|
|
192
194
|
handle = await fsp.open(file, "a", 384);
|
|
@@ -197,7 +199,11 @@ var DefaultSessionStore = class {
|
|
|
197
199
|
);
|
|
198
200
|
}
|
|
199
201
|
try {
|
|
200
|
-
return new FileSessionWriter(id, handle, startedAt, meta, this.events, {
|
|
202
|
+
return new FileSessionWriter(id, handle, startedAt, meta, this.events, {
|
|
203
|
+
dir: shardDir,
|
|
204
|
+
filePath: file,
|
|
205
|
+
secretScrubber: this.secretScrubber
|
|
206
|
+
});
|
|
201
207
|
} catch (err) {
|
|
202
208
|
await handle.close().catch(() => {
|
|
203
209
|
});
|
|
@@ -227,7 +233,7 @@ var DefaultSessionStore = class {
|
|
|
227
233
|
provider: data.metadata.provider
|
|
228
234
|
},
|
|
229
235
|
this.events,
|
|
230
|
-
{ resumed: true, dir: this.dir, filePath: file }
|
|
236
|
+
{ resumed: true, dir: this.dir, filePath: file, secretScrubber: this.secretScrubber }
|
|
231
237
|
);
|
|
232
238
|
return { writer, data };
|
|
233
239
|
} catch (err) {
|
|
@@ -275,7 +281,7 @@ var DefaultSessionStore = class {
|
|
|
275
281
|
const ids = [];
|
|
276
282
|
const entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
277
283
|
for (const entry of entries) {
|
|
278
|
-
const full =
|
|
284
|
+
const full = path11.join(dir, entry.name);
|
|
279
285
|
if (entry.isDirectory()) {
|
|
280
286
|
ids.push(...await this.collectSessionIds(full));
|
|
281
287
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -431,8 +437,9 @@ var FileSessionWriter = class {
|
|
|
431
437
|
this.meta = meta;
|
|
432
438
|
this.events = events;
|
|
433
439
|
this.resumed = opts.resumed ?? false;
|
|
434
|
-
this.manifestFile = opts.dir ?
|
|
440
|
+
this.manifestFile = opts.dir ? path11.join(opts.dir, `${id}.summary.json`) : "";
|
|
435
441
|
this.filePath = opts.filePath ?? "";
|
|
442
|
+
this.secretScrubber = opts.secretScrubber;
|
|
436
443
|
this.summary = {
|
|
437
444
|
id,
|
|
438
445
|
title: "(empty session)",
|
|
@@ -461,7 +468,29 @@ var FileSessionWriter = class {
|
|
|
461
468
|
resumed;
|
|
462
469
|
appendFailCount = 0;
|
|
463
470
|
lastAppendWarnAt = 0;
|
|
464
|
-
|
|
471
|
+
secretScrubber;
|
|
472
|
+
/**
|
|
473
|
+
* Scrub secrets out of conversation-turn events before they are observed
|
|
474
|
+
* for the summary, written to the JSONL log, or surfaced on resume. Only
|
|
475
|
+
* `user_input` / `llm_response` carry free-form user/model text; other event
|
|
476
|
+
* types either have no secret-bearing content or are already scrubbed
|
|
477
|
+
* upstream (tool results). Returns the event unchanged when no scrubber is
|
|
478
|
+
* configured.
|
|
479
|
+
*/
|
|
480
|
+
scrubEvent(event) {
|
|
481
|
+
const s = this.secretScrubber;
|
|
482
|
+
if (!s) return event;
|
|
483
|
+
if (event.type === "user_input") {
|
|
484
|
+
return {
|
|
485
|
+
...event,
|
|
486
|
+
content: typeof event.content === "string" ? s.scrub(event.content) : s.scrubObject(event.content)
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
if (event.type === "llm_response") {
|
|
490
|
+
return { ...event, content: s.scrubObject(event.content) };
|
|
491
|
+
}
|
|
492
|
+
return event;
|
|
493
|
+
}
|
|
465
494
|
pendingFileSnapshots = [];
|
|
466
495
|
/** Tracks open tool_use IDs during the current run to serialize on close for resume. */
|
|
467
496
|
openToolUses = /* @__PURE__ */ new Set();
|
|
@@ -493,9 +522,10 @@ var FileSessionWriter = class {
|
|
|
493
522
|
this.initDone = true;
|
|
494
523
|
await this.writeSessionStartLazy();
|
|
495
524
|
}
|
|
496
|
-
this.
|
|
525
|
+
const scrubbed = this.scrubEvent(event);
|
|
526
|
+
this.observeForSummary(scrubbed);
|
|
497
527
|
try {
|
|
498
|
-
await this.handle.appendFile(`${JSON.stringify(
|
|
528
|
+
await this.handle.appendFile(`${JSON.stringify(scrubbed)}
|
|
499
529
|
`, "utf8");
|
|
500
530
|
} catch (err) {
|
|
501
531
|
this.appendFailCount++;
|
|
@@ -551,7 +581,6 @@ var FileSessionWriter = class {
|
|
|
551
581
|
await this.writeFileSnapshot(promptIndex, [...this.pendingFileSnapshots]);
|
|
552
582
|
this.pendingFileSnapshots = [];
|
|
553
583
|
}
|
|
554
|
-
this.promptIndex = promptIndex + 1;
|
|
555
584
|
await this.append({
|
|
556
585
|
type: "checkpoint",
|
|
557
586
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -647,6 +676,38 @@ var FileSessionWriter = class {
|
|
|
647
676
|
`;
|
|
648
677
|
await fsp.writeFile(this.filePath, record, "utf8");
|
|
649
678
|
}
|
|
679
|
+
/**
|
|
680
|
+
* Idea #1 — write an in-flight marker. The agent loop should call
|
|
681
|
+
* this at the start of each long-running operation; a matching
|
|
682
|
+
* `clearInFlightMarker` follows on clean exit. A stale marker
|
|
683
|
+
* (no end) is what `SessionRecovery.detectStale` looks for.
|
|
684
|
+
*/
|
|
685
|
+
async writeInFlightMarker(context) {
|
|
686
|
+
if (!context || context.length > 500) {
|
|
687
|
+
throw new Error("In-flight context must be 1..500 chars");
|
|
688
|
+
}
|
|
689
|
+
await this.append({
|
|
690
|
+
type: "in_flight_start",
|
|
691
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
692
|
+
context
|
|
693
|
+
});
|
|
694
|
+
this.events?.emit("in_flight.started", { context, ts: (/* @__PURE__ */ new Date()).toISOString() });
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Idea #1 — close the in-flight marker. Idempotent in spirit
|
|
698
|
+
* (you can call it after a successful iteration even if you
|
|
699
|
+
* didn't open one this round) — but the session log records
|
|
700
|
+
* every call so postmortem tooling can see "the agent finished
|
|
701
|
+
* cleanly X times, then died without finishing Y".
|
|
702
|
+
*/
|
|
703
|
+
async clearInFlightMarker(reason) {
|
|
704
|
+
await this.append({
|
|
705
|
+
type: "in_flight_end",
|
|
706
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
707
|
+
reason
|
|
708
|
+
});
|
|
709
|
+
this.events?.emit("in_flight.ended", { reason, ts: (/* @__PURE__ */ new Date()).toISOString() });
|
|
710
|
+
}
|
|
650
711
|
};
|
|
651
712
|
function userInputTitle(content) {
|
|
652
713
|
const text = typeof content === "string" ? content : content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
|
|
@@ -655,7 +716,7 @@ function userInputTitle(content) {
|
|
|
655
716
|
var QueueStore = class {
|
|
656
717
|
file;
|
|
657
718
|
constructor(opts) {
|
|
658
|
-
this.file =
|
|
719
|
+
this.file = path11.join(opts.dir, "queue.json");
|
|
659
720
|
}
|
|
660
721
|
async write(items) {
|
|
661
722
|
if (items.length === 0) {
|
|
@@ -722,7 +783,7 @@ var DefaultAttachmentStore = class {
|
|
|
722
783
|
let data = input.data;
|
|
723
784
|
if (this.spoolDir && bytes >= this.spoolThreshold) {
|
|
724
785
|
await fsp.mkdir(this.spoolDir, { recursive: true });
|
|
725
|
-
spooledPath =
|
|
786
|
+
spooledPath = path11.join(this.spoolDir, `${id}.bin`);
|
|
726
787
|
await atomicWrite(spooledPath, input.data, {
|
|
727
788
|
encoding: input.kind === "image" ? "base64" : "utf8"
|
|
728
789
|
});
|
|
@@ -892,7 +953,7 @@ ${body.trim()}`);
|
|
|
892
953
|
async remember(text, scope = "project-memory") {
|
|
893
954
|
return this.runSerialized(scope, async () => {
|
|
894
955
|
const file = this.files[scope];
|
|
895
|
-
await ensureDir(
|
|
956
|
+
await ensureDir(path11.dirname(file));
|
|
896
957
|
let existing = "";
|
|
897
958
|
try {
|
|
898
959
|
existing = await fsp.readFile(file, "utf8");
|
|
@@ -1349,6 +1410,40 @@ var DefaultConfigLoader = class {
|
|
|
1349
1410
|
}
|
|
1350
1411
|
return Object.freeze(cfg);
|
|
1351
1412
|
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Persist a sync config to ~/.wrongstack/sync.json, with the token encrypted
|
|
1415
|
+
* by the vault (if provided). The file is isolated from the main config
|
|
1416
|
+
* hierarchy to prevent accidental commits.
|
|
1417
|
+
*/
|
|
1418
|
+
async persistSyncConfig(cfg) {
|
|
1419
|
+
let toWrite = { ...cfg };
|
|
1420
|
+
if (this.vault && toWrite.githubToken && !toWrite.githubToken.startsWith("enc:")) {
|
|
1421
|
+
toWrite = { ...toWrite, githubToken: this.vault.encrypt(toWrite.githubToken) };
|
|
1422
|
+
}
|
|
1423
|
+
await atomicWrite(this.paths.syncConfig, JSON.stringify(toWrite, null, 2), { mode: 384 });
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Read ~/.wrongstack/sync.json (encrypted GitHub token storage) and decrypt
|
|
1427
|
+
* the token if a vault is available. Returns null if the file doesn't exist.
|
|
1428
|
+
* This is separate from main config loading because sync.json is intentionally
|
|
1429
|
+
* isolated — it should never be part of project-local or env-driven config.
|
|
1430
|
+
*/
|
|
1431
|
+
async loadSyncConfig() {
|
|
1432
|
+
try {
|
|
1433
|
+
const raw = await fsp.readFile(this.paths.syncConfig, "utf8");
|
|
1434
|
+
const parsed = safeParse(raw);
|
|
1435
|
+
if (!parsed.ok || !parsed.value) return null;
|
|
1436
|
+
if (this.vault) {
|
|
1437
|
+
const decrypted = decryptConfigSecrets({ sync: parsed.value }, this.vault);
|
|
1438
|
+
return decrypted.sync ?? null;
|
|
1439
|
+
}
|
|
1440
|
+
return parsed.value;
|
|
1441
|
+
} catch (err) {
|
|
1442
|
+
if (err.code === "ENOENT") return null;
|
|
1443
|
+
console.warn("[config] Failed to load sync config:", err);
|
|
1444
|
+
return null;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1352
1447
|
async readJson(file) {
|
|
1353
1448
|
let raw;
|
|
1354
1449
|
try {
|
|
@@ -1464,7 +1559,7 @@ var RecoveryLock = class {
|
|
|
1464
1559
|
sessionStore;
|
|
1465
1560
|
probe;
|
|
1466
1561
|
constructor(opts) {
|
|
1467
|
-
this.file =
|
|
1562
|
+
this.file = path11.join(opts.dir, LOCK_FILE);
|
|
1468
1563
|
this.pid = opts.pid ?? process.pid;
|
|
1469
1564
|
this.hostname = opts.hostname ?? os.hostname();
|
|
1470
1565
|
this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
@@ -1522,7 +1617,7 @@ var RecoveryLock = class {
|
|
|
1522
1617
|
* null return before calling this.
|
|
1523
1618
|
*/
|
|
1524
1619
|
async write(sessionId) {
|
|
1525
|
-
await ensureDir(
|
|
1620
|
+
await ensureDir(path11.dirname(this.file));
|
|
1526
1621
|
const lock = {
|
|
1527
1622
|
v: 1,
|
|
1528
1623
|
sessionId,
|
|
@@ -1884,6 +1979,613 @@ function renderPlainText(meta, events) {
|
|
|
1884
1979
|
}
|
|
1885
1980
|
return lines.join("\n");
|
|
1886
1981
|
}
|
|
1982
|
+
var FILE_VERSION = 1;
|
|
1983
|
+
var MAX_TEXT_LENGTH = 2e3;
|
|
1984
|
+
var MAX_ANNOTATIONS = 1e3;
|
|
1985
|
+
var AnnotationsStore = class {
|
|
1986
|
+
dir;
|
|
1987
|
+
/** Per-session write queue. Created lazily on first add. */
|
|
1988
|
+
writeChains = /* @__PURE__ */ new Map();
|
|
1989
|
+
constructor(opts) {
|
|
1990
|
+
this.dir = opts.dir;
|
|
1991
|
+
}
|
|
1992
|
+
// ── Reads ──────────────────────────────────────────────────────────────
|
|
1993
|
+
/**
|
|
1994
|
+
* Return all annotations for `sessionId` in insertion order
|
|
1995
|
+
* (oldest first). Returns an empty array when no file exists
|
|
1996
|
+
* yet (the normal case for a fresh session).
|
|
1997
|
+
*/
|
|
1998
|
+
async list(sessionId) {
|
|
1999
|
+
const file = await this.readFile(sessionId);
|
|
2000
|
+
return file ? file.annotations : [];
|
|
2001
|
+
}
|
|
2002
|
+
/**
|
|
2003
|
+
* Convenience: only unresolved annotations, newest first — the
|
|
2004
|
+
* common UI rendering for "what still needs attention?".
|
|
2005
|
+
*/
|
|
2006
|
+
async listOpen(sessionId) {
|
|
2007
|
+
const all = await this.list(sessionId);
|
|
2008
|
+
return all.filter((a) => !a.resolved).reverse();
|
|
2009
|
+
}
|
|
2010
|
+
// ── Writes ─────────────────────────────────────────────────────────────
|
|
2011
|
+
/**
|
|
2012
|
+
* Add a new annotation. Returns the persisted record (with id
|
|
2013
|
+
* and timestamps filled in). Throws when `text` is empty or
|
|
2014
|
+
* exceeds `MAX_TEXT_LENGTH`.
|
|
2015
|
+
*/
|
|
2016
|
+
async add(input) {
|
|
2017
|
+
const text = input.text.trim();
|
|
2018
|
+
if (text.length === 0) {
|
|
2019
|
+
throw new Error("Annotation text must be non-empty");
|
|
2020
|
+
}
|
|
2021
|
+
if (text.length > MAX_TEXT_LENGTH) {
|
|
2022
|
+
throw new Error(
|
|
2023
|
+
`Annotation text exceeds ${MAX_TEXT_LENGTH} chars (got ${text.length})`
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
2026
|
+
if (!Number.isInteger(input.atEventIndex) || input.atEventIndex < 0) {
|
|
2027
|
+
throw new Error("atEventIndex must be a non-negative integer");
|
|
2028
|
+
}
|
|
2029
|
+
const annotation = {
|
|
2030
|
+
id: randomUUID(),
|
|
2031
|
+
sessionId: input.sessionId,
|
|
2032
|
+
atEventIndex: input.atEventIndex,
|
|
2033
|
+
authorId: input.authorId,
|
|
2034
|
+
authorRole: "annotator",
|
|
2035
|
+
text,
|
|
2036
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2037
|
+
resolved: false
|
|
2038
|
+
};
|
|
2039
|
+
await this.enqueue(input.sessionId, async () => {
|
|
2040
|
+
const all = await this.list(input.sessionId);
|
|
2041
|
+
all.push(annotation);
|
|
2042
|
+
if (all.length > MAX_ANNOTATIONS) {
|
|
2043
|
+
const sorted = all.map((a, i) => ({ a, i })).sort((x, y) => {
|
|
2044
|
+
if (x.a.resolved !== y.a.resolved) return x.a.resolved ? 1 : -1;
|
|
2045
|
+
return x.a.createdAt.localeCompare(y.a.createdAt);
|
|
2046
|
+
});
|
|
2047
|
+
const evictCount = all.length - MAX_ANNOTATIONS;
|
|
2048
|
+
const toEvict = new Set(sorted.slice(0, evictCount).map((s) => s.a.id));
|
|
2049
|
+
const kept = all.filter((a) => !toEvict.has(a.id));
|
|
2050
|
+
await this.writeFile(input.sessionId, { version: FILE_VERSION, annotations: kept });
|
|
2051
|
+
} else {
|
|
2052
|
+
await this.writeFile(input.sessionId, { version: FILE_VERSION, annotations: all });
|
|
2053
|
+
}
|
|
2054
|
+
});
|
|
2055
|
+
return annotation;
|
|
2056
|
+
}
|
|
2057
|
+
/**
|
|
2058
|
+
* Mark an annotation as resolved. Returns the updated record, or
|
|
2059
|
+
* `null` if no annotation with that id exists in this session.
|
|
2060
|
+
* Idempotent: resolving an already-resolved annotation refreshes
|
|
2061
|
+
* `resolvedAt` / `resolvedBy` to the latest call.
|
|
2062
|
+
*/
|
|
2063
|
+
async resolve(input) {
|
|
2064
|
+
let updated = null;
|
|
2065
|
+
await this.enqueue(input.sessionId, async () => {
|
|
2066
|
+
const all = await this.list(input.sessionId);
|
|
2067
|
+
const idx = all.findIndex((a) => a.id === input.annotationId);
|
|
2068
|
+
if (idx === -1) {
|
|
2069
|
+
updated = null;
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
const next = {
|
|
2073
|
+
...all[idx],
|
|
2074
|
+
resolved: true,
|
|
2075
|
+
resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2076
|
+
resolvedBy: input.resolvedBy
|
|
2077
|
+
};
|
|
2078
|
+
all[idx] = next;
|
|
2079
|
+
await this.writeFile(input.sessionId, { version: FILE_VERSION, annotations: all });
|
|
2080
|
+
updated = next;
|
|
2081
|
+
});
|
|
2082
|
+
return updated;
|
|
2083
|
+
}
|
|
2084
|
+
// ── Internals ──────────────────────────────────────────────────────────
|
|
2085
|
+
filePath(sessionId) {
|
|
2086
|
+
if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
|
|
2087
|
+
throw new Error(`Invalid sessionId: ${sessionId}`);
|
|
2088
|
+
}
|
|
2089
|
+
return path11.join(this.dir, `${sessionId}.annotations.json`);
|
|
2090
|
+
}
|
|
2091
|
+
async readFile(sessionId) {
|
|
2092
|
+
const fp = this.filePath(sessionId);
|
|
2093
|
+
try {
|
|
2094
|
+
const raw = await fsp.readFile(fp, "utf8");
|
|
2095
|
+
const parsed = JSON.parse(raw);
|
|
2096
|
+
if (parsed.version !== FILE_VERSION) {
|
|
2097
|
+
return { version: FILE_VERSION, annotations: [] };
|
|
2098
|
+
}
|
|
2099
|
+
return parsed;
|
|
2100
|
+
} catch (err) {
|
|
2101
|
+
if (err.code === "ENOENT") return null;
|
|
2102
|
+
return { version: FILE_VERSION, annotations: [] };
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
async writeFile(sessionId, file) {
|
|
2106
|
+
const fp = this.filePath(sessionId);
|
|
2107
|
+
await atomicWrite(fp, JSON.stringify(file, null, 2));
|
|
2108
|
+
}
|
|
2109
|
+
/**
|
|
2110
|
+
* Serialize writes per-sessionId. We chain promises instead of
|
|
2111
|
+
* using a Mutex class so the contract is obvious from the
|
|
2112
|
+
* call-site: `enqueue(sid, fn)` runs `fn` after every prior
|
|
2113
|
+
* enqueue for `sid` has settled.
|
|
2114
|
+
*/
|
|
2115
|
+
enqueue(sessionId, fn) {
|
|
2116
|
+
const prev = this.writeChains.get(sessionId) ?? Promise.resolve();
|
|
2117
|
+
const next = prev.then(fn, fn);
|
|
2118
|
+
this.writeChains.set(
|
|
2119
|
+
sessionId,
|
|
2120
|
+
next.catch(() => void 0)
|
|
2121
|
+
);
|
|
2122
|
+
return next;
|
|
2123
|
+
}
|
|
2124
|
+
};
|
|
2125
|
+
function stableStringify(value) {
|
|
2126
|
+
return JSON.stringify(sortKeys(value));
|
|
2127
|
+
}
|
|
2128
|
+
function sortKeys(value) {
|
|
2129
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
2130
|
+
if (value && typeof value === "object") {
|
|
2131
|
+
const obj = value;
|
|
2132
|
+
const sorted = {};
|
|
2133
|
+
for (const key of Object.keys(obj).sort()) {
|
|
2134
|
+
sorted[key] = sortKeys(obj[key]);
|
|
2135
|
+
}
|
|
2136
|
+
return sorted;
|
|
2137
|
+
}
|
|
2138
|
+
return value;
|
|
2139
|
+
}
|
|
2140
|
+
function hashRequest(request) {
|
|
2141
|
+
const payload = {
|
|
2142
|
+
model: request.model,
|
|
2143
|
+
system: request.system,
|
|
2144
|
+
messages: request.messages,
|
|
2145
|
+
tools: request.tools,
|
|
2146
|
+
maxTokens: request.maxTokens,
|
|
2147
|
+
temperature: request.temperature,
|
|
2148
|
+
topP: request.topP,
|
|
2149
|
+
stopSequences: request.stopSequences,
|
|
2150
|
+
toolChoice: request.toolChoice
|
|
2151
|
+
};
|
|
2152
|
+
const json = stableStringify(payload);
|
|
2153
|
+
const digest = createHash("sha256").update(json, "utf8").digest("hex");
|
|
2154
|
+
return `sha256:${digest}`;
|
|
2155
|
+
}
|
|
2156
|
+
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
2157
|
+
var ReplayLogStore = class {
|
|
2158
|
+
dir;
|
|
2159
|
+
writeChains = /* @__PURE__ */ new Map();
|
|
2160
|
+
/** Per-session hash → entry index, kept in memory after the first load. */
|
|
2161
|
+
cache = /* @__PURE__ */ new Map();
|
|
2162
|
+
maxEntries;
|
|
2163
|
+
constructor(opts) {
|
|
2164
|
+
this.dir = opts.dir;
|
|
2165
|
+
this.maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
2166
|
+
}
|
|
2167
|
+
// ── Writes ──────────────────────────────────────────────────────────────
|
|
2168
|
+
/**
|
|
2169
|
+
* Record a request/response pair. Idempotent on hash: a second
|
|
2170
|
+
* `record` for the same hash is a no-op (the existing entry wins).
|
|
2171
|
+
* Returns the hash.
|
|
2172
|
+
*/
|
|
2173
|
+
async record(input) {
|
|
2174
|
+
const hash = hashRequest(input.request);
|
|
2175
|
+
await this.enqueue(input.sessionId, async () => {
|
|
2176
|
+
const cache = await this.ensureCache(input.sessionId);
|
|
2177
|
+
if (cache.has(hash)) return;
|
|
2178
|
+
const entry = {
|
|
2179
|
+
hash,
|
|
2180
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2181
|
+
request: input.request,
|
|
2182
|
+
response: input.response
|
|
2183
|
+
};
|
|
2184
|
+
let all = [...cache.values(), entry];
|
|
2185
|
+
if (all.length > this.maxEntries) {
|
|
2186
|
+
const evictCount = all.length - this.maxEntries;
|
|
2187
|
+
all = all.slice(evictCount);
|
|
2188
|
+
}
|
|
2189
|
+
await this.writeAll(input.sessionId, all);
|
|
2190
|
+
cache.clear();
|
|
2191
|
+
for (const e of all) cache.set(e.hash, e);
|
|
2192
|
+
});
|
|
2193
|
+
return hash;
|
|
2194
|
+
}
|
|
2195
|
+
// ── Reads ───────────────────────────────────────────────────────────────
|
|
2196
|
+
/**
|
|
2197
|
+
* Look up an entry by hash. Returns `null` when the request has
|
|
2198
|
+
* not been recorded for this session. O(1) after the first call
|
|
2199
|
+
* per session (in-memory cache).
|
|
2200
|
+
*/
|
|
2201
|
+
async lookup(sessionId, hash) {
|
|
2202
|
+
const cache = await this.ensureCache(sessionId);
|
|
2203
|
+
return cache.get(hash) ?? null;
|
|
2204
|
+
}
|
|
2205
|
+
/** All recorded entries for a session, in insertion order. */
|
|
2206
|
+
async load(sessionId) {
|
|
2207
|
+
const cache = await this.ensureCache(sessionId);
|
|
2208
|
+
return [...cache.values()];
|
|
2209
|
+
}
|
|
2210
|
+
/**
|
|
2211
|
+
* List every session id that has a replay log in the store dir.
|
|
2212
|
+
* Returns an array of `{ sessionId, entryCount, path }` sorted
|
|
2213
|
+
* by sessionId for stable output. Used by `wstack replay --list`.
|
|
2214
|
+
*/
|
|
2215
|
+
async list() {
|
|
2216
|
+
let entries;
|
|
2217
|
+
try {
|
|
2218
|
+
entries = await fsp.readdir(this.dir);
|
|
2219
|
+
} catch (err) {
|
|
2220
|
+
if (err.code === "ENOENT") return [];
|
|
2221
|
+
return [];
|
|
2222
|
+
}
|
|
2223
|
+
const out = [];
|
|
2224
|
+
for (const name of entries) {
|
|
2225
|
+
if (!name.endsWith(".replay.jsonl")) continue;
|
|
2226
|
+
const sessionId = name.slice(0, -".replay.jsonl".length);
|
|
2227
|
+
const all = await this.load(sessionId);
|
|
2228
|
+
out.push({
|
|
2229
|
+
sessionId,
|
|
2230
|
+
entryCount: all.length,
|
|
2231
|
+
path: path11.join(this.dir, name)
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
2234
|
+
return out.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
|
|
2235
|
+
}
|
|
2236
|
+
// ── Internals ───────────────────────────────────────────────────────────
|
|
2237
|
+
filePath(sessionId) {
|
|
2238
|
+
if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
|
|
2239
|
+
throw new Error(`Invalid sessionId: ${sessionId}`);
|
|
2240
|
+
}
|
|
2241
|
+
return path11.join(this.dir, `${sessionId}.replay.jsonl`);
|
|
2242
|
+
}
|
|
2243
|
+
async readAll(sessionId) {
|
|
2244
|
+
const fp = this.filePath(sessionId);
|
|
2245
|
+
try {
|
|
2246
|
+
const raw = await fsp.readFile(fp, "utf8");
|
|
2247
|
+
const out = [];
|
|
2248
|
+
for (const line of raw.split("\n")) {
|
|
2249
|
+
if (!line.trim()) continue;
|
|
2250
|
+
try {
|
|
2251
|
+
const parsed = JSON.parse(line);
|
|
2252
|
+
if ("entry" in parsed && parsed.entry) {
|
|
2253
|
+
out.push(parsed.entry);
|
|
2254
|
+
} else {
|
|
2255
|
+
out.push(parsed);
|
|
2256
|
+
}
|
|
2257
|
+
} catch {
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
return out;
|
|
2261
|
+
} catch (err) {
|
|
2262
|
+
if (err.code === "ENOENT") return [];
|
|
2263
|
+
return [];
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
async writeAll(sessionId, entries) {
|
|
2267
|
+
const fp = this.filePath(sessionId);
|
|
2268
|
+
const body = entries.map((e) => JSON.stringify(e)).join("\n") + (entries.length ? "\n" : "");
|
|
2269
|
+
await atomicWrite(fp, body);
|
|
2270
|
+
}
|
|
2271
|
+
async ensureCache(sessionId) {
|
|
2272
|
+
let cache = this.cache.get(sessionId);
|
|
2273
|
+
if (cache) return cache;
|
|
2274
|
+
const all = await this.readAll(sessionId);
|
|
2275
|
+
cache = /* @__PURE__ */ new Map();
|
|
2276
|
+
for (const e of all) cache.set(e.hash, e);
|
|
2277
|
+
this.cache.set(sessionId, cache);
|
|
2278
|
+
return cache;
|
|
2279
|
+
}
|
|
2280
|
+
enqueue(sessionId, fn) {
|
|
2281
|
+
const prev = this.writeChains.get(sessionId) ?? Promise.resolve();
|
|
2282
|
+
const next = prev.then(fn, fn);
|
|
2283
|
+
this.writeChains.set(
|
|
2284
|
+
sessionId,
|
|
2285
|
+
next.catch(() => void 0)
|
|
2286
|
+
);
|
|
2287
|
+
return next;
|
|
2288
|
+
}
|
|
2289
|
+
};
|
|
2290
|
+
var SessionRecovery = class {
|
|
2291
|
+
constructor(dir) {
|
|
2292
|
+
this.dir = dir;
|
|
2293
|
+
}
|
|
2294
|
+
dir;
|
|
2295
|
+
/**
|
|
2296
|
+
* Scan a session log and return a `StaleSession` if and only
|
|
2297
|
+
* if the last event is an `in_flight_start` without a matching
|
|
2298
|
+
* `in_flight_end`. Returns `null` when:
|
|
2299
|
+
* - the log does not exist;
|
|
2300
|
+
* - the log is empty;
|
|
2301
|
+
* - the last event is `in_flight_end` (clean shutdown);
|
|
2302
|
+
* - the last event is something else (e.g. an unannotated
|
|
2303
|
+
* legacy log without in-flight markers).
|
|
2304
|
+
*/
|
|
2305
|
+
async detectStale(sessionId) {
|
|
2306
|
+
const fp = this.filePath(sessionId);
|
|
2307
|
+
let raw;
|
|
2308
|
+
try {
|
|
2309
|
+
raw = await fsp.readFile(fp, "utf8");
|
|
2310
|
+
} catch (err) {
|
|
2311
|
+
if (err.code === "ENOENT") return null;
|
|
2312
|
+
return null;
|
|
2313
|
+
}
|
|
2314
|
+
return this.parseForStale(sessionId, fp, raw);
|
|
2315
|
+
}
|
|
2316
|
+
/**
|
|
2317
|
+
* Generate a recovery plan for a session. The plan describes
|
|
2318
|
+
* "what would be re-executed" if the user chose to resume —
|
|
2319
|
+
* everything after the last `checkpoint` event, plus the
|
|
2320
|
+
* dangling in-flight marker if present.
|
|
2321
|
+
*
|
|
2322
|
+
* Returns a non-null plan for ANY session that has at least
|
|
2323
|
+
* one event after a checkpoint (or, for legacy sessions, at
|
|
2324
|
+
* least one event). Pure read; no mutation.
|
|
2325
|
+
*/
|
|
2326
|
+
async recover(sessionId) {
|
|
2327
|
+
const fp = this.filePath(sessionId);
|
|
2328
|
+
let raw;
|
|
2329
|
+
try {
|
|
2330
|
+
raw = await fsp.readFile(fp, "utf8");
|
|
2331
|
+
} catch (err) {
|
|
2332
|
+
if (err.code === "ENOENT") return null;
|
|
2333
|
+
return null;
|
|
2334
|
+
}
|
|
2335
|
+
const events = [];
|
|
2336
|
+
for (const line of raw.split("\n")) {
|
|
2337
|
+
if (!line.trim()) continue;
|
|
2338
|
+
try {
|
|
2339
|
+
events.push(JSON.parse(line));
|
|
2340
|
+
} catch {
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
if (events.length === 0) return null;
|
|
2344
|
+
let lastCheckpoint = null;
|
|
2345
|
+
let lastCheckpointIdx = -1;
|
|
2346
|
+
for (let i = 0; i < events.length; i++) {
|
|
2347
|
+
if (events[i].type === "checkpoint") {
|
|
2348
|
+
lastCheckpoint = events[i];
|
|
2349
|
+
lastCheckpointIdx = i;
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
const pendingEvents = lastCheckpointIdx >= 0 ? events.slice(lastCheckpointIdx + 1) : events;
|
|
2353
|
+
const lastEv = events[events.length - 1];
|
|
2354
|
+
const inFlightStart = lastEv.type === "in_flight_start" ? lastEv : null;
|
|
2355
|
+
const context = inFlightStart && inFlightStart.type === "in_flight_start" ? inFlightStart.context : null;
|
|
2356
|
+
return {
|
|
2357
|
+
sessionId,
|
|
2358
|
+
stale: inFlightStart !== null,
|
|
2359
|
+
lastCheckpoint,
|
|
2360
|
+
pendingEvents,
|
|
2361
|
+
inFlightStart,
|
|
2362
|
+
context
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
2365
|
+
/**
|
|
2366
|
+
* List every stale session in a directory. Returns an array
|
|
2367
|
+
* (possibly empty) sorted by `lastEventTs` descending — most
|
|
2368
|
+
* recent crash first.
|
|
2369
|
+
*/
|
|
2370
|
+
async listResumable() {
|
|
2371
|
+
let entries;
|
|
2372
|
+
try {
|
|
2373
|
+
entries = await fsp.readdir(this.dir);
|
|
2374
|
+
} catch (err) {
|
|
2375
|
+
if (err.code === "ENOENT") return [];
|
|
2376
|
+
return [];
|
|
2377
|
+
}
|
|
2378
|
+
const out = [];
|
|
2379
|
+
for (const name of entries) {
|
|
2380
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
2381
|
+
const sessionId = name.slice(0, -".jsonl".length);
|
|
2382
|
+
if (sessionId.includes(".replay") || sessionId.includes(".annotations")) continue;
|
|
2383
|
+
const stale = await this.detectStale(sessionId);
|
|
2384
|
+
if (stale) out.push(stale);
|
|
2385
|
+
}
|
|
2386
|
+
return out.sort((a, b) => b.lastEventTs.localeCompare(a.lastEventTs));
|
|
2387
|
+
}
|
|
2388
|
+
// ── Internals ──────────────────────────────────────────────────────────
|
|
2389
|
+
filePath(sessionId) {
|
|
2390
|
+
if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
|
|
2391
|
+
throw new Error(`Invalid sessionId: ${sessionId}`);
|
|
2392
|
+
}
|
|
2393
|
+
return path11.join(this.dir, `${sessionId}.jsonl`);
|
|
2394
|
+
}
|
|
2395
|
+
/**
|
|
2396
|
+
* Stream-parse the last few lines of a JSONL log. We do NOT load
|
|
2397
|
+
* the whole file into memory — for long-running sessions the log
|
|
2398
|
+
* can be megabytes. Instead we read tail-ward and find the last
|
|
2399
|
+
* `in_flight_start` / `in_flight_end` pair.
|
|
2400
|
+
*/
|
|
2401
|
+
async parseForStale(sessionId, fp, raw) {
|
|
2402
|
+
const lines = raw.split("\n");
|
|
2403
|
+
let lastEvent = null;
|
|
2404
|
+
let eventCount = 0;
|
|
2405
|
+
for (const line of lines) {
|
|
2406
|
+
if (!line.trim()) continue;
|
|
2407
|
+
try {
|
|
2408
|
+
const ev = JSON.parse(line);
|
|
2409
|
+
lastEvent = ev;
|
|
2410
|
+
eventCount++;
|
|
2411
|
+
} catch {
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
if (!lastEvent) return null;
|
|
2415
|
+
if (lastEvent.type === "in_flight_start") {
|
|
2416
|
+
return {
|
|
2417
|
+
sessionId,
|
|
2418
|
+
path: fp,
|
|
2419
|
+
lastEventTs: lastEvent.ts,
|
|
2420
|
+
context: lastEvent.context,
|
|
2421
|
+
eventCount
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2424
|
+
return null;
|
|
2425
|
+
}
|
|
2426
|
+
};
|
|
2427
|
+
var GENESIS_PREV = "0".repeat(64);
|
|
2428
|
+
var ToolAuditLog = class {
|
|
2429
|
+
dir;
|
|
2430
|
+
/** In-memory cache of the last entry's hash (per session), to compute chains efficiently. */
|
|
2431
|
+
tailHash = /* @__PURE__ */ new Map();
|
|
2432
|
+
writeChains = /* @__PURE__ */ new Map();
|
|
2433
|
+
constructor(opts) {
|
|
2434
|
+
this.dir = opts.dir;
|
|
2435
|
+
}
|
|
2436
|
+
/**
|
|
2437
|
+
* Append a tool call/result pair to the chain. Returns the
|
|
2438
|
+
* resulting entry. Idempotency is not guaranteed — if you
|
|
2439
|
+
* record the same tool_use twice you get two entries. That's
|
|
2440
|
+
* intentional: the audit log is a record, not a cache.
|
|
2441
|
+
*/
|
|
2442
|
+
async record(input) {
|
|
2443
|
+
let entry = null;
|
|
2444
|
+
await this.enqueue(input.sessionId, async () => {
|
|
2445
|
+
const prevHash = this.tailHash.get(input.sessionId) ?? GENESIS_PREV;
|
|
2446
|
+
const id = randomUUID();
|
|
2447
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2448
|
+
const content = {
|
|
2449
|
+
id,
|
|
2450
|
+
ts,
|
|
2451
|
+
prevHash,
|
|
2452
|
+
toolName: input.toolName,
|
|
2453
|
+
toolUseId: input.toolUseId,
|
|
2454
|
+
input: input.input,
|
|
2455
|
+
output: input.output,
|
|
2456
|
+
isError: input.isError
|
|
2457
|
+
};
|
|
2458
|
+
const hash = createHash("sha256").update(stableStringify2(content), "utf8").digest("hex");
|
|
2459
|
+
entry = {
|
|
2460
|
+
index: this.tailHash.has(input.sessionId) ? -1 : 0,
|
|
2461
|
+
// placeholder; recomputed below
|
|
2462
|
+
id,
|
|
2463
|
+
ts,
|
|
2464
|
+
prevHash,
|
|
2465
|
+
hash,
|
|
2466
|
+
toolName: input.toolName,
|
|
2467
|
+
toolUseId: input.toolUseId,
|
|
2468
|
+
input: input.input,
|
|
2469
|
+
output: input.output,
|
|
2470
|
+
isError: input.isError
|
|
2471
|
+
};
|
|
2472
|
+
const existing = await this.readAll(input.sessionId);
|
|
2473
|
+
const index = existing.length;
|
|
2474
|
+
const finalContent = { ...content, index };
|
|
2475
|
+
const finalHash = createHash("sha256").update(stableStringify2(finalContent), "utf8").digest("hex");
|
|
2476
|
+
entry = { ...entry, index, hash: finalHash };
|
|
2477
|
+
await this.appendLine(input.sessionId, entry);
|
|
2478
|
+
this.tailHash.set(input.sessionId, finalHash);
|
|
2479
|
+
});
|
|
2480
|
+
return entry;
|
|
2481
|
+
}
|
|
2482
|
+
/**
|
|
2483
|
+
* Walk the chain and verify every entry's hash and prevHash.
|
|
2484
|
+
* Returns a structured verdict — never throws.
|
|
2485
|
+
*/
|
|
2486
|
+
async verify(sessionId) {
|
|
2487
|
+
const entries = await this.readAll(sessionId);
|
|
2488
|
+
if (entries.length === 0) return { ok: true, entries: 0 };
|
|
2489
|
+
if (entries[0].prevHash !== GENESIS_PREV) {
|
|
2490
|
+
return {
|
|
2491
|
+
ok: false,
|
|
2492
|
+
brokenAt: 0,
|
|
2493
|
+
reason: "first entry is not the genesis (prevHash != 0\u20260)"
|
|
2494
|
+
};
|
|
2495
|
+
}
|
|
2496
|
+
let prevHash = GENESIS_PREV;
|
|
2497
|
+
for (let i = 0; i < entries.length; i++) {
|
|
2498
|
+
const e = entries[i];
|
|
2499
|
+
if (e.prevHash !== prevHash) {
|
|
2500
|
+
return {
|
|
2501
|
+
ok: false,
|
|
2502
|
+
brokenAt: i,
|
|
2503
|
+
reason: `prevHash mismatch at entry ${i} (expected ${prevHash.slice(0, 8)}\u2026, got ${e.prevHash.slice(0, 8)}\u2026)`
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
const content = {
|
|
2507
|
+
id: e.id,
|
|
2508
|
+
ts: e.ts,
|
|
2509
|
+
prevHash: e.prevHash,
|
|
2510
|
+
toolName: e.toolName,
|
|
2511
|
+
toolUseId: e.toolUseId,
|
|
2512
|
+
input: e.input,
|
|
2513
|
+
output: e.output,
|
|
2514
|
+
isError: e.isError,
|
|
2515
|
+
index: e.index
|
|
2516
|
+
};
|
|
2517
|
+
const expectedHash = createHash("sha256").update(stableStringify2(content), "utf8").digest("hex");
|
|
2518
|
+
if (expectedHash !== e.hash) {
|
|
2519
|
+
return {
|
|
2520
|
+
ok: false,
|
|
2521
|
+
brokenAt: i,
|
|
2522
|
+
reason: `hash mismatch at entry ${i} (entry content was modified)`
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2525
|
+
prevHash = e.hash;
|
|
2526
|
+
}
|
|
2527
|
+
return { ok: true, entries: entries.length };
|
|
2528
|
+
}
|
|
2529
|
+
/** All entries for a session, in insertion order. */
|
|
2530
|
+
async load(sessionId) {
|
|
2531
|
+
return this.readAll(sessionId);
|
|
2532
|
+
}
|
|
2533
|
+
// ── Internals ────────────────────────────────────────────────────────────
|
|
2534
|
+
filePath(sessionId) {
|
|
2535
|
+
if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
|
|
2536
|
+
throw new Error(`Invalid sessionId: ${sessionId}`);
|
|
2537
|
+
}
|
|
2538
|
+
return path11.join(this.dir, `${sessionId}.audit.jsonl`);
|
|
2539
|
+
}
|
|
2540
|
+
async readAll(sessionId) {
|
|
2541
|
+
const fp = this.filePath(sessionId);
|
|
2542
|
+
try {
|
|
2543
|
+
const raw = await fsp.readFile(fp, "utf8");
|
|
2544
|
+
const out = [];
|
|
2545
|
+
for (const line of raw.split("\n")) {
|
|
2546
|
+
if (!line.trim()) continue;
|
|
2547
|
+
try {
|
|
2548
|
+
out.push(JSON.parse(line));
|
|
2549
|
+
} catch {
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
return out;
|
|
2553
|
+
} catch (err) {
|
|
2554
|
+
if (err.code === "ENOENT") return [];
|
|
2555
|
+
return [];
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
async appendLine(sessionId, entry) {
|
|
2559
|
+
const fp = this.filePath(sessionId);
|
|
2560
|
+
const existing = await this.readAll(sessionId);
|
|
2561
|
+
const all = [...existing, entry];
|
|
2562
|
+
await atomicWrite(fp, all.map((e) => JSON.stringify(e)).join("\n") + (all.length ? "\n" : ""));
|
|
2563
|
+
}
|
|
2564
|
+
enqueue(sessionId, fn) {
|
|
2565
|
+
const prev = this.writeChains.get(sessionId) ?? Promise.resolve();
|
|
2566
|
+
const next = prev.then(fn, fn);
|
|
2567
|
+
this.writeChains.set(
|
|
2568
|
+
sessionId,
|
|
2569
|
+
next.catch(() => void 0)
|
|
2570
|
+
);
|
|
2571
|
+
return next;
|
|
2572
|
+
}
|
|
2573
|
+
};
|
|
2574
|
+
function stableStringify2(value) {
|
|
2575
|
+
return JSON.stringify(sortKeys2(value));
|
|
2576
|
+
}
|
|
2577
|
+
function sortKeys2(value) {
|
|
2578
|
+
if (Array.isArray(value)) return value.map(sortKeys2);
|
|
2579
|
+
if (value && typeof value === "object") {
|
|
2580
|
+
const obj = value;
|
|
2581
|
+
const sorted = {};
|
|
2582
|
+
for (const key of Object.keys(obj).sort()) {
|
|
2583
|
+
sorted[key] = sortKeys2(obj[key]);
|
|
2584
|
+
}
|
|
2585
|
+
return sorted;
|
|
2586
|
+
}
|
|
2587
|
+
return value;
|
|
2588
|
+
}
|
|
1887
2589
|
|
|
1888
2590
|
// src/storage/session-analyzer.ts
|
|
1889
2591
|
var SessionAnalyzer = class {
|
|
@@ -1989,7 +2691,7 @@ var DefaultSessionRewinder = class {
|
|
|
1989
2691
|
sessionsDir;
|
|
1990
2692
|
projectRoot;
|
|
1991
2693
|
async listCheckpoints(sessionId) {
|
|
1992
|
-
const file =
|
|
2694
|
+
const file = path11.join(this.sessionsDir, `${sessionId}.jsonl`);
|
|
1993
2695
|
const raw = await fsp.readFile(file, "utf8");
|
|
1994
2696
|
const events = parseEvents(raw);
|
|
1995
2697
|
const fileCountMap = /* @__PURE__ */ new Map();
|
|
@@ -2014,7 +2716,7 @@ var DefaultSessionRewinder = class {
|
|
|
2014
2716
|
return checkpoints;
|
|
2015
2717
|
}
|
|
2016
2718
|
async rewindToCheckpoint(sessionId, checkpointIndex) {
|
|
2017
|
-
const file =
|
|
2719
|
+
const file = path11.join(this.sessionsDir, `${sessionId}.jsonl`);
|
|
2018
2720
|
const raw = await fsp.readFile(file, "utf8");
|
|
2019
2721
|
const events = parseEvents(raw);
|
|
2020
2722
|
let targetIdx = -1;
|
|
@@ -2049,7 +2751,7 @@ var DefaultSessionRewinder = class {
|
|
|
2049
2751
|
return { ...result, toPromptIndex: checkpointIndex, removedEvents };
|
|
2050
2752
|
}
|
|
2051
2753
|
async rewindLastN(sessionId, n) {
|
|
2052
|
-
const file =
|
|
2754
|
+
const file = path11.join(this.sessionsDir, `${sessionId}.jsonl`);
|
|
2053
2755
|
const raw = await fsp.readFile(file, "utf8");
|
|
2054
2756
|
const events = parseEvents(raw);
|
|
2055
2757
|
const checkpoints = [];
|
|
@@ -2078,7 +2780,7 @@ var DefaultSessionRewinder = class {
|
|
|
2078
2780
|
return { ...result, toPromptIndex: targetIndex, removedEvents: snapshotsToRevert.length };
|
|
2079
2781
|
}
|
|
2080
2782
|
async rewindToStart(sessionId) {
|
|
2081
|
-
const file =
|
|
2783
|
+
const file = path11.join(this.sessionsDir, `${sessionId}.jsonl`);
|
|
2082
2784
|
const raw = await fsp.readFile(file, "utf8");
|
|
2083
2785
|
const events = parseEvents(raw);
|
|
2084
2786
|
const allSnapshots = [];
|
|
@@ -2114,10 +2816,10 @@ async function revertSnapshots(snapshots, projectRoot) {
|
|
|
2114
2816
|
for (const snapshot of snapshots) {
|
|
2115
2817
|
for (const file of snapshot.files) {
|
|
2116
2818
|
try {
|
|
2117
|
-
const absPath =
|
|
2118
|
-
const root =
|
|
2119
|
-
const rel =
|
|
2120
|
-
if (rel.startsWith("..") ||
|
|
2819
|
+
const absPath = path11.resolve(file.path);
|
|
2820
|
+
const root = path11.resolve(projectRoot);
|
|
2821
|
+
const rel = path11.relative(root, absPath);
|
|
2822
|
+
if (rel.startsWith("..") || path11.isAbsolute(rel)) {
|
|
2121
2823
|
errors.push(`${file.path}: path resolves outside project root \u2014 skipping`);
|
|
2122
2824
|
continue;
|
|
2123
2825
|
}
|
|
@@ -2676,8 +3378,8 @@ var FsError = class extends WrongStackError {
|
|
|
2676
3378
|
// src/storage/goal-store.ts
|
|
2677
3379
|
var MAX_JOURNAL_ENTRIES = 500;
|
|
2678
3380
|
function goalFilePath(projectRoot) {
|
|
2679
|
-
const hash = createHash("sha256").update(
|
|
2680
|
-
return
|
|
3381
|
+
const hash = createHash("sha256").update(path11.resolve(projectRoot)).digest("hex").slice(0, 12);
|
|
3382
|
+
return path11.join(os.homedir(), ".wrongstack", "projects", hash, "goal.json");
|
|
2681
3383
|
}
|
|
2682
3384
|
async function loadGoal(filePath) {
|
|
2683
3385
|
let raw;
|
|
@@ -2785,14 +3487,14 @@ var DefaultPromptStore = class {
|
|
|
2785
3487
|
}
|
|
2786
3488
|
async list() {
|
|
2787
3489
|
await ensureDir(this.dir);
|
|
2788
|
-
|
|
3490
|
+
const entries = [];
|
|
2789
3491
|
try {
|
|
2790
3492
|
const files = await fsp.readdir(this.dir);
|
|
2791
3493
|
for (const file of files) {
|
|
2792
3494
|
if (!file.endsWith(".json")) continue;
|
|
2793
3495
|
try {
|
|
2794
3496
|
const raw = JSON.parse(
|
|
2795
|
-
await fsp.readFile(
|
|
3497
|
+
await fsp.readFile(path11.join(this.dir, file), "utf8")
|
|
2796
3498
|
);
|
|
2797
3499
|
entries.push(raw.entry);
|
|
2798
3500
|
} catch {
|
|
@@ -2805,7 +3507,7 @@ var DefaultPromptStore = class {
|
|
|
2805
3507
|
);
|
|
2806
3508
|
}
|
|
2807
3509
|
async get(id) {
|
|
2808
|
-
const file =
|
|
3510
|
+
const file = path11.join(this.dir, `${id}.json`);
|
|
2809
3511
|
try {
|
|
2810
3512
|
const raw = JSON.parse(await fsp.readFile(file, "utf8"));
|
|
2811
3513
|
return raw.entry;
|
|
@@ -2815,12 +3517,12 @@ var DefaultPromptStore = class {
|
|
|
2815
3517
|
}
|
|
2816
3518
|
async save(entry) {
|
|
2817
3519
|
await ensureDir(this.dir);
|
|
2818
|
-
const file =
|
|
3520
|
+
const file = path11.join(this.dir, `${entry.id}.json`);
|
|
2819
3521
|
const raw = { version: 1, entry };
|
|
2820
3522
|
await atomicWrite(file, JSON.stringify(raw, null, 2));
|
|
2821
3523
|
}
|
|
2822
3524
|
async delete(id) {
|
|
2823
|
-
const file =
|
|
3525
|
+
const file = path11.join(this.dir, `${id}.json`);
|
|
2824
3526
|
try {
|
|
2825
3527
|
await fsp.unlink(file);
|
|
2826
3528
|
return true;
|
|
@@ -2854,7 +3556,7 @@ var CloudSync = class {
|
|
|
2854
3556
|
this.paths = paths;
|
|
2855
3557
|
this.getConfig = getConfig;
|
|
2856
3558
|
this.setConfig = setConfig;
|
|
2857
|
-
this.statePath =
|
|
3559
|
+
this.statePath = path11.join(paths.globalRoot, "sync-state.json");
|
|
2858
3560
|
}
|
|
2859
3561
|
paths;
|
|
2860
3562
|
getConfig;
|
|
@@ -2904,7 +3606,25 @@ var CloudSync = class {
|
|
|
2904
3606
|
baseTreeSha,
|
|
2905
3607
|
`Sync ${cfg.categories.join(", ")} \u2014 ${(/* @__PURE__ */ new Date()).toISOString()}`
|
|
2906
3608
|
);
|
|
2907
|
-
|
|
3609
|
+
try {
|
|
3610
|
+
await this.updateRef(token, owner, repoName, branch, commitSha);
|
|
3611
|
+
} catch (err) {
|
|
3612
|
+
if (err instanceof Error && err.message.includes("422")) {
|
|
3613
|
+
const remote = await this.getRef(token, owner, repoName, branch);
|
|
3614
|
+
const currentSha = remote.object.sha;
|
|
3615
|
+
const rebasedCommitSha = await this.createCommit(
|
|
3616
|
+
token,
|
|
3617
|
+
owner,
|
|
3618
|
+
repoName,
|
|
3619
|
+
newTreeSha,
|
|
3620
|
+
currentSha,
|
|
3621
|
+
`Sync ${cfg.categories.join(", ")} \u2014 ${(/* @__PURE__ */ new Date()).toISOString()}`
|
|
3622
|
+
);
|
|
3623
|
+
await this.updateRef(token, owner, repoName, branch, rebasedCommitSha);
|
|
3624
|
+
} else {
|
|
3625
|
+
throw err;
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
2908
3628
|
const syncState = {
|
|
2909
3629
|
version: 1,
|
|
2910
3630
|
sha: commitSha,
|
|
@@ -2941,9 +3661,9 @@ var CloudSync = class {
|
|
|
2941
3661
|
const localPath = this.categoryToPath(cat);
|
|
2942
3662
|
if (!localPath) continue;
|
|
2943
3663
|
const rel = segments.slice(2).join("/");
|
|
2944
|
-
const destPath = rel ?
|
|
3664
|
+
const destPath = rel ? path11.join(localPath, rel) : localPath;
|
|
2945
3665
|
const blobData = await this.getBlob(token, owner, repoName, entry.sha);
|
|
2946
|
-
await fsp.mkdir(
|
|
3666
|
+
await fsp.mkdir(path11.dirname(destPath), { recursive: true });
|
|
2947
3667
|
await fsp.writeFile(destPath, Buffer.from(blobData, "base64"));
|
|
2948
3668
|
}
|
|
2949
3669
|
const localRev = await this.hashLocalCategories(cfg.categories);
|
|
@@ -3048,7 +3768,7 @@ var CloudSync = class {
|
|
|
3048
3768
|
const files = await this.walkDir(localPath, localPath);
|
|
3049
3769
|
for (const file of files) {
|
|
3050
3770
|
const content = await fsp.readFile(file, "utf8");
|
|
3051
|
-
const rel =
|
|
3771
|
+
const rel = path11.relative(localPath, file).replace(/\\/g, "/");
|
|
3052
3772
|
entries.push({ path: `data/${cat}/${rel}`, content, mode: "100644" });
|
|
3053
3773
|
hashes.push(content);
|
|
3054
3774
|
}
|
|
@@ -3105,7 +3825,7 @@ var CloudSync = class {
|
|
|
3105
3825
|
const results = [];
|
|
3106
3826
|
const entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
3107
3827
|
for (const entry of entries) {
|
|
3108
|
-
const full =
|
|
3828
|
+
const full = path11.join(dir, entry.name);
|
|
3109
3829
|
if (entry.isDirectory()) {
|
|
3110
3830
|
results.push(...await this.walkDir(full, base));
|
|
3111
3831
|
} else {
|
|
@@ -3126,6 +3846,6 @@ function timeAgo(iso) {
|
|
|
3126
3846
|
return `${days}d ago`;
|
|
3127
3847
|
}
|
|
3128
3848
|
|
|
3129
|
-
export { ALL_SYNC_CATEGORIES, CloudSync, ConfigMigrationError, DEFAULT_CONFIG_MIGRATIONS, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultMemoryStore, DefaultPromptStore, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DirectorStateCheckpoint, MAX_JOURNAL_ENTRIES, QueueStore, RecoveryLock, SessionAnalyzer, addPlanItem, appendJournal, attachPlanCheckpoint, attachTodosCheckpoint, clearPlan, deriveTodosFromPlanItem, emptyGoal, emptyPlan, formatGoal, formatPlan, formatPlanTemplates, getPlanTemplate, goalFilePath, listPlanTemplates, loadDirectorState, loadGoal, loadPlan, loadTodosCheckpoint, removePlanItem, runConfigMigrations, saveGoal, savePlan, saveTodosCheckpoint, setPlanItemStatus, summarizeUsage };
|
|
3849
|
+
export { ALL_SYNC_CATEGORIES, AnnotationsStore, CloudSync, ConfigMigrationError, DEFAULT_CONFIG_MIGRATIONS, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultMemoryStore, DefaultPromptStore, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DirectorStateCheckpoint, MAX_JOURNAL_ENTRIES, QueueStore, RecoveryLock, ReplayLogStore, SessionAnalyzer, SessionRecovery, ToolAuditLog, addPlanItem, appendJournal, attachPlanCheckpoint, attachTodosCheckpoint, clearPlan, deriveTodosFromPlanItem, emptyGoal, emptyPlan, formatGoal, formatPlan, formatPlanTemplates, getPlanTemplate, goalFilePath, listPlanTemplates, loadDirectorState, loadGoal, loadPlan, loadTodosCheckpoint, removePlanItem, runConfigMigrations, saveGoal, savePlan, saveTodosCheckpoint, setPlanItemStatus, summarizeUsage };
|
|
3130
3850
|
//# sourceMappingURL=index.js.map
|
|
3131
3851
|
//# sourceMappingURL=index.js.map
|