@wrongstack/core 0.9.19 → 0.10.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/README.md +37 -33
- package/dist/{agent-bridge-DMVOX0cF.d.ts → agent-bridge-DBBGP5O4.d.ts} +1 -1
- package/dist/{agent-subagent-runner-C4qt9e5Y.d.ts → agent-subagent-runner-KwufTWwn.d.ts} +3 -4
- package/dist/compactor-D1RHFRmF.d.ts +41 -0
- package/dist/{config-CWva0qoL.d.ts → config-CJXBka2r.d.ts} +38 -2
- package/dist/{context-BRNbHmRM.d.ts → context-7u93AcGD.d.ts} +140 -0
- package/dist/coordination/index.d.ts +13 -13
- package/dist/coordination/index.js +786 -200
- package/dist/coordination/index.js.map +1 -1
- package/dist/{default-config-Uzla85H-.d.ts → default-config-DEXI4jsl.d.ts} +10 -1
- package/dist/defaults/index.d.ts +24 -24
- package/dist/defaults/index.js +1612 -528
- package/dist/defaults/index.js.map +1 -1
- package/dist/{events-CiG9qUM_.d.ts → events-Bw1K5E6K.d.ts} +49 -1
- package/dist/execution/index.d.ts +46 -32
- package/dist/execution/index.js +663 -169
- package/dist/execution/index.js.map +1 -1
- package/dist/extension/index.d.ts +6 -6
- package/dist/extension/index.js.map +1 -1
- package/dist/goal-store-BeRsj7YX.d.ts +188 -0
- package/dist/{index-p95HQ22A.d.ts → index-DIt5TIaF.d.ts} +8 -8
- package/dist/{index-aizK8olO.d.ts → index-DgQWnZxP.d.ts} +11 -8
- package/dist/index.d.ts +338 -41
- package/dist/index.js +2747 -787
- 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 +9 -9
- package/dist/kernel/index.js +57 -8
- package/dist/kernel/index.js.map +1 -1
- package/dist/{mcp-servers-BkVEqkRe.d.ts → mcp-servers-BgINZzuo.d.ts} +25 -3
- package/dist/models/index.d.ts +3 -3
- package/dist/models/index.js +164 -11
- package/dist/models/index.js.map +1 -1
- package/dist/models-registry-gwMAo6E3.d.ts +86 -0
- package/dist/{multi-agent-3ZnTB1aT.d.ts → multi-agent-C1O8EFzb.d.ts} +77 -24
- package/dist/{multi-agent-coordinator-bRaI_aD1.d.ts → multi-agent-coordinator-DoVSBFNx.d.ts} +20 -2
- package/dist/{null-fleet-bus-DKM3Iy9d.d.ts → null-fleet-bus-T-s3JaOt.d.ts} +413 -110
- package/dist/observability/index.d.ts +2 -2
- package/dist/{path-resolver-TcJfc29Y.d.ts → path-resolver-BfyK-I7w.d.ts} +2 -2
- package/dist/{permission-bPuzAy4x.d.ts → permission-B6sldrSp.d.ts} +1 -1
- package/dist/{permission-policy-BUQSutpl.d.ts → permission-policy-CtNscWOA.d.ts} +10 -9
- package/dist/{plan-templates-fkQTyz3U.d.ts → plan-templates-BWs6Je7j.d.ts} +8 -8
- package/dist/{provider-runner-BEpikbbN.d.ts → provider-runner-BmPe2RuU.d.ts} +3 -3
- package/dist/{retry-policy-BYkq0ugs.d.ts → retry-policy-KF18W4dg.d.ts} +1 -1
- package/dist/sdd/index.d.ts +9 -10
- package/dist/sdd/index.js +350 -122
- package/dist/sdd/index.js.map +1 -1
- package/dist/security/index.d.ts +65 -4
- package/dist/security/index.js +67 -19
- package/dist/security/index.js.map +1 -1
- package/dist/{selector-pox8abg0.d.ts → selector-DmXxpFyM.d.ts} +1 -1
- package/dist/{session-reader-CSWcb5Ga.d.ts → session-reader-CCSBfQmd.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 +392 -8
- package/dist/storage/index.js +796 -40
- package/dist/storage/index.js.map +1 -1
- package/dist/{system-prompt-Bs-Wliab.d.ts → system-prompt-CM6zOhd2.d.ts} +19 -1
- package/dist/{tool-executor-Boo3dekH.d.ts → tool-executor-D7RxLtg4.d.ts} +9 -4
- package/dist/types/index.d.ts +16 -16
- package/dist/types/index.js +404 -14
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.d.ts +40 -8
- package/dist/utils/index.js +127 -57
- package/dist/utils/index.js.map +1 -1
- package/dist/{wstack-paths-BCgmTNlG.d.ts → wstack-paths-eMXnY1_X.d.ts} +8 -0
- package/package.json +1 -1
- package/dist/compactor-DVTKL7XD.d.ts +0 -23
- package/dist/goal-store-C7jcumEh.d.ts +0 -96
- package/dist/models-registry-OG_30xqZ.d.ts +0 -46
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" });
|
|
@@ -178,7 +178,7 @@ var DefaultSessionStore = class {
|
|
|
178
178
|
}
|
|
179
179
|
/** Join session ID to its absolute path within the store directory. */
|
|
180
180
|
sessionPath(id, ext) {
|
|
181
|
-
return
|
|
181
|
+
return path11.join(this.dir, `${id}${ext}`);
|
|
182
182
|
}
|
|
183
183
|
async ensureShardDir(_id) {
|
|
184
184
|
await ensureDir(this.dir);
|
|
@@ -188,7 +188,7 @@ var DefaultSessionStore = class {
|
|
|
188
188
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
189
189
|
const id = meta.id ?? `${startedAt.replace(/[:.]/g, "-")}-${randomBytes(2).toString("hex")}`;
|
|
190
190
|
const shardDir = await this.ensureShardDir(id);
|
|
191
|
-
const file =
|
|
191
|
+
const file = path11.join(shardDir, `${id}.jsonl`);
|
|
192
192
|
let handle;
|
|
193
193
|
try {
|
|
194
194
|
handle = await fsp.open(file, "a", 384);
|
|
@@ -281,7 +281,7 @@ var DefaultSessionStore = class {
|
|
|
281
281
|
const ids = [];
|
|
282
282
|
const entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
283
283
|
for (const entry of entries) {
|
|
284
|
-
const full =
|
|
284
|
+
const full = path11.join(dir, entry.name);
|
|
285
285
|
if (entry.isDirectory()) {
|
|
286
286
|
ids.push(...await this.collectSessionIds(full));
|
|
287
287
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -437,7 +437,7 @@ var FileSessionWriter = class {
|
|
|
437
437
|
this.meta = meta;
|
|
438
438
|
this.events = events;
|
|
439
439
|
this.resumed = opts.resumed ?? false;
|
|
440
|
-
this.manifestFile = opts.dir ?
|
|
440
|
+
this.manifestFile = opts.dir ? path11.join(opts.dir, `${id}.summary.json`) : "";
|
|
441
441
|
this.filePath = opts.filePath ?? "";
|
|
442
442
|
this.secretScrubber = opts.secretScrubber;
|
|
443
443
|
this.summary = {
|
|
@@ -491,7 +491,6 @@ var FileSessionWriter = class {
|
|
|
491
491
|
}
|
|
492
492
|
return event;
|
|
493
493
|
}
|
|
494
|
-
promptIndex = 0;
|
|
495
494
|
pendingFileSnapshots = [];
|
|
496
495
|
/** Tracks open tool_use IDs during the current run to serialize on close for resume. */
|
|
497
496
|
openToolUses = /* @__PURE__ */ new Set();
|
|
@@ -582,7 +581,6 @@ var FileSessionWriter = class {
|
|
|
582
581
|
await this.writeFileSnapshot(promptIndex, [...this.pendingFileSnapshots]);
|
|
583
582
|
this.pendingFileSnapshots = [];
|
|
584
583
|
}
|
|
585
|
-
this.promptIndex = promptIndex + 1;
|
|
586
584
|
await this.append({
|
|
587
585
|
type: "checkpoint",
|
|
588
586
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -678,6 +676,38 @@ var FileSessionWriter = class {
|
|
|
678
676
|
`;
|
|
679
677
|
await fsp.writeFile(this.filePath, record, "utf8");
|
|
680
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
|
+
}
|
|
681
711
|
};
|
|
682
712
|
function userInputTitle(content) {
|
|
683
713
|
const text = typeof content === "string" ? content : content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
|
|
@@ -686,7 +716,7 @@ function userInputTitle(content) {
|
|
|
686
716
|
var QueueStore = class {
|
|
687
717
|
file;
|
|
688
718
|
constructor(opts) {
|
|
689
|
-
this.file =
|
|
719
|
+
this.file = path11.join(opts.dir, "queue.json");
|
|
690
720
|
}
|
|
691
721
|
async write(items) {
|
|
692
722
|
if (items.length === 0) {
|
|
@@ -734,7 +764,7 @@ function isPersistedQueueItem(v) {
|
|
|
734
764
|
return typeof o["displayText"] === "string" && Array.isArray(o["blocks"]);
|
|
735
765
|
}
|
|
736
766
|
var DEFAULT_SPOOL_THRESHOLD = 256 * 1024;
|
|
737
|
-
var PLACEHOLDER_RE = /\[(pasted|image|file) #(\d+)\]/g;
|
|
767
|
+
var PLACEHOLDER_RE = /\[(pasted|image|file) #(\d+)[^\]]*\]|\[file:([^\]]+)\]/g;
|
|
738
768
|
var DefaultAttachmentStore = class {
|
|
739
769
|
items = /* @__PURE__ */ new Map();
|
|
740
770
|
refs = [];
|
|
@@ -753,7 +783,7 @@ var DefaultAttachmentStore = class {
|
|
|
753
783
|
let data = input.data;
|
|
754
784
|
if (this.spoolDir && bytes >= this.spoolThreshold) {
|
|
755
785
|
await fsp.mkdir(this.spoolDir, { recursive: true });
|
|
756
|
-
spooledPath =
|
|
786
|
+
spooledPath = path11.join(this.spoolDir, `${id}.bin`);
|
|
757
787
|
await atomicWrite(spooledPath, input.data, {
|
|
758
788
|
encoding: input.kind === "image" ? "base64" : "utf8"
|
|
759
789
|
});
|
|
@@ -788,9 +818,15 @@ var DefaultAttachmentStore = class {
|
|
|
788
818
|
const idx = m.index ?? 0;
|
|
789
819
|
const before = text.slice(lastIndex, idx);
|
|
790
820
|
if (before) blocks.push({ type: "text", text: before });
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
821
|
+
let ref;
|
|
822
|
+
if (m[3] !== void 0) {
|
|
823
|
+
const wantPath = m[3];
|
|
824
|
+
ref = findLast(this.refs, (r) => r.kind === "file" && refPath(r) === wantPath);
|
|
825
|
+
} else {
|
|
826
|
+
const kind = prefixToKind(m[1]);
|
|
827
|
+
const seq = Number(m[2]);
|
|
828
|
+
ref = this.refs.find((r) => r.kind === kind && r.seq === seq);
|
|
829
|
+
}
|
|
794
830
|
const att = ref ? this.items.get(ref.id) : void 0;
|
|
795
831
|
if (!att) {
|
|
796
832
|
blocks.push({ type: "text", text: m[0] });
|
|
@@ -843,6 +879,15 @@ function prefixToKind(prefix) {
|
|
|
843
879
|
if (prefix === "image") return "image";
|
|
844
880
|
return "file";
|
|
845
881
|
}
|
|
882
|
+
function refPath(ref) {
|
|
883
|
+
return ref.meta.filename ?? ref.meta.label;
|
|
884
|
+
}
|
|
885
|
+
function findLast(arr, pred) {
|
|
886
|
+
for (let i = arr.length - 1; i >= 0; i--) {
|
|
887
|
+
if (pred(arr[i])) return arr[i];
|
|
888
|
+
}
|
|
889
|
+
return void 0;
|
|
890
|
+
}
|
|
846
891
|
function mergeAdjacentText(blocks) {
|
|
847
892
|
const out = [];
|
|
848
893
|
for (const b of blocks) {
|
|
@@ -923,7 +968,7 @@ ${body.trim()}`);
|
|
|
923
968
|
async remember(text, scope = "project-memory") {
|
|
924
969
|
return this.runSerialized(scope, async () => {
|
|
925
970
|
const file = this.files[scope];
|
|
926
|
-
await ensureDir(
|
|
971
|
+
await ensureDir(path11.dirname(file));
|
|
927
972
|
let existing = "";
|
|
928
973
|
try {
|
|
929
974
|
existing = await fsp.readFile(file, "utf8");
|
|
@@ -1218,6 +1263,14 @@ var DEFAULT_CONTEXT_CONFIG = Object.freeze({
|
|
|
1218
1263
|
preserveK: 10,
|
|
1219
1264
|
eliseThreshold: 2e3
|
|
1220
1265
|
});
|
|
1266
|
+
var DEFAULT_SESSION_LOGGING_CONFIG = Object.freeze({
|
|
1267
|
+
auditLevel: "standard",
|
|
1268
|
+
sampling: {
|
|
1269
|
+
toolProgress: {
|
|
1270
|
+
sampleRate: 8
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1221
1274
|
|
|
1222
1275
|
// src/storage/config-loader.ts
|
|
1223
1276
|
var BEHAVIOR_DEFAULTS = {
|
|
@@ -1246,7 +1299,8 @@ var BEHAVIOR_DEFAULTS = {
|
|
|
1246
1299
|
memory: true,
|
|
1247
1300
|
modelsRegistry: true,
|
|
1248
1301
|
skills: true
|
|
1249
|
-
}
|
|
1302
|
+
},
|
|
1303
|
+
session: { ...DEFAULT_SESSION_LOGGING_CONFIG }
|
|
1250
1304
|
};
|
|
1251
1305
|
var ENV_MAP = {
|
|
1252
1306
|
WRONGSTACK_PROVIDER: (c, v) => {
|
|
@@ -1529,7 +1583,7 @@ var RecoveryLock = class {
|
|
|
1529
1583
|
sessionStore;
|
|
1530
1584
|
probe;
|
|
1531
1585
|
constructor(opts) {
|
|
1532
|
-
this.file =
|
|
1586
|
+
this.file = path11.join(opts.dir, LOCK_FILE);
|
|
1533
1587
|
this.pid = opts.pid ?? process.pid;
|
|
1534
1588
|
this.hostname = opts.hostname ?? os.hostname();
|
|
1535
1589
|
this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
@@ -1587,7 +1641,7 @@ var RecoveryLock = class {
|
|
|
1587
1641
|
* null return before calling this.
|
|
1588
1642
|
*/
|
|
1589
1643
|
async write(sessionId) {
|
|
1590
|
-
await ensureDir(
|
|
1644
|
+
await ensureDir(path11.dirname(this.file));
|
|
1591
1645
|
const lock = {
|
|
1592
1646
|
v: 1,
|
|
1593
1647
|
sessionId,
|
|
@@ -1949,6 +2003,613 @@ function renderPlainText(meta, events) {
|
|
|
1949
2003
|
}
|
|
1950
2004
|
return lines.join("\n");
|
|
1951
2005
|
}
|
|
2006
|
+
var FILE_VERSION = 1;
|
|
2007
|
+
var MAX_TEXT_LENGTH = 2e3;
|
|
2008
|
+
var MAX_ANNOTATIONS = 1e3;
|
|
2009
|
+
var AnnotationsStore = class {
|
|
2010
|
+
dir;
|
|
2011
|
+
/** Per-session write queue. Created lazily on first add. */
|
|
2012
|
+
writeChains = /* @__PURE__ */ new Map();
|
|
2013
|
+
constructor(opts) {
|
|
2014
|
+
this.dir = opts.dir;
|
|
2015
|
+
}
|
|
2016
|
+
// ── Reads ──────────────────────────────────────────────────────────────
|
|
2017
|
+
/**
|
|
2018
|
+
* Return all annotations for `sessionId` in insertion order
|
|
2019
|
+
* (oldest first). Returns an empty array when no file exists
|
|
2020
|
+
* yet (the normal case for a fresh session).
|
|
2021
|
+
*/
|
|
2022
|
+
async list(sessionId) {
|
|
2023
|
+
const file = await this.readFile(sessionId);
|
|
2024
|
+
return file ? file.annotations : [];
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* Convenience: only unresolved annotations, newest first — the
|
|
2028
|
+
* common UI rendering for "what still needs attention?".
|
|
2029
|
+
*/
|
|
2030
|
+
async listOpen(sessionId) {
|
|
2031
|
+
const all = await this.list(sessionId);
|
|
2032
|
+
return all.filter((a) => !a.resolved).reverse();
|
|
2033
|
+
}
|
|
2034
|
+
// ── Writes ─────────────────────────────────────────────────────────────
|
|
2035
|
+
/**
|
|
2036
|
+
* Add a new annotation. Returns the persisted record (with id
|
|
2037
|
+
* and timestamps filled in). Throws when `text` is empty or
|
|
2038
|
+
* exceeds `MAX_TEXT_LENGTH`.
|
|
2039
|
+
*/
|
|
2040
|
+
async add(input) {
|
|
2041
|
+
const text = input.text.trim();
|
|
2042
|
+
if (text.length === 0) {
|
|
2043
|
+
throw new Error("Annotation text must be non-empty");
|
|
2044
|
+
}
|
|
2045
|
+
if (text.length > MAX_TEXT_LENGTH) {
|
|
2046
|
+
throw new Error(
|
|
2047
|
+
`Annotation text exceeds ${MAX_TEXT_LENGTH} chars (got ${text.length})`
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
if (!Number.isInteger(input.atEventIndex) || input.atEventIndex < 0) {
|
|
2051
|
+
throw new Error("atEventIndex must be a non-negative integer");
|
|
2052
|
+
}
|
|
2053
|
+
const annotation = {
|
|
2054
|
+
id: randomUUID(),
|
|
2055
|
+
sessionId: input.sessionId,
|
|
2056
|
+
atEventIndex: input.atEventIndex,
|
|
2057
|
+
authorId: input.authorId,
|
|
2058
|
+
authorRole: "annotator",
|
|
2059
|
+
text,
|
|
2060
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2061
|
+
resolved: false
|
|
2062
|
+
};
|
|
2063
|
+
await this.enqueue(input.sessionId, async () => {
|
|
2064
|
+
const all = await this.list(input.sessionId);
|
|
2065
|
+
all.push(annotation);
|
|
2066
|
+
if (all.length > MAX_ANNOTATIONS) {
|
|
2067
|
+
const sorted = all.map((a, i) => ({ a, i })).sort((x, y) => {
|
|
2068
|
+
if (x.a.resolved !== y.a.resolved) return x.a.resolved ? 1 : -1;
|
|
2069
|
+
return x.a.createdAt.localeCompare(y.a.createdAt);
|
|
2070
|
+
});
|
|
2071
|
+
const evictCount = all.length - MAX_ANNOTATIONS;
|
|
2072
|
+
const toEvict = new Set(sorted.slice(0, evictCount).map((s) => s.a.id));
|
|
2073
|
+
const kept = all.filter((a) => !toEvict.has(a.id));
|
|
2074
|
+
await this.writeFile(input.sessionId, { version: FILE_VERSION, annotations: kept });
|
|
2075
|
+
} else {
|
|
2076
|
+
await this.writeFile(input.sessionId, { version: FILE_VERSION, annotations: all });
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
2079
|
+
return annotation;
|
|
2080
|
+
}
|
|
2081
|
+
/**
|
|
2082
|
+
* Mark an annotation as resolved. Returns the updated record, or
|
|
2083
|
+
* `null` if no annotation with that id exists in this session.
|
|
2084
|
+
* Idempotent: resolving an already-resolved annotation refreshes
|
|
2085
|
+
* `resolvedAt` / `resolvedBy` to the latest call.
|
|
2086
|
+
*/
|
|
2087
|
+
async resolve(input) {
|
|
2088
|
+
let updated = null;
|
|
2089
|
+
await this.enqueue(input.sessionId, async () => {
|
|
2090
|
+
const all = await this.list(input.sessionId);
|
|
2091
|
+
const idx = all.findIndex((a) => a.id === input.annotationId);
|
|
2092
|
+
if (idx === -1) {
|
|
2093
|
+
updated = null;
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
const next = {
|
|
2097
|
+
...all[idx],
|
|
2098
|
+
resolved: true,
|
|
2099
|
+
resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2100
|
+
resolvedBy: input.resolvedBy
|
|
2101
|
+
};
|
|
2102
|
+
all[idx] = next;
|
|
2103
|
+
await this.writeFile(input.sessionId, { version: FILE_VERSION, annotations: all });
|
|
2104
|
+
updated = next;
|
|
2105
|
+
});
|
|
2106
|
+
return updated;
|
|
2107
|
+
}
|
|
2108
|
+
// ── Internals ──────────────────────────────────────────────────────────
|
|
2109
|
+
filePath(sessionId) {
|
|
2110
|
+
if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
|
|
2111
|
+
throw new Error(`Invalid sessionId: ${sessionId}`);
|
|
2112
|
+
}
|
|
2113
|
+
return path11.join(this.dir, `${sessionId}.annotations.json`);
|
|
2114
|
+
}
|
|
2115
|
+
async readFile(sessionId) {
|
|
2116
|
+
const fp = this.filePath(sessionId);
|
|
2117
|
+
try {
|
|
2118
|
+
const raw = await fsp.readFile(fp, "utf8");
|
|
2119
|
+
const parsed = JSON.parse(raw);
|
|
2120
|
+
if (parsed.version !== FILE_VERSION) {
|
|
2121
|
+
return { version: FILE_VERSION, annotations: [] };
|
|
2122
|
+
}
|
|
2123
|
+
return parsed;
|
|
2124
|
+
} catch (err) {
|
|
2125
|
+
if (err.code === "ENOENT") return null;
|
|
2126
|
+
return { version: FILE_VERSION, annotations: [] };
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
async writeFile(sessionId, file) {
|
|
2130
|
+
const fp = this.filePath(sessionId);
|
|
2131
|
+
await atomicWrite(fp, JSON.stringify(file, null, 2));
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2134
|
+
* Serialize writes per-sessionId. We chain promises instead of
|
|
2135
|
+
* using a Mutex class so the contract is obvious from the
|
|
2136
|
+
* call-site: `enqueue(sid, fn)` runs `fn` after every prior
|
|
2137
|
+
* enqueue for `sid` has settled.
|
|
2138
|
+
*/
|
|
2139
|
+
enqueue(sessionId, fn) {
|
|
2140
|
+
const prev = this.writeChains.get(sessionId) ?? Promise.resolve();
|
|
2141
|
+
const next = prev.then(fn, fn);
|
|
2142
|
+
this.writeChains.set(
|
|
2143
|
+
sessionId,
|
|
2144
|
+
next.catch(() => void 0)
|
|
2145
|
+
);
|
|
2146
|
+
return next;
|
|
2147
|
+
}
|
|
2148
|
+
};
|
|
2149
|
+
function stableStringify(value) {
|
|
2150
|
+
return JSON.stringify(sortKeys(value));
|
|
2151
|
+
}
|
|
2152
|
+
function sortKeys(value) {
|
|
2153
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
2154
|
+
if (value && typeof value === "object") {
|
|
2155
|
+
const obj = value;
|
|
2156
|
+
const sorted = {};
|
|
2157
|
+
for (const key of Object.keys(obj).sort()) {
|
|
2158
|
+
sorted[key] = sortKeys(obj[key]);
|
|
2159
|
+
}
|
|
2160
|
+
return sorted;
|
|
2161
|
+
}
|
|
2162
|
+
return value;
|
|
2163
|
+
}
|
|
2164
|
+
function hashRequest(request) {
|
|
2165
|
+
const payload = {
|
|
2166
|
+
model: request.model,
|
|
2167
|
+
system: request.system,
|
|
2168
|
+
messages: request.messages,
|
|
2169
|
+
tools: request.tools,
|
|
2170
|
+
maxTokens: request.maxTokens,
|
|
2171
|
+
temperature: request.temperature,
|
|
2172
|
+
topP: request.topP,
|
|
2173
|
+
stopSequences: request.stopSequences,
|
|
2174
|
+
toolChoice: request.toolChoice
|
|
2175
|
+
};
|
|
2176
|
+
const json = stableStringify(payload);
|
|
2177
|
+
const digest = createHash("sha256").update(json, "utf8").digest("hex");
|
|
2178
|
+
return `sha256:${digest}`;
|
|
2179
|
+
}
|
|
2180
|
+
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
2181
|
+
var ReplayLogStore = class {
|
|
2182
|
+
dir;
|
|
2183
|
+
writeChains = /* @__PURE__ */ new Map();
|
|
2184
|
+
/** Per-session hash → entry index, kept in memory after the first load. */
|
|
2185
|
+
cache = /* @__PURE__ */ new Map();
|
|
2186
|
+
maxEntries;
|
|
2187
|
+
constructor(opts) {
|
|
2188
|
+
this.dir = opts.dir;
|
|
2189
|
+
this.maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
2190
|
+
}
|
|
2191
|
+
// ── Writes ──────────────────────────────────────────────────────────────
|
|
2192
|
+
/**
|
|
2193
|
+
* Record a request/response pair. Idempotent on hash: a second
|
|
2194
|
+
* `record` for the same hash is a no-op (the existing entry wins).
|
|
2195
|
+
* Returns the hash.
|
|
2196
|
+
*/
|
|
2197
|
+
async record(input) {
|
|
2198
|
+
const hash = hashRequest(input.request);
|
|
2199
|
+
await this.enqueue(input.sessionId, async () => {
|
|
2200
|
+
const cache = await this.ensureCache(input.sessionId);
|
|
2201
|
+
if (cache.has(hash)) return;
|
|
2202
|
+
const entry = {
|
|
2203
|
+
hash,
|
|
2204
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2205
|
+
request: input.request,
|
|
2206
|
+
response: input.response
|
|
2207
|
+
};
|
|
2208
|
+
let all = [...cache.values(), entry];
|
|
2209
|
+
if (all.length > this.maxEntries) {
|
|
2210
|
+
const evictCount = all.length - this.maxEntries;
|
|
2211
|
+
all = all.slice(evictCount);
|
|
2212
|
+
}
|
|
2213
|
+
await this.writeAll(input.sessionId, all);
|
|
2214
|
+
cache.clear();
|
|
2215
|
+
for (const e of all) cache.set(e.hash, e);
|
|
2216
|
+
});
|
|
2217
|
+
return hash;
|
|
2218
|
+
}
|
|
2219
|
+
// ── Reads ───────────────────────────────────────────────────────────────
|
|
2220
|
+
/**
|
|
2221
|
+
* Look up an entry by hash. Returns `null` when the request has
|
|
2222
|
+
* not been recorded for this session. O(1) after the first call
|
|
2223
|
+
* per session (in-memory cache).
|
|
2224
|
+
*/
|
|
2225
|
+
async lookup(sessionId, hash) {
|
|
2226
|
+
const cache = await this.ensureCache(sessionId);
|
|
2227
|
+
return cache.get(hash) ?? null;
|
|
2228
|
+
}
|
|
2229
|
+
/** All recorded entries for a session, in insertion order. */
|
|
2230
|
+
async load(sessionId) {
|
|
2231
|
+
const cache = await this.ensureCache(sessionId);
|
|
2232
|
+
return [...cache.values()];
|
|
2233
|
+
}
|
|
2234
|
+
/**
|
|
2235
|
+
* List every session id that has a replay log in the store dir.
|
|
2236
|
+
* Returns an array of `{ sessionId, entryCount, path }` sorted
|
|
2237
|
+
* by sessionId for stable output. Used by `wstack replay --list`.
|
|
2238
|
+
*/
|
|
2239
|
+
async list() {
|
|
2240
|
+
let entries;
|
|
2241
|
+
try {
|
|
2242
|
+
entries = await fsp.readdir(this.dir);
|
|
2243
|
+
} catch (err) {
|
|
2244
|
+
if (err.code === "ENOENT") return [];
|
|
2245
|
+
return [];
|
|
2246
|
+
}
|
|
2247
|
+
const out = [];
|
|
2248
|
+
for (const name of entries) {
|
|
2249
|
+
if (!name.endsWith(".replay.jsonl")) continue;
|
|
2250
|
+
const sessionId = name.slice(0, -".replay.jsonl".length);
|
|
2251
|
+
const all = await this.load(sessionId);
|
|
2252
|
+
out.push({
|
|
2253
|
+
sessionId,
|
|
2254
|
+
entryCount: all.length,
|
|
2255
|
+
path: path11.join(this.dir, name)
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
2258
|
+
return out.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
|
|
2259
|
+
}
|
|
2260
|
+
// ── Internals ───────────────────────────────────────────────────────────
|
|
2261
|
+
filePath(sessionId) {
|
|
2262
|
+
if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
|
|
2263
|
+
throw new Error(`Invalid sessionId: ${sessionId}`);
|
|
2264
|
+
}
|
|
2265
|
+
return path11.join(this.dir, `${sessionId}.replay.jsonl`);
|
|
2266
|
+
}
|
|
2267
|
+
async readAll(sessionId) {
|
|
2268
|
+
const fp = this.filePath(sessionId);
|
|
2269
|
+
try {
|
|
2270
|
+
const raw = await fsp.readFile(fp, "utf8");
|
|
2271
|
+
const out = [];
|
|
2272
|
+
for (const line of raw.split("\n")) {
|
|
2273
|
+
if (!line.trim()) continue;
|
|
2274
|
+
try {
|
|
2275
|
+
const parsed = JSON.parse(line);
|
|
2276
|
+
if ("entry" in parsed && parsed.entry) {
|
|
2277
|
+
out.push(parsed.entry);
|
|
2278
|
+
} else {
|
|
2279
|
+
out.push(parsed);
|
|
2280
|
+
}
|
|
2281
|
+
} catch {
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
return out;
|
|
2285
|
+
} catch (err) {
|
|
2286
|
+
if (err.code === "ENOENT") return [];
|
|
2287
|
+
return [];
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
async writeAll(sessionId, entries) {
|
|
2291
|
+
const fp = this.filePath(sessionId);
|
|
2292
|
+
const body = entries.map((e) => JSON.stringify(e)).join("\n") + (entries.length ? "\n" : "");
|
|
2293
|
+
await atomicWrite(fp, body);
|
|
2294
|
+
}
|
|
2295
|
+
async ensureCache(sessionId) {
|
|
2296
|
+
let cache = this.cache.get(sessionId);
|
|
2297
|
+
if (cache) return cache;
|
|
2298
|
+
const all = await this.readAll(sessionId);
|
|
2299
|
+
cache = /* @__PURE__ */ new Map();
|
|
2300
|
+
for (const e of all) cache.set(e.hash, e);
|
|
2301
|
+
this.cache.set(sessionId, cache);
|
|
2302
|
+
return cache;
|
|
2303
|
+
}
|
|
2304
|
+
enqueue(sessionId, fn) {
|
|
2305
|
+
const prev = this.writeChains.get(sessionId) ?? Promise.resolve();
|
|
2306
|
+
const next = prev.then(fn, fn);
|
|
2307
|
+
this.writeChains.set(
|
|
2308
|
+
sessionId,
|
|
2309
|
+
next.catch(() => void 0)
|
|
2310
|
+
);
|
|
2311
|
+
return next;
|
|
2312
|
+
}
|
|
2313
|
+
};
|
|
2314
|
+
var SessionRecovery = class {
|
|
2315
|
+
constructor(dir) {
|
|
2316
|
+
this.dir = dir;
|
|
2317
|
+
}
|
|
2318
|
+
dir;
|
|
2319
|
+
/**
|
|
2320
|
+
* Scan a session log and return a `StaleSession` if and only
|
|
2321
|
+
* if the last event is an `in_flight_start` without a matching
|
|
2322
|
+
* `in_flight_end`. Returns `null` when:
|
|
2323
|
+
* - the log does not exist;
|
|
2324
|
+
* - the log is empty;
|
|
2325
|
+
* - the last event is `in_flight_end` (clean shutdown);
|
|
2326
|
+
* - the last event is something else (e.g. an unannotated
|
|
2327
|
+
* legacy log without in-flight markers).
|
|
2328
|
+
*/
|
|
2329
|
+
async detectStale(sessionId) {
|
|
2330
|
+
const fp = this.filePath(sessionId);
|
|
2331
|
+
let raw;
|
|
2332
|
+
try {
|
|
2333
|
+
raw = await fsp.readFile(fp, "utf8");
|
|
2334
|
+
} catch (err) {
|
|
2335
|
+
if (err.code === "ENOENT") return null;
|
|
2336
|
+
return null;
|
|
2337
|
+
}
|
|
2338
|
+
return this.parseForStale(sessionId, fp, raw);
|
|
2339
|
+
}
|
|
2340
|
+
/**
|
|
2341
|
+
* Generate a recovery plan for a session. The plan describes
|
|
2342
|
+
* "what would be re-executed" if the user chose to resume —
|
|
2343
|
+
* everything after the last `checkpoint` event, plus the
|
|
2344
|
+
* dangling in-flight marker if present.
|
|
2345
|
+
*
|
|
2346
|
+
* Returns a non-null plan for ANY session that has at least
|
|
2347
|
+
* one event after a checkpoint (or, for legacy sessions, at
|
|
2348
|
+
* least one event). Pure read; no mutation.
|
|
2349
|
+
*/
|
|
2350
|
+
async recover(sessionId) {
|
|
2351
|
+
const fp = this.filePath(sessionId);
|
|
2352
|
+
let raw;
|
|
2353
|
+
try {
|
|
2354
|
+
raw = await fsp.readFile(fp, "utf8");
|
|
2355
|
+
} catch (err) {
|
|
2356
|
+
if (err.code === "ENOENT") return null;
|
|
2357
|
+
return null;
|
|
2358
|
+
}
|
|
2359
|
+
const events = [];
|
|
2360
|
+
for (const line of raw.split("\n")) {
|
|
2361
|
+
if (!line.trim()) continue;
|
|
2362
|
+
try {
|
|
2363
|
+
events.push(JSON.parse(line));
|
|
2364
|
+
} catch {
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
if (events.length === 0) return null;
|
|
2368
|
+
let lastCheckpoint = null;
|
|
2369
|
+
let lastCheckpointIdx = -1;
|
|
2370
|
+
for (let i = 0; i < events.length; i++) {
|
|
2371
|
+
if (events[i].type === "checkpoint") {
|
|
2372
|
+
lastCheckpoint = events[i];
|
|
2373
|
+
lastCheckpointIdx = i;
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
const pendingEvents = lastCheckpointIdx >= 0 ? events.slice(lastCheckpointIdx + 1) : events;
|
|
2377
|
+
const lastEv = events[events.length - 1];
|
|
2378
|
+
const inFlightStart = lastEv.type === "in_flight_start" ? lastEv : null;
|
|
2379
|
+
const context = inFlightStart && inFlightStart.type === "in_flight_start" ? inFlightStart.context : null;
|
|
2380
|
+
return {
|
|
2381
|
+
sessionId,
|
|
2382
|
+
stale: inFlightStart !== null,
|
|
2383
|
+
lastCheckpoint,
|
|
2384
|
+
pendingEvents,
|
|
2385
|
+
inFlightStart,
|
|
2386
|
+
context
|
|
2387
|
+
};
|
|
2388
|
+
}
|
|
2389
|
+
/**
|
|
2390
|
+
* List every stale session in a directory. Returns an array
|
|
2391
|
+
* (possibly empty) sorted by `lastEventTs` descending — most
|
|
2392
|
+
* recent crash first.
|
|
2393
|
+
*/
|
|
2394
|
+
async listResumable() {
|
|
2395
|
+
let entries;
|
|
2396
|
+
try {
|
|
2397
|
+
entries = await fsp.readdir(this.dir);
|
|
2398
|
+
} catch (err) {
|
|
2399
|
+
if (err.code === "ENOENT") return [];
|
|
2400
|
+
return [];
|
|
2401
|
+
}
|
|
2402
|
+
const out = [];
|
|
2403
|
+
for (const name of entries) {
|
|
2404
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
2405
|
+
const sessionId = name.slice(0, -".jsonl".length);
|
|
2406
|
+
if (sessionId.includes(".replay") || sessionId.includes(".annotations")) continue;
|
|
2407
|
+
const stale = await this.detectStale(sessionId);
|
|
2408
|
+
if (stale) out.push(stale);
|
|
2409
|
+
}
|
|
2410
|
+
return out.sort((a, b) => b.lastEventTs.localeCompare(a.lastEventTs));
|
|
2411
|
+
}
|
|
2412
|
+
// ── Internals ──────────────────────────────────────────────────────────
|
|
2413
|
+
filePath(sessionId) {
|
|
2414
|
+
if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
|
|
2415
|
+
throw new Error(`Invalid sessionId: ${sessionId}`);
|
|
2416
|
+
}
|
|
2417
|
+
return path11.join(this.dir, `${sessionId}.jsonl`);
|
|
2418
|
+
}
|
|
2419
|
+
/**
|
|
2420
|
+
* Stream-parse the last few lines of a JSONL log. We do NOT load
|
|
2421
|
+
* the whole file into memory — for long-running sessions the log
|
|
2422
|
+
* can be megabytes. Instead we read tail-ward and find the last
|
|
2423
|
+
* `in_flight_start` / `in_flight_end` pair.
|
|
2424
|
+
*/
|
|
2425
|
+
async parseForStale(sessionId, fp, raw) {
|
|
2426
|
+
const lines = raw.split("\n");
|
|
2427
|
+
let lastEvent = null;
|
|
2428
|
+
let eventCount = 0;
|
|
2429
|
+
for (const line of lines) {
|
|
2430
|
+
if (!line.trim()) continue;
|
|
2431
|
+
try {
|
|
2432
|
+
const ev = JSON.parse(line);
|
|
2433
|
+
lastEvent = ev;
|
|
2434
|
+
eventCount++;
|
|
2435
|
+
} catch {
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
if (!lastEvent) return null;
|
|
2439
|
+
if (lastEvent.type === "in_flight_start") {
|
|
2440
|
+
return {
|
|
2441
|
+
sessionId,
|
|
2442
|
+
path: fp,
|
|
2443
|
+
lastEventTs: lastEvent.ts,
|
|
2444
|
+
context: lastEvent.context,
|
|
2445
|
+
eventCount
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
return null;
|
|
2449
|
+
}
|
|
2450
|
+
};
|
|
2451
|
+
var GENESIS_PREV = "0".repeat(64);
|
|
2452
|
+
var ToolAuditLog = class {
|
|
2453
|
+
dir;
|
|
2454
|
+
/** In-memory cache of the last entry's hash (per session), to compute chains efficiently. */
|
|
2455
|
+
tailHash = /* @__PURE__ */ new Map();
|
|
2456
|
+
writeChains = /* @__PURE__ */ new Map();
|
|
2457
|
+
constructor(opts) {
|
|
2458
|
+
this.dir = opts.dir;
|
|
2459
|
+
}
|
|
2460
|
+
/**
|
|
2461
|
+
* Append a tool call/result pair to the chain. Returns the
|
|
2462
|
+
* resulting entry. Idempotency is not guaranteed — if you
|
|
2463
|
+
* record the same tool_use twice you get two entries. That's
|
|
2464
|
+
* intentional: the audit log is a record, not a cache.
|
|
2465
|
+
*/
|
|
2466
|
+
async record(input) {
|
|
2467
|
+
let entry = null;
|
|
2468
|
+
await this.enqueue(input.sessionId, async () => {
|
|
2469
|
+
const prevHash = this.tailHash.get(input.sessionId) ?? GENESIS_PREV;
|
|
2470
|
+
const id = randomUUID();
|
|
2471
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2472
|
+
const content = {
|
|
2473
|
+
id,
|
|
2474
|
+
ts,
|
|
2475
|
+
prevHash,
|
|
2476
|
+
toolName: input.toolName,
|
|
2477
|
+
toolUseId: input.toolUseId,
|
|
2478
|
+
input: input.input,
|
|
2479
|
+
output: input.output,
|
|
2480
|
+
isError: input.isError
|
|
2481
|
+
};
|
|
2482
|
+
const hash = createHash("sha256").update(stableStringify2(content), "utf8").digest("hex");
|
|
2483
|
+
entry = {
|
|
2484
|
+
index: this.tailHash.has(input.sessionId) ? -1 : 0,
|
|
2485
|
+
// placeholder; recomputed below
|
|
2486
|
+
id,
|
|
2487
|
+
ts,
|
|
2488
|
+
prevHash,
|
|
2489
|
+
hash,
|
|
2490
|
+
toolName: input.toolName,
|
|
2491
|
+
toolUseId: input.toolUseId,
|
|
2492
|
+
input: input.input,
|
|
2493
|
+
output: input.output,
|
|
2494
|
+
isError: input.isError
|
|
2495
|
+
};
|
|
2496
|
+
const existing = await this.readAll(input.sessionId);
|
|
2497
|
+
const index = existing.length;
|
|
2498
|
+
const finalContent = { ...content, index };
|
|
2499
|
+
const finalHash = createHash("sha256").update(stableStringify2(finalContent), "utf8").digest("hex");
|
|
2500
|
+
entry = { ...entry, index, hash: finalHash };
|
|
2501
|
+
await this.appendLine(input.sessionId, entry);
|
|
2502
|
+
this.tailHash.set(input.sessionId, finalHash);
|
|
2503
|
+
});
|
|
2504
|
+
return entry;
|
|
2505
|
+
}
|
|
2506
|
+
/**
|
|
2507
|
+
* Walk the chain and verify every entry's hash and prevHash.
|
|
2508
|
+
* Returns a structured verdict — never throws.
|
|
2509
|
+
*/
|
|
2510
|
+
async verify(sessionId) {
|
|
2511
|
+
const entries = await this.readAll(sessionId);
|
|
2512
|
+
if (entries.length === 0) return { ok: true, entries: 0 };
|
|
2513
|
+
if (entries[0].prevHash !== GENESIS_PREV) {
|
|
2514
|
+
return {
|
|
2515
|
+
ok: false,
|
|
2516
|
+
brokenAt: 0,
|
|
2517
|
+
reason: "first entry is not the genesis (prevHash != 0\u20260)"
|
|
2518
|
+
};
|
|
2519
|
+
}
|
|
2520
|
+
let prevHash = GENESIS_PREV;
|
|
2521
|
+
for (let i = 0; i < entries.length; i++) {
|
|
2522
|
+
const e = entries[i];
|
|
2523
|
+
if (e.prevHash !== prevHash) {
|
|
2524
|
+
return {
|
|
2525
|
+
ok: false,
|
|
2526
|
+
brokenAt: i,
|
|
2527
|
+
reason: `prevHash mismatch at entry ${i} (expected ${prevHash.slice(0, 8)}\u2026, got ${e.prevHash.slice(0, 8)}\u2026)`
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
const content = {
|
|
2531
|
+
id: e.id,
|
|
2532
|
+
ts: e.ts,
|
|
2533
|
+
prevHash: e.prevHash,
|
|
2534
|
+
toolName: e.toolName,
|
|
2535
|
+
toolUseId: e.toolUseId,
|
|
2536
|
+
input: e.input,
|
|
2537
|
+
output: e.output,
|
|
2538
|
+
isError: e.isError,
|
|
2539
|
+
index: e.index
|
|
2540
|
+
};
|
|
2541
|
+
const expectedHash = createHash("sha256").update(stableStringify2(content), "utf8").digest("hex");
|
|
2542
|
+
if (expectedHash !== e.hash) {
|
|
2543
|
+
return {
|
|
2544
|
+
ok: false,
|
|
2545
|
+
brokenAt: i,
|
|
2546
|
+
reason: `hash mismatch at entry ${i} (entry content was modified)`
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
prevHash = e.hash;
|
|
2550
|
+
}
|
|
2551
|
+
return { ok: true, entries: entries.length };
|
|
2552
|
+
}
|
|
2553
|
+
/** All entries for a session, in insertion order. */
|
|
2554
|
+
async load(sessionId) {
|
|
2555
|
+
return this.readAll(sessionId);
|
|
2556
|
+
}
|
|
2557
|
+
// ── Internals ────────────────────────────────────────────────────────────
|
|
2558
|
+
filePath(sessionId) {
|
|
2559
|
+
if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
|
|
2560
|
+
throw new Error(`Invalid sessionId: ${sessionId}`);
|
|
2561
|
+
}
|
|
2562
|
+
return path11.join(this.dir, `${sessionId}.audit.jsonl`);
|
|
2563
|
+
}
|
|
2564
|
+
async readAll(sessionId) {
|
|
2565
|
+
const fp = this.filePath(sessionId);
|
|
2566
|
+
try {
|
|
2567
|
+
const raw = await fsp.readFile(fp, "utf8");
|
|
2568
|
+
const out = [];
|
|
2569
|
+
for (const line of raw.split("\n")) {
|
|
2570
|
+
if (!line.trim()) continue;
|
|
2571
|
+
try {
|
|
2572
|
+
out.push(JSON.parse(line));
|
|
2573
|
+
} catch {
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
return out;
|
|
2577
|
+
} catch (err) {
|
|
2578
|
+
if (err.code === "ENOENT") return [];
|
|
2579
|
+
return [];
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
async appendLine(sessionId, entry) {
|
|
2583
|
+
const fp = this.filePath(sessionId);
|
|
2584
|
+
const existing = await this.readAll(sessionId);
|
|
2585
|
+
const all = [...existing, entry];
|
|
2586
|
+
await atomicWrite(fp, all.map((e) => JSON.stringify(e)).join("\n") + (all.length ? "\n" : ""));
|
|
2587
|
+
}
|
|
2588
|
+
enqueue(sessionId, fn) {
|
|
2589
|
+
const prev = this.writeChains.get(sessionId) ?? Promise.resolve();
|
|
2590
|
+
const next = prev.then(fn, fn);
|
|
2591
|
+
this.writeChains.set(
|
|
2592
|
+
sessionId,
|
|
2593
|
+
next.catch(() => void 0)
|
|
2594
|
+
);
|
|
2595
|
+
return next;
|
|
2596
|
+
}
|
|
2597
|
+
};
|
|
2598
|
+
function stableStringify2(value) {
|
|
2599
|
+
return JSON.stringify(sortKeys2(value));
|
|
2600
|
+
}
|
|
2601
|
+
function sortKeys2(value) {
|
|
2602
|
+
if (Array.isArray(value)) return value.map(sortKeys2);
|
|
2603
|
+
if (value && typeof value === "object") {
|
|
2604
|
+
const obj = value;
|
|
2605
|
+
const sorted = {};
|
|
2606
|
+
for (const key of Object.keys(obj).sort()) {
|
|
2607
|
+
sorted[key] = sortKeys2(obj[key]);
|
|
2608
|
+
}
|
|
2609
|
+
return sorted;
|
|
2610
|
+
}
|
|
2611
|
+
return value;
|
|
2612
|
+
}
|
|
1952
2613
|
|
|
1953
2614
|
// src/storage/session-analyzer.ts
|
|
1954
2615
|
var SessionAnalyzer = class {
|
|
@@ -2054,7 +2715,7 @@ var DefaultSessionRewinder = class {
|
|
|
2054
2715
|
sessionsDir;
|
|
2055
2716
|
projectRoot;
|
|
2056
2717
|
async listCheckpoints(sessionId) {
|
|
2057
|
-
const file =
|
|
2718
|
+
const file = path11.join(this.sessionsDir, `${sessionId}.jsonl`);
|
|
2058
2719
|
const raw = await fsp.readFile(file, "utf8");
|
|
2059
2720
|
const events = parseEvents(raw);
|
|
2060
2721
|
const fileCountMap = /* @__PURE__ */ new Map();
|
|
@@ -2079,7 +2740,7 @@ var DefaultSessionRewinder = class {
|
|
|
2079
2740
|
return checkpoints;
|
|
2080
2741
|
}
|
|
2081
2742
|
async rewindToCheckpoint(sessionId, checkpointIndex) {
|
|
2082
|
-
const file =
|
|
2743
|
+
const file = path11.join(this.sessionsDir, `${sessionId}.jsonl`);
|
|
2083
2744
|
const raw = await fsp.readFile(file, "utf8");
|
|
2084
2745
|
const events = parseEvents(raw);
|
|
2085
2746
|
let targetIdx = -1;
|
|
@@ -2114,7 +2775,7 @@ var DefaultSessionRewinder = class {
|
|
|
2114
2775
|
return { ...result, toPromptIndex: checkpointIndex, removedEvents };
|
|
2115
2776
|
}
|
|
2116
2777
|
async rewindLastN(sessionId, n) {
|
|
2117
|
-
const file =
|
|
2778
|
+
const file = path11.join(this.sessionsDir, `${sessionId}.jsonl`);
|
|
2118
2779
|
const raw = await fsp.readFile(file, "utf8");
|
|
2119
2780
|
const events = parseEvents(raw);
|
|
2120
2781
|
const checkpoints = [];
|
|
@@ -2143,7 +2804,7 @@ var DefaultSessionRewinder = class {
|
|
|
2143
2804
|
return { ...result, toPromptIndex: targetIndex, removedEvents: snapshotsToRevert.length };
|
|
2144
2805
|
}
|
|
2145
2806
|
async rewindToStart(sessionId) {
|
|
2146
|
-
const file =
|
|
2807
|
+
const file = path11.join(this.sessionsDir, `${sessionId}.jsonl`);
|
|
2147
2808
|
const raw = await fsp.readFile(file, "utf8");
|
|
2148
2809
|
const events = parseEvents(raw);
|
|
2149
2810
|
const allSnapshots = [];
|
|
@@ -2179,10 +2840,10 @@ async function revertSnapshots(snapshots, projectRoot) {
|
|
|
2179
2840
|
for (const snapshot of snapshots) {
|
|
2180
2841
|
for (const file of snapshot.files) {
|
|
2181
2842
|
try {
|
|
2182
|
-
const absPath =
|
|
2183
|
-
const root =
|
|
2184
|
-
const rel =
|
|
2185
|
-
if (rel.startsWith("..") ||
|
|
2843
|
+
const absPath = path11.resolve(file.path);
|
|
2844
|
+
const root = path11.resolve(projectRoot);
|
|
2845
|
+
const rel = path11.relative(root, absPath);
|
|
2846
|
+
if (rel.startsWith("..") || path11.isAbsolute(rel)) {
|
|
2186
2847
|
errors.push(`${file.path}: path resolves outside project root \u2014 skipping`);
|
|
2187
2848
|
continue;
|
|
2188
2849
|
}
|
|
@@ -2741,8 +3402,8 @@ var FsError = class extends WrongStackError {
|
|
|
2741
3402
|
// src/storage/goal-store.ts
|
|
2742
3403
|
var MAX_JOURNAL_ENTRIES = 500;
|
|
2743
3404
|
function goalFilePath(projectRoot) {
|
|
2744
|
-
const hash = createHash("sha256").update(
|
|
2745
|
-
return
|
|
3405
|
+
const hash = createHash("sha256").update(path11.resolve(projectRoot)).digest("hex").slice(0, 12);
|
|
3406
|
+
return path11.join(os.homedir(), ".wrongstack", "projects", hash, "goal.json");
|
|
2746
3407
|
}
|
|
2747
3408
|
async function loadGoal(filePath) {
|
|
2748
3409
|
let raw;
|
|
@@ -2850,14 +3511,14 @@ var DefaultPromptStore = class {
|
|
|
2850
3511
|
}
|
|
2851
3512
|
async list() {
|
|
2852
3513
|
await ensureDir(this.dir);
|
|
2853
|
-
|
|
3514
|
+
const entries = [];
|
|
2854
3515
|
try {
|
|
2855
3516
|
const files = await fsp.readdir(this.dir);
|
|
2856
3517
|
for (const file of files) {
|
|
2857
3518
|
if (!file.endsWith(".json")) continue;
|
|
2858
3519
|
try {
|
|
2859
3520
|
const raw = JSON.parse(
|
|
2860
|
-
await fsp.readFile(
|
|
3521
|
+
await fsp.readFile(path11.join(this.dir, file), "utf8")
|
|
2861
3522
|
);
|
|
2862
3523
|
entries.push(raw.entry);
|
|
2863
3524
|
} catch {
|
|
@@ -2870,7 +3531,7 @@ var DefaultPromptStore = class {
|
|
|
2870
3531
|
);
|
|
2871
3532
|
}
|
|
2872
3533
|
async get(id) {
|
|
2873
|
-
const file =
|
|
3534
|
+
const file = path11.join(this.dir, `${id}.json`);
|
|
2874
3535
|
try {
|
|
2875
3536
|
const raw = JSON.parse(await fsp.readFile(file, "utf8"));
|
|
2876
3537
|
return raw.entry;
|
|
@@ -2880,12 +3541,12 @@ var DefaultPromptStore = class {
|
|
|
2880
3541
|
}
|
|
2881
3542
|
async save(entry) {
|
|
2882
3543
|
await ensureDir(this.dir);
|
|
2883
|
-
const file =
|
|
3544
|
+
const file = path11.join(this.dir, `${entry.id}.json`);
|
|
2884
3545
|
const raw = { version: 1, entry };
|
|
2885
3546
|
await atomicWrite(file, JSON.stringify(raw, null, 2));
|
|
2886
3547
|
}
|
|
2887
3548
|
async delete(id) {
|
|
2888
|
-
const file =
|
|
3549
|
+
const file = path11.join(this.dir, `${id}.json`);
|
|
2889
3550
|
try {
|
|
2890
3551
|
await fsp.unlink(file);
|
|
2891
3552
|
return true;
|
|
@@ -2919,7 +3580,7 @@ var CloudSync = class {
|
|
|
2919
3580
|
this.paths = paths;
|
|
2920
3581
|
this.getConfig = getConfig;
|
|
2921
3582
|
this.setConfig = setConfig;
|
|
2922
|
-
this.statePath =
|
|
3583
|
+
this.statePath = path11.join(paths.globalRoot, "sync-state.json");
|
|
2923
3584
|
}
|
|
2924
3585
|
paths;
|
|
2925
3586
|
getConfig;
|
|
@@ -3024,9 +3685,9 @@ var CloudSync = class {
|
|
|
3024
3685
|
const localPath = this.categoryToPath(cat);
|
|
3025
3686
|
if (!localPath) continue;
|
|
3026
3687
|
const rel = segments.slice(2).join("/");
|
|
3027
|
-
const destPath = rel ?
|
|
3688
|
+
const destPath = rel ? path11.join(localPath, rel) : localPath;
|
|
3028
3689
|
const blobData = await this.getBlob(token, owner, repoName, entry.sha);
|
|
3029
|
-
await fsp.mkdir(
|
|
3690
|
+
await fsp.mkdir(path11.dirname(destPath), { recursive: true });
|
|
3030
3691
|
await fsp.writeFile(destPath, Buffer.from(blobData, "base64"));
|
|
3031
3692
|
}
|
|
3032
3693
|
const localRev = await this.hashLocalCategories(cfg.categories);
|
|
@@ -3131,7 +3792,7 @@ var CloudSync = class {
|
|
|
3131
3792
|
const files = await this.walkDir(localPath, localPath);
|
|
3132
3793
|
for (const file of files) {
|
|
3133
3794
|
const content = await fsp.readFile(file, "utf8");
|
|
3134
|
-
const rel =
|
|
3795
|
+
const rel = path11.relative(localPath, file).replace(/\\/g, "/");
|
|
3135
3796
|
entries.push({ path: `data/${cat}/${rel}`, content, mode: "100644" });
|
|
3136
3797
|
hashes.push(content);
|
|
3137
3798
|
}
|
|
@@ -3188,7 +3849,7 @@ var CloudSync = class {
|
|
|
3188
3849
|
const results = [];
|
|
3189
3850
|
const entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
3190
3851
|
for (const entry of entries) {
|
|
3191
|
-
const full =
|
|
3852
|
+
const full = path11.join(dir, entry.name);
|
|
3192
3853
|
if (entry.isDirectory()) {
|
|
3193
3854
|
results.push(...await this.walkDir(full, base));
|
|
3194
3855
|
} else {
|
|
@@ -3209,6 +3870,101 @@ function timeAgo(iso) {
|
|
|
3209
3870
|
return `${days}d ago`;
|
|
3210
3871
|
}
|
|
3211
3872
|
|
|
3212
|
-
|
|
3873
|
+
// src/storage/session-event-bridge.ts
|
|
3874
|
+
var CORE_RECONSTRUCT_EVENTS = /* @__PURE__ */ new Set([
|
|
3875
|
+
"session_start",
|
|
3876
|
+
"session_resumed",
|
|
3877
|
+
"user_input",
|
|
3878
|
+
"llm_response",
|
|
3879
|
+
"tool_result",
|
|
3880
|
+
"checkpoint",
|
|
3881
|
+
"file_snapshot",
|
|
3882
|
+
"rewound",
|
|
3883
|
+
"in_flight_start",
|
|
3884
|
+
"in_flight_end",
|
|
3885
|
+
"session_end"
|
|
3886
|
+
]);
|
|
3887
|
+
var STANDARD_AUDIT_EVENTS = /* @__PURE__ */ new Set([
|
|
3888
|
+
"llm_request",
|
|
3889
|
+
"tool_use",
|
|
3890
|
+
"tool_call_start",
|
|
3891
|
+
"tool_call_end",
|
|
3892
|
+
"compaction",
|
|
3893
|
+
"error",
|
|
3894
|
+
"message_truncated",
|
|
3895
|
+
"provider_retry",
|
|
3896
|
+
"provider_error"
|
|
3897
|
+
]);
|
|
3898
|
+
var FULL_ONLY_EVENTS = /* @__PURE__ */ new Set([
|
|
3899
|
+
"tool_progress"
|
|
3900
|
+
]);
|
|
3901
|
+
function isAllowed(type, level) {
|
|
3902
|
+
if (CORE_RECONSTRUCT_EVENTS.has(type)) return true;
|
|
3903
|
+
if (level === "minimal") return false;
|
|
3904
|
+
if (STANDARD_AUDIT_EVENTS.has(type)) return true;
|
|
3905
|
+
if (level === "standard") return false;
|
|
3906
|
+
if (FULL_ONLY_EVENTS.has(type)) {
|
|
3907
|
+
return level === "full";
|
|
3908
|
+
}
|
|
3909
|
+
return true;
|
|
3910
|
+
}
|
|
3911
|
+
function createSessionEventBridge(writer, level = "standard", options = {}) {
|
|
3912
|
+
const normalizedLevel = level ?? "standard";
|
|
3913
|
+
const progressCounters = /* @__PURE__ */ new Map();
|
|
3914
|
+
const toolProgressConfig = options.sampling?.toolProgress ?? {};
|
|
3915
|
+
const TOOL_PROGRESS_SAMPLE_RATE = toolProgressConfig.sampleRate ?? 8;
|
|
3916
|
+
function shouldSample(event) {
|
|
3917
|
+
if (event.type !== "tool_progress") return true;
|
|
3918
|
+
const progEvent = event;
|
|
3919
|
+
const innerType = progEvent.event?.type;
|
|
3920
|
+
if (innerType === "warning" || innerType === "metric" || innerType === "file_changed") {
|
|
3921
|
+
return true;
|
|
3922
|
+
}
|
|
3923
|
+
if (innerType === "log" || innerType === "partial_output") {
|
|
3924
|
+
const key = progEvent.id || progEvent.name;
|
|
3925
|
+
const count = (progressCounters.get(key) || 0) + 1;
|
|
3926
|
+
progressCounters.set(key, count);
|
|
3927
|
+
return count === 1 || count % TOOL_PROGRESS_SAMPLE_RATE === 0;
|
|
3928
|
+
}
|
|
3929
|
+
return true;
|
|
3930
|
+
}
|
|
3931
|
+
return {
|
|
3932
|
+
level: normalizedLevel,
|
|
3933
|
+
allows(type) {
|
|
3934
|
+
return isAllowed(type, normalizedLevel);
|
|
3935
|
+
},
|
|
3936
|
+
async append(event) {
|
|
3937
|
+
if (!writer) return;
|
|
3938
|
+
if (!isAllowed(event.type, normalizedLevel)) return;
|
|
3939
|
+
if (!shouldSample(event)) return;
|
|
3940
|
+
try {
|
|
3941
|
+
await writer.append(event);
|
|
3942
|
+
} catch (err) {
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
};
|
|
3946
|
+
}
|
|
3947
|
+
function resolveAuditLevel(cfg) {
|
|
3948
|
+
const raw = cfg?.session?.auditLevel;
|
|
3949
|
+
if (raw === "minimal" || raw === "standard" || raw === "full") {
|
|
3950
|
+
return raw;
|
|
3951
|
+
}
|
|
3952
|
+
return "standard";
|
|
3953
|
+
}
|
|
3954
|
+
function resolveSessionLoggingConfig(cfg) {
|
|
3955
|
+
const session = cfg?.session ?? {};
|
|
3956
|
+
const auditLevel = resolveAuditLevel(cfg);
|
|
3957
|
+
const toolProgressSampleRate = session.sampling?.toolProgress?.sampleRate ?? 8;
|
|
3958
|
+
return {
|
|
3959
|
+
auditLevel,
|
|
3960
|
+
sampling: {
|
|
3961
|
+
toolProgress: {
|
|
3962
|
+
sampleRate: Math.max(1, Math.floor(toolProgressSampleRate))
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
};
|
|
3966
|
+
}
|
|
3967
|
+
|
|
3968
|
+
export { ALL_SYNC_CATEGORIES, AnnotationsStore, CORE_RECONSTRUCT_EVENTS, CloudSync, ConfigMigrationError, DEFAULT_CONFIG_MIGRATIONS, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultMemoryStore, DefaultPromptStore, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DirectorStateCheckpoint, MAX_JOURNAL_ENTRIES, QueueStore, RecoveryLock, ReplayLogStore, STANDARD_AUDIT_EVENTS, SessionAnalyzer, SessionRecovery, ToolAuditLog, addPlanItem, appendJournal, attachPlanCheckpoint, attachTodosCheckpoint, clearPlan, createSessionEventBridge, deriveTodosFromPlanItem, emptyGoal, emptyPlan, formatGoal, formatPlan, formatPlanTemplates, getPlanTemplate, goalFilePath, listPlanTemplates, loadDirectorState, loadGoal, loadPlan, loadTodosCheckpoint, removePlanItem, resolveAuditLevel, resolveSessionLoggingConfig, runConfigMigrations, saveGoal, savePlan, saveTodosCheckpoint, setPlanItemStatus, summarizeUsage };
|
|
3213
3969
|
//# sourceMappingURL=index.js.map
|
|
3214
3970
|
//# sourceMappingURL=index.js.map
|