@wrongstack/core 0.257.2 → 0.264.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agent-bridge-BrxWHEOm.d.ts → agent-bridge-D8sa1vtv.d.ts} +1 -1
- package/dist/{agent-subagent-runner-US741uBH.d.ts → agent-subagent-runner-c9DLkaas.d.ts} +31 -9
- package/dist/{brain-TjEEwSpw.d.ts → brain-O1IdKPaK.d.ts} +59 -2
- package/dist/{compactor-C5sT4U7I.d.ts → compactor-BBy0rCtB.d.ts} +1 -1
- package/dist/{config-DuAu23zm.d.ts → config-Dz2F3H2K.d.ts} +7 -1
- package/dist/{context-CGdgA0q6.d.ts → context-BGSpZNSE.d.ts} +33 -0
- package/dist/coordination/index.d.ts +1681 -15
- package/dist/coordination/index.js +2826 -405
- package/dist/coordination/index.js.map +1 -1
- package/dist/defaults/index.d.ts +25 -25
- package/dist/defaults/index.js +2258 -1433
- package/dist/defaults/index.js.map +1 -1
- package/dist/dispatcher-types.d-BBeXBQgS.d.ts +66 -0
- package/dist/execution/index.d.ts +15 -15
- package/dist/execution/index.js +502 -398
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/prompt-enhancer.d.ts +2 -2
- package/dist/execution/prompt-enhancer.js +7 -1
- package/dist/execution/prompt-enhancer.js.map +1 -1
- package/dist/extension/index.d.ts +6 -6
- package/dist/extension/index.js.map +1 -1
- package/dist/{goal-preamble-CznHTZqP.d.ts → goal-preamble-DzjFuN3p.d.ts} +21 -9
- package/dist/{goal-store-CV9Yz2X_.d.ts → goal-store-CxWmCGbH.d.ts} +4 -2
- package/dist/{index-CC0Mcm05.d.ts → index-CYIQrXVF.d.ts} +8 -8
- package/dist/{index-CitPrI3a.d.ts → index-CbLSI66_.d.ts} +5 -5
- package/dist/index.d.ts +50 -94
- package/dist/index.js +16009 -12406
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/index.d.ts +6 -6
- package/dist/kernel/index.d.ts +9 -9
- package/dist/kernel/index.js +6 -1
- package/dist/kernel/index.js.map +1 -1
- package/dist/{llm-selector-CJ4SyAFE.d.ts → llm-selector-DzxuZnNz.d.ts} +2 -2
- package/dist/{mcp-servers-D8YnLaEp.d.ts → mcp-servers-DC4QRPUI.d.ts} +3 -3
- package/dist/models/index.d.ts +5 -5
- package/dist/models/index.js +6 -1
- package/dist/models/index.js.map +1 -1
- package/dist/{models-registry-ByZCdFuQ.d.ts → models-registry-B_siPxqN.d.ts} +1 -1
- package/dist/{multi-agent-coordinator-DqTUEAeC.d.ts → multi-agent-coordinator-CK5Jdj9K.d.ts} +2 -2
- package/dist/{null-fleet-bus-B5mfTJXT.d.ts → null-fleet-bus-DgvD4SCO.d.ts} +13 -8
- package/dist/observability/index.d.ts +2 -2
- package/dist/observability/index.js +8 -3
- package/dist/observability/index.js.map +1 -1
- package/dist/{parallel-eternal-engine-C0juOszP.d.ts → parallel-eternal-engine-bK0JQBR_.d.ts} +13 -9
- package/dist/{path-resolver-CbkT-RMU.d.ts → path-resolver-BPEDlN38.d.ts} +3 -3
- package/dist/{permission-CwBBpCoF.d.ts → permission-4yvGmMRB.d.ts} +1 -1
- package/dist/{permission-policy-B8rSu908.d.ts → permission-policy-C6XpsBOy.d.ts} +3 -2
- package/dist/{pipeline-JG8XoudC.d.ts → pipeline-CXCeMz8J.d.ts} +58 -3
- package/dist/{plan-templates-DPiQMkBz.d.ts → plan-templates-BvzRBkJc.d.ts} +32 -11
- package/dist/{provider-runner-hM7EXlLI.d.ts → provider-runner-C5aQpDWE.d.ts} +3 -3
- package/dist/{retry-policy-Tg7LXkoK.d.ts → retry-policy-CFhdtRzz.d.ts} +1 -1
- package/dist/sdd/index.d.ts +8 -8
- package/dist/sdd/index.js +59 -31
- package/dist/sdd/index.js.map +1 -1
- package/dist/{secret-vault-gxtFZYBt.d.ts → secret-vault-CxiVLbt1.d.ts} +1 -1
- package/dist/security/index.d.ts +4 -4
- package/dist/security/index.js +238 -204
- package/dist/security/index.js.map +1 -1
- package/dist/{selector-DWsqVjGf.d.ts → selector-gIuhRTkN.d.ts} +1 -1
- package/dist/{session-event-bridge-BAFWdgQ3.d.ts → session-event-bridge-DkvvrpDt.d.ts} +8 -2
- package/dist/{session-reader-CqRvaL5v.d.ts → session-reader-KdfVwkKP.d.ts} +1 -1
- package/dist/skills/index.js +67 -64
- package/dist/skills/index.js.map +1 -1
- package/dist/storage/index.d.ts +50 -22
- package/dist/storage/index.js +1654 -525
- package/dist/storage/index.js.map +1 -1
- package/dist/tools/index.d.ts +57 -0
- package/dist/tools/index.js +411 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/types/index.d.ts +19 -19
- package/dist/types/index.js +711 -694
- package/dist/types/index.js.map +1 -1
- package/dist/utils/error.d.ts +7 -0
- package/dist/utils/error.js +8 -0
- package/dist/utils/error.js.map +1 -0
- package/dist/utils/index.d.ts +7 -67
- package/dist/utils/index.js +17 -5
- package/dist/utils/index.js.map +1 -1
- package/package.json +5 -1
- package/skills/output-standards/SKILL.md +14 -9
- package/skills/output-standards/SKILL.save.md +3 -2
- package/dist/package-outdated-watcher-BSgR_kK-.d.ts +0 -581
package/dist/storage/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { randomBytes, randomUUID, createHash } from 'crypto';
|
|
2
2
|
import * as fsp from 'fs/promises';
|
|
3
|
-
import * as
|
|
4
|
-
import 'fs';
|
|
3
|
+
import * as path2 from 'path';
|
|
5
4
|
import * as os from 'os';
|
|
6
5
|
import { hostname } from 'os';
|
|
6
|
+
import 'fs';
|
|
7
7
|
|
|
8
8
|
// src/utils/expect-defined.ts
|
|
9
9
|
function expectDefined(value, label) {
|
|
@@ -15,9 +15,9 @@ function expectDefined(value, label) {
|
|
|
15
15
|
return value;
|
|
16
16
|
}
|
|
17
17
|
async function atomicWrite(targetPath, content, opts = {}) {
|
|
18
|
-
const dir =
|
|
18
|
+
const dir = path2.dirname(targetPath);
|
|
19
19
|
await fsp.mkdir(dir, { recursive: true });
|
|
20
|
-
const tmp =
|
|
20
|
+
const tmp = path2.join(dir, `.${path2.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
|
|
21
21
|
try {
|
|
22
22
|
if (typeof content === "string") {
|
|
23
23
|
await fsp.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
|
|
@@ -35,8 +35,8 @@ async function atomicWrite(targetPath, content, opts = {}) {
|
|
|
35
35
|
}
|
|
36
36
|
let mode;
|
|
37
37
|
try {
|
|
38
|
-
const
|
|
39
|
-
mode =
|
|
38
|
+
const stat6 = await fsp.stat(targetPath);
|
|
39
|
+
mode = stat6.mode & 511;
|
|
40
40
|
} catch {
|
|
41
41
|
mode = opts.mode;
|
|
42
42
|
}
|
|
@@ -56,9 +56,9 @@ async function ensureDir(dir) {
|
|
|
56
56
|
await fsp.mkdir(dir, { recursive: true });
|
|
57
57
|
}
|
|
58
58
|
async function withFileLock(targetPath, fn, opts = {}) {
|
|
59
|
-
const dir =
|
|
59
|
+
const dir = path2.dirname(targetPath);
|
|
60
60
|
await fsp.mkdir(dir, { recursive: true });
|
|
61
|
-
const lockPath =
|
|
61
|
+
const lockPath = path2.join(dir, `.${path2.basename(targetPath)}.lock`);
|
|
62
62
|
const timeoutMs = opts.timeoutMs ?? 5e3;
|
|
63
63
|
const staleMs = opts.staleMs ?? 3e4;
|
|
64
64
|
const started = Date.now();
|
|
@@ -69,10 +69,15 @@ async function withFileLock(targetPath, fn, opts = {}) {
|
|
|
69
69
|
await handle.writeFile(`${process.pid}:${Date.now()}`);
|
|
70
70
|
break;
|
|
71
71
|
} catch (err) {
|
|
72
|
-
|
|
72
|
+
const code = err.code;
|
|
73
|
+
if (code === "ENOENT") {
|
|
74
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (code !== "EEXIST") throw err;
|
|
73
78
|
try {
|
|
74
|
-
const
|
|
75
|
-
if (Date.now() -
|
|
79
|
+
const stat6 = await fsp.stat(lockPath);
|
|
80
|
+
if (Date.now() - stat6.mtimeMs > staleMs) {
|
|
76
81
|
await fsp.unlink(lockPath);
|
|
77
82
|
continue;
|
|
78
83
|
}
|
|
@@ -214,6 +219,215 @@ function isEmptyMessage(msg) {
|
|
|
214
219
|
return msg.content.length === 0;
|
|
215
220
|
}
|
|
216
221
|
|
|
222
|
+
// src/utils/error.ts
|
|
223
|
+
function toErrorMessage(err) {
|
|
224
|
+
return err instanceof Error ? err.message : String(err);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/utils/safe-json.ts
|
|
228
|
+
function safeParse(input, maxBytes = 5e6) {
|
|
229
|
+
if (input.length > maxBytes) {
|
|
230
|
+
return { ok: false, error: `Input exceeds limit (${maxBytes} bytes)` };
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
return { ok: true, value: JSON.parse(input) };
|
|
234
|
+
} catch (err) {
|
|
235
|
+
return {
|
|
236
|
+
ok: false,
|
|
237
|
+
error: toErrorMessage(err)
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/utils/term.ts
|
|
243
|
+
var hasStdout = () => typeof process !== "undefined" && !!process.stdout;
|
|
244
|
+
function isStdoutTTY() {
|
|
245
|
+
return hasStdout() && Boolean(process.stdout.isTTY);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/utils/color.ts
|
|
249
|
+
var isColorTty = () => {
|
|
250
|
+
if (envFlag(process.env.NO_COLOR)) return false;
|
|
251
|
+
if (envFlag(process.env.FORCE_COLOR)) return true;
|
|
252
|
+
return isStdoutTTY();
|
|
253
|
+
};
|
|
254
|
+
function envFlag(value) {
|
|
255
|
+
if (value === void 0) return false;
|
|
256
|
+
if (value.trim() === "") return false;
|
|
257
|
+
return !/^(0|false|no|off)$/i.test(value.trim());
|
|
258
|
+
}
|
|
259
|
+
var COLOR = isColorTty();
|
|
260
|
+
var wrap = (open6, close) => (s) => COLOR ? `\x1B[${open6}m${s}\x1B[${close}m` : s;
|
|
261
|
+
var color = {
|
|
262
|
+
reset: wrap("0", "0"),
|
|
263
|
+
bold: wrap("1", "22"),
|
|
264
|
+
dim: wrap("2", "22"),
|
|
265
|
+
italic: wrap("3", "23"),
|
|
266
|
+
underline: wrap("4", "24"),
|
|
267
|
+
red: wrap("31", "39"),
|
|
268
|
+
green: wrap("32", "39"),
|
|
269
|
+
yellow: wrap("33", "39"),
|
|
270
|
+
blue: wrap("34", "39"),
|
|
271
|
+
magenta: wrap("35", "39"),
|
|
272
|
+
cyan: wrap("36", "39"),
|
|
273
|
+
gray: wrap("90", "39"),
|
|
274
|
+
amber: wrap("38;5;214", "39"),
|
|
275
|
+
pink: wrap("38;5;205", "39"),
|
|
276
|
+
bgRed: wrap("41", "49"),
|
|
277
|
+
bgGreen: wrap("42", "49")
|
|
278
|
+
};
|
|
279
|
+
function projectHash(absRoot) {
|
|
280
|
+
return createHash("sha256").update(path2.resolve(absRoot)).digest("hex").slice(0, 12);
|
|
281
|
+
}
|
|
282
|
+
function projectSlug(absRoot) {
|
|
283
|
+
const base = slugify(path2.basename(absRoot));
|
|
284
|
+
const hash = createHash("sha256").update(path2.resolve(absRoot)).digest("hex").slice(0, 6);
|
|
285
|
+
return `${base}-${hash}`;
|
|
286
|
+
}
|
|
287
|
+
function slugify(name) {
|
|
288
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
|
|
289
|
+
}
|
|
290
|
+
function wstackGlobalRoot() {
|
|
291
|
+
const fromEnv = process.env["WRONGSTACK_HOME"];
|
|
292
|
+
if (fromEnv && fromEnv.trim().length > 0) return path2.resolve(fromEnv);
|
|
293
|
+
return path2.join(os.homedir(), ".wrongstack");
|
|
294
|
+
}
|
|
295
|
+
function resolveWstackPaths(opts) {
|
|
296
|
+
const globalRoot = opts.globalRoot ?? (opts.userHome ? path2.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
|
|
297
|
+
const hash = projectHash(opts.projectRoot);
|
|
298
|
+
const slug = projectSlug(opts.projectRoot);
|
|
299
|
+
const projectDir = path2.join(globalRoot, "projects", slug);
|
|
300
|
+
return {
|
|
301
|
+
globalRoot,
|
|
302
|
+
configDir: globalRoot,
|
|
303
|
+
globalConfig: path2.join(globalRoot, "config.json"),
|
|
304
|
+
secretsKey: path2.join(globalRoot, ".key"),
|
|
305
|
+
globalMemory: path2.join(globalRoot, "memory.md"),
|
|
306
|
+
globalSkills: path2.join(globalRoot, "skills"),
|
|
307
|
+
globalPrompts: path2.join(globalRoot, "prompts"),
|
|
308
|
+
cacheDir: path2.join(globalRoot, "cache"),
|
|
309
|
+
modelsCache: path2.join(globalRoot, "cache", "models.dev.json"),
|
|
310
|
+
modelsOverlayCache: path2.join(globalRoot, "cache", "models-overlay.json"),
|
|
311
|
+
historyFile: path2.join(globalRoot, "history"),
|
|
312
|
+
logFile: path2.join(globalRoot, "logs", "wrongstack.log"),
|
|
313
|
+
projectDir,
|
|
314
|
+
projectCodebaseIndex: path2.join(projectDir, "codebase-index"),
|
|
315
|
+
projectMemory: path2.join(projectDir, "memory.md"),
|
|
316
|
+
projectSessions: path2.join(projectDir, "sessions"),
|
|
317
|
+
projectTrust: path2.join(projectDir, "trust.json"),
|
|
318
|
+
projectMeta: path2.join(projectDir, "meta.json"),
|
|
319
|
+
projectLocalConfig: path2.join(projectDir, "config.local.json"),
|
|
320
|
+
inProjectConfig: path2.join(opts.projectRoot, ".wrongstack", "config.json"),
|
|
321
|
+
inProjectAgentsFile: path2.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
|
|
322
|
+
inProjectSkills: path2.join(opts.projectRoot, ".wrongstack", "skills"),
|
|
323
|
+
inProjectWorktrees: path2.join(opts.projectRoot, ".wrongstack", "worktrees"),
|
|
324
|
+
projectHash: hash,
|
|
325
|
+
projectSlug: slug,
|
|
326
|
+
projectGoal: path2.join(projectDir, "goal.json"),
|
|
327
|
+
projectSpecs: path2.join(projectDir, "specs"),
|
|
328
|
+
projectTaskGraphs: path2.join(projectDir, "task-graphs"),
|
|
329
|
+
projectSddSession: path2.join(projectDir, "sdd-session.json"),
|
|
330
|
+
projectPlan: path2.join(projectDir, "plan.json"),
|
|
331
|
+
projectAutophase: path2.join(projectDir, "autophase"),
|
|
332
|
+
syncConfig: path2.join(globalRoot, "sync.json")
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/utils/deep-merge.ts
|
|
337
|
+
var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set([
|
|
338
|
+
"__proto__",
|
|
339
|
+
"constructor",
|
|
340
|
+
"prototype",
|
|
341
|
+
"__defineGetter__",
|
|
342
|
+
"__defineSetter__",
|
|
343
|
+
"__lookupGetter__",
|
|
344
|
+
"__lookupSetter__"
|
|
345
|
+
]);
|
|
346
|
+
function isPrimitiveArray(a) {
|
|
347
|
+
return a.every((v) => v === null || typeof v !== "object" && typeof v !== "function");
|
|
348
|
+
}
|
|
349
|
+
function deepMerge(base, patch, options = {}) {
|
|
350
|
+
const {
|
|
351
|
+
conflictResolution = "prefer-patch",
|
|
352
|
+
arrayMode = "replace",
|
|
353
|
+
protectProto = true,
|
|
354
|
+
onNonPrimitiveArrayReplace
|
|
355
|
+
} = options;
|
|
356
|
+
if (typeof base !== "object" || base === null) {
|
|
357
|
+
return conflictResolution === "prefer-patch" ? patch : base;
|
|
358
|
+
}
|
|
359
|
+
if (typeof patch !== "object" || patch === null) {
|
|
360
|
+
return conflictResolution === "prefer-patch" ? patch : base;
|
|
361
|
+
}
|
|
362
|
+
if (Array.isArray(base) && Array.isArray(patch)) {
|
|
363
|
+
if (arrayMode === "concat-primitives" && isPrimitiveArray(base) && isPrimitiveArray(patch)) {
|
|
364
|
+
return [.../* @__PURE__ */ new Set([...base, ...patch])];
|
|
365
|
+
}
|
|
366
|
+
return conflictResolution === "prefer-patch" ? patch : base;
|
|
367
|
+
}
|
|
368
|
+
if (Array.isArray(base) || Array.isArray(patch)) {
|
|
369
|
+
return conflictResolution === "prefer-patch" ? patch : base;
|
|
370
|
+
}
|
|
371
|
+
const baseObj = base;
|
|
372
|
+
const patchObj = patch;
|
|
373
|
+
const out = { ...baseObj };
|
|
374
|
+
for (const [k, v] of Object.entries(patchObj)) {
|
|
375
|
+
if (protectProto && FORBIDDEN_PROTO_KEYS.has(k)) continue;
|
|
376
|
+
const existing = out[k];
|
|
377
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
|
|
378
|
+
out[k] = deepMerge(existing, v, options);
|
|
379
|
+
} else if (Array.isArray(v) && Array.isArray(existing)) {
|
|
380
|
+
if (onNonPrimitiveArrayReplace && !isPrimitiveArray(v)) {
|
|
381
|
+
onNonPrimitiveArrayReplace(k, existing.length, v.length);
|
|
382
|
+
}
|
|
383
|
+
out[k] = deepMerge(existing, v, options);
|
|
384
|
+
} else if (v !== void 0) {
|
|
385
|
+
if (onNonPrimitiveArrayReplace && Array.isArray(v) && !isPrimitiveArray(v)) {
|
|
386
|
+
const existingLen = Array.isArray(existing) ? existing.length : 0;
|
|
387
|
+
onNonPrimitiveArrayReplace(k, existingLen, v.length);
|
|
388
|
+
}
|
|
389
|
+
out[k] = v;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return out;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/utils/regex-guard.ts
|
|
396
|
+
var MAX_PATTERN_LEN = 512;
|
|
397
|
+
var DANGEROUS_PATTERNS = [
|
|
398
|
+
/(\([^)]*[+*][^)]*\))[+*]/,
|
|
399
|
+
// (a+)+, (.*)+, etc
|
|
400
|
+
/(\(\?:[^)]*[+*][^)]*\))[+*]/
|
|
401
|
+
// same, with non-capturing group
|
|
402
|
+
];
|
|
403
|
+
function compileUserRegex(pattern, flags) {
|
|
404
|
+
if (typeof pattern !== "string") {
|
|
405
|
+
return { ok: false, reason: "pattern must be a string" };
|
|
406
|
+
}
|
|
407
|
+
if (pattern.length === 0) {
|
|
408
|
+
return { ok: false, reason: "pattern is empty" };
|
|
409
|
+
}
|
|
410
|
+
if (pattern.length > MAX_PATTERN_LEN) {
|
|
411
|
+
return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
|
|
412
|
+
}
|
|
413
|
+
for (const rx of DANGEROUS_PATTERNS) {
|
|
414
|
+
if (rx.test(pattern)) {
|
|
415
|
+
return {
|
|
416
|
+
ok: false,
|
|
417
|
+
reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
return { ok: true, regex: new RegExp(pattern, flags) };
|
|
423
|
+
} catch (err) {
|
|
424
|
+
return {
|
|
425
|
+
ok: false,
|
|
426
|
+
reason: err instanceof Error ? err.message : "invalid regex"
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
217
431
|
// src/storage/session-store.ts
|
|
218
432
|
function sanitizeModel(model) {
|
|
219
433
|
return model.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
@@ -234,13 +448,47 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
234
448
|
this.events = opts.events;
|
|
235
449
|
this.secretScrubber = opts.secretScrubber;
|
|
236
450
|
}
|
|
451
|
+
// ── Storage event helpers ───────────────────────────────────────────────────
|
|
452
|
+
emitRead(sessionId, filePath, operation, outcome, durationMs, error) {
|
|
453
|
+
this.events?.emit("storage.read", {
|
|
454
|
+
sessionId,
|
|
455
|
+
store: "session",
|
|
456
|
+
filePath,
|
|
457
|
+
operation,
|
|
458
|
+
outcome,
|
|
459
|
+
durationMs,
|
|
460
|
+
...error !== void 0 ? { error } : {}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
emitWrite(sessionId, filePath, operation, outcome, durationMs, eventCount, error) {
|
|
464
|
+
this.events?.emit("storage.write", {
|
|
465
|
+
sessionId,
|
|
466
|
+
store: "session",
|
|
467
|
+
filePath,
|
|
468
|
+
operation,
|
|
469
|
+
outcome,
|
|
470
|
+
durationMs,
|
|
471
|
+
...eventCount !== void 0 ? { eventCount } : {},
|
|
472
|
+
...error !== void 0 ? { error } : {}
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
emitError(sessionId, filePath, operation, error, recoverable) {
|
|
476
|
+
this.events?.emit("storage.error", {
|
|
477
|
+
sessionId,
|
|
478
|
+
store: "session",
|
|
479
|
+
filePath,
|
|
480
|
+
operation,
|
|
481
|
+
error,
|
|
482
|
+
recoverable
|
|
483
|
+
});
|
|
484
|
+
}
|
|
237
485
|
/** Absolute path to the session index file. */
|
|
238
486
|
get indexFile() {
|
|
239
|
-
return
|
|
487
|
+
return path2.join(this.dir, "_index.jsonl");
|
|
240
488
|
}
|
|
241
489
|
/** Join session ID to its absolute path within the store directory. */
|
|
242
490
|
sessionPath(id, ext) {
|
|
243
|
-
return
|
|
491
|
+
return path2.join(this.dir, `${id}${ext}`);
|
|
244
492
|
}
|
|
245
493
|
/**
|
|
246
494
|
* Ensure the directory implied by the session ID exists. When the ID
|
|
@@ -248,7 +496,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
248
496
|
* subdirectory so sessions group naturally by day.
|
|
249
497
|
*/
|
|
250
498
|
async ensureShardDir(id) {
|
|
251
|
-
const dirPath =
|
|
499
|
+
const dirPath = path2.dirname(path2.join(this.dir, id));
|
|
252
500
|
await ensureDir(dirPath);
|
|
253
501
|
return dirPath;
|
|
254
502
|
}
|
|
@@ -256,23 +504,27 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
256
504
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
257
505
|
const id = meta.id && meta.id.length > 0 ? meta.id : generateSessionId(startedAt, meta.model ?? meta.provider);
|
|
258
506
|
const shardDir = await this.ensureShardDir(id);
|
|
259
|
-
const file =
|
|
507
|
+
const file = path2.join(shardDir, `${path2.basename(id)}.jsonl`);
|
|
508
|
+
const t0 = Date.now();
|
|
260
509
|
let handle;
|
|
261
510
|
try {
|
|
262
511
|
handle = await fsp.open(file, "a", 384);
|
|
263
512
|
} catch (err) {
|
|
513
|
+
this.emitError(id, file, "create", toErrorMessage(err), false);
|
|
264
514
|
throw new Error(
|
|
265
|
-
`Failed to open session file: ${
|
|
515
|
+
`Failed to open session file: ${toErrorMessage(err)}`,
|
|
266
516
|
{ cause: err }
|
|
267
517
|
);
|
|
268
518
|
}
|
|
269
519
|
try {
|
|
270
|
-
|
|
520
|
+
const writer = new FileSessionWriter(id, handle, startedAt, meta, this.events, {
|
|
271
521
|
dir: shardDir,
|
|
272
522
|
filePath: file,
|
|
273
523
|
secretScrubber: this.secretScrubber,
|
|
274
524
|
onClose: (s) => this.appendToIndex(s)
|
|
275
525
|
});
|
|
526
|
+
this.emitWrite(id, file, "create", "success", Date.now() - t0);
|
|
527
|
+
return writer;
|
|
276
528
|
} catch (err) {
|
|
277
529
|
await handle.close().catch((e) => console.warn(JSON.stringify({
|
|
278
530
|
level: "warn",
|
|
@@ -280,18 +532,21 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
280
532
|
message: e instanceof Error ? e.message : String(e),
|
|
281
533
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
282
534
|
})));
|
|
535
|
+
this.emitError(id, file, "create", toErrorMessage(err), true);
|
|
283
536
|
throw err;
|
|
284
537
|
}
|
|
285
538
|
}
|
|
286
539
|
async resume(id) {
|
|
287
540
|
const file = this.sessionPath(id, ".jsonl");
|
|
541
|
+
const t0 = Date.now();
|
|
288
542
|
const data = await this.load(id);
|
|
289
543
|
let handle;
|
|
290
544
|
try {
|
|
291
545
|
handle = await fsp.open(file, "a", 384);
|
|
292
546
|
} catch (err) {
|
|
547
|
+
this.emitError(id, file, "resume", toErrorMessage(err), false);
|
|
293
548
|
throw new Error(
|
|
294
|
-
`Failed to open session "${id}" for append: ${
|
|
549
|
+
`Failed to open session "${id}" for append: ${toErrorMessage(err)}`,
|
|
295
550
|
{ cause: err }
|
|
296
551
|
);
|
|
297
552
|
}
|
|
@@ -311,12 +566,13 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
311
566
|
// Shard directory (sessions/<date>/) — must match create() so the
|
|
312
567
|
// .summary.json sidecar lands next to the JSONL instead of the
|
|
313
568
|
// sessions root (where summaryFor() would never find it).
|
|
314
|
-
dir:
|
|
569
|
+
dir: path2.dirname(file),
|
|
315
570
|
filePath: file,
|
|
316
571
|
secretScrubber: this.secretScrubber,
|
|
317
572
|
onClose: (s) => this.appendToIndex(s)
|
|
318
573
|
}
|
|
319
574
|
);
|
|
575
|
+
this.emitWrite(id, file, "resume", "success", Date.now() - t0);
|
|
320
576
|
return { writer, data };
|
|
321
577
|
} catch (err) {
|
|
322
578
|
await handle.close().catch((e) => console.warn(JSON.stringify({
|
|
@@ -325,27 +581,39 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
325
581
|
message: e instanceof Error ? e.message : String(e),
|
|
326
582
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
327
583
|
})));
|
|
584
|
+
this.emitError(id, file, "resume", toErrorMessage(err), true);
|
|
328
585
|
throw err;
|
|
329
586
|
}
|
|
330
587
|
}
|
|
331
588
|
async load(id) {
|
|
332
589
|
const file = this.sessionPath(id, ".jsonl");
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
590
|
+
const t0 = Date.now();
|
|
591
|
+
let outcome = "success";
|
|
592
|
+
let errorMsg;
|
|
593
|
+
try {
|
|
594
|
+
const raw = await fsp.readFile(file, "utf8");
|
|
595
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
596
|
+
const events = [];
|
|
597
|
+
for (const line of lines) {
|
|
598
|
+
try {
|
|
599
|
+
const parsed = JSON.parse(line);
|
|
600
|
+
if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
|
|
601
|
+
events.push(parsed);
|
|
602
|
+
}
|
|
603
|
+
} catch {
|
|
341
604
|
}
|
|
342
|
-
} catch {
|
|
343
605
|
}
|
|
606
|
+
const meta = this.metaFromEvents(id, events);
|
|
607
|
+
const { messages, usage } = this.replay(events, id);
|
|
608
|
+
const toolCallEnds = extractToolCallEnds(events);
|
|
609
|
+
return { metadata: meta, events, messages, usage, toolCallEnds };
|
|
610
|
+
} catch (err) {
|
|
611
|
+
outcome = "failure";
|
|
612
|
+
errorMsg = toErrorMessage(err);
|
|
613
|
+
throw err;
|
|
614
|
+
} finally {
|
|
615
|
+
this.emitRead(id, file, "load", outcome, Date.now() - t0, errorMsg);
|
|
344
616
|
}
|
|
345
|
-
const meta = this.metaFromEvents(id, events);
|
|
346
|
-
const { messages, usage } = this.replay(events, id);
|
|
347
|
-
const toolCallEnds = extractToolCallEnds(events);
|
|
348
|
-
return { metadata: meta, events, messages, usage, toolCallEnds };
|
|
349
617
|
}
|
|
350
618
|
async list(limit = 20) {
|
|
351
619
|
try {
|
|
@@ -412,12 +680,22 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
412
680
|
* (keep latest per session), and rewrite. Atomic via temp+rename.
|
|
413
681
|
*/
|
|
414
682
|
async compactIndex() {
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
683
|
+
const t0 = Date.now();
|
|
684
|
+
let outcome = "success";
|
|
685
|
+
let errorMsg;
|
|
686
|
+
try {
|
|
687
|
+
const entries = await this.readIndex();
|
|
688
|
+
if (entries.length === 0) return;
|
|
689
|
+
const tmp = `${this.indexFile}.compact.tmp`;
|
|
690
|
+
const lines = entries.map((s) => JSON.stringify(s)).join("\n") + "\n";
|
|
691
|
+
await fsp.writeFile(tmp, lines, "utf8");
|
|
692
|
+
await fsp.rename(tmp, this.indexFile);
|
|
693
|
+
} catch (err) {
|
|
694
|
+
outcome = "failure";
|
|
695
|
+
errorMsg = toErrorMessage(err);
|
|
696
|
+
} finally {
|
|
697
|
+
this.emitWrite("~compact~", this.indexFile, "compact", outcome, Date.now() - t0, void 0, errorMsg);
|
|
698
|
+
}
|
|
421
699
|
}
|
|
422
700
|
/**
|
|
423
701
|
* Read the index file and return deduplicated session summaries.
|
|
@@ -482,7 +760,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
482
760
|
continue;
|
|
483
761
|
if (entry.isDirectory()) {
|
|
484
762
|
const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
|
|
485
|
-
ids.push(...await this.collectSessionIds(
|
|
763
|
+
ids.push(...await this.collectSessionIds(path2.join(dir, entry.name), childPrefix, depth + 1));
|
|
486
764
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
487
765
|
if (entry.name === "_index.jsonl") continue;
|
|
488
766
|
const base = entry.name.replace(/\.jsonl$/, "");
|
|
@@ -493,22 +771,31 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
493
771
|
}
|
|
494
772
|
async summaryFor(id) {
|
|
495
773
|
const manifest = this.sessionPath(id, ".summary.json");
|
|
774
|
+
const t0 = Date.now();
|
|
775
|
+
let outcome = "success";
|
|
776
|
+
let errorMsg;
|
|
496
777
|
try {
|
|
497
778
|
const raw = await fsp.readFile(manifest, "utf8");
|
|
779
|
+
this.emitRead(id, manifest, "summary", "success", Date.now() - t0);
|
|
498
780
|
return JSON.parse(raw);
|
|
499
781
|
} catch {
|
|
500
782
|
const full = this.sessionPath(id, ".jsonl");
|
|
501
|
-
const
|
|
502
|
-
const summary = await this.summarize(id,
|
|
783
|
+
const stat6 = await fsp.stat(full);
|
|
784
|
+
const summary = await this.summarize(id, stat6.mtime.toISOString());
|
|
503
785
|
await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
|
|
786
|
+
const msg = toErrorMessage(err);
|
|
787
|
+
this.emitError(id, manifest, "summary_fallback", msg, true);
|
|
504
788
|
console.warn(JSON.stringify({
|
|
505
789
|
level: "warn",
|
|
506
790
|
event: "session_store.manifest_write_failed",
|
|
507
791
|
sessionId: id,
|
|
508
|
-
message:
|
|
792
|
+
message: msg,
|
|
509
793
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
510
794
|
}));
|
|
511
795
|
});
|
|
796
|
+
outcome = "failure";
|
|
797
|
+
errorMsg = "summary fallback \u2014 manifest rebuilt";
|
|
798
|
+
this.emitRead(id, manifest, "summary", outcome, Date.now() - t0, errorMsg);
|
|
512
799
|
return summary;
|
|
513
800
|
}
|
|
514
801
|
}
|
|
@@ -524,14 +811,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
524
811
|
async deleteSession(id) {
|
|
525
812
|
const jsonlPath = this.sessionPath(id, ".jsonl");
|
|
526
813
|
const summaryPath = this.sessionPath(id, ".summary.json");
|
|
527
|
-
const shardDir =
|
|
528
|
-
const base =
|
|
529
|
-
const sessDir =
|
|
814
|
+
const shardDir = path2.dirname(path2.join(this.dir, id));
|
|
815
|
+
const base = path2.basename(id);
|
|
816
|
+
const sessDir = path2.join(shardDir, base);
|
|
530
817
|
const deletions = [
|
|
531
818
|
fsp.unlink(jsonlPath),
|
|
532
819
|
fsp.unlink(summaryPath),
|
|
533
|
-
fsp.unlink(
|
|
534
|
-
fsp.unlink(
|
|
820
|
+
fsp.unlink(path2.join(shardDir, `${base}.plan.json`)),
|
|
821
|
+
fsp.unlink(path2.join(shardDir, `${base}.todos.json`))
|
|
535
822
|
];
|
|
536
823
|
const results = await Promise.allSettled(deletions);
|
|
537
824
|
for (const r of results) {
|
|
@@ -553,7 +840,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
553
840
|
level: "warn",
|
|
554
841
|
event: "session_store.rmdir_failed",
|
|
555
842
|
sessionId: id,
|
|
556
|
-
message:
|
|
843
|
+
message: toErrorMessage(err),
|
|
557
844
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
558
845
|
}));
|
|
559
846
|
});
|
|
@@ -567,17 +854,17 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
567
854
|
let deleted = 0;
|
|
568
855
|
let activeSessionId = null;
|
|
569
856
|
try {
|
|
570
|
-
const raw = await fsp.readFile(
|
|
857
|
+
const raw = await fsp.readFile(path2.join(this.dir, "active.json"), "utf8");
|
|
571
858
|
const active = JSON.parse(raw);
|
|
572
859
|
activeSessionId = active.sessionId ?? null;
|
|
573
860
|
} catch {
|
|
574
861
|
}
|
|
575
862
|
const isPrunableJsonl = (name) => name.endsWith(".jsonl") && name !== "_index.jsonl" && name !== "_mailbox.jsonl" && !name.endsWith(".replay.jsonl") && !name.endsWith(".audit.jsonl");
|
|
576
863
|
const pruneFile = async (dir, name, prefix) => {
|
|
577
|
-
const jsonlPath =
|
|
864
|
+
const jsonlPath = path2.join(dir, name);
|
|
578
865
|
try {
|
|
579
|
-
const
|
|
580
|
-
if (
|
|
866
|
+
const stat6 = await fsp.stat(jsonlPath);
|
|
867
|
+
if (stat6.mtimeMs >= cutoff) return;
|
|
581
868
|
} catch {
|
|
582
869
|
return;
|
|
583
870
|
}
|
|
@@ -594,7 +881,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
594
881
|
continue;
|
|
595
882
|
}
|
|
596
883
|
if (!entry.isDirectory()) continue;
|
|
597
|
-
const dateDir =
|
|
884
|
+
const dateDir = path2.join(this.dir, entry.name);
|
|
598
885
|
const files = await fsp.readdir(dateDir, { withFileTypes: true }).catch(() => []);
|
|
599
886
|
for (const file of files) {
|
|
600
887
|
if (!file.isFile() || !isPrunableJsonl(file.name)) continue;
|
|
@@ -606,7 +893,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
606
893
|
}
|
|
607
894
|
for (const entry of entries) {
|
|
608
895
|
if (!entry.isDirectory()) continue;
|
|
609
|
-
const dateDir =
|
|
896
|
+
const dateDir = path2.join(this.dir, entry.name);
|
|
610
897
|
try {
|
|
611
898
|
const remaining = await fsp.readdir(dateDir);
|
|
612
899
|
if (remaining.length === 0) {
|
|
@@ -774,14 +1061,14 @@ function extractToolCallEnds(events) {
|
|
|
774
1061
|
return result;
|
|
775
1062
|
}
|
|
776
1063
|
var FileSessionWriter = class _FileSessionWriter {
|
|
777
|
-
constructor(id, handle, startedAt, meta, events, opts = {}) {
|
|
1064
|
+
constructor(id, handle, startedAt, meta, events, opts = {}, traceId) {
|
|
778
1065
|
this.id = id;
|
|
779
1066
|
this.handle = handle;
|
|
780
1067
|
this.startedAt = startedAt;
|
|
781
1068
|
this.meta = meta;
|
|
782
1069
|
this.events = events;
|
|
783
1070
|
this.resumed = opts.resumed ?? false;
|
|
784
|
-
this.manifestFile = opts.dir ?
|
|
1071
|
+
this.manifestFile = opts.dir ? path2.join(opts.dir, `${path2.basename(id)}.summary.json`) : "";
|
|
785
1072
|
this.filePath = opts.filePath ?? "";
|
|
786
1073
|
this.secretScrubber = opts.secretScrubber;
|
|
787
1074
|
this.onCloseCb = opts.onClose;
|
|
@@ -793,6 +1080,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
793
1080
|
provider: meta.provider ?? "unknown",
|
|
794
1081
|
tokenTotal: 0
|
|
795
1082
|
};
|
|
1083
|
+
this.traceId = traceId;
|
|
796
1084
|
}
|
|
797
1085
|
id;
|
|
798
1086
|
handle;
|
|
@@ -825,6 +1113,8 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
825
1113
|
lastAppendWarnAt = 0;
|
|
826
1114
|
secretScrubber;
|
|
827
1115
|
onCloseCb;
|
|
1116
|
+
/** Implements SessionWriter.traceId — propagated from ContextInit.traceId. */
|
|
1117
|
+
traceId;
|
|
828
1118
|
// ── Write buffer — batches events to reduce per-event disk I/O ─────────
|
|
829
1119
|
//
|
|
830
1120
|
// Every append() pushes the scrubbed event into an in-memory buffer instead
|
|
@@ -978,9 +1268,14 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
978
1268
|
const eventCount = this.writeBuffer.length;
|
|
979
1269
|
const batch = this.writeBuffer.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
980
1270
|
this.writeBuffer = [];
|
|
1271
|
+
const t0 = Date.now();
|
|
1272
|
+
let outcome = "success";
|
|
1273
|
+
let errorMsg;
|
|
981
1274
|
try {
|
|
982
1275
|
await this.enqueueWrite(batch);
|
|
983
1276
|
} catch (err) {
|
|
1277
|
+
outcome = "failure";
|
|
1278
|
+
errorMsg = toErrorMessage(err);
|
|
984
1279
|
this.appendFailCount += eventCount;
|
|
985
1280
|
const now = Date.now();
|
|
986
1281
|
if (now - this.lastAppendWarnAt > 5e3) {
|
|
@@ -988,12 +1283,24 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
988
1283
|
const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
|
|
989
1284
|
console.warn(
|
|
990
1285
|
"[session] flush failed:",
|
|
991
|
-
|
|
1286
|
+
toErrorMessage(err),
|
|
992
1287
|
tail
|
|
993
1288
|
);
|
|
994
1289
|
this.lastAppendWarnAt = now;
|
|
995
1290
|
this.appendFailCount = 0;
|
|
996
1291
|
}
|
|
1292
|
+
} finally {
|
|
1293
|
+
this.events?.emit("storage.write", {
|
|
1294
|
+
sessionId: this.id,
|
|
1295
|
+
store: "session",
|
|
1296
|
+
filePath: this.filePath,
|
|
1297
|
+
operation: "flush",
|
|
1298
|
+
outcome,
|
|
1299
|
+
durationMs: Date.now() - t0,
|
|
1300
|
+
...errorMsg !== void 0 ? { error: errorMsg } : {},
|
|
1301
|
+
...eventCount !== void 0 ? { eventCount } : {},
|
|
1302
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
1303
|
+
});
|
|
997
1304
|
}
|
|
998
1305
|
}
|
|
999
1306
|
observeForSummary(event) {
|
|
@@ -1059,14 +1366,46 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
1059
1366
|
outcome: this.outcome ?? "completed"
|
|
1060
1367
|
};
|
|
1061
1368
|
if (this.manifestFile) {
|
|
1369
|
+
const t0 = Date.now();
|
|
1370
|
+
let outcome = "success";
|
|
1371
|
+
let errorMsg;
|
|
1062
1372
|
try {
|
|
1063
1373
|
await atomicWrite(this.manifestFile, JSON.stringify(this.summary), { mode: 384 });
|
|
1064
|
-
} catch {
|
|
1374
|
+
} catch (err) {
|
|
1375
|
+
outcome = "failure";
|
|
1376
|
+
errorMsg = toErrorMessage(err);
|
|
1377
|
+
} finally {
|
|
1378
|
+
this.events?.emit("storage.write", {
|
|
1379
|
+
sessionId: this.id,
|
|
1380
|
+
store: "session",
|
|
1381
|
+
filePath: this.manifestFile,
|
|
1382
|
+
operation: "close",
|
|
1383
|
+
outcome,
|
|
1384
|
+
durationMs: Date.now() - t0,
|
|
1385
|
+
...errorMsg !== void 0 ? { error: errorMsg } : {},
|
|
1386
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
1387
|
+
});
|
|
1065
1388
|
}
|
|
1066
1389
|
}
|
|
1390
|
+
const idxT0 = Date.now();
|
|
1391
|
+
let idxOutcome = "success";
|
|
1392
|
+
let idxError;
|
|
1067
1393
|
try {
|
|
1068
1394
|
await this.onCloseCb?.(this.summary);
|
|
1069
|
-
} catch {
|
|
1395
|
+
} catch (err) {
|
|
1396
|
+
idxOutcome = "failure";
|
|
1397
|
+
idxError = toErrorMessage(err);
|
|
1398
|
+
} finally {
|
|
1399
|
+
this.events?.emit("storage.write", {
|
|
1400
|
+
sessionId: this.summary.id,
|
|
1401
|
+
store: "session",
|
|
1402
|
+
filePath: this.filePath,
|
|
1403
|
+
operation: "index_append",
|
|
1404
|
+
outcome: idxOutcome,
|
|
1405
|
+
durationMs: Date.now() - idxT0,
|
|
1406
|
+
...idxError !== void 0 ? { error: idxError } : {},
|
|
1407
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
1408
|
+
});
|
|
1070
1409
|
}
|
|
1071
1410
|
try {
|
|
1072
1411
|
await this.handle.close();
|
|
@@ -1225,28 +1564,86 @@ function userInputTitle(content) {
|
|
|
1225
1564
|
}
|
|
1226
1565
|
var QueueStore = class {
|
|
1227
1566
|
file;
|
|
1567
|
+
// Use `| undefined` (not `?`) so exactOptionalPropertyTypes doesn't
|
|
1568
|
+
// reject assigning an optional constructor parameter to these fields.
|
|
1569
|
+
events;
|
|
1570
|
+
traceId;
|
|
1228
1571
|
constructor(opts) {
|
|
1229
|
-
this.file =
|
|
1572
|
+
this.file = path2.join(opts.dir, "queue.json");
|
|
1573
|
+
this.events = opts.events;
|
|
1574
|
+
this.traceId = opts.traceId;
|
|
1230
1575
|
}
|
|
1231
1576
|
async write(items) {
|
|
1577
|
+
const t0 = Date.now();
|
|
1232
1578
|
if (items.length === 0) {
|
|
1233
1579
|
await this.clear();
|
|
1234
1580
|
return;
|
|
1235
1581
|
}
|
|
1236
|
-
|
|
1582
|
+
try {
|
|
1583
|
+
await atomicWrite(this.file, JSON.stringify(items), { mode: 384 });
|
|
1584
|
+
this.events?.emit("storage.write", {
|
|
1585
|
+
sessionId: this.traceId ?? "~boot~",
|
|
1586
|
+
store: "queue",
|
|
1587
|
+
filePath: this.file,
|
|
1588
|
+
operation: "write",
|
|
1589
|
+
outcome: "success",
|
|
1590
|
+
durationMs: Date.now() - t0,
|
|
1591
|
+
...this.traceId !== void 0 && { traceId: this.traceId }
|
|
1592
|
+
});
|
|
1593
|
+
} catch (err) {
|
|
1594
|
+
this.events?.emit("storage.error", {
|
|
1595
|
+
sessionId: this.traceId ?? "~boot~",
|
|
1596
|
+
store: "queue",
|
|
1597
|
+
filePath: this.file,
|
|
1598
|
+
operation: "write",
|
|
1599
|
+
outcome: "failure",
|
|
1600
|
+
error: toErrorMessage(err),
|
|
1601
|
+
recoverable: false,
|
|
1602
|
+
...this.traceId !== void 0 && { traceId: this.traceId }
|
|
1603
|
+
});
|
|
1604
|
+
console.warn(JSON.stringify({
|
|
1605
|
+
level: "warn",
|
|
1606
|
+
event: "queue_store.write_failed",
|
|
1607
|
+
path: this.file,
|
|
1608
|
+
message: toErrorMessage(err),
|
|
1609
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1610
|
+
}));
|
|
1611
|
+
}
|
|
1237
1612
|
}
|
|
1238
1613
|
async read() {
|
|
1614
|
+
const t0 = Date.now();
|
|
1239
1615
|
let raw;
|
|
1240
1616
|
try {
|
|
1241
1617
|
raw = await fsp.readFile(this.file, "utf8");
|
|
1242
1618
|
} catch (err) {
|
|
1243
1619
|
const code = err.code;
|
|
1244
|
-
if (code === "ENOENT")
|
|
1620
|
+
if (code === "ENOENT") {
|
|
1621
|
+
this.events?.emit("storage.read", {
|
|
1622
|
+
sessionId: this.traceId ?? "~boot~",
|
|
1623
|
+
store: "queue",
|
|
1624
|
+
filePath: this.file,
|
|
1625
|
+
operation: "read",
|
|
1626
|
+
outcome: "success",
|
|
1627
|
+
durationMs: Date.now() - t0,
|
|
1628
|
+
...this.traceId !== void 0 && { traceId: this.traceId }
|
|
1629
|
+
});
|
|
1630
|
+
return [];
|
|
1631
|
+
}
|
|
1632
|
+
this.events?.emit("storage.error", {
|
|
1633
|
+
sessionId: this.traceId ?? "~boot~",
|
|
1634
|
+
store: "queue",
|
|
1635
|
+
filePath: this.file,
|
|
1636
|
+
operation: "read",
|
|
1637
|
+
outcome: "failure",
|
|
1638
|
+
error: toErrorMessage(err),
|
|
1639
|
+
recoverable: true,
|
|
1640
|
+
...this.traceId !== void 0 && { traceId: this.traceId }
|
|
1641
|
+
});
|
|
1245
1642
|
console.warn(JSON.stringify({
|
|
1246
1643
|
level: "warn",
|
|
1247
1644
|
event: "queue_store.read_failed",
|
|
1248
1645
|
path: this.file,
|
|
1249
|
-
message:
|
|
1646
|
+
message: toErrorMessage(err),
|
|
1250
1647
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1251
1648
|
}));
|
|
1252
1649
|
return [];
|
|
@@ -1255,9 +1652,40 @@ var QueueStore = class {
|
|
|
1255
1652
|
try {
|
|
1256
1653
|
parsed = JSON.parse(raw);
|
|
1257
1654
|
} catch {
|
|
1655
|
+
this.events?.emit("storage.read", {
|
|
1656
|
+
sessionId: this.traceId ?? "~boot~",
|
|
1657
|
+
store: "queue",
|
|
1658
|
+
filePath: this.file,
|
|
1659
|
+
operation: "read",
|
|
1660
|
+
outcome: "failure",
|
|
1661
|
+
durationMs: Date.now() - t0,
|
|
1662
|
+
error: "parse_failed",
|
|
1663
|
+
...this.traceId !== void 0 && { traceId: this.traceId }
|
|
1664
|
+
});
|
|
1665
|
+
return [];
|
|
1666
|
+
}
|
|
1667
|
+
if (!Array.isArray(parsed)) {
|
|
1668
|
+
this.events?.emit("storage.read", {
|
|
1669
|
+
sessionId: this.traceId ?? "~boot~",
|
|
1670
|
+
store: "queue",
|
|
1671
|
+
filePath: this.file,
|
|
1672
|
+
operation: "read",
|
|
1673
|
+
outcome: "failure",
|
|
1674
|
+
durationMs: Date.now() - t0,
|
|
1675
|
+
error: "invalid_schema",
|
|
1676
|
+
...this.traceId !== void 0 && { traceId: this.traceId }
|
|
1677
|
+
});
|
|
1258
1678
|
return [];
|
|
1259
1679
|
}
|
|
1260
|
-
|
|
1680
|
+
this.events?.emit("storage.read", {
|
|
1681
|
+
sessionId: this.traceId ?? "~boot~",
|
|
1682
|
+
store: "queue",
|
|
1683
|
+
filePath: this.file,
|
|
1684
|
+
operation: "read",
|
|
1685
|
+
outcome: "success",
|
|
1686
|
+
durationMs: Date.now() - t0,
|
|
1687
|
+
...this.traceId !== void 0 && { traceId: this.traceId }
|
|
1688
|
+
});
|
|
1261
1689
|
const out = [];
|
|
1262
1690
|
for (const v of parsed) {
|
|
1263
1691
|
if (isPersistedQueueItem(v)) out.push(v);
|
|
@@ -1265,11 +1693,31 @@ var QueueStore = class {
|
|
|
1265
1693
|
return out;
|
|
1266
1694
|
}
|
|
1267
1695
|
async clear() {
|
|
1696
|
+
const t0 = Date.now();
|
|
1268
1697
|
try {
|
|
1269
1698
|
await fsp.unlink(this.file);
|
|
1699
|
+
this.events?.emit("storage.write", {
|
|
1700
|
+
sessionId: this.traceId ?? "~boot~",
|
|
1701
|
+
store: "queue",
|
|
1702
|
+
filePath: this.file,
|
|
1703
|
+
operation: "clear",
|
|
1704
|
+
outcome: "success",
|
|
1705
|
+
durationMs: Date.now() - t0,
|
|
1706
|
+
...this.traceId !== void 0 && { traceId: this.traceId }
|
|
1707
|
+
});
|
|
1270
1708
|
} catch (err) {
|
|
1271
1709
|
const code = err.code;
|
|
1272
1710
|
if (code === "ENOENT") return;
|
|
1711
|
+
this.events?.emit("storage.error", {
|
|
1712
|
+
sessionId: this.traceId ?? "~boot~",
|
|
1713
|
+
store: "queue",
|
|
1714
|
+
filePath: this.file,
|
|
1715
|
+
operation: "clear",
|
|
1716
|
+
outcome: "failure",
|
|
1717
|
+
error: toErrorMessage(err),
|
|
1718
|
+
recoverable: true,
|
|
1719
|
+
...this.traceId !== void 0 && { traceId: this.traceId }
|
|
1720
|
+
});
|
|
1273
1721
|
console.warn(JSON.stringify({
|
|
1274
1722
|
level: "warn",
|
|
1275
1723
|
event: "queue_store.clear_failed",
|
|
@@ -1305,7 +1753,7 @@ var DefaultAttachmentStore = class {
|
|
|
1305
1753
|
let data = input.data;
|
|
1306
1754
|
if (this.spoolDir && bytes >= this.spoolThreshold) {
|
|
1307
1755
|
await fsp.mkdir(this.spoolDir, { recursive: true });
|
|
1308
|
-
spooledPath =
|
|
1756
|
+
spooledPath = path2.join(this.spoolDir, `${id}.bin`);
|
|
1309
1757
|
await atomicWrite(spooledPath, input.data, {
|
|
1310
1758
|
encoding: input.kind === "image" ? "base64" : "utf8"
|
|
1311
1759
|
});
|
|
@@ -1516,7 +1964,7 @@ var FileMemoryBackend = class {
|
|
|
1516
1964
|
}
|
|
1517
1965
|
async remember(scope, entry, filePath) {
|
|
1518
1966
|
const file = this.resolveFile(filePath, scope);
|
|
1519
|
-
await ensureDir(
|
|
1967
|
+
await ensureDir(path2.dirname(file));
|
|
1520
1968
|
let existing = "";
|
|
1521
1969
|
try {
|
|
1522
1970
|
existing = await fsp.readFile(file, "utf8");
|
|
@@ -1652,6 +2100,7 @@ var MAX_BYTES_TOTAL = 32e3;
|
|
|
1652
2100
|
var DefaultMemoryStore = class {
|
|
1653
2101
|
files;
|
|
1654
2102
|
events;
|
|
2103
|
+
traceId;
|
|
1655
2104
|
backend;
|
|
1656
2105
|
/**
|
|
1657
2106
|
* Per-scope serialization queue. `remember` / `forget` / `consolidate` /
|
|
@@ -1717,15 +2166,70 @@ var DefaultMemoryStore = class {
|
|
|
1717
2166
|
if (writeErr) {
|
|
1718
2167
|
parts.push(`> \u26A0\uFE0F Memory write error (${labelOf(scope)}): ${writeErr.message}`);
|
|
1719
2168
|
}
|
|
1720
|
-
const
|
|
1721
|
-
|
|
2169
|
+
const t0 = Date.now();
|
|
2170
|
+
const filePath = this.files[scope];
|
|
2171
|
+
try {
|
|
2172
|
+
const body = await this.backend.readAll(scope, filePath);
|
|
2173
|
+
const dur = Date.now() - t0;
|
|
2174
|
+
this.events?.emit("storage.read", {
|
|
2175
|
+
sessionId: "~memory~",
|
|
2176
|
+
store: "memory",
|
|
2177
|
+
filePath,
|
|
2178
|
+
operation: "readAll",
|
|
2179
|
+
outcome: "success",
|
|
2180
|
+
durationMs: dur,
|
|
2181
|
+
...this.traceId !== void 0 && { traceId: this.traceId }
|
|
2182
|
+
});
|
|
2183
|
+
if (body.trim()) parts.push(`## ${labelOf(scope)}
|
|
1722
2184
|
|
|
1723
2185
|
${body.trim()}`);
|
|
2186
|
+
} catch (err) {
|
|
2187
|
+
const dur = Date.now() - t0;
|
|
2188
|
+
this.events?.emit("storage.read", {
|
|
2189
|
+
sessionId: "~memory~",
|
|
2190
|
+
store: "memory",
|
|
2191
|
+
filePath,
|
|
2192
|
+
operation: "readAll",
|
|
2193
|
+
outcome: "failure",
|
|
2194
|
+
durationMs: dur,
|
|
2195
|
+
error: toErrorMessage(err),
|
|
2196
|
+
...this.traceId !== void 0 && { traceId: this.traceId }
|
|
2197
|
+
});
|
|
2198
|
+
throw err;
|
|
2199
|
+
}
|
|
1724
2200
|
}
|
|
1725
2201
|
return parts.join("\n\n");
|
|
1726
2202
|
}
|
|
1727
2203
|
async read(scope) {
|
|
1728
|
-
|
|
2204
|
+
const t0 = Date.now();
|
|
2205
|
+
const filePath = this.files[scope];
|
|
2206
|
+
try {
|
|
2207
|
+
const body = await this.backend.readAll(scope, filePath);
|
|
2208
|
+
const dur = Date.now() - t0;
|
|
2209
|
+
this.events?.emit("storage.read", {
|
|
2210
|
+
sessionId: "~memory~",
|
|
2211
|
+
store: "memory",
|
|
2212
|
+
filePath,
|
|
2213
|
+
operation: "read",
|
|
2214
|
+
outcome: "success",
|
|
2215
|
+
durationMs: dur,
|
|
2216
|
+
...this.traceId !== void 0 && { traceId: this.traceId }
|
|
2217
|
+
});
|
|
2218
|
+
return body;
|
|
2219
|
+
} catch (err) {
|
|
2220
|
+
const dur = Date.now() - t0;
|
|
2221
|
+
this.events?.emit("storage.read", {
|
|
2222
|
+
sessionId: "~memory~",
|
|
2223
|
+
store: "memory",
|
|
2224
|
+
filePath,
|
|
2225
|
+
operation: "read",
|
|
2226
|
+
outcome: "failure",
|
|
2227
|
+
durationMs: dur,
|
|
2228
|
+
error: toErrorMessage(err),
|
|
2229
|
+
...this.traceId !== void 0 && { traceId: this.traceId }
|
|
2230
|
+
});
|
|
2231
|
+
throw err;
|
|
2232
|
+
}
|
|
1729
2233
|
}
|
|
1730
2234
|
/**
|
|
1731
2235
|
* List entries from a scope, newest first. Delegates to the backend
|
|
@@ -1751,7 +2255,34 @@ ${body.trim()}`);
|
|
|
1751
2255
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1752
2256
|
return this.runSerialized(scope, async () => {
|
|
1753
2257
|
const entry = { scope, text, ts, ...metadata };
|
|
1754
|
-
|
|
2258
|
+
const filePath = this.files[scope];
|
|
2259
|
+
const t0 = Date.now();
|
|
2260
|
+
try {
|
|
2261
|
+
await this.backend.remember(scope, entry, filePath);
|
|
2262
|
+
const dur = Date.now() - t0;
|
|
2263
|
+
this.events?.emit("storage.write", {
|
|
2264
|
+
sessionId: "~memory~",
|
|
2265
|
+
store: "memory",
|
|
2266
|
+
filePath,
|
|
2267
|
+
operation: "remember",
|
|
2268
|
+
outcome: "success",
|
|
2269
|
+
durationMs: dur,
|
|
2270
|
+
...this.traceId !== void 0 && { traceId: this.traceId }
|
|
2271
|
+
});
|
|
2272
|
+
} catch (err) {
|
|
2273
|
+
const dur = Date.now() - t0;
|
|
2274
|
+
this.events?.emit("storage.write", {
|
|
2275
|
+
sessionId: "~memory~",
|
|
2276
|
+
store: "memory",
|
|
2277
|
+
filePath,
|
|
2278
|
+
operation: "remember",
|
|
2279
|
+
outcome: "failure",
|
|
2280
|
+
durationMs: dur,
|
|
2281
|
+
error: toErrorMessage(err),
|
|
2282
|
+
...this.traceId !== void 0 && { traceId: this.traceId }
|
|
2283
|
+
});
|
|
2284
|
+
throw err;
|
|
2285
|
+
}
|
|
1755
2286
|
const raw = await this.backend.readAll(scope, this.files[scope]);
|
|
1756
2287
|
if (Buffer.byteLength(raw, "utf8") > MAX_BYTES_TOTAL) {
|
|
1757
2288
|
const removed = await this.backend.consolidate(scope, this.files[scope]);
|
|
@@ -1879,7 +2410,35 @@ ${body.trim()}`);
|
|
|
1879
2410
|
}
|
|
1880
2411
|
async forget(query, scope = "project-memory") {
|
|
1881
2412
|
return this.runSerialized(scope, async () => {
|
|
1882
|
-
const
|
|
2413
|
+
const filePath = this.files[scope];
|
|
2414
|
+
const t0 = Date.now();
|
|
2415
|
+
let removed = 0;
|
|
2416
|
+
try {
|
|
2417
|
+
removed = await this.backend.forget(scope, query, filePath);
|
|
2418
|
+
const dur = Date.now() - t0;
|
|
2419
|
+
this.events?.emit("storage.write", {
|
|
2420
|
+
sessionId: "~memory~",
|
|
2421
|
+
store: "memory",
|
|
2422
|
+
filePath,
|
|
2423
|
+
operation: "forget",
|
|
2424
|
+
outcome: "success",
|
|
2425
|
+
durationMs: dur,
|
|
2426
|
+
...this.traceId !== void 0 && { traceId: this.traceId }
|
|
2427
|
+
});
|
|
2428
|
+
} catch (err) {
|
|
2429
|
+
const dur = Date.now() - t0;
|
|
2430
|
+
this.events?.emit("storage.write", {
|
|
2431
|
+
sessionId: "~memory~",
|
|
2432
|
+
store: "memory",
|
|
2433
|
+
filePath,
|
|
2434
|
+
operation: "forget",
|
|
2435
|
+
outcome: "failure",
|
|
2436
|
+
durationMs: dur,
|
|
2437
|
+
error: toErrorMessage(err),
|
|
2438
|
+
...this.traceId !== void 0 && { traceId: this.traceId }
|
|
2439
|
+
});
|
|
2440
|
+
throw err;
|
|
2441
|
+
}
|
|
1883
2442
|
if (removed > 0) {
|
|
1884
2443
|
this.events?.emit("memory.forgotten", {
|
|
1885
2444
|
scope,
|
|
@@ -1893,7 +2452,35 @@ ${body.trim()}`);
|
|
|
1893
2452
|
}
|
|
1894
2453
|
async consolidate(scope) {
|
|
1895
2454
|
return this.runSerialized(scope, async () => {
|
|
1896
|
-
const
|
|
2455
|
+
const filePath = this.files[scope];
|
|
2456
|
+
const t0 = Date.now();
|
|
2457
|
+
let removed = 0;
|
|
2458
|
+
try {
|
|
2459
|
+
removed = await this.backend.consolidate(scope, filePath);
|
|
2460
|
+
const dur = Date.now() - t0;
|
|
2461
|
+
this.events?.emit("storage.write", {
|
|
2462
|
+
sessionId: "~memory~",
|
|
2463
|
+
store: "memory",
|
|
2464
|
+
filePath,
|
|
2465
|
+
operation: "consolidate",
|
|
2466
|
+
outcome: "success",
|
|
2467
|
+
durationMs: dur,
|
|
2468
|
+
...this.traceId !== void 0 && { traceId: this.traceId }
|
|
2469
|
+
});
|
|
2470
|
+
} catch (err) {
|
|
2471
|
+
const dur = Date.now() - t0;
|
|
2472
|
+
this.events?.emit("storage.write", {
|
|
2473
|
+
sessionId: "~memory~",
|
|
2474
|
+
store: "memory",
|
|
2475
|
+
filePath,
|
|
2476
|
+
operation: "consolidate",
|
|
2477
|
+
outcome: "failure",
|
|
2478
|
+
durationMs: dur,
|
|
2479
|
+
error: toErrorMessage(err),
|
|
2480
|
+
...this.traceId !== void 0 && { traceId: this.traceId }
|
|
2481
|
+
});
|
|
2482
|
+
throw err;
|
|
2483
|
+
}
|
|
1897
2484
|
if (removed > 0) {
|
|
1898
2485
|
this.events?.emit("memory.consolidated", {
|
|
1899
2486
|
scope,
|
|
@@ -1906,7 +2493,34 @@ ${body.trim()}`);
|
|
|
1906
2493
|
async clear(scope) {
|
|
1907
2494
|
if (scope) {
|
|
1908
2495
|
await this.runSerialized(scope, async () => {
|
|
1909
|
-
|
|
2496
|
+
const filePath = this.files[scope];
|
|
2497
|
+
const t0 = Date.now();
|
|
2498
|
+
try {
|
|
2499
|
+
await this.backend.clear(scope, filePath);
|
|
2500
|
+
const dur = Date.now() - t0;
|
|
2501
|
+
this.events?.emit("storage.write", {
|
|
2502
|
+
sessionId: "~memory~",
|
|
2503
|
+
store: "memory",
|
|
2504
|
+
filePath,
|
|
2505
|
+
operation: "clear",
|
|
2506
|
+
outcome: "success",
|
|
2507
|
+
durationMs: dur,
|
|
2508
|
+
...this.traceId !== void 0 && { traceId: this.traceId }
|
|
2509
|
+
});
|
|
2510
|
+
} catch (err) {
|
|
2511
|
+
const dur = Date.now() - t0;
|
|
2512
|
+
this.events?.emit("storage.write", {
|
|
2513
|
+
sessionId: "~memory~",
|
|
2514
|
+
store: "memory",
|
|
2515
|
+
filePath,
|
|
2516
|
+
operation: "clear",
|
|
2517
|
+
outcome: "failure",
|
|
2518
|
+
durationMs: dur,
|
|
2519
|
+
error: toErrorMessage(err),
|
|
2520
|
+
...this.traceId !== void 0 && { traceId: this.traceId }
|
|
2521
|
+
});
|
|
2522
|
+
throw err;
|
|
2523
|
+
}
|
|
1910
2524
|
this.events?.emit("memory.cleared", { scope });
|
|
1911
2525
|
await this.mirrorBackup(scope);
|
|
1912
2526
|
});
|
|
@@ -1914,15 +2528,54 @@ ${body.trim()}`);
|
|
|
1914
2528
|
}
|
|
1915
2529
|
await Promise.all(
|
|
1916
2530
|
["project-agents", "project-memory", "user-memory"].map(
|
|
1917
|
-
(s) => this.runSerialized(s, async () => {
|
|
1918
|
-
|
|
2531
|
+
async (s) => this.runSerialized(s, async () => {
|
|
2532
|
+
const filePath = this.files[s];
|
|
2533
|
+
const t0 = Date.now();
|
|
2534
|
+
try {
|
|
2535
|
+
await this.backend.clear(s, filePath);
|
|
2536
|
+
const dur = Date.now() - t0;
|
|
2537
|
+
this.events?.emit("storage.write", {
|
|
2538
|
+
sessionId: "~memory~",
|
|
2539
|
+
store: "memory",
|
|
2540
|
+
filePath,
|
|
2541
|
+
operation: "clear",
|
|
2542
|
+
outcome: "success",
|
|
2543
|
+
durationMs: dur,
|
|
2544
|
+
...this.traceId !== void 0 && { traceId: this.traceId }
|
|
2545
|
+
});
|
|
2546
|
+
} catch (err) {
|
|
2547
|
+
const dur = Date.now() - t0;
|
|
2548
|
+
this.events?.emit("storage.write", {
|
|
2549
|
+
sessionId: "~memory~",
|
|
2550
|
+
store: "memory",
|
|
2551
|
+
filePath,
|
|
2552
|
+
operation: "clear",
|
|
2553
|
+
outcome: "failure",
|
|
2554
|
+
durationMs: dur,
|
|
2555
|
+
error: toErrorMessage(err),
|
|
2556
|
+
...this.traceId !== void 0 && { traceId: this.traceId }
|
|
2557
|
+
});
|
|
2558
|
+
throw err;
|
|
2559
|
+
}
|
|
1919
2560
|
this.events?.emit("memory.cleared", { scope: s });
|
|
1920
2561
|
await this.mirrorBackup(s);
|
|
1921
2562
|
})
|
|
1922
2563
|
)
|
|
1923
2564
|
);
|
|
1924
2565
|
}
|
|
1925
|
-
/**
|
|
2566
|
+
/**
|
|
2567
|
+
* Return a new MemoryStore proxy that carries `traceId` on every storage
|
|
2568
|
+
* event. The original store is left unchanged — callers that need a
|
|
2569
|
+
* trace-decorated view (e.g. session-run tools) receive the proxy while
|
|
2570
|
+
* the singleton remains trace-free for boot-time use.
|
|
2571
|
+
*
|
|
2572
|
+
* The proxy implements the full `MemoryStore` interface; all other
|
|
2573
|
+
* properties (backend, etc.) are delegated to the original store.
|
|
2574
|
+
*/
|
|
2575
|
+
withTraceId(traceId) {
|
|
2576
|
+
this.traceId = traceId;
|
|
2577
|
+
return this;
|
|
2578
|
+
}
|
|
1926
2579
|
async mirrorBackup(scope) {
|
|
1927
2580
|
if (!this.persistBackup || scope === "project-agents") return;
|
|
1928
2581
|
try {
|
|
@@ -2460,7 +3113,7 @@ var DefaultConfigStore = class {
|
|
|
2460
3113
|
console.error(JSON.stringify({
|
|
2461
3114
|
level: "error",
|
|
2462
3115
|
event: "config_store.watcher_threw",
|
|
2463
|
-
message:
|
|
3116
|
+
message: toErrorMessage(err),
|
|
2464
3117
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2465
3118
|
}));
|
|
2466
3119
|
}
|
|
@@ -2484,67 +3137,6 @@ function deepFreeze(obj) {
|
|
|
2484
3137
|
}
|
|
2485
3138
|
return Object.freeze(obj);
|
|
2486
3139
|
}
|
|
2487
|
-
|
|
2488
|
-
// src/utils/deep-merge.ts
|
|
2489
|
-
var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set([
|
|
2490
|
-
"__proto__",
|
|
2491
|
-
"constructor",
|
|
2492
|
-
"prototype",
|
|
2493
|
-
"__defineGetter__",
|
|
2494
|
-
"__defineSetter__",
|
|
2495
|
-
"__lookupGetter__",
|
|
2496
|
-
"__lookupSetter__"
|
|
2497
|
-
]);
|
|
2498
|
-
function isPrimitiveArray(a) {
|
|
2499
|
-
return a.every((v) => v === null || typeof v !== "object" && typeof v !== "function");
|
|
2500
|
-
}
|
|
2501
|
-
function deepMerge(base, patch, options = {}) {
|
|
2502
|
-
const {
|
|
2503
|
-
conflictResolution = "prefer-patch",
|
|
2504
|
-
arrayMode = "replace",
|
|
2505
|
-
protectProto = true,
|
|
2506
|
-
onNonPrimitiveArrayReplace
|
|
2507
|
-
} = options;
|
|
2508
|
-
if (typeof base !== "object" || base === null) {
|
|
2509
|
-
return conflictResolution === "prefer-patch" ? patch : base;
|
|
2510
|
-
}
|
|
2511
|
-
if (typeof patch !== "object" || patch === null) {
|
|
2512
|
-
return conflictResolution === "prefer-patch" ? patch : base;
|
|
2513
|
-
}
|
|
2514
|
-
if (Array.isArray(base) && Array.isArray(patch)) {
|
|
2515
|
-
if (arrayMode === "concat-primitives" && isPrimitiveArray(base) && isPrimitiveArray(patch)) {
|
|
2516
|
-
return [.../* @__PURE__ */ new Set([...base, ...patch])];
|
|
2517
|
-
}
|
|
2518
|
-
return conflictResolution === "prefer-patch" ? patch : base;
|
|
2519
|
-
}
|
|
2520
|
-
if (Array.isArray(base) || Array.isArray(patch)) {
|
|
2521
|
-
return conflictResolution === "prefer-patch" ? patch : base;
|
|
2522
|
-
}
|
|
2523
|
-
const baseObj = base;
|
|
2524
|
-
const patchObj = patch;
|
|
2525
|
-
const out = { ...baseObj };
|
|
2526
|
-
for (const [k, v] of Object.entries(patchObj)) {
|
|
2527
|
-
if (protectProto && FORBIDDEN_PROTO_KEYS.has(k)) continue;
|
|
2528
|
-
const existing = out[k];
|
|
2529
|
-
if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
|
|
2530
|
-
out[k] = deepMerge(existing, v, options);
|
|
2531
|
-
} else if (Array.isArray(v) && Array.isArray(existing)) {
|
|
2532
|
-
if (onNonPrimitiveArrayReplace && !isPrimitiveArray(v)) {
|
|
2533
|
-
onNonPrimitiveArrayReplace(k, existing.length, v.length);
|
|
2534
|
-
}
|
|
2535
|
-
out[k] = deepMerge(existing, v, options);
|
|
2536
|
-
} else if (v !== void 0) {
|
|
2537
|
-
if (onNonPrimitiveArrayReplace && Array.isArray(v) && !isPrimitiveArray(v)) {
|
|
2538
|
-
const existingLen = Array.isArray(existing) ? existing.length : 0;
|
|
2539
|
-
onNonPrimitiveArrayReplace(k, existingLen, v.length);
|
|
2540
|
-
}
|
|
2541
|
-
out[k] = v;
|
|
2542
|
-
}
|
|
2543
|
-
}
|
|
2544
|
-
return out;
|
|
2545
|
-
}
|
|
2546
|
-
|
|
2547
|
-
// src/security/secret-vault.ts
|
|
2548
3140
|
function decryptConfigSecrets(cfg, vault, opts) {
|
|
2549
3141
|
const warn = ((msg) => console.warn(msg));
|
|
2550
3142
|
return walk(cfg, vault, (v, key) => {
|
|
@@ -2635,21 +3227,6 @@ function isContextWindowModeId(id) {
|
|
|
2635
3227
|
return CONTEXT_WINDOW_MODES.some((m) => m.id === id);
|
|
2636
3228
|
}
|
|
2637
3229
|
|
|
2638
|
-
// src/utils/safe-json.ts
|
|
2639
|
-
function safeParse(input, maxBytes = 5e6) {
|
|
2640
|
-
if (input.length > maxBytes) {
|
|
2641
|
-
return { ok: false, error: `Input exceeds limit (${maxBytes} bytes)` };
|
|
2642
|
-
}
|
|
2643
|
-
try {
|
|
2644
|
-
return { ok: true, value: JSON.parse(input) };
|
|
2645
|
-
} catch (err) {
|
|
2646
|
-
return {
|
|
2647
|
-
ok: false,
|
|
2648
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2649
|
-
};
|
|
2650
|
-
}
|
|
2651
|
-
}
|
|
2652
|
-
|
|
2653
3230
|
// src/types/default-config.ts
|
|
2654
3231
|
var DEFAULT_TOOLS_CONFIG = Object.freeze({
|
|
2655
3232
|
defaultExecutionStrategy: "smart",
|
|
@@ -2673,6 +3250,13 @@ var DEFAULT_SESSION_LOGGING_CONFIG = Object.freeze({
|
|
|
2673
3250
|
});
|
|
2674
3251
|
|
|
2675
3252
|
// src/storage/config-loader.ts
|
|
3253
|
+
function storageErrorString(err) {
|
|
3254
|
+
if (err instanceof Error) {
|
|
3255
|
+
const code = err.code;
|
|
3256
|
+
return code ? `${code}: ${err.message}` : err.message;
|
|
3257
|
+
}
|
|
3258
|
+
return String(err);
|
|
3259
|
+
}
|
|
2676
3260
|
var BEHAVIOR_DEFAULTS = {
|
|
2677
3261
|
version: 1,
|
|
2678
3262
|
context: {
|
|
@@ -2775,11 +3359,15 @@ var DefaultConfigLoader = class {
|
|
|
2775
3359
|
strict;
|
|
2776
3360
|
vault;
|
|
2777
3361
|
extraSources;
|
|
3362
|
+
events;
|
|
3363
|
+
traceId;
|
|
2778
3364
|
constructor(opts) {
|
|
2779
3365
|
this.paths = opts.paths;
|
|
2780
3366
|
this.strict = opts.strict ?? false;
|
|
2781
3367
|
this.vault = opts.vault;
|
|
2782
3368
|
this.extraSources = opts.sources ?? [];
|
|
3369
|
+
this.events = opts.events;
|
|
3370
|
+
this.traceId = opts.traceId;
|
|
2783
3371
|
}
|
|
2784
3372
|
async load(opts = {}) {
|
|
2785
3373
|
let cfg = { ...BEHAVIOR_DEFAULTS };
|
|
@@ -2811,7 +3399,7 @@ var DefaultConfigLoader = class {
|
|
|
2811
3399
|
level: "warn",
|
|
2812
3400
|
event: "config.source_load_failed",
|
|
2813
3401
|
source: src.name,
|
|
2814
|
-
message:
|
|
3402
|
+
message: toErrorMessage(err),
|
|
2815
3403
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2816
3404
|
}));
|
|
2817
3405
|
}
|
|
@@ -2856,7 +3444,33 @@ var DefaultConfigLoader = class {
|
|
|
2856
3444
|
if (this.vault && toWrite.githubToken && !toWrite.githubToken.startsWith("enc:")) {
|
|
2857
3445
|
toWrite = { ...toWrite, githubToken: this.vault.encrypt(toWrite.githubToken) };
|
|
2858
3446
|
}
|
|
2859
|
-
|
|
3447
|
+
const fp = this.paths.syncConfig;
|
|
3448
|
+
const t0 = Date.now();
|
|
3449
|
+
try {
|
|
3450
|
+
await atomicWrite(fp, JSON.stringify(toWrite, null, 2), { mode: 384 });
|
|
3451
|
+
this.events?.emit("storage.write", {
|
|
3452
|
+
sessionId: "~config~",
|
|
3453
|
+
store: "config",
|
|
3454
|
+
filePath: fp,
|
|
3455
|
+
operation: "persist_sync",
|
|
3456
|
+
outcome: "success",
|
|
3457
|
+
durationMs: Date.now() - t0,
|
|
3458
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
3459
|
+
});
|
|
3460
|
+
} catch (err) {
|
|
3461
|
+
this.events?.emit("storage.error", {
|
|
3462
|
+
sessionId: "~config~",
|
|
3463
|
+
store: "config",
|
|
3464
|
+
filePath: fp,
|
|
3465
|
+
operation: "persist_sync",
|
|
3466
|
+
outcome: "failure",
|
|
3467
|
+
error: storageErrorString(err),
|
|
3468
|
+
recoverable: false,
|
|
3469
|
+
durationMs: Date.now() - t0,
|
|
3470
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
3471
|
+
});
|
|
3472
|
+
throw err;
|
|
3473
|
+
}
|
|
2860
3474
|
}
|
|
2861
3475
|
/**
|
|
2862
3476
|
* Read ~/.wrongstack/sync.json (encrypted GitHub token storage) and decrypt
|
|
@@ -2865,21 +3479,64 @@ var DefaultConfigLoader = class {
|
|
|
2865
3479
|
* isolated — it should never be part of project-local or env-driven config.
|
|
2866
3480
|
*/
|
|
2867
3481
|
async loadSyncConfig() {
|
|
3482
|
+
const fp = this.paths.syncConfig;
|
|
3483
|
+
const t0 = Date.now();
|
|
2868
3484
|
try {
|
|
2869
|
-
const raw = await fsp.readFile(
|
|
3485
|
+
const raw = await fsp.readFile(fp, "utf8");
|
|
2870
3486
|
const parsed = safeParse(raw);
|
|
2871
|
-
if (!parsed.ok || !parsed.value)
|
|
3487
|
+
if (!parsed.ok || !parsed.value) {
|
|
3488
|
+
this.events?.emit("storage.read", {
|
|
3489
|
+
sessionId: "~config~",
|
|
3490
|
+
store: "config",
|
|
3491
|
+
filePath: fp,
|
|
3492
|
+
operation: "load_sync",
|
|
3493
|
+
outcome: "failure",
|
|
3494
|
+
durationMs: Date.now() - t0,
|
|
3495
|
+
error: "parse error or empty file",
|
|
3496
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
3497
|
+
});
|
|
3498
|
+
return null;
|
|
3499
|
+
}
|
|
2872
3500
|
if (this.vault) {
|
|
2873
3501
|
const decrypted = decryptConfigSecrets({ sync: parsed.value }, this.vault);
|
|
2874
|
-
|
|
3502
|
+
const result = decrypted.sync ?? null;
|
|
3503
|
+
this.events?.emit("storage.read", {
|
|
3504
|
+
sessionId: "~config~",
|
|
3505
|
+
store: "config",
|
|
3506
|
+
filePath: fp,
|
|
3507
|
+
operation: "load_sync",
|
|
3508
|
+
outcome: "success",
|
|
3509
|
+
durationMs: Date.now() - t0,
|
|
3510
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
3511
|
+
});
|
|
3512
|
+
return result;
|
|
2875
3513
|
}
|
|
3514
|
+
this.events?.emit("storage.read", {
|
|
3515
|
+
sessionId: "~config~",
|
|
3516
|
+
store: "config",
|
|
3517
|
+
filePath: fp,
|
|
3518
|
+
operation: "load_sync",
|
|
3519
|
+
outcome: "success",
|
|
3520
|
+
durationMs: Date.now() - t0,
|
|
3521
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
3522
|
+
});
|
|
2876
3523
|
return parsed.value;
|
|
2877
3524
|
} catch (err) {
|
|
2878
3525
|
if (err.code === "ENOENT") return null;
|
|
3526
|
+
this.events?.emit("storage.read", {
|
|
3527
|
+
sessionId: "~config~",
|
|
3528
|
+
store: "config",
|
|
3529
|
+
filePath: fp,
|
|
3530
|
+
operation: "load_sync",
|
|
3531
|
+
outcome: "failure",
|
|
3532
|
+
durationMs: Date.now() - t0,
|
|
3533
|
+
error: storageErrorString(err),
|
|
3534
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
3535
|
+
});
|
|
2879
3536
|
console.warn(JSON.stringify({
|
|
2880
3537
|
level: "warn",
|
|
2881
3538
|
event: "config.sync_load_failed",
|
|
2882
|
-
message:
|
|
3539
|
+
message: toErrorMessage(err),
|
|
2883
3540
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2884
3541
|
}));
|
|
2885
3542
|
return null;
|
|
@@ -2887,15 +3544,26 @@ var DefaultConfigLoader = class {
|
|
|
2887
3544
|
}
|
|
2888
3545
|
async readJson(file) {
|
|
2889
3546
|
let raw;
|
|
3547
|
+
const t0 = Date.now();
|
|
2890
3548
|
try {
|
|
2891
3549
|
raw = await fsp.readFile(file, "utf8");
|
|
2892
3550
|
} catch (err) {
|
|
2893
3551
|
if (err.code !== "ENOENT") {
|
|
3552
|
+
this.events?.emit("storage.read", {
|
|
3553
|
+
sessionId: "~config~",
|
|
3554
|
+
store: "config",
|
|
3555
|
+
filePath: file,
|
|
3556
|
+
operation: "read_json",
|
|
3557
|
+
outcome: "failure",
|
|
3558
|
+
durationMs: Date.now() - t0,
|
|
3559
|
+
error: storageErrorString(err),
|
|
3560
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
3561
|
+
});
|
|
2894
3562
|
console.warn(JSON.stringify({
|
|
2895
3563
|
level: "warn",
|
|
2896
3564
|
event: "config.read_failed",
|
|
2897
3565
|
path: file,
|
|
2898
|
-
message:
|
|
3566
|
+
message: toErrorMessage(err),
|
|
2899
3567
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2900
3568
|
}));
|
|
2901
3569
|
}
|
|
@@ -2903,6 +3571,16 @@ var DefaultConfigLoader = class {
|
|
|
2903
3571
|
}
|
|
2904
3572
|
const parsed = safeParse(raw);
|
|
2905
3573
|
if (!parsed.ok || !parsed.value) {
|
|
3574
|
+
this.events?.emit("storage.read", {
|
|
3575
|
+
sessionId: "~config~",
|
|
3576
|
+
store: "config",
|
|
3577
|
+
filePath: file,
|
|
3578
|
+
operation: "read_json",
|
|
3579
|
+
outcome: "failure",
|
|
3580
|
+
durationMs: Date.now() - t0,
|
|
3581
|
+
error: "parse error or empty file",
|
|
3582
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
3583
|
+
});
|
|
2906
3584
|
console.warn(JSON.stringify({
|
|
2907
3585
|
level: "warn",
|
|
2908
3586
|
event: "config.parse_failed",
|
|
@@ -3036,7 +3714,7 @@ var RecoveryLock = class {
|
|
|
3036
3714
|
sessionStore;
|
|
3037
3715
|
probe;
|
|
3038
3716
|
constructor(opts) {
|
|
3039
|
-
this.file =
|
|
3717
|
+
this.file = path2.join(opts.dir, LOCK_FILE);
|
|
3040
3718
|
this.pid = opts.pid ?? process.pid;
|
|
3041
3719
|
this.hostname = opts.hostname ?? os.hostname();
|
|
3042
3720
|
this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
@@ -3097,7 +3775,7 @@ var RecoveryLock = class {
|
|
|
3097
3775
|
* null return before calling this.
|
|
3098
3776
|
*/
|
|
3099
3777
|
async write(sessionId) {
|
|
3100
|
-
await ensureDir(
|
|
3778
|
+
await ensureDir(path2.dirname(this.file));
|
|
3101
3779
|
const lock = {
|
|
3102
3780
|
v: 1,
|
|
3103
3781
|
sessionId,
|
|
@@ -3164,42 +3842,6 @@ function defaultIsPidAlive(pid) {
|
|
|
3164
3842
|
}
|
|
3165
3843
|
}
|
|
3166
3844
|
|
|
3167
|
-
// src/utils/regex-guard.ts
|
|
3168
|
-
var MAX_PATTERN_LEN = 512;
|
|
3169
|
-
var DANGEROUS_PATTERNS = [
|
|
3170
|
-
/(\([^)]*[+*][^)]*\))[+*]/,
|
|
3171
|
-
// (a+)+, (.*)+, etc
|
|
3172
|
-
/(\(\?:[^)]*[+*][^)]*\))[+*]/
|
|
3173
|
-
// same, with non-capturing group
|
|
3174
|
-
];
|
|
3175
|
-
function compileUserRegex(pattern, flags) {
|
|
3176
|
-
if (typeof pattern !== "string") {
|
|
3177
|
-
return { ok: false, reason: "pattern must be a string" };
|
|
3178
|
-
}
|
|
3179
|
-
if (pattern.length === 0) {
|
|
3180
|
-
return { ok: false, reason: "pattern is empty" };
|
|
3181
|
-
}
|
|
3182
|
-
if (pattern.length > MAX_PATTERN_LEN) {
|
|
3183
|
-
return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
|
|
3184
|
-
}
|
|
3185
|
-
for (const rx of DANGEROUS_PATTERNS) {
|
|
3186
|
-
if (rx.test(pattern)) {
|
|
3187
|
-
return {
|
|
3188
|
-
ok: false,
|
|
3189
|
-
reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
|
|
3190
|
-
};
|
|
3191
|
-
}
|
|
3192
|
-
}
|
|
3193
|
-
try {
|
|
3194
|
-
return { ok: true, regex: new RegExp(pattern, flags) };
|
|
3195
|
-
} catch (err) {
|
|
3196
|
-
return {
|
|
3197
|
-
ok: false,
|
|
3198
|
-
reason: err instanceof Error ? err.message : "invalid regex"
|
|
3199
|
-
};
|
|
3200
|
-
}
|
|
3201
|
-
}
|
|
3202
|
-
|
|
3203
3845
|
// src/storage/session-reader.ts
|
|
3204
3846
|
var DefaultSessionReader = class {
|
|
3205
3847
|
store;
|
|
@@ -3479,9 +4121,9 @@ function sessionScopedPath(dir, sessionId, suffix) {
|
|
|
3479
4121
|
if (!sessionId || sessionId.includes("\\") || sessionId.includes("..")) {
|
|
3480
4122
|
throw invalid(sessionId);
|
|
3481
4123
|
}
|
|
3482
|
-
const resolved =
|
|
3483
|
-
const rel =
|
|
3484
|
-
if (rel.startsWith("..") ||
|
|
4124
|
+
const resolved = path2.resolve(dir, `${sessionId}${suffix}`);
|
|
4125
|
+
const rel = path2.relative(path2.resolve(dir), resolved);
|
|
4126
|
+
if (rel.startsWith("..") || path2.isAbsolute(rel)) {
|
|
3485
4127
|
throw invalid(sessionId);
|
|
3486
4128
|
}
|
|
3487
4129
|
return resolved;
|
|
@@ -3501,20 +4143,53 @@ var MAX_TEXT_LENGTH = 2e3;
|
|
|
3501
4143
|
var MAX_ANNOTATIONS = 1e3;
|
|
3502
4144
|
var AnnotationsStore = class {
|
|
3503
4145
|
dir;
|
|
4146
|
+
events;
|
|
4147
|
+
traceId;
|
|
3504
4148
|
/** Per-session write queue. Created lazily on first add. */
|
|
3505
4149
|
writeChains = /* @__PURE__ */ new Map();
|
|
3506
4150
|
constructor(opts) {
|
|
3507
4151
|
this.dir = opts.dir;
|
|
4152
|
+
this.events = opts.events;
|
|
4153
|
+
this.traceId = opts.traceId;
|
|
3508
4154
|
}
|
|
3509
4155
|
// ── Reads ──────────────────────────────────────────────────────────────
|
|
3510
4156
|
/**
|
|
3511
4157
|
* Return all annotations for `sessionId` in insertion order
|
|
3512
4158
|
* (oldest first). Returns an empty array when no file exists
|
|
3513
|
-
* yet (the normal case for a fresh session)
|
|
4159
|
+
* yet (the normal case for a fresh session) and also degrades
|
|
4160
|
+
* gracefully to `[]` on a read error (permissions, corruption) —
|
|
4161
|
+
* the failure is still surfaced via a `storage.read` event so it
|
|
4162
|
+
* never silently hides I/O problems from observers.
|
|
3514
4163
|
*/
|
|
3515
4164
|
async list(sessionId) {
|
|
3516
|
-
const
|
|
3517
|
-
|
|
4165
|
+
const t0 = Date.now();
|
|
4166
|
+
const fp = this.filePath(sessionId);
|
|
4167
|
+
try {
|
|
4168
|
+
const file = await this.readFile(sessionId);
|
|
4169
|
+
const durationMs = Date.now() - t0;
|
|
4170
|
+
this.events?.emit("storage.read", {
|
|
4171
|
+
sessionId,
|
|
4172
|
+
store: "annotations",
|
|
4173
|
+
filePath: fp,
|
|
4174
|
+
operation: "list",
|
|
4175
|
+
outcome: "success",
|
|
4176
|
+
durationMs,
|
|
4177
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
4178
|
+
});
|
|
4179
|
+
return file ? file.annotations : [];
|
|
4180
|
+
} catch (err) {
|
|
4181
|
+
this.events?.emit("storage.read", {
|
|
4182
|
+
sessionId,
|
|
4183
|
+
store: "annotations",
|
|
4184
|
+
filePath: fp,
|
|
4185
|
+
operation: "list",
|
|
4186
|
+
outcome: "failure",
|
|
4187
|
+
durationMs: Date.now() - t0,
|
|
4188
|
+
error: toErrorMessage(err),
|
|
4189
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
4190
|
+
});
|
|
4191
|
+
return [];
|
|
4192
|
+
}
|
|
3518
4193
|
}
|
|
3519
4194
|
/**
|
|
3520
4195
|
* Convenience: only unresolved annotations, newest first — the
|
|
@@ -3566,25 +4241,62 @@ var AnnotationsStore = class {
|
|
|
3566
4241
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3567
4242
|
resolved: false
|
|
3568
4243
|
};
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
const
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
4244
|
+
const fp = this.filePath(input.sessionId);
|
|
4245
|
+
const t0 = Date.now();
|
|
4246
|
+
try {
|
|
4247
|
+
await this.enqueue(input.sessionId, async () => {
|
|
4248
|
+
await withFileLock(fp, async () => {
|
|
4249
|
+
const all = await this.list(input.sessionId);
|
|
4250
|
+
all.push(annotation);
|
|
4251
|
+
if (all.length > MAX_ANNOTATIONS) {
|
|
4252
|
+
const sorted = all.map((a, i) => ({ a, i })).sort((x, y) => {
|
|
4253
|
+
if (x.a.resolved !== y.a.resolved) return x.a.resolved ? 1 : -1;
|
|
4254
|
+
return x.a.createdAt.localeCompare(y.a.createdAt);
|
|
4255
|
+
});
|
|
4256
|
+
const evictCount = all.length - MAX_ANNOTATIONS;
|
|
4257
|
+
const toEvict = new Set(sorted.slice(0, evictCount).map((s) => s.a.id));
|
|
4258
|
+
const kept = all.filter((a) => !toEvict.has(a.id));
|
|
4259
|
+
await this.writeFile(input.sessionId, { version: FILE_VERSION, annotations: kept });
|
|
4260
|
+
const durationMs = Date.now() - t0;
|
|
4261
|
+
this.events?.emit("storage.write", {
|
|
4262
|
+
sessionId: input.sessionId,
|
|
4263
|
+
store: "annotations",
|
|
4264
|
+
filePath: fp,
|
|
4265
|
+
operation: "evict",
|
|
4266
|
+
outcome: "success",
|
|
4267
|
+
durationMs,
|
|
4268
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
4269
|
+
});
|
|
4270
|
+
} else {
|
|
4271
|
+
await this.writeFile(input.sessionId, { version: FILE_VERSION, annotations: all });
|
|
4272
|
+
const durationMs = Date.now() - t0;
|
|
4273
|
+
this.events?.emit("storage.write", {
|
|
4274
|
+
sessionId: input.sessionId,
|
|
4275
|
+
store: "annotations",
|
|
4276
|
+
filePath: fp,
|
|
4277
|
+
operation: "add",
|
|
4278
|
+
outcome: "success",
|
|
4279
|
+
durationMs,
|
|
4280
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
4281
|
+
});
|
|
4282
|
+
}
|
|
4283
|
+
});
|
|
3585
4284
|
});
|
|
3586
|
-
|
|
3587
|
-
|
|
4285
|
+
return annotation;
|
|
4286
|
+
} catch (err) {
|
|
4287
|
+
this.events?.emit("storage.error", {
|
|
4288
|
+
sessionId: input.sessionId,
|
|
4289
|
+
store: "annotations",
|
|
4290
|
+
filePath: fp,
|
|
4291
|
+
operation: "add",
|
|
4292
|
+
outcome: "failure",
|
|
4293
|
+
error: toErrorMessage(err),
|
|
4294
|
+
recoverable: false,
|
|
4295
|
+
durationMs: Date.now() - t0,
|
|
4296
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
4297
|
+
});
|
|
4298
|
+
throw err;
|
|
4299
|
+
}
|
|
3588
4300
|
}
|
|
3589
4301
|
/**
|
|
3590
4302
|
* Mark an annotation as resolved. Returns the updated record, or
|
|
@@ -3594,26 +4306,53 @@ var AnnotationsStore = class {
|
|
|
3594
4306
|
*/
|
|
3595
4307
|
async resolve(input) {
|
|
3596
4308
|
let updated = null;
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
4309
|
+
const fp = this.filePath(input.sessionId);
|
|
4310
|
+
const t0 = Date.now();
|
|
4311
|
+
try {
|
|
4312
|
+
await this.enqueue(input.sessionId, async () => {
|
|
4313
|
+
await withFileLock(fp, async () => {
|
|
4314
|
+
const all = await this.list(input.sessionId);
|
|
4315
|
+
const idx = all.findIndex((a) => a.id === input.annotationId);
|
|
4316
|
+
if (idx === -1) {
|
|
4317
|
+
updated = null;
|
|
4318
|
+
return;
|
|
4319
|
+
}
|
|
4320
|
+
const next = {
|
|
4321
|
+
...expectDefined(all[idx]),
|
|
4322
|
+
resolved: true,
|
|
4323
|
+
resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4324
|
+
resolvedBy: input.resolvedBy
|
|
4325
|
+
};
|
|
4326
|
+
all[idx] = next;
|
|
4327
|
+
await this.writeFile(input.sessionId, { version: FILE_VERSION, annotations: all });
|
|
4328
|
+
updated = next;
|
|
4329
|
+
const durationMs = Date.now() - t0;
|
|
4330
|
+
this.events?.emit("storage.write", {
|
|
4331
|
+
sessionId: input.sessionId,
|
|
4332
|
+
store: "annotations",
|
|
4333
|
+
filePath: fp,
|
|
4334
|
+
operation: "resolve",
|
|
4335
|
+
outcome: "success",
|
|
4336
|
+
durationMs,
|
|
4337
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
4338
|
+
});
|
|
4339
|
+
});
|
|
3614
4340
|
});
|
|
3615
|
-
|
|
3616
|
-
|
|
4341
|
+
return updated;
|
|
4342
|
+
} catch (err) {
|
|
4343
|
+
this.events?.emit("storage.error", {
|
|
4344
|
+
sessionId: input.sessionId,
|
|
4345
|
+
store: "annotations",
|
|
4346
|
+
filePath: fp,
|
|
4347
|
+
operation: "resolve",
|
|
4348
|
+
outcome: "failure",
|
|
4349
|
+
error: toErrorMessage(err),
|
|
4350
|
+
recoverable: false,
|
|
4351
|
+
durationMs: Date.now() - t0,
|
|
4352
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
4353
|
+
});
|
|
4354
|
+
throw err;
|
|
4355
|
+
}
|
|
3617
4356
|
}
|
|
3618
4357
|
// ── Internals ──────────────────────────────────────────────────────────
|
|
3619
4358
|
filePath(sessionId) {
|
|
@@ -3621,16 +4360,21 @@ var AnnotationsStore = class {
|
|
|
3621
4360
|
}
|
|
3622
4361
|
async readFile(sessionId) {
|
|
3623
4362
|
const fp = this.filePath(sessionId);
|
|
4363
|
+
let raw;
|
|
4364
|
+
try {
|
|
4365
|
+
raw = await fsp.readFile(fp, "utf8");
|
|
4366
|
+
} catch (err) {
|
|
4367
|
+
if (err.code === "ENOENT") return null;
|
|
4368
|
+
throw err;
|
|
4369
|
+
}
|
|
3624
4370
|
try {
|
|
3625
|
-
const raw = await fsp.readFile(fp, "utf8");
|
|
3626
4371
|
const parsed = JSON.parse(raw);
|
|
3627
4372
|
if (parsed.version !== FILE_VERSION) {
|
|
3628
4373
|
return { version: FILE_VERSION, annotations: [] };
|
|
3629
4374
|
}
|
|
3630
4375
|
return parsed;
|
|
3631
|
-
} catch
|
|
3632
|
-
|
|
3633
|
-
return { version: FILE_VERSION, annotations: [] };
|
|
4376
|
+
} catch {
|
|
4377
|
+
return null;
|
|
3634
4378
|
}
|
|
3635
4379
|
}
|
|
3636
4380
|
async writeFile(sessionId, file) {
|
|
@@ -3684,9 +4428,20 @@ function hashRequest(request) {
|
|
|
3684
4428
|
const digest = createHash("sha256").update(json, "utf8").digest("hex");
|
|
3685
4429
|
return `sha256:${digest}`;
|
|
3686
4430
|
}
|
|
4431
|
+
|
|
4432
|
+
// src/storage/replay-log-store.ts
|
|
4433
|
+
function storageErrorString2(err) {
|
|
4434
|
+
if (err instanceof Error) {
|
|
4435
|
+
const code = err.code;
|
|
4436
|
+
return code ? `${code}: ${err.message}` : err.message;
|
|
4437
|
+
}
|
|
4438
|
+
return String(err);
|
|
4439
|
+
}
|
|
3687
4440
|
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
3688
4441
|
var ReplayLogStore = class {
|
|
3689
4442
|
dir;
|
|
4443
|
+
events;
|
|
4444
|
+
traceId;
|
|
3690
4445
|
writeChains = /* @__PURE__ */ new Map();
|
|
3691
4446
|
/** Per-session hash → entry index, kept in memory after the first load. */
|
|
3692
4447
|
cache = /* @__PURE__ */ new Map();
|
|
@@ -3695,6 +4450,8 @@ var ReplayLogStore = class {
|
|
|
3695
4450
|
maxEntries;
|
|
3696
4451
|
constructor(opts) {
|
|
3697
4452
|
this.dir = opts.dir;
|
|
4453
|
+
this.events = opts.events;
|
|
4454
|
+
this.traceId = opts.traceId;
|
|
3698
4455
|
this.maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
3699
4456
|
}
|
|
3700
4457
|
// ── Writes ──────────────────────────────────────────────────────────────
|
|
@@ -3705,38 +4462,61 @@ var ReplayLogStore = class {
|
|
|
3705
4462
|
*/
|
|
3706
4463
|
async record(input) {
|
|
3707
4464
|
const hash = hashRequest(input.request);
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
4465
|
+
const fp = this.filePath(input.sessionId);
|
|
4466
|
+
const t0 = Date.now();
|
|
4467
|
+
try {
|
|
4468
|
+
await this.enqueue(input.sessionId, async () => {
|
|
4469
|
+
await withFileLock(fp, async () => {
|
|
4470
|
+
const cache = await this.ensureCache(input.sessionId);
|
|
4471
|
+
if (cache.has(hash)) return;
|
|
4472
|
+
const entry = {
|
|
4473
|
+
hash,
|
|
4474
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4475
|
+
request: input.request,
|
|
4476
|
+
response: input.response
|
|
4477
|
+
};
|
|
4478
|
+
const currentCount = this.diskCount.get(input.sessionId) ?? 0;
|
|
4479
|
+
const willEvict = currentCount + 1 > this.maxEntries;
|
|
4480
|
+
if (!willEvict) {
|
|
4481
|
+
await fsp.appendFile(fp, JSON.stringify(entry) + "\n", "utf8");
|
|
4482
|
+
cache.set(hash, entry);
|
|
4483
|
+
this.diskCount.set(input.sessionId, currentCount + 1);
|
|
4484
|
+
this.events?.emit("storage.write", {
|
|
4485
|
+
sessionId: input.sessionId,
|
|
4486
|
+
store: "replay",
|
|
4487
|
+
filePath: fp,
|
|
4488
|
+
operation: "record",
|
|
4489
|
+
outcome: "success",
|
|
4490
|
+
durationMs: Date.now() - t0,
|
|
4491
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
4492
|
+
});
|
|
4493
|
+
return;
|
|
4494
|
+
}
|
|
4495
|
+
const all = await this.readAll(input.sessionId);
|
|
4496
|
+
all.push(entry);
|
|
4497
|
+
const keep = all.slice(-this.maxEntries);
|
|
4498
|
+
const refreshed = /* @__PURE__ */ new Map();
|
|
4499
|
+
for (const e of keep) refreshed.set(e.hash, e);
|
|
4500
|
+
this.cache.set(input.sessionId, refreshed);
|
|
4501
|
+
this.diskCount.set(input.sessionId, keep.length);
|
|
4502
|
+
await this.writeAll(input.sessionId, keep, "compact");
|
|
4503
|
+
});
|
|
3724
4504
|
});
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
4505
|
+
return hash;
|
|
4506
|
+
} catch (err) {
|
|
4507
|
+
this.events?.emit("storage.error", {
|
|
4508
|
+
sessionId: input.sessionId,
|
|
4509
|
+
store: "replay",
|
|
4510
|
+
filePath: fp,
|
|
4511
|
+
operation: "record",
|
|
4512
|
+
outcome: "failure",
|
|
4513
|
+
error: storageErrorString2(err),
|
|
4514
|
+
recoverable: false,
|
|
4515
|
+
durationMs: Date.now() - t0,
|
|
4516
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
4517
|
+
});
|
|
4518
|
+
throw err;
|
|
4519
|
+
}
|
|
3740
4520
|
}
|
|
3741
4521
|
// ── Reads ───────────────────────────────────────────────────────────────
|
|
3742
4522
|
/**
|
|
@@ -3745,13 +4525,65 @@ var ReplayLogStore = class {
|
|
|
3745
4525
|
* per session (in-memory cache).
|
|
3746
4526
|
*/
|
|
3747
4527
|
async lookup(sessionId, hash) {
|
|
3748
|
-
const
|
|
3749
|
-
|
|
4528
|
+
const fp = this.filePath(sessionId);
|
|
4529
|
+
const t0 = Date.now();
|
|
4530
|
+
try {
|
|
4531
|
+
const cache = await this.ensureCache(sessionId);
|
|
4532
|
+
this.events?.emit("storage.read", {
|
|
4533
|
+
sessionId,
|
|
4534
|
+
store: "replay",
|
|
4535
|
+
filePath: fp,
|
|
4536
|
+
operation: "lookup",
|
|
4537
|
+
outcome: "success",
|
|
4538
|
+
durationMs: Date.now() - t0,
|
|
4539
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
4540
|
+
});
|
|
4541
|
+
return cache.get(hash) ?? null;
|
|
4542
|
+
} catch (err) {
|
|
4543
|
+
this.events?.emit("storage.read", {
|
|
4544
|
+
sessionId,
|
|
4545
|
+
store: "replay",
|
|
4546
|
+
filePath: fp,
|
|
4547
|
+
operation: "lookup",
|
|
4548
|
+
outcome: "failure",
|
|
4549
|
+
durationMs: Date.now() - t0,
|
|
4550
|
+
error: storageErrorString2(err),
|
|
4551
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
4552
|
+
});
|
|
4553
|
+
throw err;
|
|
4554
|
+
}
|
|
3750
4555
|
}
|
|
3751
4556
|
/** All recorded entries for a session, in insertion order. */
|
|
3752
4557
|
async load(sessionId) {
|
|
3753
|
-
const
|
|
3754
|
-
|
|
4558
|
+
const fp = this.filePath(sessionId);
|
|
4559
|
+
const t0 = Date.now();
|
|
4560
|
+
try {
|
|
4561
|
+
const cache = await this.ensureCache(sessionId);
|
|
4562
|
+
const durationMs = Date.now() - t0;
|
|
4563
|
+
this.events?.emit("storage.read", {
|
|
4564
|
+
sessionId,
|
|
4565
|
+
store: "replay",
|
|
4566
|
+
filePath: fp,
|
|
4567
|
+
operation: "load",
|
|
4568
|
+
outcome: "success",
|
|
4569
|
+
durationMs,
|
|
4570
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
4571
|
+
});
|
|
4572
|
+
return [...cache.values()];
|
|
4573
|
+
} catch (err) {
|
|
4574
|
+
const durationMs = Date.now() - t0;
|
|
4575
|
+
this.events?.emit("storage.read", {
|
|
4576
|
+
sessionId,
|
|
4577
|
+
store: "replay",
|
|
4578
|
+
filePath: fp,
|
|
4579
|
+
operation: "load",
|
|
4580
|
+
outcome: "failure",
|
|
4581
|
+
durationMs,
|
|
4582
|
+
error: storageErrorString2(err),
|
|
4583
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
4584
|
+
});
|
|
4585
|
+
throw err;
|
|
4586
|
+
}
|
|
3755
4587
|
}
|
|
3756
4588
|
/**
|
|
3757
4589
|
* List every session id that has a replay log in the store dir.
|
|
@@ -3770,7 +4602,7 @@ var ReplayLogStore = class {
|
|
|
3770
4602
|
level: "warn",
|
|
3771
4603
|
event: "replay_log_store.list_readdir_failed",
|
|
3772
4604
|
dir,
|
|
3773
|
-
message:
|
|
4605
|
+
message: toErrorMessage(err),
|
|
3774
4606
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3775
4607
|
}));
|
|
3776
4608
|
}
|
|
@@ -3779,7 +4611,7 @@ var ReplayLogStore = class {
|
|
|
3779
4611
|
for (const entry of entries) {
|
|
3780
4612
|
if (entry.name.startsWith(".")) continue;
|
|
3781
4613
|
if (entry.isDirectory()) {
|
|
3782
|
-
if (depth === 0) await scan(
|
|
4614
|
+
if (depth === 0) await scan(path2.join(dir, entry.name), entry.name, depth + 1);
|
|
3783
4615
|
continue;
|
|
3784
4616
|
}
|
|
3785
4617
|
if (!entry.isFile() || !entry.name.endsWith(".replay.jsonl")) continue;
|
|
@@ -3789,7 +4621,7 @@ var ReplayLogStore = class {
|
|
|
3789
4621
|
out.push({
|
|
3790
4622
|
sessionId,
|
|
3791
4623
|
entryCount: all.length,
|
|
3792
|
-
path:
|
|
4624
|
+
path: path2.join(dir, entry.name)
|
|
3793
4625
|
});
|
|
3794
4626
|
}
|
|
3795
4627
|
};
|
|
@@ -3821,13 +4653,24 @@ var ReplayLogStore = class {
|
|
|
3821
4653
|
return out;
|
|
3822
4654
|
} catch (err) {
|
|
3823
4655
|
if (err.code === "ENOENT") return [];
|
|
3824
|
-
|
|
4656
|
+
throw err;
|
|
3825
4657
|
}
|
|
3826
4658
|
}
|
|
3827
|
-
async writeAll(sessionId, entries) {
|
|
4659
|
+
async writeAll(sessionId, entries, operation = "record") {
|
|
3828
4660
|
const fp = this.filePath(sessionId);
|
|
4661
|
+
const t0 = Date.now();
|
|
3829
4662
|
const body = entries.map((e) => JSON.stringify(e)).join("\n") + (entries.length ? "\n" : "");
|
|
3830
4663
|
await atomicWrite(fp, body);
|
|
4664
|
+
const durationMs = Date.now() - t0;
|
|
4665
|
+
this.events?.emit("storage.write", {
|
|
4666
|
+
sessionId,
|
|
4667
|
+
store: "replay",
|
|
4668
|
+
filePath: fp,
|
|
4669
|
+
operation,
|
|
4670
|
+
outcome: "success",
|
|
4671
|
+
durationMs,
|
|
4672
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
4673
|
+
});
|
|
3831
4674
|
}
|
|
3832
4675
|
async ensureCache(sessionId) {
|
|
3833
4676
|
let cache = this.cache.get(sessionId);
|
|
@@ -3867,15 +4710,15 @@ var SessionRecovery = class {
|
|
|
3867
4710
|
async detectStale(sessionId) {
|
|
3868
4711
|
const fp = this.filePath(sessionId);
|
|
3869
4712
|
const TAIL_SIZE = 8192;
|
|
3870
|
-
let
|
|
4713
|
+
let stat6;
|
|
3871
4714
|
try {
|
|
3872
|
-
|
|
4715
|
+
stat6 = await fsp.stat(fp);
|
|
3873
4716
|
} catch (err) {
|
|
3874
4717
|
if (err.code === "ENOENT") return null;
|
|
3875
4718
|
return null;
|
|
3876
4719
|
}
|
|
3877
|
-
if (
|
|
3878
|
-
const position = Math.max(0,
|
|
4720
|
+
if (stat6.size === 0) return null;
|
|
4721
|
+
const position = Math.max(0, stat6.size - TAIL_SIZE);
|
|
3879
4722
|
const buf = Buffer.alloc(TAIL_SIZE);
|
|
3880
4723
|
let fh;
|
|
3881
4724
|
try {
|
|
@@ -3979,7 +4822,7 @@ var SessionRecovery = class {
|
|
|
3979
4822
|
continue;
|
|
3980
4823
|
if (entry.isDirectory()) {
|
|
3981
4824
|
if (depth === 0) {
|
|
3982
|
-
await collect(
|
|
4825
|
+
await collect(path2.join(dir, entry.name), entry.name, depth + 1);
|
|
3983
4826
|
}
|
|
3984
4827
|
continue;
|
|
3985
4828
|
}
|
|
@@ -4005,16 +4848,27 @@ var GENESIS_PREV = "0".repeat(64);
|
|
|
4005
4848
|
var DEFAULT_FSYNC_EVERY = 100;
|
|
4006
4849
|
var ToolAuditLog = class {
|
|
4007
4850
|
dir;
|
|
4851
|
+
events;
|
|
4852
|
+
traceId;
|
|
4008
4853
|
/** In-memory cache of the last entry's hash (per session), to compute chains efficiently. */
|
|
4009
4854
|
tailHash = /* @__PURE__ */ new Map();
|
|
4010
4855
|
/** In-memory counter for entry indices — avoids re-reading the file on every write. */
|
|
4011
4856
|
tailIndex = /* @__PURE__ */ new Map();
|
|
4857
|
+
/**
|
|
4858
|
+
* File mtime+size recorded after our last write, per session. Used to
|
|
4859
|
+
* detect cross-process writes (session handoff, recovery) that would
|
|
4860
|
+
* invalidate the in-memory tail cache: if the stat no longer matches
|
|
4861
|
+
* we re-read the file to re-establish the chain tip before appending.
|
|
4862
|
+
*/
|
|
4863
|
+
tailStat = /* @__PURE__ */ new Map();
|
|
4012
4864
|
/** Tracks writes since last fsync, per session. */
|
|
4013
4865
|
unSyncedWrites = /* @__PURE__ */ new Map();
|
|
4014
4866
|
writeChains = /* @__PURE__ */ new Map();
|
|
4015
4867
|
fsyncEvery;
|
|
4016
4868
|
constructor(opts) {
|
|
4017
4869
|
this.dir = opts.dir;
|
|
4870
|
+
this.events = opts.events;
|
|
4871
|
+
this.traceId = opts.traceId;
|
|
4018
4872
|
this.fsyncEvery = opts.fsyncEvery ?? DEFAULT_FSYNC_EVERY;
|
|
4019
4873
|
}
|
|
4020
4874
|
/**
|
|
@@ -4025,96 +4879,223 @@ var ToolAuditLog = class {
|
|
|
4025
4879
|
*/
|
|
4026
4880
|
async record(input) {
|
|
4027
4881
|
let entry;
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4882
|
+
const fp = this.filePath(input.sessionId);
|
|
4883
|
+
const t0 = Date.now();
|
|
4884
|
+
try {
|
|
4885
|
+
await this.enqueue(input.sessionId, async () => {
|
|
4886
|
+
await withFileLock(fp, async () => {
|
|
4887
|
+
const tip = await this._resolveChainTip(input.sessionId, fp);
|
|
4888
|
+
const prevHash = tip.prevHash;
|
|
4889
|
+
const index = tip.nextIndex;
|
|
4890
|
+
const id = randomUUID();
|
|
4891
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
4892
|
+
const content = {
|
|
4893
|
+
id,
|
|
4894
|
+
ts,
|
|
4895
|
+
prevHash,
|
|
4896
|
+
toolName: input.toolName,
|
|
4897
|
+
toolUseId: input.toolUseId,
|
|
4898
|
+
input: input.input,
|
|
4899
|
+
output: input.output,
|
|
4900
|
+
isError: input.isError,
|
|
4901
|
+
index
|
|
4902
|
+
};
|
|
4903
|
+
const hash = createHash("sha256").update(stableStringify2(content), "utf8").digest("hex");
|
|
4904
|
+
entry = {
|
|
4905
|
+
id,
|
|
4906
|
+
ts,
|
|
4907
|
+
prevHash,
|
|
4908
|
+
hash,
|
|
4909
|
+
toolName: input.toolName,
|
|
4910
|
+
toolUseId: input.toolUseId,
|
|
4911
|
+
input: input.input,
|
|
4912
|
+
output: input.output,
|
|
4913
|
+
isError: input.isError,
|
|
4914
|
+
index
|
|
4915
|
+
};
|
|
4916
|
+
await fsp.appendFile(fp, JSON.stringify(entry) + "\n", "utf8");
|
|
4917
|
+
try {
|
|
4918
|
+
const st = await fsp.stat(fp);
|
|
4919
|
+
this.tailStat.set(input.sessionId, { mtimeMs: st.mtimeMs, size: st.size });
|
|
4920
|
+
} catch {
|
|
4921
|
+
}
|
|
4922
|
+
this.tailHash.set(input.sessionId, hash);
|
|
4923
|
+
this.tailIndex.set(input.sessionId, index + 1);
|
|
4924
|
+
await this._trackUnsynced(input.sessionId, fp);
|
|
4925
|
+
const durationMs = Date.now() - t0;
|
|
4926
|
+
this.events?.emit("storage.write", {
|
|
4927
|
+
sessionId: input.sessionId,
|
|
4928
|
+
store: "audit",
|
|
4929
|
+
filePath: fp,
|
|
4930
|
+
operation: "record",
|
|
4931
|
+
outcome: "success",
|
|
4932
|
+
durationMs,
|
|
4933
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
4934
|
+
});
|
|
4935
|
+
});
|
|
4064
4936
|
});
|
|
4065
|
-
|
|
4066
|
-
|
|
4937
|
+
return entry;
|
|
4938
|
+
} catch (err) {
|
|
4939
|
+
this.events?.emit("storage.error", {
|
|
4940
|
+
sessionId: input.sessionId,
|
|
4941
|
+
store: "audit",
|
|
4942
|
+
filePath: fp,
|
|
4943
|
+
operation: "record",
|
|
4944
|
+
outcome: "failure",
|
|
4945
|
+
error: toErrorMessage(err),
|
|
4946
|
+
recoverable: false,
|
|
4947
|
+
durationMs: Date.now() - t0,
|
|
4948
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
4949
|
+
});
|
|
4950
|
+
throw err;
|
|
4951
|
+
}
|
|
4952
|
+
}
|
|
4953
|
+
/**
|
|
4954
|
+
* Resolve the chain tip (previous hash + next index) for an append.
|
|
4955
|
+
* Uses the in-memory `tailHash`/`tailIndex` cache when the file's
|
|
4956
|
+
* stat matches our last known write; falls back to a full read on
|
|
4957
|
+
* cache miss or when an external writer has extended the file.
|
|
4958
|
+
*/
|
|
4959
|
+
async _resolveChainTip(sessionId, fp) {
|
|
4960
|
+
const cachedHash = this.tailHash.get(sessionId);
|
|
4961
|
+
const cachedIndex = this.tailIndex.get(sessionId);
|
|
4962
|
+
const cachedStat = this.tailStat.get(sessionId);
|
|
4963
|
+
if (cachedHash !== void 0 && cachedIndex !== void 0 && cachedStat) {
|
|
4964
|
+
try {
|
|
4965
|
+
const st = await fsp.stat(fp);
|
|
4966
|
+
if (st.mtimeMs === cachedStat.mtimeMs && st.size === cachedStat.size) {
|
|
4967
|
+
return { prevHash: cachedHash, nextIndex: cachedIndex };
|
|
4968
|
+
}
|
|
4969
|
+
} catch (err) {
|
|
4970
|
+
if (err.code === "ENOENT") {
|
|
4971
|
+
this.tailHash.delete(sessionId);
|
|
4972
|
+
this.tailIndex.delete(sessionId);
|
|
4973
|
+
this.tailStat.delete(sessionId);
|
|
4974
|
+
return { prevHash: GENESIS_PREV, nextIndex: 0 };
|
|
4975
|
+
}
|
|
4976
|
+
throw err;
|
|
4977
|
+
}
|
|
4978
|
+
}
|
|
4979
|
+
const entries = await this.readAll(sessionId);
|
|
4980
|
+
const prev = entries.at(-1);
|
|
4981
|
+
const prevHash = prev?.hash ?? GENESIS_PREV;
|
|
4982
|
+
const nextIndex = prev ? prev.index + 1 : 0;
|
|
4983
|
+
this.tailHash.set(sessionId, prevHash);
|
|
4984
|
+
this.tailIndex.set(sessionId, nextIndex);
|
|
4985
|
+
try {
|
|
4986
|
+
const st = await fsp.stat(fp);
|
|
4987
|
+
this.tailStat.set(sessionId, { mtimeMs: st.mtimeMs, size: st.size });
|
|
4988
|
+
} catch {
|
|
4989
|
+
}
|
|
4990
|
+
return { prevHash, nextIndex };
|
|
4067
4991
|
}
|
|
4068
4992
|
/**
|
|
4069
4993
|
* Walk the chain and verify every entry's hash and prevHash.
|
|
4070
4994
|
* Returns a structured verdict — never throws.
|
|
4071
4995
|
*/
|
|
4072
4996
|
async verify(sessionId) {
|
|
4073
|
-
const
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4997
|
+
const fp = this.filePath(sessionId);
|
|
4998
|
+
const t0 = Date.now();
|
|
4999
|
+
let entries;
|
|
5000
|
+
try {
|
|
5001
|
+
entries = await this.readAll(sessionId);
|
|
5002
|
+
} catch (err) {
|
|
5003
|
+
this.events?.emit("storage.read", {
|
|
5004
|
+
sessionId,
|
|
5005
|
+
store: "audit",
|
|
5006
|
+
filePath: fp,
|
|
5007
|
+
operation: "verify",
|
|
5008
|
+
outcome: "failure",
|
|
5009
|
+
durationMs: Date.now() - t0,
|
|
5010
|
+
error: toErrorMessage(err),
|
|
5011
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
5012
|
+
});
|
|
5013
|
+
return { ok: true, entries: 0 };
|
|
4081
5014
|
}
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
if (e.prevHash !== prevHash) {
|
|
5015
|
+
const verdict = (() => {
|
|
5016
|
+
if (entries.length === 0) return { ok: true, entries: 0 };
|
|
5017
|
+
if (entries[0]?.prevHash !== GENESIS_PREV) {
|
|
4086
5018
|
return {
|
|
4087
5019
|
ok: false,
|
|
4088
|
-
brokenAt:
|
|
4089
|
-
reason:
|
|
5020
|
+
brokenAt: 0,
|
|
5021
|
+
reason: "first entry is not the genesis (prevHash != 0\u20260)"
|
|
4090
5022
|
};
|
|
4091
5023
|
}
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
5024
|
+
let prevHash = GENESIS_PREV;
|
|
5025
|
+
for (let i = 0; i < entries.length; i++) {
|
|
5026
|
+
const e = expectDefined(entries[i]);
|
|
5027
|
+
if (e.prevHash !== prevHash) {
|
|
5028
|
+
return {
|
|
5029
|
+
ok: false,
|
|
5030
|
+
brokenAt: i,
|
|
5031
|
+
reason: `prevHash mismatch at entry ${i} (expected ${prevHash.slice(0, 8)}\u2026, got ${e.prevHash.slice(0, 8)}\u2026)`
|
|
5032
|
+
};
|
|
5033
|
+
}
|
|
5034
|
+
const content = {
|
|
5035
|
+
id: e.id,
|
|
5036
|
+
ts: e.ts,
|
|
5037
|
+
prevHash: e.prevHash,
|
|
5038
|
+
toolName: e.toolName,
|
|
5039
|
+
toolUseId: e.toolUseId,
|
|
5040
|
+
input: e.input,
|
|
5041
|
+
output: e.output,
|
|
5042
|
+
isError: e.isError,
|
|
5043
|
+
index: e.index
|
|
4109
5044
|
};
|
|
5045
|
+
const expectedHash = createHash("sha256").update(stableStringify2(content), "utf8").digest("hex");
|
|
5046
|
+
if (expectedHash !== e.hash) {
|
|
5047
|
+
return {
|
|
5048
|
+
ok: false,
|
|
5049
|
+
brokenAt: i,
|
|
5050
|
+
reason: `hash mismatch at entry ${i} (entry content was modified)`
|
|
5051
|
+
};
|
|
5052
|
+
}
|
|
5053
|
+
prevHash = e.hash;
|
|
4110
5054
|
}
|
|
4111
|
-
|
|
4112
|
-
}
|
|
4113
|
-
|
|
5055
|
+
return { ok: true, entries: entries.length };
|
|
5056
|
+
})();
|
|
5057
|
+
this.events?.emit("storage.read", {
|
|
5058
|
+
sessionId,
|
|
5059
|
+
store: "audit",
|
|
5060
|
+
filePath: fp,
|
|
5061
|
+
operation: "verify",
|
|
5062
|
+
outcome: verdict.ok ? "success" : "failure",
|
|
5063
|
+
durationMs: Date.now() - t0,
|
|
5064
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
5065
|
+
});
|
|
5066
|
+
return verdict;
|
|
4114
5067
|
}
|
|
4115
5068
|
/** All entries for a session, in insertion order. */
|
|
4116
5069
|
async load(sessionId) {
|
|
4117
|
-
|
|
5070
|
+
const fp = this.filePath(sessionId);
|
|
5071
|
+
const t0 = Date.now();
|
|
5072
|
+
try {
|
|
5073
|
+
const entries = await this.readAll(sessionId);
|
|
5074
|
+
const durationMs = Date.now() - t0;
|
|
5075
|
+
this.events?.emit("storage.read", {
|
|
5076
|
+
sessionId,
|
|
5077
|
+
store: "audit",
|
|
5078
|
+
filePath: fp,
|
|
5079
|
+
operation: "load",
|
|
5080
|
+
outcome: "success",
|
|
5081
|
+
durationMs,
|
|
5082
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
5083
|
+
});
|
|
5084
|
+
return entries;
|
|
5085
|
+
} catch (err) {
|
|
5086
|
+
const durationMs = Date.now() - t0;
|
|
5087
|
+
this.events?.emit("storage.read", {
|
|
5088
|
+
sessionId,
|
|
5089
|
+
store: "audit",
|
|
5090
|
+
filePath: fp,
|
|
5091
|
+
operation: "load",
|
|
5092
|
+
outcome: "failure",
|
|
5093
|
+
durationMs,
|
|
5094
|
+
error: toErrorMessage(err),
|
|
5095
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
5096
|
+
});
|
|
5097
|
+
throw err;
|
|
5098
|
+
}
|
|
4118
5099
|
}
|
|
4119
5100
|
// ── Internals ────────────────────────────────────────────────────────────
|
|
4120
5101
|
filePath(sessionId) {
|
|
@@ -4136,13 +5117,15 @@ var ToolAuditLog = class {
|
|
|
4136
5117
|
return out;
|
|
4137
5118
|
} catch (err) {
|
|
4138
5119
|
if (err.code === "ENOENT") return [];
|
|
4139
|
-
|
|
5120
|
+
throw err;
|
|
4140
5121
|
}
|
|
4141
5122
|
}
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
5123
|
+
/**
|
|
5124
|
+
* Tracks writes since last fsync and triggers periodic fsync.
|
|
5125
|
+
* Called after each O(1) append to maintain the same durability
|
|
5126
|
+
* guarantees as the old writeAll approach.
|
|
5127
|
+
*/
|
|
5128
|
+
async _trackUnsynced(sessionId, fp) {
|
|
4146
5129
|
const count = (this.unSyncedWrites.get(sessionId) ?? 0) + 1;
|
|
4147
5130
|
this.unSyncedWrites.set(sessionId, count);
|
|
4148
5131
|
if (this.fsyncEvery !== Number.POSITIVE_INFINITY && count % this.fsyncEvery === 0) {
|
|
@@ -4311,7 +5294,7 @@ var SessionRegistry = class {
|
|
|
4311
5294
|
heartbeatTimer = null;
|
|
4312
5295
|
currentSessionId = null;
|
|
4313
5296
|
constructor(globalRoot) {
|
|
4314
|
-
this.filePath =
|
|
5297
|
+
this.filePath = path2.join(globalRoot, REGISTRY_FILE);
|
|
4315
5298
|
}
|
|
4316
5299
|
// ── Public API ──────────────────────────────────────────────────────────
|
|
4317
5300
|
/**
|
|
@@ -4478,7 +5461,7 @@ var SessionRegistry = class {
|
|
|
4478
5461
|
const retryDelayMs = 20;
|
|
4479
5462
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
4480
5463
|
try {
|
|
4481
|
-
await fsp.mkdir(
|
|
5464
|
+
await fsp.mkdir(path2.dirname(this.filePath), { recursive: true });
|
|
4482
5465
|
const lockHandle = await fsp.open(lockPath, "wx").catch(() => null);
|
|
4483
5466
|
if (!lockHandle) {
|
|
4484
5467
|
await new Promise((r) => setTimeout(r, retryDelayMs * (attempt + 1)));
|
|
@@ -4691,7 +5674,7 @@ var DefaultSessionRewinder = class {
|
|
|
4691
5674
|
sessionsDir;
|
|
4692
5675
|
projectRoot;
|
|
4693
5676
|
async listCheckpoints(sessionId) {
|
|
4694
|
-
const file =
|
|
5677
|
+
const file = path2.join(this.sessionsDir, `${sessionId}.jsonl`);
|
|
4695
5678
|
const raw = await fsp.readFile(file, "utf8");
|
|
4696
5679
|
const events = parseEvents(raw);
|
|
4697
5680
|
const fileCountMap = /* @__PURE__ */ new Map();
|
|
@@ -4716,7 +5699,7 @@ var DefaultSessionRewinder = class {
|
|
|
4716
5699
|
return checkpoints;
|
|
4717
5700
|
}
|
|
4718
5701
|
async rewindToCheckpoint(sessionId, checkpointIndex) {
|
|
4719
|
-
const file =
|
|
5702
|
+
const file = path2.join(this.sessionsDir, `${sessionId}.jsonl`);
|
|
4720
5703
|
const raw = await fsp.readFile(file, "utf8");
|
|
4721
5704
|
const events = parseEvents(raw);
|
|
4722
5705
|
let targetIdx = -1;
|
|
@@ -4755,7 +5738,7 @@ var DefaultSessionRewinder = class {
|
|
|
4755
5738
|
return { ...result, toPromptIndex: checkpointIndex, removedEvents };
|
|
4756
5739
|
}
|
|
4757
5740
|
async rewindLastN(sessionId, n) {
|
|
4758
|
-
const file =
|
|
5741
|
+
const file = path2.join(this.sessionsDir, `${sessionId}.jsonl`);
|
|
4759
5742
|
const raw = await fsp.readFile(file, "utf8");
|
|
4760
5743
|
const events = parseEvents(raw);
|
|
4761
5744
|
const checkpoints = [];
|
|
@@ -4784,7 +5767,7 @@ var DefaultSessionRewinder = class {
|
|
|
4784
5767
|
return { ...result, toPromptIndex: targetIndex, removedEvents: snapshotsToRevert.length };
|
|
4785
5768
|
}
|
|
4786
5769
|
async rewindToStart(sessionId) {
|
|
4787
|
-
const file =
|
|
5770
|
+
const file = path2.join(this.sessionsDir, `${sessionId}.jsonl`);
|
|
4788
5771
|
const raw = await fsp.readFile(file, "utf8");
|
|
4789
5772
|
const events = parseEvents(raw);
|
|
4790
5773
|
const allSnapshots = [];
|
|
@@ -4820,10 +5803,10 @@ async function revertSnapshots(snapshots, projectRoot) {
|
|
|
4820
5803
|
for (const snapshot of snapshots) {
|
|
4821
5804
|
for (const file of snapshot.files) {
|
|
4822
5805
|
try {
|
|
4823
|
-
const absPath =
|
|
4824
|
-
const root =
|
|
4825
|
-
const rel =
|
|
4826
|
-
if (rel.startsWith("..") ||
|
|
5806
|
+
const absPath = path2.resolve(file.path);
|
|
5807
|
+
const root = path2.resolve(projectRoot);
|
|
5808
|
+
const rel = path2.relative(root, absPath);
|
|
5809
|
+
if (rel.startsWith("..") || path2.isAbsolute(rel)) {
|
|
4827
5810
|
errors.push(`${file.path}: path resolves outside project root \u2014 skipping`);
|
|
4828
5811
|
continue;
|
|
4829
5812
|
}
|
|
@@ -4842,30 +5825,72 @@ async function revertSnapshots(snapshots, projectRoot) {
|
|
|
4842
5825
|
}
|
|
4843
5826
|
}
|
|
4844
5827
|
} catch (err) {
|
|
4845
|
-
errors.push(`${file.path}: ${
|
|
5828
|
+
errors.push(`${file.path}: ${toErrorMessage(err)}`);
|
|
4846
5829
|
}
|
|
4847
5830
|
}
|
|
4848
5831
|
}
|
|
4849
5832
|
return { revertedFiles, errors };
|
|
4850
5833
|
}
|
|
4851
|
-
async function loadTodosCheckpoint(filePath) {
|
|
5834
|
+
async function loadTodosCheckpoint(filePath, events, traceId) {
|
|
5835
|
+
const t0 = Date.now();
|
|
4852
5836
|
let raw;
|
|
4853
5837
|
try {
|
|
4854
5838
|
raw = await fsp.readFile(filePath, "utf8");
|
|
4855
|
-
} catch {
|
|
5839
|
+
} catch (err) {
|
|
5840
|
+
events?.emit("storage.error", {
|
|
5841
|
+
sessionId: traceId ?? "~boot~",
|
|
5842
|
+
store: "todos",
|
|
5843
|
+
filePath,
|
|
5844
|
+
operation: "load",
|
|
5845
|
+
outcome: "failure",
|
|
5846
|
+
error: toErrorMessage(err),
|
|
5847
|
+
recoverable: true
|
|
5848
|
+
});
|
|
4856
5849
|
return null;
|
|
4857
5850
|
}
|
|
4858
5851
|
try {
|
|
4859
5852
|
const parsed = JSON.parse(raw);
|
|
4860
|
-
if (parsed?.version !== 1 || !Array.isArray(parsed.todos))
|
|
5853
|
+
if (parsed?.version !== 1 || !Array.isArray(parsed.todos)) {
|
|
5854
|
+
events?.emit("storage.read", {
|
|
5855
|
+
sessionId: traceId ?? "~boot~",
|
|
5856
|
+
store: "todos",
|
|
5857
|
+
filePath,
|
|
5858
|
+
operation: "load",
|
|
5859
|
+
outcome: "failure",
|
|
5860
|
+
durationMs: Date.now() - t0,
|
|
5861
|
+
error: "invalid_schema",
|
|
5862
|
+
...traceId !== void 0 && { traceId }
|
|
5863
|
+
});
|
|
5864
|
+
return null;
|
|
5865
|
+
}
|
|
5866
|
+
events?.emit("storage.read", {
|
|
5867
|
+
sessionId: traceId ?? "~boot~",
|
|
5868
|
+
store: "todos",
|
|
5869
|
+
filePath,
|
|
5870
|
+
operation: "load",
|
|
5871
|
+
outcome: "success",
|
|
5872
|
+
durationMs: Date.now() - t0,
|
|
5873
|
+
...traceId !== void 0 && { traceId }
|
|
5874
|
+
});
|
|
4861
5875
|
return parsed.todos.filter(
|
|
4862
5876
|
(t) => !!t && typeof t.id === "string" && typeof t.content === "string" && typeof t.status === "string" && (t.activeForm === void 0 || typeof t.activeForm === "string")
|
|
4863
5877
|
);
|
|
4864
5878
|
} catch {
|
|
5879
|
+
events?.emit("storage.read", {
|
|
5880
|
+
sessionId: traceId ?? "~boot~",
|
|
5881
|
+
store: "todos",
|
|
5882
|
+
filePath,
|
|
5883
|
+
operation: "load",
|
|
5884
|
+
outcome: "failure",
|
|
5885
|
+
durationMs: Date.now() - t0,
|
|
5886
|
+
error: "parse_failed",
|
|
5887
|
+
...traceId !== void 0 && { traceId }
|
|
5888
|
+
});
|
|
4865
5889
|
return null;
|
|
4866
5890
|
}
|
|
4867
5891
|
}
|
|
4868
|
-
async function saveTodosCheckpoint(filePath, sessionId, todos) {
|
|
5892
|
+
async function saveTodosCheckpoint(filePath, sessionId, todos, events, traceId) {
|
|
5893
|
+
const t0 = Date.now();
|
|
4869
5894
|
const payload = {
|
|
4870
5895
|
version: 1,
|
|
4871
5896
|
sessionId,
|
|
@@ -4874,22 +5899,40 @@ async function saveTodosCheckpoint(filePath, sessionId, todos) {
|
|
|
4874
5899
|
};
|
|
4875
5900
|
try {
|
|
4876
5901
|
await atomicWrite(filePath, JSON.stringify(payload, null, 2), { mode: 384 });
|
|
5902
|
+
events?.emit("storage.write", {
|
|
5903
|
+
sessionId: traceId ?? sessionId,
|
|
5904
|
+
store: "todos",
|
|
5905
|
+
filePath,
|
|
5906
|
+
operation: "save",
|
|
5907
|
+
outcome: "success",
|
|
5908
|
+
durationMs: Date.now() - t0,
|
|
5909
|
+
...traceId !== void 0 && { traceId }
|
|
5910
|
+
});
|
|
4877
5911
|
} catch (err) {
|
|
5912
|
+
events?.emit("storage.error", {
|
|
5913
|
+
sessionId: traceId ?? sessionId,
|
|
5914
|
+
store: "todos",
|
|
5915
|
+
filePath,
|
|
5916
|
+
operation: "save",
|
|
5917
|
+
outcome: "failure",
|
|
5918
|
+
error: toErrorMessage(err),
|
|
5919
|
+
recoverable: false
|
|
5920
|
+
});
|
|
4878
5921
|
console.warn(JSON.stringify({
|
|
4879
5922
|
level: "warn",
|
|
4880
5923
|
event: "todos_checkpoint.save_failed",
|
|
4881
|
-
message:
|
|
5924
|
+
message: toErrorMessage(err),
|
|
4882
5925
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4883
5926
|
}));
|
|
4884
5927
|
}
|
|
4885
5928
|
}
|
|
4886
|
-
function attachTodosCheckpoint(state, filePath, sessionId) {
|
|
5929
|
+
function attachTodosCheckpoint(state, filePath, sessionId, events, traceId) {
|
|
4887
5930
|
let timer = null;
|
|
4888
5931
|
let pending = null;
|
|
4889
5932
|
let writeChain = Promise.resolve();
|
|
4890
5933
|
const enqueueWrite = (todos) => {
|
|
4891
|
-
writeChain = writeChain.then(() => saveTodosCheckpoint(filePath, sessionId, todos)).catch((err) => {
|
|
4892
|
-
const msg =
|
|
5934
|
+
writeChain = writeChain.then(() => saveTodosCheckpoint(filePath, sessionId, todos, events, traceId)).catch((err) => {
|
|
5935
|
+
const msg = toErrorMessage(err);
|
|
4893
5936
|
console.error(JSON.stringify({
|
|
4894
5937
|
level: "error",
|
|
4895
5938
|
event: "todos_checkpoint.write_chain_failed",
|
|
@@ -4927,28 +5970,82 @@ function attachTodosCheckpoint(state, filePath, sessionId) {
|
|
|
4927
5970
|
}
|
|
4928
5971
|
};
|
|
4929
5972
|
}
|
|
4930
|
-
async function loadPlan(filePath) {
|
|
5973
|
+
async function loadPlan(filePath, events) {
|
|
5974
|
+
const t0 = Date.now();
|
|
4931
5975
|
let raw;
|
|
4932
5976
|
try {
|
|
4933
5977
|
raw = await fsp.readFile(filePath, "utf8");
|
|
4934
|
-
} catch {
|
|
5978
|
+
} catch (err) {
|
|
5979
|
+
events?.emit("storage.error", {
|
|
5980
|
+
sessionId: "~boot~",
|
|
5981
|
+
store: "plan",
|
|
5982
|
+
filePath,
|
|
5983
|
+
operation: "load",
|
|
5984
|
+
error: toErrorMessage(err),
|
|
5985
|
+
recoverable: true
|
|
5986
|
+
});
|
|
4935
5987
|
return null;
|
|
4936
5988
|
}
|
|
4937
5989
|
try {
|
|
4938
5990
|
const parsed = JSON.parse(raw);
|
|
4939
|
-
if (parsed?.version !== 1 || !Array.isArray(parsed.items))
|
|
5991
|
+
if (parsed?.version !== 1 || !Array.isArray(parsed.items)) {
|
|
5992
|
+
events?.emit("storage.read", {
|
|
5993
|
+
sessionId: "~boot~",
|
|
5994
|
+
store: "plan",
|
|
5995
|
+
filePath,
|
|
5996
|
+
operation: "load",
|
|
5997
|
+
outcome: "failure",
|
|
5998
|
+
durationMs: Date.now() - t0,
|
|
5999
|
+
error: "invalid_schema"
|
|
6000
|
+
});
|
|
6001
|
+
return null;
|
|
6002
|
+
}
|
|
6003
|
+
events?.emit("storage.read", {
|
|
6004
|
+
sessionId: "~boot~",
|
|
6005
|
+
store: "plan",
|
|
6006
|
+
filePath,
|
|
6007
|
+
operation: "load",
|
|
6008
|
+
outcome: "success",
|
|
6009
|
+
durationMs: Date.now() - t0
|
|
6010
|
+
});
|
|
4940
6011
|
return parsed;
|
|
4941
6012
|
} catch {
|
|
6013
|
+
events?.emit("storage.read", {
|
|
6014
|
+
sessionId: "~boot~",
|
|
6015
|
+
store: "plan",
|
|
6016
|
+
filePath,
|
|
6017
|
+
operation: "load",
|
|
6018
|
+
outcome: "failure",
|
|
6019
|
+
durationMs: Date.now() - t0,
|
|
6020
|
+
error: "parse_failed"
|
|
6021
|
+
});
|
|
4942
6022
|
return null;
|
|
4943
6023
|
}
|
|
4944
6024
|
}
|
|
4945
|
-
async function savePlan(filePath, plan) {
|
|
6025
|
+
async function savePlan(filePath, plan, events) {
|
|
6026
|
+
const t0 = Date.now();
|
|
4946
6027
|
try {
|
|
4947
6028
|
await atomicWrite(filePath, JSON.stringify(plan, null, 2), { mode: 384 });
|
|
6029
|
+
events?.emit("storage.write", {
|
|
6030
|
+
sessionId: "~boot~",
|
|
6031
|
+
store: "plan",
|
|
6032
|
+
filePath,
|
|
6033
|
+
operation: "save",
|
|
6034
|
+
outcome: "success",
|
|
6035
|
+
durationMs: Date.now() - t0
|
|
6036
|
+
});
|
|
4948
6037
|
} catch (err) {
|
|
6038
|
+
events?.emit("storage.error", {
|
|
6039
|
+
sessionId: "~boot~",
|
|
6040
|
+
store: "plan",
|
|
6041
|
+
filePath,
|
|
6042
|
+
operation: "save",
|
|
6043
|
+
error: toErrorMessage(err),
|
|
6044
|
+
recoverable: false
|
|
6045
|
+
});
|
|
4949
6046
|
console.warn(
|
|
4950
6047
|
"[plan-store] save failed:",
|
|
4951
|
-
|
|
6048
|
+
toErrorMessage(err)
|
|
4952
6049
|
);
|
|
4953
6050
|
}
|
|
4954
6051
|
}
|
|
@@ -5179,37 +6276,98 @@ function emptyTaskFile(sessionId) {
|
|
|
5179
6276
|
tasks: []
|
|
5180
6277
|
};
|
|
5181
6278
|
}
|
|
5182
|
-
async function loadTasks(filePath) {
|
|
6279
|
+
async function loadTasks(filePath, events, traceId) {
|
|
6280
|
+
const t0 = Date.now();
|
|
5183
6281
|
let raw;
|
|
5184
6282
|
try {
|
|
5185
6283
|
raw = await fsp.readFile(filePath, "utf8");
|
|
5186
|
-
} catch {
|
|
6284
|
+
} catch (err) {
|
|
6285
|
+
events?.emit("storage.error", {
|
|
6286
|
+
sessionId: traceId ?? "~boot~",
|
|
6287
|
+
store: "tasks",
|
|
6288
|
+
filePath,
|
|
6289
|
+
operation: "load",
|
|
6290
|
+
outcome: "failure",
|
|
6291
|
+
error: toErrorMessage(err),
|
|
6292
|
+
recoverable: true
|
|
6293
|
+
});
|
|
5187
6294
|
return null;
|
|
5188
6295
|
}
|
|
5189
6296
|
try {
|
|
5190
6297
|
const parsed = JSON.parse(raw);
|
|
5191
|
-
if (parsed?.version !== 1 || !Array.isArray(parsed.tasks))
|
|
6298
|
+
if (parsed?.version !== 1 || !Array.isArray(parsed.tasks)) {
|
|
6299
|
+
events?.emit("storage.read", {
|
|
6300
|
+
sessionId: traceId ?? "~boot~",
|
|
6301
|
+
store: "tasks",
|
|
6302
|
+
filePath,
|
|
6303
|
+
operation: "load",
|
|
6304
|
+
outcome: "failure",
|
|
6305
|
+
durationMs: Date.now() - t0,
|
|
6306
|
+
error: "invalid_schema",
|
|
6307
|
+
...traceId !== void 0 && { traceId }
|
|
6308
|
+
});
|
|
6309
|
+
return null;
|
|
6310
|
+
}
|
|
6311
|
+
events?.emit("storage.read", {
|
|
6312
|
+
sessionId: traceId ?? "~boot~",
|
|
6313
|
+
store: "tasks",
|
|
6314
|
+
filePath,
|
|
6315
|
+
operation: "load",
|
|
6316
|
+
outcome: "success",
|
|
6317
|
+
durationMs: Date.now() - t0,
|
|
6318
|
+
...traceId !== void 0 && { traceId }
|
|
6319
|
+
});
|
|
5192
6320
|
return parsed;
|
|
5193
6321
|
} catch {
|
|
6322
|
+
events?.emit("storage.read", {
|
|
6323
|
+
sessionId: traceId ?? "~boot~",
|
|
6324
|
+
store: "tasks",
|
|
6325
|
+
filePath,
|
|
6326
|
+
operation: "load",
|
|
6327
|
+
outcome: "failure",
|
|
6328
|
+
durationMs: Date.now() - t0,
|
|
6329
|
+
error: "parse_failed",
|
|
6330
|
+
...traceId !== void 0 && { traceId }
|
|
6331
|
+
});
|
|
5194
6332
|
return null;
|
|
5195
6333
|
}
|
|
5196
6334
|
}
|
|
5197
|
-
async function saveTasks(filePath, tasks) {
|
|
6335
|
+
async function saveTasks(filePath, tasks, events, traceId) {
|
|
6336
|
+
const t0 = Date.now();
|
|
5198
6337
|
try {
|
|
5199
6338
|
tasks.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5200
6339
|
await atomicWrite(filePath, JSON.stringify(tasks, null, 2), { mode: 384 });
|
|
6340
|
+
events?.emit("storage.write", {
|
|
6341
|
+
sessionId: traceId ?? "~boot~",
|
|
6342
|
+
store: "tasks",
|
|
6343
|
+
filePath,
|
|
6344
|
+
operation: "save",
|
|
6345
|
+
outcome: "success",
|
|
6346
|
+
durationMs: Date.now() - t0,
|
|
6347
|
+
...traceId !== void 0 && { traceId }
|
|
6348
|
+
});
|
|
5201
6349
|
} catch (err) {
|
|
6350
|
+
events?.emit("storage.error", {
|
|
6351
|
+
sessionId: traceId ?? "~boot~",
|
|
6352
|
+
store: "tasks",
|
|
6353
|
+
filePath,
|
|
6354
|
+
operation: "save",
|
|
6355
|
+
outcome: "failure",
|
|
6356
|
+
error: toErrorMessage(err),
|
|
6357
|
+
recoverable: false,
|
|
6358
|
+
...traceId !== void 0 && { traceId }
|
|
6359
|
+
});
|
|
5202
6360
|
console.warn(
|
|
5203
6361
|
"[task-store] save failed:",
|
|
5204
|
-
|
|
6362
|
+
toErrorMessage(err)
|
|
5205
6363
|
);
|
|
5206
6364
|
}
|
|
5207
6365
|
}
|
|
5208
|
-
async function mutateTasks(filePath, sessionId, fn) {
|
|
6366
|
+
async function mutateTasks(filePath, sessionId, fn, events, traceId) {
|
|
5209
6367
|
return withFileLock(filePath, async () => {
|
|
5210
|
-
const file = await loadTasks(filePath) ?? emptyTaskFile(sessionId);
|
|
6368
|
+
const file = await loadTasks(filePath, events, traceId) ?? emptyTaskFile(sessionId);
|
|
5211
6369
|
const updated = await fn(file);
|
|
5212
|
-
await saveTasks(filePath, updated);
|
|
6370
|
+
await saveTasks(filePath, updated, events, traceId);
|
|
5213
6371
|
return updated;
|
|
5214
6372
|
});
|
|
5215
6373
|
}
|
|
@@ -5380,7 +6538,7 @@ var DirectorStateCheckpoint = class {
|
|
|
5380
6538
|
} catch (err) {
|
|
5381
6539
|
console.warn(
|
|
5382
6540
|
"[director-state] checkpoint write failed:",
|
|
5383
|
-
|
|
6541
|
+
toErrorMessage(err)
|
|
5384
6542
|
);
|
|
5385
6543
|
} finally {
|
|
5386
6544
|
this.writing = false;
|
|
@@ -5391,113 +6549,36 @@ var DirectorStateCheckpoint = class {
|
|
|
5391
6549
|
}
|
|
5392
6550
|
}
|
|
5393
6551
|
};
|
|
5394
|
-
|
|
5395
|
-
// src/utils/term.ts
|
|
5396
|
-
var hasStdout = () => typeof process !== "undefined" && !!process.stdout;
|
|
5397
|
-
function isStdoutTTY() {
|
|
5398
|
-
return hasStdout() && Boolean(process.stdout.isTTY);
|
|
5399
|
-
}
|
|
5400
|
-
|
|
5401
|
-
// src/utils/color.ts
|
|
5402
|
-
var isColorTty = () => {
|
|
5403
|
-
if (envFlag(process.env.NO_COLOR)) return false;
|
|
5404
|
-
if (envFlag(process.env.FORCE_COLOR)) return true;
|
|
5405
|
-
return isStdoutTTY();
|
|
5406
|
-
};
|
|
5407
|
-
function envFlag(value) {
|
|
5408
|
-
if (value === void 0) return false;
|
|
5409
|
-
if (value.trim() === "") return false;
|
|
5410
|
-
return !/^(0|false|no|off)$/i.test(value.trim());
|
|
5411
|
-
}
|
|
5412
|
-
var COLOR = isColorTty();
|
|
5413
|
-
var wrap = (open6, close) => (s) => COLOR ? `\x1B[${open6}m${s}\x1B[${close}m` : s;
|
|
5414
|
-
var color = {
|
|
5415
|
-
reset: wrap("0", "0"),
|
|
5416
|
-
bold: wrap("1", "22"),
|
|
5417
|
-
dim: wrap("2", "22"),
|
|
5418
|
-
italic: wrap("3", "23"),
|
|
5419
|
-
underline: wrap("4", "24"),
|
|
5420
|
-
red: wrap("31", "39"),
|
|
5421
|
-
green: wrap("32", "39"),
|
|
5422
|
-
yellow: wrap("33", "39"),
|
|
5423
|
-
blue: wrap("34", "39"),
|
|
5424
|
-
magenta: wrap("35", "39"),
|
|
5425
|
-
cyan: wrap("36", "39"),
|
|
5426
|
-
gray: wrap("90", "39"),
|
|
5427
|
-
amber: wrap("38;5;214", "39"),
|
|
5428
|
-
pink: wrap("38;5;205", "39"),
|
|
5429
|
-
bgRed: wrap("41", "49"),
|
|
5430
|
-
bgGreen: wrap("42", "49")
|
|
5431
|
-
};
|
|
5432
|
-
function projectHash(absRoot) {
|
|
5433
|
-
return createHash("sha256").update(path13.resolve(absRoot)).digest("hex").slice(0, 12);
|
|
5434
|
-
}
|
|
5435
|
-
function projectSlug(absRoot) {
|
|
5436
|
-
const base = slugify(path13.basename(absRoot));
|
|
5437
|
-
const hash = createHash("sha256").update(path13.resolve(absRoot)).digest("hex").slice(0, 6);
|
|
5438
|
-
return `${base}-${hash}`;
|
|
5439
|
-
}
|
|
5440
|
-
function slugify(name) {
|
|
5441
|
-
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
|
|
5442
|
-
}
|
|
5443
|
-
function wstackGlobalRoot() {
|
|
5444
|
-
const fromEnv = process.env["WRONGSTACK_HOME"];
|
|
5445
|
-
if (fromEnv && fromEnv.trim().length > 0) return path13.resolve(fromEnv);
|
|
5446
|
-
return path13.join(os.homedir(), ".wrongstack");
|
|
5447
|
-
}
|
|
5448
|
-
function resolveWstackPaths(opts) {
|
|
5449
|
-
const globalRoot = opts.globalRoot ?? (opts.userHome ? path13.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
|
|
5450
|
-
const hash = projectHash(opts.projectRoot);
|
|
5451
|
-
const slug = projectSlug(opts.projectRoot);
|
|
5452
|
-
const projectDir = path13.join(globalRoot, "projects", slug);
|
|
5453
|
-
return {
|
|
5454
|
-
globalRoot,
|
|
5455
|
-
configDir: globalRoot,
|
|
5456
|
-
globalConfig: path13.join(globalRoot, "config.json"),
|
|
5457
|
-
secretsKey: path13.join(globalRoot, ".key"),
|
|
5458
|
-
globalMemory: path13.join(globalRoot, "memory.md"),
|
|
5459
|
-
globalSkills: path13.join(globalRoot, "skills"),
|
|
5460
|
-
globalPrompts: path13.join(globalRoot, "prompts"),
|
|
5461
|
-
cacheDir: path13.join(globalRoot, "cache"),
|
|
5462
|
-
modelsCache: path13.join(globalRoot, "cache", "models.dev.json"),
|
|
5463
|
-
modelsOverlayCache: path13.join(globalRoot, "cache", "models-overlay.json"),
|
|
5464
|
-
historyFile: path13.join(globalRoot, "history"),
|
|
5465
|
-
logFile: path13.join(globalRoot, "logs", "wrongstack.log"),
|
|
5466
|
-
projectDir,
|
|
5467
|
-
projectCodebaseIndex: path13.join(projectDir, "codebase-index"),
|
|
5468
|
-
projectMemory: path13.join(projectDir, "memory.md"),
|
|
5469
|
-
projectSessions: path13.join(projectDir, "sessions"),
|
|
5470
|
-
projectTrust: path13.join(projectDir, "trust.json"),
|
|
5471
|
-
projectMeta: path13.join(projectDir, "meta.json"),
|
|
5472
|
-
projectLocalConfig: path13.join(projectDir, "config.local.json"),
|
|
5473
|
-
inProjectConfig: path13.join(opts.projectRoot, ".wrongstack", "config.json"),
|
|
5474
|
-
inProjectAgentsFile: path13.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
|
|
5475
|
-
inProjectSkills: path13.join(opts.projectRoot, ".wrongstack", "skills"),
|
|
5476
|
-
inProjectWorktrees: path13.join(opts.projectRoot, ".wrongstack", "worktrees"),
|
|
5477
|
-
projectHash: hash,
|
|
5478
|
-
projectSlug: slug,
|
|
5479
|
-
projectGoal: path13.join(projectDir, "goal.json"),
|
|
5480
|
-
projectSpecs: path13.join(projectDir, "specs"),
|
|
5481
|
-
projectTaskGraphs: path13.join(projectDir, "task-graphs"),
|
|
5482
|
-
projectSddSession: path13.join(projectDir, "sdd-session.json"),
|
|
5483
|
-
projectPlan: path13.join(projectDir, "plan.json"),
|
|
5484
|
-
projectAutophase: path13.join(projectDir, "autophase"),
|
|
5485
|
-
syncConfig: path13.join(globalRoot, "sync.json")
|
|
5486
|
-
};
|
|
5487
|
-
}
|
|
5488
|
-
|
|
5489
|
-
// src/storage/goal-store.ts
|
|
5490
6552
|
var MAX_JOURNAL_ENTRIES = 500;
|
|
5491
6553
|
function goalFilePath(projectRoot) {
|
|
5492
6554
|
return resolveWstackPaths({ projectRoot }).projectGoal;
|
|
5493
6555
|
}
|
|
5494
|
-
async function loadGoal(filePath) {
|
|
6556
|
+
async function loadGoal(filePath, events) {
|
|
6557
|
+
const t0 = Date.now();
|
|
5495
6558
|
let raw;
|
|
5496
6559
|
try {
|
|
5497
6560
|
raw = await fsp.readFile(filePath, "utf8");
|
|
5498
6561
|
} catch (err) {
|
|
5499
6562
|
const code = err.code;
|
|
5500
|
-
if (code === "ENOENT")
|
|
6563
|
+
if (code === "ENOENT") {
|
|
6564
|
+
events?.emit("storage.read", {
|
|
6565
|
+
sessionId: "~boot~",
|
|
6566
|
+
store: "goal",
|
|
6567
|
+
filePath,
|
|
6568
|
+
operation: "load",
|
|
6569
|
+
outcome: "success",
|
|
6570
|
+
durationMs: Date.now() - t0
|
|
6571
|
+
});
|
|
6572
|
+
return null;
|
|
6573
|
+
}
|
|
6574
|
+
events?.emit("storage.error", {
|
|
6575
|
+
sessionId: "~boot~",
|
|
6576
|
+
store: "goal",
|
|
6577
|
+
filePath,
|
|
6578
|
+
operation: "load",
|
|
6579
|
+
error: toErrorMessage(err),
|
|
6580
|
+
recoverable: false
|
|
6581
|
+
});
|
|
5501
6582
|
throw err;
|
|
5502
6583
|
}
|
|
5503
6584
|
try {
|
|
@@ -5510,8 +6591,25 @@ async function loadGoal(filePath) {
|
|
|
5510
6591
|
message: "invalid schema \u2014 consider deleting and re-creating",
|
|
5511
6592
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5512
6593
|
}));
|
|
6594
|
+
events?.emit("storage.read", {
|
|
6595
|
+
sessionId: "~boot~",
|
|
6596
|
+
store: "goal",
|
|
6597
|
+
filePath,
|
|
6598
|
+
operation: "load",
|
|
6599
|
+
outcome: "failure",
|
|
6600
|
+
durationMs: Date.now() - t0,
|
|
6601
|
+
error: "invalid_schema"
|
|
6602
|
+
});
|
|
5513
6603
|
return null;
|
|
5514
6604
|
}
|
|
6605
|
+
events?.emit("storage.read", {
|
|
6606
|
+
sessionId: "~boot~",
|
|
6607
|
+
store: "goal",
|
|
6608
|
+
filePath,
|
|
6609
|
+
operation: "load",
|
|
6610
|
+
outcome: "success",
|
|
6611
|
+
durationMs: Date.now() - t0
|
|
6612
|
+
});
|
|
5515
6613
|
return parsed;
|
|
5516
6614
|
} catch {
|
|
5517
6615
|
console.warn(JSON.stringify({
|
|
@@ -5521,15 +6619,41 @@ async function loadGoal(filePath) {
|
|
|
5521
6619
|
message: "JSON parse failed \u2014 consider deleting and re-creating",
|
|
5522
6620
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5523
6621
|
}));
|
|
6622
|
+
events?.emit("storage.read", {
|
|
6623
|
+
sessionId: "~boot~",
|
|
6624
|
+
store: "goal",
|
|
6625
|
+
filePath,
|
|
6626
|
+
operation: "load",
|
|
6627
|
+
outcome: "failure",
|
|
6628
|
+
durationMs: Date.now() - t0,
|
|
6629
|
+
error: "parse_failed"
|
|
6630
|
+
});
|
|
5524
6631
|
return null;
|
|
5525
6632
|
}
|
|
5526
6633
|
}
|
|
5527
|
-
async function saveGoal(filePath, goal) {
|
|
6634
|
+
async function saveGoal(filePath, goal, events) {
|
|
6635
|
+
const t0 = Date.now();
|
|
5528
6636
|
try {
|
|
5529
6637
|
await atomicWrite(filePath, JSON.stringify(goal, null, 2), { mode: 384 });
|
|
6638
|
+
events?.emit("storage.write", {
|
|
6639
|
+
sessionId: "~boot~",
|
|
6640
|
+
store: "goal",
|
|
6641
|
+
filePath,
|
|
6642
|
+
operation: "save",
|
|
6643
|
+
outcome: "success",
|
|
6644
|
+
durationMs: Date.now() - t0
|
|
6645
|
+
});
|
|
5530
6646
|
} catch (err) {
|
|
6647
|
+
events?.emit("storage.error", {
|
|
6648
|
+
sessionId: "~boot~",
|
|
6649
|
+
store: "goal",
|
|
6650
|
+
filePath,
|
|
6651
|
+
operation: "save",
|
|
6652
|
+
error: toErrorMessage(err),
|
|
6653
|
+
recoverable: false
|
|
6654
|
+
});
|
|
5531
6655
|
throw new FsError({
|
|
5532
|
-
message:
|
|
6656
|
+
message: toErrorMessage(err),
|
|
5533
6657
|
code: ERROR_CODES.FS_ATOMIC_WRITE_FAILED,
|
|
5534
6658
|
path: filePath,
|
|
5535
6659
|
cause: err
|
|
@@ -5691,7 +6815,7 @@ var DefaultPromptStore = class {
|
|
|
5691
6815
|
if (!file.endsWith(".json")) continue;
|
|
5692
6816
|
try {
|
|
5693
6817
|
const raw = JSON.parse(
|
|
5694
|
-
await fsp.readFile(
|
|
6818
|
+
await fsp.readFile(path2.join(this.dir, file), "utf8")
|
|
5695
6819
|
);
|
|
5696
6820
|
entries.push(raw.entry);
|
|
5697
6821
|
} catch {
|
|
@@ -5704,7 +6828,7 @@ var DefaultPromptStore = class {
|
|
|
5704
6828
|
);
|
|
5705
6829
|
}
|
|
5706
6830
|
async get(id) {
|
|
5707
|
-
const file =
|
|
6831
|
+
const file = path2.join(this.dir, `${id}.json`);
|
|
5708
6832
|
try {
|
|
5709
6833
|
const raw = JSON.parse(await fsp.readFile(file, "utf8"));
|
|
5710
6834
|
return raw.entry;
|
|
@@ -5714,12 +6838,12 @@ var DefaultPromptStore = class {
|
|
|
5714
6838
|
}
|
|
5715
6839
|
async save(entry) {
|
|
5716
6840
|
await ensureDir(this.dir);
|
|
5717
|
-
const file =
|
|
6841
|
+
const file = path2.join(this.dir, `${entry.id}.json`);
|
|
5718
6842
|
const raw = { version: 1, entry };
|
|
5719
6843
|
await atomicWrite(file, JSON.stringify(raw, null, 2));
|
|
5720
6844
|
}
|
|
5721
6845
|
async delete(id) {
|
|
5722
|
-
const file =
|
|
6846
|
+
const file = path2.join(this.dir, `${id}.json`);
|
|
5723
6847
|
try {
|
|
5724
6848
|
await fsp.unlink(file);
|
|
5725
6849
|
return true;
|
|
@@ -5753,7 +6877,7 @@ var CloudSync = class {
|
|
|
5753
6877
|
this.paths = paths;
|
|
5754
6878
|
this.getConfig = getConfig;
|
|
5755
6879
|
this.setConfig = setConfig;
|
|
5756
|
-
this.statePath =
|
|
6880
|
+
this.statePath = path2.join(paths.globalRoot, "sync-state.json");
|
|
5757
6881
|
}
|
|
5758
6882
|
paths;
|
|
5759
6883
|
getConfig;
|
|
@@ -5860,7 +6984,7 @@ var CloudSync = class {
|
|
|
5860
6984
|
const rel = segments.slice(2).join("/");
|
|
5861
6985
|
const destPath = resolvePulledCategoryPath(cat, localPath, rel, entry.path);
|
|
5862
6986
|
const blobData = await this.getBlob(token, owner, repoName, entry.sha);
|
|
5863
|
-
await fsp.mkdir(
|
|
6987
|
+
await fsp.mkdir(path2.dirname(destPath), { recursive: true });
|
|
5864
6988
|
await fsp.writeFile(destPath, Buffer.from(blobData, "base64"));
|
|
5865
6989
|
}
|
|
5866
6990
|
const localRev = await this.hashLocalCategories(cfg.categories);
|
|
@@ -5967,12 +7091,12 @@ var CloudSync = class {
|
|
|
5967
7091
|
const localPath = this.categoryToPath(cat);
|
|
5968
7092
|
if (!localPath) continue;
|
|
5969
7093
|
try {
|
|
5970
|
-
const
|
|
5971
|
-
if (
|
|
7094
|
+
const stat6 = await fsp.stat(localPath);
|
|
7095
|
+
if (stat6.isDirectory()) {
|
|
5972
7096
|
const files = await this.walkDir(localPath, localPath);
|
|
5973
7097
|
for (const file of files) {
|
|
5974
7098
|
const content = await fsp.readFile(file, "utf8");
|
|
5975
|
-
const rel =
|
|
7099
|
+
const rel = path2.relative(localPath, file).replace(/\\/g, "/");
|
|
5976
7100
|
entries.push({ path: `data/${cat}/${rel}`, content, mode: "100644" });
|
|
5977
7101
|
hashes.push(content);
|
|
5978
7102
|
}
|
|
@@ -5993,8 +7117,8 @@ var CloudSync = class {
|
|
|
5993
7117
|
const localPath = this.categoryToPath(cat);
|
|
5994
7118
|
if (!localPath) continue;
|
|
5995
7119
|
try {
|
|
5996
|
-
const
|
|
5997
|
-
if (
|
|
7120
|
+
const stat6 = await fsp.stat(localPath);
|
|
7121
|
+
if (stat6.isDirectory()) {
|
|
5998
7122
|
const files = await this.walkDir(localPath, localPath);
|
|
5999
7123
|
for (const file of files) {
|
|
6000
7124
|
const content = await fsp.readFile(file);
|
|
@@ -6029,7 +7153,7 @@ var CloudSync = class {
|
|
|
6029
7153
|
const results = [];
|
|
6030
7154
|
const entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
6031
7155
|
for (const entry of entries) {
|
|
6032
|
-
const full =
|
|
7156
|
+
const full = path2.join(dir, entry.name);
|
|
6033
7157
|
if (entry.isDirectory()) {
|
|
6034
7158
|
results.push(...await this.walkDir(full, base));
|
|
6035
7159
|
} else {
|
|
@@ -6051,9 +7175,9 @@ function resolvePulledCategoryPath(cat, localPath, rel, remotePath) {
|
|
|
6051
7175
|
return localPath;
|
|
6052
7176
|
}
|
|
6053
7177
|
if (!rel) return localPath;
|
|
6054
|
-
const normalizedRel =
|
|
6055
|
-
const traversesUp = normalizedRel === ".." || normalizedRel.startsWith(`..${
|
|
6056
|
-
if (
|
|
7178
|
+
const normalizedRel = path2.normalize(rel);
|
|
7179
|
+
const traversesUp = normalizedRel === ".." || normalizedRel.startsWith(`..${path2.sep}`);
|
|
7180
|
+
if (path2.isAbsolute(normalizedRel) || traversesUp) {
|
|
6057
7181
|
throw new FsError({
|
|
6058
7182
|
message: `Refusing CloudSync path traversal: ${remotePath}`,
|
|
6059
7183
|
code: ERROR_CODES.FS_DELETE_FAILED,
|
|
@@ -6061,10 +7185,10 @@ function resolvePulledCategoryPath(cat, localPath, rel, remotePath) {
|
|
|
6061
7185
|
context: { reason: "path_traversal", normalizedRel }
|
|
6062
7186
|
});
|
|
6063
7187
|
}
|
|
6064
|
-
const dest =
|
|
6065
|
-
const root =
|
|
6066
|
-
const relative4 =
|
|
6067
|
-
if (relative4.startsWith("..") ||
|
|
7188
|
+
const dest = path2.resolve(localPath, normalizedRel);
|
|
7189
|
+
const root = path2.resolve(localPath);
|
|
7190
|
+
const relative4 = path2.relative(root, dest);
|
|
7191
|
+
if (relative4.startsWith("..") || path2.isAbsolute(relative4)) {
|
|
6068
7192
|
throw new FsError({
|
|
6069
7193
|
message: `Refusing CloudSync path outside category root: ${remotePath}`,
|
|
6070
7194
|
code: ERROR_CODES.FS_DELETE_FAILED,
|
|
@@ -6124,7 +7248,7 @@ function isAllowed(type, level) {
|
|
|
6124
7248
|
return true;
|
|
6125
7249
|
}
|
|
6126
7250
|
function createSessionEventBridge(writer, level = "standard", options = {}) {
|
|
6127
|
-
|
|
7251
|
+
let currentLevel = level ?? "standard";
|
|
6128
7252
|
const resolveWriter = typeof writer === "function" ? writer : () => writer;
|
|
6129
7253
|
const progressCounters = /* @__PURE__ */ new Map();
|
|
6130
7254
|
const toolProgressConfig = options.sampling?.toolProgress ?? {};
|
|
@@ -6145,14 +7269,19 @@ function createSessionEventBridge(writer, level = "standard", options = {}) {
|
|
|
6145
7269
|
return true;
|
|
6146
7270
|
}
|
|
6147
7271
|
return {
|
|
6148
|
-
level
|
|
7272
|
+
get level() {
|
|
7273
|
+
return currentLevel;
|
|
7274
|
+
},
|
|
7275
|
+
setAuditLevel(next) {
|
|
7276
|
+
currentLevel = next ?? "standard";
|
|
7277
|
+
},
|
|
6149
7278
|
allows(type) {
|
|
6150
|
-
return isAllowed(type,
|
|
7279
|
+
return isAllowed(type, currentLevel);
|
|
6151
7280
|
},
|
|
6152
7281
|
async append(event) {
|
|
6153
7282
|
const target = resolveWriter();
|
|
6154
7283
|
if (!target) return;
|
|
6155
|
-
if (!isAllowed(event.type,
|
|
7284
|
+
if (!isAllowed(event.type, currentLevel)) return;
|
|
6156
7285
|
if (!shouldSample(event)) return;
|
|
6157
7286
|
try {
|
|
6158
7287
|
await target.append(event);
|
|
@@ -6163,7 +7292,7 @@ function createSessionEventBridge(writer, level = "standard", options = {}) {
|
|
|
6163
7292
|
const target = resolveWriter();
|
|
6164
7293
|
if (!target || events.length === 0) return;
|
|
6165
7294
|
const allowed = events.filter(
|
|
6166
|
-
(e) => isAllowed(e.type,
|
|
7295
|
+
(e) => isAllowed(e.type, currentLevel) && shouldSample(e)
|
|
6167
7296
|
);
|
|
6168
7297
|
if (allowed.length === 0) return;
|
|
6169
7298
|
try {
|