@wrongstack/core 0.148.0 → 0.236.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-r9y6gdn4.d.ts → agent-bridge-Cimv7bK7.d.ts} +1 -1
- package/dist/{agent-subagent-runner-1GeQE_L0.d.ts → agent-subagent-runner-C658wj_c.d.ts} +9 -8
- package/dist/{brain-Cp_3GIS2.d.ts → brain-sCZ3lCjq.d.ts} +28 -2
- package/dist/{compactor-BueGt7LG.d.ts → compactor-BRfg3QPd.d.ts} +1 -1
- package/dist/{config-BaVThgnT.d.ts → config-Koq6f3fs.d.ts} +2 -2
- package/dist/{context-C7G_MtLV.d.ts → context-CLz3z_E8.d.ts} +126 -2
- package/dist/coordination/index.d.ts +70 -13
- package/dist/coordination/index.js +2126 -151
- package/dist/coordination/index.js.map +1 -1
- package/dist/defaults/index.d.ts +27 -27
- package/dist/defaults/index.js +1328 -354
- package/dist/defaults/index.js.map +1 -1
- package/dist/execution/index.d.ts +45 -16
- package/dist/execution/index.js +367 -59
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/prompt-enhancer.d.ts +86 -0
- package/dist/execution/prompt-enhancer.js +125 -0
- package/dist/execution/prompt-enhancer.js.map +1 -0
- package/dist/extension/index.d.ts +6 -6
- package/dist/extension/index.js +3 -1
- package/dist/extension/index.js.map +1 -1
- package/dist/{goal-preamble-CYJLg0wk.d.ts → goal-preamble-CnbzyVvl.d.ts} +19 -10
- package/dist/{index-BZdezm3g.d.ts → index-BlMqh5GO.d.ts} +8 -8
- package/dist/{index-CPweVoFM.d.ts → index-C2eSNPsB.d.ts} +7 -5
- package/dist/index.d.ts +439 -129
- package/dist/index.js +5206 -905
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/index.d.ts +7 -7
- package/dist/infrastructure/index.js +72 -15
- package/dist/infrastructure/index.js.map +1 -1
- package/dist/kernel/index.d.ts +9 -9
- package/dist/kernel/index.js +7 -1
- package/dist/kernel/index.js.map +1 -1
- package/dist/{llm-selector-CP72f1lC.d.ts → llm-selector-D22R4AFz.d.ts} +2 -2
- package/dist/logger-DmmQhf4P.d.ts +65 -0
- package/dist/{mcp-servers-Bl5LTvQg.d.ts → mcp-servers-DFbirBv6.d.ts} +11 -4
- package/dist/models/index.d.ts +5 -5
- package/dist/models/index.js +89 -9
- package/dist/models/index.js.map +1 -1
- package/dist/{models-registry-D90K9UnM.d.ts → models-registry-CnJRjTXc.d.ts} +1 -1
- package/dist/{multi-agent-coordinator-QWEzJDlm.d.ts → multi-agent-coordinator-60weDZoA.d.ts} +8 -8
- package/dist/{null-fleet-bus-BUyfqh23.d.ts → null-fleet-bus-1068dEnr.d.ts} +7 -7
- package/dist/observability/index.d.ts +2 -2
- package/dist/package-outdated-watcher-pzJ5w7y8.d.ts +560 -0
- package/dist/{parallel-eternal-engine-C75QuhAI.d.ts → parallel-eternal-engine-DtG1fjc9.d.ts} +13 -9
- package/dist/{path-resolver-DRjQBkoO.d.ts → path-resolver-CA1ULU0J.d.ts} +3 -3
- package/dist/{permission-B7nKnEvQ.d.ts → permission-DbWPbuoA.d.ts} +1 -1
- package/dist/{permission-policy-8-6zBmfA.d.ts → permission-policy-AOk0LVsV.d.ts} +2 -2
- package/dist/pipeline-DsmlwTXu.d.ts +493 -0
- package/dist/{plan-templates-CkKNPU3I.d.ts → plan-templates-DPABrDvy.d.ts} +19 -8
- package/dist/{provider-runner-BNpuIyOL.d.ts → provider-runner-D0HgUqwV.d.ts} +3 -3
- package/dist/{retry-policy-rutAfVeR.d.ts → retry-policy-BVnkbMET.d.ts} +1 -1
- package/dist/sdd/index.d.ts +8 -8
- package/dist/sdd/index.js +358 -85
- package/dist/sdd/index.js.map +1 -1
- package/dist/{secret-vault-DoISxaKO.d.ts → secret-vault-BJDY28ev.d.ts} +7 -1
- package/dist/{secret-vault-BTcC_T5v.d.ts → secret-vault-CeVNiy_f.d.ts} +4 -3
- package/dist/security/index.d.ts +6 -5
- package/dist/security/index.js +214 -35
- package/dist/security/index.js.map +1 -1
- package/dist/{selector-4vDFZKt3.d.ts → selector-Cb4_9-hf.d.ts} +1 -1
- package/dist/{session-event-bridge-DWlvglC2.d.ts → session-event-bridge-BhtkkFFy.d.ts} +4 -2
- package/dist/{session-reader-BAtCxdaw.d.ts → session-reader-CCOssnBS.d.ts} +1 -1
- package/dist/skills/index.js +171 -21
- package/dist/skills/index.js.map +1 -1
- package/dist/storage/index.d.ts +151 -13
- package/dist/storage/index.js +1117 -256
- package/dist/storage/index.js.map +1 -1
- package/dist/types/index.d.ts +68 -21
- package/dist/types/index.js +616 -74
- package/dist/types/index.js.map +1 -1
- package/dist/utils/expect-defined.js +3 -1
- package/dist/utils/expect-defined.js.map +1 -1
- package/dist/utils/index.d.ts +80 -4
- package/dist/utils/index.js +100 -15
- package/dist/utils/index.js.map +1 -1
- package/dist/{wstack-paths-DD50Omgn.d.ts → wstack-paths-CJjEwPXn.d.ts} +14 -1
- package/package.json +7 -3
- package/skills/chimera/SKILL.md +105 -0
- package/skills/research-web/SKILL.md +342 -0
- package/dist/logger-B9J5puGM.d.ts +0 -32
- package/dist/pipeline-BG7UgbDc.d.ts +0 -239
package/dist/storage/index.js
CHANGED
|
@@ -8,7 +8,9 @@ import { hostname } from 'os';
|
|
|
8
8
|
// src/utils/expect-defined.ts
|
|
9
9
|
function expectDefined(value, label) {
|
|
10
10
|
if (value === null || value === void 0) {
|
|
11
|
-
|
|
11
|
+
const err = new Error("Expected value to be defined");
|
|
12
|
+
err.name = "ExpectDefinedError";
|
|
13
|
+
throw err;
|
|
12
14
|
}
|
|
13
15
|
return value;
|
|
14
16
|
}
|
|
@@ -80,7 +82,7 @@ async function withFileLock(targetPath, fn, opts = {}) {
|
|
|
80
82
|
if (Date.now() - started >= timeoutMs) {
|
|
81
83
|
throw new Error(`Timed out waiting for file lock: ${targetPath}`);
|
|
82
84
|
}
|
|
83
|
-
await new Promise((
|
|
85
|
+
await new Promise((resolve5) => setTimeout(resolve5, 25));
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
88
|
try {
|
|
@@ -114,7 +116,7 @@ async function renameWithRetry(from, to) {
|
|
|
114
116
|
if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
|
|
115
117
|
throw err;
|
|
116
118
|
}
|
|
117
|
-
await new Promise((
|
|
119
|
+
await new Promise((resolve5) => setTimeout(resolve5, delays[i]));
|
|
118
120
|
}
|
|
119
121
|
}
|
|
120
122
|
throw lastErr;
|
|
@@ -272,7 +274,12 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
272
274
|
onClose: (s) => this.appendToIndex(s)
|
|
273
275
|
});
|
|
274
276
|
} catch (err) {
|
|
275
|
-
await handle.close().catch((e) => console.warn(
|
|
277
|
+
await handle.close().catch((e) => console.warn(JSON.stringify({
|
|
278
|
+
level: "warn",
|
|
279
|
+
event: "session_store.handle_close_failed",
|
|
280
|
+
message: e instanceof Error ? e.message : String(e),
|
|
281
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
282
|
+
})));
|
|
276
283
|
throw err;
|
|
277
284
|
}
|
|
278
285
|
}
|
|
@@ -299,11 +306,25 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
299
306
|
provider: data.metadata.provider
|
|
300
307
|
},
|
|
301
308
|
this.events,
|
|
302
|
-
{
|
|
309
|
+
{
|
|
310
|
+
resumed: true,
|
|
311
|
+
// Shard directory (sessions/<date>/) — must match create() so the
|
|
312
|
+
// .summary.json sidecar lands next to the JSONL instead of the
|
|
313
|
+
// sessions root (where summaryFor() would never find it).
|
|
314
|
+
dir: path13.dirname(file),
|
|
315
|
+
filePath: file,
|
|
316
|
+
secretScrubber: this.secretScrubber,
|
|
317
|
+
onClose: (s) => this.appendToIndex(s)
|
|
318
|
+
}
|
|
303
319
|
);
|
|
304
320
|
return { writer, data };
|
|
305
321
|
} catch (err) {
|
|
306
|
-
await handle.close().catch((e) => console.warn(
|
|
322
|
+
await handle.close().catch((e) => console.warn(JSON.stringify({
|
|
323
|
+
level: "warn",
|
|
324
|
+
event: "session_store.handle_close_failed",
|
|
325
|
+
message: e instanceof Error ? e.message : String(e),
|
|
326
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
327
|
+
})));
|
|
307
328
|
throw err;
|
|
308
329
|
}
|
|
309
330
|
}
|
|
@@ -323,7 +344,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
323
344
|
}
|
|
324
345
|
const meta = this.metaFromEvents(id, events);
|
|
325
346
|
const { messages, usage } = this.replay(events, id);
|
|
326
|
-
|
|
347
|
+
const toolCallEnds = extractToolCallEnds(events);
|
|
348
|
+
return { metadata: meta, events, messages, usage, toolCallEnds };
|
|
327
349
|
}
|
|
328
350
|
async list(limit = 20) {
|
|
329
351
|
try {
|
|
@@ -479,10 +501,13 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
479
501
|
const stat5 = await fsp.stat(full);
|
|
480
502
|
const summary = await this.summarize(id, stat5.mtime.toISOString());
|
|
481
503
|
await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
|
|
482
|
-
console.warn(
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
504
|
+
console.warn(JSON.stringify({
|
|
505
|
+
level: "warn",
|
|
506
|
+
event: "session_store.manifest_write_failed",
|
|
507
|
+
sessionId: id,
|
|
508
|
+
message: err instanceof Error ? err.message : String(err),
|
|
509
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
510
|
+
}));
|
|
486
511
|
});
|
|
487
512
|
return summary;
|
|
488
513
|
}
|
|
@@ -490,17 +515,48 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
490
515
|
/**
|
|
491
516
|
* Delete a session and all associated files: JSONL, summary, plan/todos
|
|
492
517
|
* sidecars, and the session directory (fleet.json, shared/, subagents/).
|
|
518
|
+
*
|
|
519
|
+
* Individual file deletions are best-effort (logged as structured warnings),
|
|
520
|
+
* but a tombstone is always written so readIndex() filters this session out.
|
|
521
|
+
* If the session directory itself can't be removed, the error is surfaced
|
|
522
|
+
* to the caller so prune() can report it.
|
|
493
523
|
*/
|
|
494
524
|
async deleteSession(id) {
|
|
495
|
-
|
|
496
|
-
|
|
525
|
+
const jsonlPath = this.sessionPath(id, ".jsonl");
|
|
526
|
+
const summaryPath = this.sessionPath(id, ".summary.json");
|
|
497
527
|
const shardDir = path13.dirname(path13.join(this.dir, id));
|
|
498
528
|
const base = path13.basename(id);
|
|
499
|
-
for (const ext of [".plan.json", ".todos.json"]) {
|
|
500
|
-
await fsp.unlink(path13.join(shardDir, `${base}${ext}`)).catch((err) => console.warn(`[session-store] delete ${ext} failed: ${err}`));
|
|
501
|
-
}
|
|
502
529
|
const sessDir = path13.join(shardDir, base);
|
|
503
|
-
|
|
530
|
+
const deletions = [
|
|
531
|
+
fsp.unlink(jsonlPath),
|
|
532
|
+
fsp.unlink(summaryPath),
|
|
533
|
+
fsp.unlink(path13.join(shardDir, `${base}.plan.json`)),
|
|
534
|
+
fsp.unlink(path13.join(shardDir, `${base}.todos.json`))
|
|
535
|
+
];
|
|
536
|
+
const results = await Promise.allSettled(deletions);
|
|
537
|
+
for (const r of results) {
|
|
538
|
+
if (r.status === "rejected") {
|
|
539
|
+
const msg = r.reason instanceof Error ? r.reason.message : String(r.reason);
|
|
540
|
+
if (r.reason?.code !== "ENOENT") {
|
|
541
|
+
console.warn(JSON.stringify({
|
|
542
|
+
level: "warn",
|
|
543
|
+
event: "session_store.delete_failed",
|
|
544
|
+
sessionId: id,
|
|
545
|
+
message: msg,
|
|
546
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
547
|
+
}));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
await fsp.rm(sessDir, { recursive: true, force: true }).catch((err) => {
|
|
552
|
+
console.warn(JSON.stringify({
|
|
553
|
+
level: "warn",
|
|
554
|
+
event: "session_store.rmdir_failed",
|
|
555
|
+
sessionId: id,
|
|
556
|
+
message: err instanceof Error ? err.message : String(err),
|
|
557
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
558
|
+
}));
|
|
559
|
+
});
|
|
504
560
|
await this.writeTombstone(id);
|
|
505
561
|
}
|
|
506
562
|
async delete(id) {
|
|
@@ -516,24 +572,33 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
516
572
|
activeSessionId = active.sessionId ?? null;
|
|
517
573
|
} catch {
|
|
518
574
|
}
|
|
575
|
+
const isPrunableJsonl = (name) => name.endsWith(".jsonl") && name !== "_index.jsonl" && name !== "_mailbox.jsonl" && !name.endsWith(".replay.jsonl") && !name.endsWith(".audit.jsonl");
|
|
576
|
+
const pruneFile = async (dir, name, prefix) => {
|
|
577
|
+
const jsonlPath = path13.join(dir, name);
|
|
578
|
+
try {
|
|
579
|
+
const stat5 = await fsp.stat(jsonlPath);
|
|
580
|
+
if (stat5.mtimeMs >= cutoff) return;
|
|
581
|
+
} catch {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const base = name.replace(/\.jsonl$/, "");
|
|
585
|
+
const id = prefix ? `${prefix}/${base}` : base;
|
|
586
|
+
if (activeSessionId && id === activeSessionId) return;
|
|
587
|
+
await this.deleteSession(id);
|
|
588
|
+
deleted++;
|
|
589
|
+
};
|
|
519
590
|
const entries = await fsp.readdir(this.dir, { withFileTypes: true }).catch(() => []);
|
|
520
591
|
for (const entry of entries) {
|
|
592
|
+
if (entry.isFile()) {
|
|
593
|
+
if (isPrunableJsonl(entry.name)) await pruneFile(this.dir, entry.name, "");
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
521
596
|
if (!entry.isDirectory()) continue;
|
|
522
597
|
const dateDir = path13.join(this.dir, entry.name);
|
|
523
598
|
const files = await fsp.readdir(dateDir, { withFileTypes: true }).catch(() => []);
|
|
524
599
|
for (const file of files) {
|
|
525
|
-
if (!file.isFile() || !file.name
|
|
526
|
-
|
|
527
|
-
try {
|
|
528
|
-
const stat5 = await fsp.stat(jsonlPath);
|
|
529
|
-
if (stat5.mtimeMs >= cutoff) continue;
|
|
530
|
-
} catch {
|
|
531
|
-
continue;
|
|
532
|
-
}
|
|
533
|
-
const id = `${entry.name}/${file.name.replace(/\.jsonl$/, "")}`;
|
|
534
|
-
if (activeSessionId && id === activeSessionId) continue;
|
|
535
|
-
await this.deleteSession(id);
|
|
536
|
-
deleted++;
|
|
600
|
+
if (!file.isFile() || !isPrunableJsonl(file.name)) continue;
|
|
601
|
+
await pruneFile(dateDir, file.name, entry.name);
|
|
537
602
|
}
|
|
538
603
|
}
|
|
539
604
|
if (deleted > 0) {
|
|
@@ -622,7 +687,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
622
687
|
}
|
|
623
688
|
metaFromEvents(id, events) {
|
|
624
689
|
const start = events.find((e) => e.type === "session_start");
|
|
625
|
-
const end = events.
|
|
690
|
+
const end = events.findLast((e) => e.type === "session_end");
|
|
626
691
|
return {
|
|
627
692
|
id,
|
|
628
693
|
startedAt: start?.ts ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
@@ -639,9 +704,9 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
639
704
|
for (const e of events) {
|
|
640
705
|
if (e.type === "user_input") {
|
|
641
706
|
openToolUses.clear();
|
|
642
|
-
messages.push({ role: "user", content: e.content });
|
|
707
|
+
messages.push({ role: "user", content: e.content, ts: e.ts });
|
|
643
708
|
} else if (e.type === "llm_response") {
|
|
644
|
-
messages.push({ role: "assistant", content: e.content });
|
|
709
|
+
messages.push({ role: "assistant", content: e.content, ts: e.ts });
|
|
645
710
|
for (const b of e.content) {
|
|
646
711
|
if (b.type === "tool_use") openToolUses.add(b.id);
|
|
647
712
|
}
|
|
@@ -660,25 +725,18 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
660
725
|
continue;
|
|
661
726
|
}
|
|
662
727
|
openToolUses.delete(e.id);
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
}
|
|
670
|
-
];
|
|
728
|
+
const resultBlock = {
|
|
729
|
+
type: "tool_result",
|
|
730
|
+
tool_use_id: e.id,
|
|
731
|
+
content: typeof e.content === "string" ? e.content : JSON.stringify(e.content),
|
|
732
|
+
is_error: e.isError
|
|
733
|
+
};
|
|
671
734
|
const last = messages[messages.length - 1];
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
} else if (typeof last.content === "string") {
|
|
676
|
-
last.content = [{ type: "text", text: last.content }, ...content];
|
|
677
|
-
} else {
|
|
678
|
-
messages.push({ role: "user", content });
|
|
679
|
-
}
|
|
735
|
+
const lastIsToolResultUser = last?.role === "user" && Array.isArray(last.content) && last.content.every((b) => b.type === "tool_result");
|
|
736
|
+
if (lastIsToolResultUser && Array.isArray(last.content)) {
|
|
737
|
+
last.content.push(resultBlock);
|
|
680
738
|
} else {
|
|
681
|
-
messages.push({ role: "user", content });
|
|
739
|
+
messages.push({ role: "user", content: [resultBlock], ts: e.ts });
|
|
682
740
|
}
|
|
683
741
|
}
|
|
684
742
|
}
|
|
@@ -698,7 +756,24 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
698
756
|
return { messages: repaired.messages, usage };
|
|
699
757
|
}
|
|
700
758
|
};
|
|
701
|
-
|
|
759
|
+
function extractToolCallEnds(events) {
|
|
760
|
+
const result = [];
|
|
761
|
+
for (const e of events) {
|
|
762
|
+
if (e.type === "tool_call_end") {
|
|
763
|
+
result.push({
|
|
764
|
+
name: e.name,
|
|
765
|
+
id: e.id,
|
|
766
|
+
durationMs: e.durationMs,
|
|
767
|
+
ok: e.ok ?? false,
|
|
768
|
+
outputBytes: e.outputBytes,
|
|
769
|
+
outputTokens: e.outputTokens,
|
|
770
|
+
outputLines: e.outputLines
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return result;
|
|
775
|
+
}
|
|
776
|
+
var FileSessionWriter = class _FileSessionWriter {
|
|
702
777
|
constructor(id, handle, startedAt, meta, events, opts = {}) {
|
|
703
778
|
this.id = id;
|
|
704
779
|
this.handle = handle;
|
|
@@ -725,7 +800,7 @@ var FileSessionWriter = class {
|
|
|
725
800
|
meta;
|
|
726
801
|
events;
|
|
727
802
|
closed = false;
|
|
728
|
-
|
|
803
|
+
closePromise = null;
|
|
729
804
|
manifestFile;
|
|
730
805
|
summary;
|
|
731
806
|
tokenIn = 0;
|
|
@@ -734,12 +809,51 @@ var FileSessionWriter = class {
|
|
|
734
809
|
get transcriptPath() {
|
|
735
810
|
return this.filePath || void 0;
|
|
736
811
|
}
|
|
737
|
-
|
|
812
|
+
/**
|
|
813
|
+
* Lazy session_start/session_resumed init, shared by all appenders.
|
|
814
|
+
* A single promise (not a boolean) so a second append racing the first
|
|
815
|
+
* can't push its event into the buffer BEFORE the first append's event —
|
|
816
|
+
* every appender awaits the same init and resumes in FIFO call order.
|
|
817
|
+
*/
|
|
818
|
+
initPromise = null;
|
|
819
|
+
ensureInit() {
|
|
820
|
+
if (!this.initPromise) this.initPromise = this.writeSessionStartLazy();
|
|
821
|
+
return this.initPromise;
|
|
822
|
+
}
|
|
738
823
|
resumed;
|
|
739
824
|
appendFailCount = 0;
|
|
740
825
|
lastAppendWarnAt = 0;
|
|
741
826
|
secretScrubber;
|
|
742
827
|
onCloseCb;
|
|
828
|
+
// ── Write buffer — batches events to reduce per-event disk I/O ─────────
|
|
829
|
+
//
|
|
830
|
+
// Every append() pushes the scrubbed event into an in-memory buffer instead
|
|
831
|
+
// of calling handle.appendFile() synchronously. The buffer flushes to disk
|
|
832
|
+
// when it reaches FLUSH_SIZE events OR after FLUSH_INTERVAL_MS of inactivity.
|
|
833
|
+
// This cuts the number of disk writes by ~95% without changing the on-disk
|
|
834
|
+
// format — the JSONL is still one JSON object per line.
|
|
835
|
+
writeBuffer = [];
|
|
836
|
+
flushTimer = null;
|
|
837
|
+
static FLUSH_INTERVAL_MS = 500;
|
|
838
|
+
static FLUSH_SIZE = 50;
|
|
839
|
+
// ── Write serialization ─────────────────────────────────────────────────
|
|
840
|
+
//
|
|
841
|
+
// All disk writes are funneled through a FIFO promise chain. Without it,
|
|
842
|
+
// a timer-driven flush racing an explicit flush()/close() issues two
|
|
843
|
+
// concurrent appendFile() calls on the shared O_APPEND handle — the kernel
|
|
844
|
+
// may complete them out of order (chronology breaks) or, for large
|
|
845
|
+
// batches, interleave partial writes (torn JSONL lines). The chain keeps
|
|
846
|
+
// exactly one write in flight; failures don't break the chain.
|
|
847
|
+
writeChain = Promise.resolve();
|
|
848
|
+
/** Enqueue a write on the FIFO chain. Resolves/rejects with that write. */
|
|
849
|
+
enqueueWrite(data) {
|
|
850
|
+
const write = this.writeChain.then(() => this.handle.appendFile(data, "utf8"));
|
|
851
|
+
this.writeChain = write.then(
|
|
852
|
+
() => void 0,
|
|
853
|
+
() => void 0
|
|
854
|
+
);
|
|
855
|
+
return write;
|
|
856
|
+
}
|
|
743
857
|
// ── Enriched summary tracking ──────────────────────────────────────────
|
|
744
858
|
iterationCount = 0;
|
|
745
859
|
toolCallCount = 0;
|
|
@@ -789,31 +903,91 @@ var FileSessionWriter = class {
|
|
|
789
903
|
})}
|
|
790
904
|
`;
|
|
791
905
|
try {
|
|
792
|
-
|
|
793
|
-
await fsp.writeFile(this.filePath, record, { flag: "a", mode: 384 });
|
|
794
|
-
}
|
|
906
|
+
await this.enqueueWrite(record);
|
|
795
907
|
} catch {
|
|
796
908
|
}
|
|
797
909
|
}
|
|
798
910
|
async append(event) {
|
|
799
911
|
if (this.closed) return;
|
|
800
|
-
|
|
801
|
-
this.initDone = true;
|
|
802
|
-
await this.writeSessionStartLazy();
|
|
803
|
-
}
|
|
912
|
+
await this.ensureInit();
|
|
804
913
|
const scrubbed = this.scrubEvent(event);
|
|
805
914
|
this.observeForSummary(scrubbed);
|
|
915
|
+
this.writeBuffer.push(scrubbed);
|
|
916
|
+
if (this.writeBuffer.length >= _FileSessionWriter.FLUSH_SIZE) {
|
|
917
|
+
if (this.flushTimer) {
|
|
918
|
+
clearTimeout(this.flushTimer);
|
|
919
|
+
this.flushTimer = null;
|
|
920
|
+
}
|
|
921
|
+
await this.flushBuffer();
|
|
922
|
+
} else {
|
|
923
|
+
this.scheduleFlush();
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
async appendBatch(events) {
|
|
927
|
+
if (this.closed || events.length === 0) return;
|
|
928
|
+
await this.ensureInit();
|
|
929
|
+
for (const event of events) {
|
|
930
|
+
const scrubbed = this.scrubEvent(event);
|
|
931
|
+
this.observeForSummary(scrubbed);
|
|
932
|
+
this.writeBuffer.push(scrubbed);
|
|
933
|
+
}
|
|
934
|
+
if (this.writeBuffer.length >= _FileSessionWriter.FLUSH_SIZE) {
|
|
935
|
+
if (this.flushTimer) {
|
|
936
|
+
clearTimeout(this.flushTimer);
|
|
937
|
+
this.flushTimer = null;
|
|
938
|
+
}
|
|
939
|
+
await this.flushBuffer();
|
|
940
|
+
} else {
|
|
941
|
+
this.scheduleFlush();
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Flush buffered events to disk immediately. Critical events
|
|
946
|
+
* (user_input, llm_response) call this so they survive SIGKILL/crash
|
|
947
|
+
* instead of sitting in the in-memory buffer for up to 500ms.
|
|
948
|
+
*
|
|
949
|
+
* Idempotent — cancels any pending timer and writes whatever has
|
|
950
|
+
* accumulated in the buffer. Safe to call even when the buffer
|
|
951
|
+
* is empty (no-op).
|
|
952
|
+
*/
|
|
953
|
+
async flush() {
|
|
954
|
+
if (this.flushTimer) {
|
|
955
|
+
clearTimeout(this.flushTimer);
|
|
956
|
+
this.flushTimer = null;
|
|
957
|
+
}
|
|
958
|
+
await this.flushBuffer();
|
|
959
|
+
}
|
|
960
|
+
/** Schedule a deferred flush. No-op if a timer is already pending. */
|
|
961
|
+
scheduleFlush() {
|
|
962
|
+
if (this.flushTimer) return;
|
|
963
|
+
this.flushTimer = setTimeout(() => {
|
|
964
|
+
this.flushTimer = null;
|
|
965
|
+
this.flushBuffer().catch(() => {
|
|
966
|
+
});
|
|
967
|
+
}, _FileSessionWriter.FLUSH_INTERVAL_MS);
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Flush all buffered events to disk as a single appendFile call.
|
|
971
|
+
* Errors use the same throttled-warning pattern the old per-event
|
|
972
|
+
* append path used — one warning every 5s with a suppressed count.
|
|
973
|
+
* On failure the buffer is cleared (events are best-effort, same as
|
|
974
|
+
* the old per-event path where a failed write was silently dropped).
|
|
975
|
+
*/
|
|
976
|
+
async flushBuffer() {
|
|
977
|
+
if (this.writeBuffer.length === 0) return;
|
|
978
|
+
const eventCount = this.writeBuffer.length;
|
|
979
|
+
const batch = this.writeBuffer.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
980
|
+
this.writeBuffer = [];
|
|
806
981
|
try {
|
|
807
|
-
await this.
|
|
808
|
-
`, "utf8");
|
|
982
|
+
await this.enqueueWrite(batch);
|
|
809
983
|
} catch (err) {
|
|
810
|
-
this.appendFailCount
|
|
984
|
+
this.appendFailCount += eventCount;
|
|
811
985
|
const now = Date.now();
|
|
812
986
|
if (now - this.lastAppendWarnAt > 5e3) {
|
|
813
987
|
const suppressed = this.appendFailCount - 1;
|
|
814
988
|
const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
|
|
815
989
|
console.warn(
|
|
816
|
-
"[session]
|
|
990
|
+
"[session] flush failed:",
|
|
817
991
|
err instanceof Error ? err.message : String(err),
|
|
818
992
|
tail
|
|
819
993
|
);
|
|
@@ -823,6 +997,11 @@ var FileSessionWriter = class {
|
|
|
823
997
|
}
|
|
824
998
|
}
|
|
825
999
|
observeForSummary(event) {
|
|
1000
|
+
if (event.type === "llm_response") {
|
|
1001
|
+
for (const block of event.content) {
|
|
1002
|
+
if (block.type === "tool_use") this.openToolUses.add(block.id);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
826
1005
|
if (event.type === "tool_use") {
|
|
827
1006
|
this.openToolUses.add(event.id);
|
|
828
1007
|
} else if (event.type === "tool_call_start") {
|
|
@@ -856,9 +1035,18 @@ var FileSessionWriter = class {
|
|
|
856
1035
|
}
|
|
857
1036
|
}
|
|
858
1037
|
async close() {
|
|
859
|
-
if (this.
|
|
860
|
-
this.
|
|
1038
|
+
if (this.closePromise) return this.closePromise;
|
|
1039
|
+
this.closePromise = this.doClose();
|
|
1040
|
+
return this.closePromise;
|
|
1041
|
+
}
|
|
1042
|
+
async doClose() {
|
|
861
1043
|
this.closed = true;
|
|
1044
|
+
if (this.flushTimer) {
|
|
1045
|
+
clearTimeout(this.flushTimer);
|
|
1046
|
+
this.flushTimer = null;
|
|
1047
|
+
}
|
|
1048
|
+
await this.flushBuffer();
|
|
1049
|
+
await this.writeChain;
|
|
862
1050
|
this.summary = {
|
|
863
1051
|
...this.summary,
|
|
864
1052
|
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -914,6 +1102,12 @@ var FileSessionWriter = class {
|
|
|
914
1102
|
}
|
|
915
1103
|
async truncateToCheckpoint(targetPromptIndex) {
|
|
916
1104
|
if (!this.filePath) return 0;
|
|
1105
|
+
if (this.flushTimer) {
|
|
1106
|
+
clearTimeout(this.flushTimer);
|
|
1107
|
+
this.flushTimer = null;
|
|
1108
|
+
}
|
|
1109
|
+
await this.flushBuffer();
|
|
1110
|
+
await this.writeChain;
|
|
917
1111
|
const raw = await fsp.readFile(this.filePath, "utf8");
|
|
918
1112
|
const lines = raw.split("\n");
|
|
919
1113
|
const kept = [];
|
|
@@ -976,6 +1170,12 @@ var FileSessionWriter = class {
|
|
|
976
1170
|
}
|
|
977
1171
|
async clearSession() {
|
|
978
1172
|
if (!this.filePath) return;
|
|
1173
|
+
if (this.flushTimer) {
|
|
1174
|
+
clearTimeout(this.flushTimer);
|
|
1175
|
+
this.flushTimer = null;
|
|
1176
|
+
}
|
|
1177
|
+
this.writeBuffer = [];
|
|
1178
|
+
await this.writeChain;
|
|
979
1179
|
const record = `${JSON.stringify({
|
|
980
1180
|
type: "session_start",
|
|
981
1181
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1042,7 +1242,13 @@ var QueueStore = class {
|
|
|
1042
1242
|
} catch (err) {
|
|
1043
1243
|
const code = err.code;
|
|
1044
1244
|
if (code === "ENOENT") return [];
|
|
1045
|
-
console.warn(
|
|
1245
|
+
console.warn(JSON.stringify({
|
|
1246
|
+
level: "warn",
|
|
1247
|
+
event: "queue_store.read_failed",
|
|
1248
|
+
path: this.file,
|
|
1249
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1250
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1251
|
+
}));
|
|
1046
1252
|
return [];
|
|
1047
1253
|
}
|
|
1048
1254
|
let parsed;
|
|
@@ -1064,7 +1270,13 @@ var QueueStore = class {
|
|
|
1064
1270
|
} catch (err) {
|
|
1065
1271
|
const code = err.code;
|
|
1066
1272
|
if (code === "ENOENT") return;
|
|
1067
|
-
console.warn(
|
|
1273
|
+
console.warn(JSON.stringify({
|
|
1274
|
+
level: "warn",
|
|
1275
|
+
event: "queue_store.clear_failed",
|
|
1276
|
+
path: this.file,
|
|
1277
|
+
message: err.message,
|
|
1278
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1279
|
+
}));
|
|
1068
1280
|
}
|
|
1069
1281
|
}
|
|
1070
1282
|
};
|
|
@@ -1315,7 +1527,7 @@ var FileMemoryBackend = class {
|
|
|
1315
1527
|
const line = `
|
|
1316
1528
|
- [${entry.ts}] ${id}${meta} ${entry.text.replace(/\n/g, " ")}
|
|
1317
1529
|
`;
|
|
1318
|
-
const next = existing.trim() ? existing.replace(/\n+$/, "") + line : `#
|
|
1530
|
+
const next = existing.trim() ? existing.replace(/\n+$/, "") + line : `# Agent Memory
|
|
1319
1531
|
${line}`;
|
|
1320
1532
|
await atomicWrite(file, next);
|
|
1321
1533
|
}
|
|
@@ -1483,10 +1695,9 @@ var DefaultMemoryStore = class {
|
|
|
1483
1695
|
}
|
|
1484
1696
|
async runSerialized(scope, work) {
|
|
1485
1697
|
const prior = this.writeChain.get(scope) ?? Promise.resolve();
|
|
1486
|
-
prior.catch((err) => {
|
|
1698
|
+
const next = prior.catch((err) => {
|
|
1487
1699
|
this.writeErrors.set(scope, err);
|
|
1488
|
-
});
|
|
1489
|
-
const next = prior.catch(() => void 0).then(work);
|
|
1700
|
+
}).then(() => work());
|
|
1490
1701
|
this.writeChain.set(scope, next);
|
|
1491
1702
|
try {
|
|
1492
1703
|
return await next;
|
|
@@ -1716,9 +1927,9 @@ ${body.trim()}`);
|
|
|
1716
1927
|
if (!this.persistBackup || scope === "project-agents") return;
|
|
1717
1928
|
try {
|
|
1718
1929
|
const content = await this.backend.readAll(scope, this.files[scope]);
|
|
1719
|
-
const { writeFile:
|
|
1720
|
-
await
|
|
1721
|
-
await
|
|
1930
|
+
const { writeFile: writeFile7, mkdir: mkdir7 } = await import('fs/promises');
|
|
1931
|
+
await mkdir7(this.backupDir, { recursive: true });
|
|
1932
|
+
await writeFile7(`${this.backupDir}/${scope}.md`, content, "utf8");
|
|
1722
1933
|
} catch {
|
|
1723
1934
|
}
|
|
1724
1935
|
}
|
|
@@ -2112,6 +2323,97 @@ var SessionMemoryConsolidator = class {
|
|
|
2112
2323
|
};
|
|
2113
2324
|
};
|
|
2114
2325
|
|
|
2326
|
+
// src/types/errors.ts
|
|
2327
|
+
var ERROR_CODES = {
|
|
2328
|
+
// Config
|
|
2329
|
+
CONFIG_INVALID: "CONFIG_INVALID",
|
|
2330
|
+
// Session
|
|
2331
|
+
SESSION_NOT_FOUND: "SESSION_NOT_FOUND",
|
|
2332
|
+
SESSION_CORRUPTED: "SESSION_CORRUPTED",
|
|
2333
|
+
SESSION_WRITE_FAILED: "SESSION_WRITE_FAILED",
|
|
2334
|
+
// File system
|
|
2335
|
+
FS_READ_FAILED: "FS_READ_FAILED",
|
|
2336
|
+
FS_DELETE_FAILED: "FS_DELETE_FAILED",
|
|
2337
|
+
FS_ATOMIC_WRITE_FAILED: "FS_ATOMIC_WRITE_FAILED",
|
|
2338
|
+
// General
|
|
2339
|
+
VALIDATION_ERROR: "VALIDATION_ERROR",
|
|
2340
|
+
UNKNOWN: "UNKNOWN"
|
|
2341
|
+
};
|
|
2342
|
+
var WrongStackError = class extends Error {
|
|
2343
|
+
code;
|
|
2344
|
+
subsystem;
|
|
2345
|
+
severity;
|
|
2346
|
+
recoverable;
|
|
2347
|
+
context;
|
|
2348
|
+
constructor(opts) {
|
|
2349
|
+
super(opts.message, { cause: opts.cause });
|
|
2350
|
+
this.name = "WrongStackError";
|
|
2351
|
+
this.code = opts.code;
|
|
2352
|
+
this.subsystem = opts.subsystem;
|
|
2353
|
+
this.severity = opts.severity ?? "error";
|
|
2354
|
+
this.recoverable = opts.recoverable ?? false;
|
|
2355
|
+
this.context = opts.context;
|
|
2356
|
+
}
|
|
2357
|
+
/**
|
|
2358
|
+
* Render a one-line user-facing description.
|
|
2359
|
+
* Subclasses should override for domain-specific formatting.
|
|
2360
|
+
*/
|
|
2361
|
+
describe() {
|
|
2362
|
+
const ctx = this.context ? ` ${formatContext(this.context)}` : "";
|
|
2363
|
+
return `${this.code}: ${this.message}${ctx}`;
|
|
2364
|
+
}
|
|
2365
|
+
};
|
|
2366
|
+
function formatContext(ctx) {
|
|
2367
|
+
const parts = Object.entries(ctx).filter(([, v]) => v !== void 0).slice(0, 3).map(([k, v]) => `${k}=${String(v)}`);
|
|
2368
|
+
return parts.length > 0 ? `[${parts.join(" ")}]` : "";
|
|
2369
|
+
}
|
|
2370
|
+
var ConfigError = class extends WrongStackError {
|
|
2371
|
+
constructor(opts) {
|
|
2372
|
+
super({
|
|
2373
|
+
message: opts.message,
|
|
2374
|
+
code: opts.code,
|
|
2375
|
+
subsystem: "config",
|
|
2376
|
+
severity: "fatal",
|
|
2377
|
+
recoverable: false,
|
|
2378
|
+
context: opts.context,
|
|
2379
|
+
cause: opts.cause
|
|
2380
|
+
});
|
|
2381
|
+
this.name = "ConfigError";
|
|
2382
|
+
}
|
|
2383
|
+
};
|
|
2384
|
+
var SessionError = class extends WrongStackError {
|
|
2385
|
+
sessionId;
|
|
2386
|
+
constructor(opts) {
|
|
2387
|
+
super({
|
|
2388
|
+
message: opts.message,
|
|
2389
|
+
code: opts.code,
|
|
2390
|
+
subsystem: "session",
|
|
2391
|
+
severity: opts.code === ERROR_CODES.SESSION_WRITE_FAILED ? "error" : "warning",
|
|
2392
|
+
recoverable: opts.code !== ERROR_CODES.SESSION_CORRUPTED,
|
|
2393
|
+
context: { sessionId: opts.sessionId, ...opts.context },
|
|
2394
|
+
cause: opts.cause
|
|
2395
|
+
});
|
|
2396
|
+
this.name = "SessionError";
|
|
2397
|
+
this.sessionId = opts.sessionId;
|
|
2398
|
+
}
|
|
2399
|
+
};
|
|
2400
|
+
var FsError = class extends WrongStackError {
|
|
2401
|
+
path;
|
|
2402
|
+
constructor(opts) {
|
|
2403
|
+
super({
|
|
2404
|
+
message: opts.message,
|
|
2405
|
+
code: opts.code,
|
|
2406
|
+
subsystem: "fs",
|
|
2407
|
+
severity: "error",
|
|
2408
|
+
recoverable: opts.code !== ERROR_CODES.FS_READ_FAILED,
|
|
2409
|
+
context: { path: opts.path, ...opts.context },
|
|
2410
|
+
cause: opts.cause
|
|
2411
|
+
});
|
|
2412
|
+
this.name = "FsError";
|
|
2413
|
+
this.path = opts.path;
|
|
2414
|
+
}
|
|
2415
|
+
};
|
|
2416
|
+
|
|
2115
2417
|
// src/storage/config-store.ts
|
|
2116
2418
|
function stripEphemeralFields(cfg) {
|
|
2117
2419
|
const env = cfg._envSource;
|
|
@@ -2143,7 +2445,11 @@ var DefaultConfigStore = class {
|
|
|
2143
2445
|
const scrubbed = stripEphemeralFields(partial);
|
|
2144
2446
|
const next = deepFreeze(structuredClone({ ...this.current, ...scrubbed }));
|
|
2145
2447
|
if (next.version !== 1) {
|
|
2146
|
-
throw new
|
|
2448
|
+
throw new ConfigError({
|
|
2449
|
+
message: `ConfigStore.update: version must remain 1, got ${String(next.version)}`,
|
|
2450
|
+
code: ERROR_CODES.CONFIG_INVALID,
|
|
2451
|
+
context: { field: "version", actual: next.version }
|
|
2452
|
+
});
|
|
2147
2453
|
}
|
|
2148
2454
|
const prev = this.current;
|
|
2149
2455
|
this.current = next;
|
|
@@ -2151,7 +2457,12 @@ var DefaultConfigStore = class {
|
|
|
2151
2457
|
try {
|
|
2152
2458
|
w(next, prev);
|
|
2153
2459
|
} catch (err) {
|
|
2154
|
-
console.error(
|
|
2460
|
+
console.error(JSON.stringify({
|
|
2461
|
+
level: "error",
|
|
2462
|
+
event: "config_store.watcher_threw",
|
|
2463
|
+
message: err instanceof Error ? err.message : String(err),
|
|
2464
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2465
|
+
}));
|
|
2155
2466
|
}
|
|
2156
2467
|
}
|
|
2157
2468
|
return next;
|
|
@@ -2173,6 +2484,67 @@ function deepFreeze(obj) {
|
|
|
2173
2484
|
}
|
|
2174
2485
|
return Object.freeze(obj);
|
|
2175
2486
|
}
|
|
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
|
|
2176
2548
|
function decryptConfigSecrets(cfg, vault, opts) {
|
|
2177
2549
|
const warn = ((msg) => console.warn(msg));
|
|
2178
2550
|
return walk(cfg, vault, (v, key) => {
|
|
@@ -2387,43 +2759,16 @@ var defaultIndexing = {
|
|
|
2387
2759
|
watchExternal: true,
|
|
2388
2760
|
debounceMs: 400
|
|
2389
2761
|
};
|
|
2390
|
-
function
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
"__defineSetter__",
|
|
2399
|
-
"__lookupGetter__",
|
|
2400
|
-
"__lookupSetter__"
|
|
2401
|
-
]);
|
|
2402
|
-
function deepMerge(base, patch) {
|
|
2403
|
-
if (typeof base !== "object" || base === null) return patch ?? base;
|
|
2404
|
-
if (typeof patch !== "object" || patch === null) return base;
|
|
2405
|
-
const out = { ...base };
|
|
2406
|
-
for (const [k, v] of Object.entries(patch)) {
|
|
2407
|
-
if (FORBIDDEN_PROTO_KEYS.has(k)) continue;
|
|
2408
|
-
const existing = out[k];
|
|
2409
|
-
if (Array.isArray(v)) {
|
|
2410
|
-
if (Array.isArray(existing) && isPrimitiveArray(v) && isPrimitiveArray(existing)) {
|
|
2411
|
-
out[k] = [.../* @__PURE__ */ new Set([...existing, ...v])];
|
|
2412
|
-
} else {
|
|
2413
|
-
out[k] = v;
|
|
2414
|
-
if (envBoolOptional(process.env.WRONGSTACK_DEBUG_CONFIG)) {
|
|
2415
|
-
console.warn(
|
|
2416
|
-
`[config] Non-primitive array for "${k}" replaced (global + local config merge). Global entries: ${existing?.length ?? 0}, local entries: ${v.length}.`
|
|
2417
|
-
);
|
|
2418
|
-
}
|
|
2419
|
-
}
|
|
2420
|
-
} else if (typeof v === "object" && v !== null && typeof existing === "object" && existing !== null) {
|
|
2421
|
-
out[k] = deepMerge(existing, v);
|
|
2422
|
-
} else if (v !== void 0) {
|
|
2423
|
-
out[k] = v;
|
|
2424
|
-
}
|
|
2762
|
+
function deepMerge2(base, patch) {
|
|
2763
|
+
const opts = { arrayMode: "concat-primitives" };
|
|
2764
|
+
if (envBoolOptional(process.env.WRONGSTACK_DEBUG_CONFIG)) {
|
|
2765
|
+
opts.onNonPrimitiveArrayReplace = (key, existingLen, patchLen) => {
|
|
2766
|
+
console.warn(
|
|
2767
|
+
`[config] Non-primitive array for "${key}" replaced (global + local config merge). Global entries: ${existingLen}, local entries: ${patchLen}.`
|
|
2768
|
+
);
|
|
2769
|
+
};
|
|
2425
2770
|
}
|
|
2426
|
-
return
|
|
2771
|
+
return deepMerge(base, patch, opts);
|
|
2427
2772
|
}
|
|
2428
2773
|
var DefaultConfigLoader = class {
|
|
2429
2774
|
paths;
|
|
@@ -2443,9 +2788,9 @@ var DefaultConfigLoader = class {
|
|
|
2443
2788
|
this.readJson(this.paths.projectLocalConfig),
|
|
2444
2789
|
this.readJson(this.paths.inProjectConfig)
|
|
2445
2790
|
]);
|
|
2446
|
-
cfg =
|
|
2447
|
-
cfg =
|
|
2448
|
-
cfg =
|
|
2791
|
+
cfg = deepMerge2(cfg, global);
|
|
2792
|
+
cfg = deepMerge2(cfg, local);
|
|
2793
|
+
cfg = deepMerge2(cfg, inProject);
|
|
2449
2794
|
for (const [key, fn] of Object.entries(ENV_MAP)) {
|
|
2450
2795
|
const v = process.env[key];
|
|
2451
2796
|
if (v) fn(cfg, v);
|
|
@@ -2459,14 +2804,20 @@ var DefaultConfigLoader = class {
|
|
|
2459
2804
|
try {
|
|
2460
2805
|
const patch = await src.read();
|
|
2461
2806
|
if (patch && Object.keys(patch).length > 0) {
|
|
2462
|
-
cfg =
|
|
2807
|
+
cfg = deepMerge2(cfg, patch);
|
|
2463
2808
|
}
|
|
2464
2809
|
} catch (err) {
|
|
2465
|
-
console.warn(
|
|
2810
|
+
console.warn(JSON.stringify({
|
|
2811
|
+
level: "warn",
|
|
2812
|
+
event: "config.source_load_failed",
|
|
2813
|
+
source: src.name,
|
|
2814
|
+
message: err instanceof Error ? err.message : String(err),
|
|
2815
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2816
|
+
}));
|
|
2466
2817
|
}
|
|
2467
2818
|
}
|
|
2468
2819
|
if (opts.cliFlags) {
|
|
2469
|
-
cfg =
|
|
2820
|
+
cfg = deepMerge2(cfg, opts.cliFlags);
|
|
2470
2821
|
}
|
|
2471
2822
|
if (this.vault) {
|
|
2472
2823
|
cfg = decryptConfigSecrets(cfg, this.vault);
|
|
@@ -2525,7 +2876,12 @@ var DefaultConfigLoader = class {
|
|
|
2525
2876
|
return parsed.value;
|
|
2526
2877
|
} catch (err) {
|
|
2527
2878
|
if (err.code === "ENOENT") return null;
|
|
2528
|
-
console.warn(
|
|
2879
|
+
console.warn(JSON.stringify({
|
|
2880
|
+
level: "warn",
|
|
2881
|
+
event: "config.sync_load_failed",
|
|
2882
|
+
message: err instanceof Error ? err.message : String(err),
|
|
2883
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2884
|
+
}));
|
|
2529
2885
|
return null;
|
|
2530
2886
|
}
|
|
2531
2887
|
}
|
|
@@ -2535,33 +2891,63 @@ var DefaultConfigLoader = class {
|
|
|
2535
2891
|
raw = await fsp.readFile(file, "utf8");
|
|
2536
2892
|
} catch (err) {
|
|
2537
2893
|
if (err.code !== "ENOENT") {
|
|
2538
|
-
console.warn(
|
|
2894
|
+
console.warn(JSON.stringify({
|
|
2895
|
+
level: "warn",
|
|
2896
|
+
event: "config.read_failed",
|
|
2897
|
+
path: file,
|
|
2898
|
+
message: err instanceof Error ? err.message : String(err),
|
|
2899
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2900
|
+
}));
|
|
2539
2901
|
}
|
|
2540
2902
|
return {};
|
|
2541
2903
|
}
|
|
2542
2904
|
const parsed = safeParse(raw);
|
|
2543
2905
|
if (!parsed.ok || !parsed.value) {
|
|
2544
|
-
console.warn(
|
|
2545
|
-
|
|
2546
|
-
|
|
2906
|
+
console.warn(JSON.stringify({
|
|
2907
|
+
level: "warn",
|
|
2908
|
+
event: "config.parse_failed",
|
|
2909
|
+
path: file,
|
|
2910
|
+
message: "invalid JSON \u2014 falling back to defaults for this layer",
|
|
2911
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2912
|
+
}));
|
|
2547
2913
|
return {};
|
|
2548
2914
|
}
|
|
2549
2915
|
return parsed.value;
|
|
2550
2916
|
}
|
|
2551
2917
|
validateBehavior(cfg) {
|
|
2552
|
-
if (cfg.version === void 0) throw new
|
|
2553
|
-
|
|
2918
|
+
if (cfg.version === void 0) throw new ConfigError({
|
|
2919
|
+
message: "Config: missing version field",
|
|
2920
|
+
code: ERROR_CODES.CONFIG_INVALID,
|
|
2921
|
+
context: { field: "version" }
|
|
2922
|
+
});
|
|
2923
|
+
if (cfg.version !== 1) throw new ConfigError({
|
|
2924
|
+
message: `Config: unsupported version ${cfg.version}`,
|
|
2925
|
+
code: ERROR_CODES.CONFIG_INVALID,
|
|
2926
|
+
context: { field: "version", actual: cfg.version }
|
|
2927
|
+
});
|
|
2554
2928
|
const c = cfg.context;
|
|
2555
|
-
if (!c) throw new
|
|
2929
|
+
if (!c) throw new ConfigError({
|
|
2930
|
+
message: "Config: missing context section",
|
|
2931
|
+
code: ERROR_CODES.CONFIG_INVALID,
|
|
2932
|
+
context: { field: "context" }
|
|
2933
|
+
});
|
|
2556
2934
|
const fields = ["warnThreshold", "softThreshold", "hardThreshold"];
|
|
2557
2935
|
for (const f of fields) {
|
|
2558
2936
|
const v = c[f];
|
|
2559
2937
|
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
2560
|
-
throw new
|
|
2938
|
+
throw new ConfigError({
|
|
2939
|
+
message: `Config: context.${String(f)} must be a finite number (got ${typeof v})`,
|
|
2940
|
+
code: ERROR_CODES.CONFIG_INVALID,
|
|
2941
|
+
context: { field: `context.${String(f)}`, actualType: typeof v }
|
|
2942
|
+
});
|
|
2561
2943
|
}
|
|
2562
2944
|
}
|
|
2563
2945
|
if (c.warnThreshold >= c.softThreshold || c.softThreshold >= c.hardThreshold) {
|
|
2564
|
-
throw new
|
|
2946
|
+
throw new ConfigError({
|
|
2947
|
+
message: "Config: context thresholds must satisfy warn < soft < hard",
|
|
2948
|
+
code: ERROR_CODES.CONFIG_INVALID,
|
|
2949
|
+
context: { warn: c.warnThreshold, soft: c.softThreshold, hard: c.hardThreshold }
|
|
2950
|
+
});
|
|
2565
2951
|
}
|
|
2566
2952
|
if (c.mode !== void 0 && !isContextWindowModeId(c.mode)) {
|
|
2567
2953
|
const known = listContextWindowModes().map((m) => m.id).join(", ");
|
|
@@ -2573,12 +2959,18 @@ var DefaultConfigLoader = class {
|
|
|
2573
2959
|
}
|
|
2574
2960
|
validateIdentity(cfg) {
|
|
2575
2961
|
if (!cfg.provider) {
|
|
2576
|
-
throw new
|
|
2577
|
-
"Config: no provider configured. Run `wstack init` or set WRONGSTACK_PROVIDER."
|
|
2578
|
-
|
|
2962
|
+
throw new ConfigError({
|
|
2963
|
+
message: "Config: no provider configured. Run `wstack init` or set WRONGSTACK_PROVIDER.",
|
|
2964
|
+
code: ERROR_CODES.CONFIG_INVALID,
|
|
2965
|
+
context: { field: "provider" }
|
|
2966
|
+
});
|
|
2579
2967
|
}
|
|
2580
2968
|
if (!cfg.model) {
|
|
2581
|
-
throw new
|
|
2969
|
+
throw new ConfigError({
|
|
2970
|
+
message: "Config: no model configured. Run `wstack init` or set WRONGSTACK_MODEL.",
|
|
2971
|
+
code: ERROR_CODES.CONFIG_INVALID,
|
|
2972
|
+
context: { field: "model" }
|
|
2973
|
+
});
|
|
2582
2974
|
}
|
|
2583
2975
|
}
|
|
2584
2976
|
};
|
|
@@ -2676,7 +3068,10 @@ var RecoveryLock = class {
|
|
|
2676
3068
|
if (this.sessionStore) {
|
|
2677
3069
|
try {
|
|
2678
3070
|
const data = await this.sessionStore.load(lock.sessionId);
|
|
2679
|
-
const
|
|
3071
|
+
const lastEnd = data.events.findLastIndex((e) => e.type === "session_end");
|
|
3072
|
+
const closed = lastEnd >= 0 && !data.events.slice(lastEnd + 1).some(
|
|
3073
|
+
(e) => e.type === "user_input" || e.type === "llm_response" || e.type === "in_flight_start"
|
|
3074
|
+
);
|
|
2680
3075
|
if (closed) return null;
|
|
2681
3076
|
messageCount = data.messages.length;
|
|
2682
3077
|
} catch {
|
|
@@ -3080,6 +3475,27 @@ function renderPlainText(meta, events) {
|
|
|
3080
3475
|
}
|
|
3081
3476
|
return lines.join("\n");
|
|
3082
3477
|
}
|
|
3478
|
+
function sessionScopedPath(dir, sessionId, suffix) {
|
|
3479
|
+
if (!sessionId || sessionId.includes("\\") || sessionId.includes("..")) {
|
|
3480
|
+
throw invalid(sessionId);
|
|
3481
|
+
}
|
|
3482
|
+
const resolved = path13.resolve(dir, `${sessionId}${suffix}`);
|
|
3483
|
+
const rel = path13.relative(path13.resolve(dir), resolved);
|
|
3484
|
+
if (rel.startsWith("..") || path13.isAbsolute(rel)) {
|
|
3485
|
+
throw invalid(sessionId);
|
|
3486
|
+
}
|
|
3487
|
+
return resolved;
|
|
3488
|
+
}
|
|
3489
|
+
function invalid(sessionId) {
|
|
3490
|
+
return new FsError({
|
|
3491
|
+
message: `Invalid sessionId: ${sessionId}`,
|
|
3492
|
+
code: ERROR_CODES.FS_DELETE_FAILED,
|
|
3493
|
+
path: sessionId,
|
|
3494
|
+
context: { reason: "path_traversal" }
|
|
3495
|
+
});
|
|
3496
|
+
}
|
|
3497
|
+
|
|
3498
|
+
// src/storage/annotations-store.ts
|
|
3083
3499
|
var FILE_VERSION = 1;
|
|
3084
3500
|
var MAX_TEXT_LENGTH = 2e3;
|
|
3085
3501
|
var MAX_ANNOTATIONS = 1e3;
|
|
@@ -3117,13 +3533,28 @@ var AnnotationsStore = class {
|
|
|
3117
3533
|
async add(input) {
|
|
3118
3534
|
const text = input.text.trim();
|
|
3119
3535
|
if (text.length === 0) {
|
|
3120
|
-
throw new
|
|
3536
|
+
throw new WrongStackError({
|
|
3537
|
+
message: "Annotation text must be non-empty",
|
|
3538
|
+
code: ERROR_CODES.VALIDATION_ERROR,
|
|
3539
|
+
subsystem: "general",
|
|
3540
|
+
context: { field: "text", sessionId: input.sessionId }
|
|
3541
|
+
});
|
|
3121
3542
|
}
|
|
3122
3543
|
if (text.length > MAX_TEXT_LENGTH) {
|
|
3123
|
-
throw new
|
|
3544
|
+
throw new WrongStackError({
|
|
3545
|
+
message: `Annotation text exceeds ${MAX_TEXT_LENGTH} chars (got ${text.length})`,
|
|
3546
|
+
code: ERROR_CODES.VALIDATION_ERROR,
|
|
3547
|
+
subsystem: "general",
|
|
3548
|
+
context: { field: "text", maxLength: MAX_TEXT_LENGTH, actualLength: text.length }
|
|
3549
|
+
});
|
|
3124
3550
|
}
|
|
3125
3551
|
if (!Number.isInteger(input.atEventIndex) || input.atEventIndex < 0) {
|
|
3126
|
-
throw new
|
|
3552
|
+
throw new WrongStackError({
|
|
3553
|
+
message: "atEventIndex must be a non-negative integer",
|
|
3554
|
+
code: ERROR_CODES.VALIDATION_ERROR,
|
|
3555
|
+
subsystem: "general",
|
|
3556
|
+
context: { field: "atEventIndex", value: input.atEventIndex }
|
|
3557
|
+
});
|
|
3127
3558
|
}
|
|
3128
3559
|
const annotation = {
|
|
3129
3560
|
id: randomUUID(),
|
|
@@ -3186,10 +3617,7 @@ var AnnotationsStore = class {
|
|
|
3186
3617
|
}
|
|
3187
3618
|
// ── Internals ──────────────────────────────────────────────────────────
|
|
3188
3619
|
filePath(sessionId) {
|
|
3189
|
-
|
|
3190
|
-
throw new Error(`Invalid sessionId: ${sessionId}`);
|
|
3191
|
-
}
|
|
3192
|
-
return path13.join(this.dir, `${sessionId}.annotations.json`);
|
|
3620
|
+
return sessionScopedPath(this.dir, sessionId, ".annotations.json");
|
|
3193
3621
|
}
|
|
3194
3622
|
async readFile(sessionId) {
|
|
3195
3623
|
const fp = this.filePath(sessionId);
|
|
@@ -3331,32 +3759,46 @@ var ReplayLogStore = class {
|
|
|
3331
3759
|
* by sessionId for stable output. Used by `wstack replay --list`.
|
|
3332
3760
|
*/
|
|
3333
3761
|
async list() {
|
|
3334
|
-
let entries;
|
|
3335
|
-
try {
|
|
3336
|
-
entries = await fsp.readdir(this.dir);
|
|
3337
|
-
} catch (err) {
|
|
3338
|
-
if (err.code === "ENOENT") return [];
|
|
3339
|
-
return [];
|
|
3340
|
-
}
|
|
3341
3762
|
const out = [];
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3763
|
+
const scan = async (dir, prefix, depth) => {
|
|
3764
|
+
let entries;
|
|
3765
|
+
try {
|
|
3766
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
3767
|
+
} catch (err) {
|
|
3768
|
+
if (depth === 0 && err.code !== "ENOENT") {
|
|
3769
|
+
console.warn(JSON.stringify({
|
|
3770
|
+
level: "warn",
|
|
3771
|
+
event: "replay_log_store.list_readdir_failed",
|
|
3772
|
+
dir,
|
|
3773
|
+
message: err instanceof Error ? err.message : String(err),
|
|
3774
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3775
|
+
}));
|
|
3776
|
+
}
|
|
3777
|
+
return;
|
|
3778
|
+
}
|
|
3779
|
+
for (const entry of entries) {
|
|
3780
|
+
if (entry.name.startsWith(".")) continue;
|
|
3781
|
+
if (entry.isDirectory()) {
|
|
3782
|
+
if (depth === 0) await scan(path13.join(dir, entry.name), entry.name, depth + 1);
|
|
3783
|
+
continue;
|
|
3784
|
+
}
|
|
3785
|
+
if (!entry.isFile() || !entry.name.endsWith(".replay.jsonl")) continue;
|
|
3786
|
+
const base = entry.name.slice(0, -".replay.jsonl".length);
|
|
3787
|
+
const sessionId = prefix ? `${prefix}/${base}` : base;
|
|
3788
|
+
const all = await this.load(sessionId);
|
|
3789
|
+
out.push({
|
|
3790
|
+
sessionId,
|
|
3791
|
+
entryCount: all.length,
|
|
3792
|
+
path: path13.join(dir, entry.name)
|
|
3793
|
+
});
|
|
3794
|
+
}
|
|
3795
|
+
};
|
|
3796
|
+
await scan(this.dir, "", 0);
|
|
3352
3797
|
return out.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
|
|
3353
3798
|
}
|
|
3354
3799
|
// ── Internals ───────────────────────────────────────────────────────────
|
|
3355
3800
|
filePath(sessionId) {
|
|
3356
|
-
|
|
3357
|
-
throw new Error(`Invalid sessionId: ${sessionId}`);
|
|
3358
|
-
}
|
|
3359
|
-
return path13.join(this.dir, `${sessionId}.replay.jsonl`);
|
|
3801
|
+
return sessionScopedPath(this.dir, sessionId, ".replay.jsonl");
|
|
3360
3802
|
}
|
|
3361
3803
|
async readAll(sessionId) {
|
|
3362
3804
|
const fp = this.filePath(sessionId);
|
|
@@ -3523,29 +3965,40 @@ var SessionRecovery = class {
|
|
|
3523
3965
|
* recent crash first.
|
|
3524
3966
|
*/
|
|
3525
3967
|
async listResumable() {
|
|
3526
|
-
let entries;
|
|
3527
|
-
try {
|
|
3528
|
-
entries = await fsp.readdir(this.dir);
|
|
3529
|
-
} catch (err) {
|
|
3530
|
-
if (err.code === "ENOENT") return [];
|
|
3531
|
-
return [];
|
|
3532
|
-
}
|
|
3533
3968
|
const out = [];
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3969
|
+
const collect = async (dir, prefix, depth) => {
|
|
3970
|
+
let entries;
|
|
3971
|
+
try {
|
|
3972
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
3973
|
+
} catch {
|
|
3974
|
+
return;
|
|
3975
|
+
}
|
|
3976
|
+
for (const entry of entries) {
|
|
3977
|
+
if (entry.name.startsWith(".")) continue;
|
|
3978
|
+
if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments")
|
|
3979
|
+
continue;
|
|
3980
|
+
if (entry.isDirectory()) {
|
|
3981
|
+
if (depth === 0) {
|
|
3982
|
+
await collect(path13.join(dir, entry.name), entry.name, depth + 1);
|
|
3983
|
+
}
|
|
3984
|
+
continue;
|
|
3985
|
+
}
|
|
3986
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
|
|
3987
|
+
if (entry.name === "_index.jsonl" || entry.name === "_mailbox.jsonl") continue;
|
|
3988
|
+
const base = entry.name.slice(0, -".jsonl".length);
|
|
3989
|
+
if (base.includes(".replay") || base.includes(".annotations") || base.includes(".audit"))
|
|
3990
|
+
continue;
|
|
3991
|
+
const sessionId = prefix ? `${prefix}/${base}` : base;
|
|
3992
|
+
const stale = await this.detectStale(sessionId);
|
|
3993
|
+
if (stale) out.push(stale);
|
|
3994
|
+
}
|
|
3995
|
+
};
|
|
3996
|
+
await collect(this.dir, "", 0);
|
|
3541
3997
|
return out.sort((a, b) => b.lastEventTs.localeCompare(a.lastEventTs));
|
|
3542
3998
|
}
|
|
3543
3999
|
// ── Internals ──────────────────────────────────────────────────────────
|
|
3544
4000
|
filePath(sessionId) {
|
|
3545
|
-
|
|
3546
|
-
throw new Error(`Invalid sessionId: ${sessionId}`);
|
|
3547
|
-
}
|
|
3548
|
-
return path13.join(this.dir, `${sessionId}.jsonl`);
|
|
4001
|
+
return sessionScopedPath(this.dir, sessionId, ".jsonl");
|
|
3549
4002
|
}
|
|
3550
4003
|
};
|
|
3551
4004
|
var GENESIS_PREV = "0".repeat(64);
|
|
@@ -3571,7 +4024,7 @@ var ToolAuditLog = class {
|
|
|
3571
4024
|
* intentional: the audit log is a record, not a cache.
|
|
3572
4025
|
*/
|
|
3573
4026
|
async record(input) {
|
|
3574
|
-
let entry
|
|
4027
|
+
let entry;
|
|
3575
4028
|
await this.enqueue(input.sessionId, async () => {
|
|
3576
4029
|
await withFileLock(this.filePath(input.sessionId), async () => {
|
|
3577
4030
|
const entries = await this.readAll(input.sessionId);
|
|
@@ -3665,10 +4118,7 @@ var ToolAuditLog = class {
|
|
|
3665
4118
|
}
|
|
3666
4119
|
// ── Internals ────────────────────────────────────────────────────────────
|
|
3667
4120
|
filePath(sessionId) {
|
|
3668
|
-
|
|
3669
|
-
throw new Error(`Invalid sessionId: ${sessionId}`);
|
|
3670
|
-
}
|
|
3671
|
-
return path13.join(this.dir, `${sessionId}.audit.jsonl`);
|
|
4121
|
+
return sessionScopedPath(this.dir, sessionId, ".audit.jsonl");
|
|
3672
4122
|
}
|
|
3673
4123
|
async readAll(sessionId) {
|
|
3674
4124
|
const fp = this.filePath(sessionId);
|
|
@@ -3845,6 +4295,387 @@ var SessionAnalyzer = class {
|
|
|
3845
4295
|
return last - first;
|
|
3846
4296
|
}
|
|
3847
4297
|
};
|
|
4298
|
+
var REGISTRY_FILE = "session-registry.json";
|
|
4299
|
+
var HEARTBEAT_INTERVAL_MS = 5e3;
|
|
4300
|
+
var STALE_TIMEOUT_MS = 3e4;
|
|
4301
|
+
function pidAlive(pid) {
|
|
4302
|
+
try {
|
|
4303
|
+
process.kill(pid, 0);
|
|
4304
|
+
return true;
|
|
4305
|
+
} catch {
|
|
4306
|
+
return false;
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
var SessionRegistry = class {
|
|
4310
|
+
filePath;
|
|
4311
|
+
heartbeatTimer = null;
|
|
4312
|
+
currentSessionId = null;
|
|
4313
|
+
constructor(globalRoot) {
|
|
4314
|
+
this.filePath = path13.join(globalRoot, REGISTRY_FILE);
|
|
4315
|
+
}
|
|
4316
|
+
// ── Public API ──────────────────────────────────────────────────────────
|
|
4317
|
+
/**
|
|
4318
|
+
* Register the current session. Call once on session start.
|
|
4319
|
+
* Starts the heartbeat timer.
|
|
4320
|
+
*/
|
|
4321
|
+
async register(entry) {
|
|
4322
|
+
this.currentSessionId = entry.sessionId;
|
|
4323
|
+
const full = {
|
|
4324
|
+
...entry,
|
|
4325
|
+
status: "active",
|
|
4326
|
+
lastHeartbeatAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4327
|
+
agentCount: entry.agents?.length ?? 0,
|
|
4328
|
+
agents: entry.agents ?? []
|
|
4329
|
+
};
|
|
4330
|
+
await this.atomicUpdate((registry) => {
|
|
4331
|
+
const now = Date.now();
|
|
4332
|
+
for (const [id, existing] of Object.entries(registry)) {
|
|
4333
|
+
if (existing.pid === entry.pid) continue;
|
|
4334
|
+
const heartbeatAge = now - new Date(existing.lastHeartbeatAt).getTime();
|
|
4335
|
+
if (heartbeatAge > STALE_TIMEOUT_MS && !pidAlive(existing.pid)) {
|
|
4336
|
+
delete registry[id];
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
registry[entry.sessionId] = full;
|
|
4340
|
+
});
|
|
4341
|
+
this.heartbeatTimer = setInterval(() => {
|
|
4342
|
+
void this.heartbeat();
|
|
4343
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
4344
|
+
if (this.heartbeatTimer.unref) this.heartbeatTimer.unref();
|
|
4345
|
+
}
|
|
4346
|
+
/**
|
|
4347
|
+
* Update agent status for the current session. Call on every
|
|
4348
|
+
* significant status change (agent start, tool start, user wait, error).
|
|
4349
|
+
*/
|
|
4350
|
+
async updateAgents(agents) {
|
|
4351
|
+
if (!this.currentSessionId) return;
|
|
4352
|
+
await this.atomicUpdate((registry) => {
|
|
4353
|
+
const entry = registry[this.currentSessionId];
|
|
4354
|
+
if (!entry) return;
|
|
4355
|
+
entry.agents = agents;
|
|
4356
|
+
entry.agentCount = agents.length;
|
|
4357
|
+
const hasRunning = agents.some((a) => a.status === "running" || a.status === "streaming");
|
|
4358
|
+
const hasWaiting = agents.some((a) => a.status === "waiting_user");
|
|
4359
|
+
const hasError = agents.some((a) => a.status === "error");
|
|
4360
|
+
entry.status = hasRunning ? "active" : hasWaiting ? "active" : hasError ? "active" : "idle";
|
|
4361
|
+
entry.lastHeartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4362
|
+
});
|
|
4363
|
+
}
|
|
4364
|
+
/**
|
|
4365
|
+
* Mark the session as closing. Called during shutdown.
|
|
4366
|
+
* Stops the heartbeat timer.
|
|
4367
|
+
*/
|
|
4368
|
+
async markClosing() {
|
|
4369
|
+
if (this.heartbeatTimer) {
|
|
4370
|
+
clearInterval(this.heartbeatTimer);
|
|
4371
|
+
this.heartbeatTimer = null;
|
|
4372
|
+
}
|
|
4373
|
+
if (!this.currentSessionId) return;
|
|
4374
|
+
await this.atomicUpdate((registry) => {
|
|
4375
|
+
const entry = registry[this.currentSessionId];
|
|
4376
|
+
if (!entry) return;
|
|
4377
|
+
entry.status = "closing";
|
|
4378
|
+
entry.lastHeartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4379
|
+
});
|
|
4380
|
+
}
|
|
4381
|
+
/**
|
|
4382
|
+
* Remove the current session from the registry. Call on clean exit.
|
|
4383
|
+
*/
|
|
4384
|
+
async unregister() {
|
|
4385
|
+
if (this.heartbeatTimer) {
|
|
4386
|
+
clearInterval(this.heartbeatTimer);
|
|
4387
|
+
this.heartbeatTimer = null;
|
|
4388
|
+
}
|
|
4389
|
+
if (!this.currentSessionId) return;
|
|
4390
|
+
const sid = this.currentSessionId;
|
|
4391
|
+
this.currentSessionId = null;
|
|
4392
|
+
await this.atomicUpdate((registry) => {
|
|
4393
|
+
delete registry[sid];
|
|
4394
|
+
});
|
|
4395
|
+
}
|
|
4396
|
+
/**
|
|
4397
|
+
* List all non-stale sessions. Prunes stale entries automatically.
|
|
4398
|
+
*/
|
|
4399
|
+
async list() {
|
|
4400
|
+
const registry = await this.readAndPrune();
|
|
4401
|
+
return Object.values(registry);
|
|
4402
|
+
}
|
|
4403
|
+
/**
|
|
4404
|
+
* Get a single session entry by ID. Returns undefined if not found or stale.
|
|
4405
|
+
*/
|
|
4406
|
+
async get(sessionId) {
|
|
4407
|
+
const registry = await this.readAndPrune();
|
|
4408
|
+
return registry[sessionId];
|
|
4409
|
+
}
|
|
4410
|
+
/**
|
|
4411
|
+
* List all sessions for a specific project (by slug).
|
|
4412
|
+
*/
|
|
4413
|
+
async listByProject(projectSlug2) {
|
|
4414
|
+
const all = await this.list();
|
|
4415
|
+
return all.filter((e) => e.projectSlug === projectSlug2);
|
|
4416
|
+
}
|
|
4417
|
+
/**
|
|
4418
|
+
* Return the registry file path. Useful for WebUI to watch/read.
|
|
4419
|
+
*/
|
|
4420
|
+
get registryPath() {
|
|
4421
|
+
return this.filePath;
|
|
4422
|
+
}
|
|
4423
|
+
// ── Internal ────────────────────────────────────────────────────────────
|
|
4424
|
+
async heartbeat() {
|
|
4425
|
+
if (!this.currentSessionId) return;
|
|
4426
|
+
try {
|
|
4427
|
+
const raw = await fsp.readFile(this.filePath, "utf8").catch(() => "{}");
|
|
4428
|
+
const registry = JSON.parse(raw);
|
|
4429
|
+
const entry = registry[this.currentSessionId];
|
|
4430
|
+
if (entry) {
|
|
4431
|
+
entry.lastHeartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4432
|
+
if (entry.status !== "closing") {
|
|
4433
|
+
const hasRunning = (entry.agents ?? []).some(
|
|
4434
|
+
(a) => a.status === "running" || a.status === "streaming"
|
|
4435
|
+
);
|
|
4436
|
+
entry.status = hasRunning ? "active" : "idle";
|
|
4437
|
+
}
|
|
4438
|
+
await this.writeAtomic(registry);
|
|
4439
|
+
}
|
|
4440
|
+
} catch {
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
4443
|
+
async readAndPrune() {
|
|
4444
|
+
try {
|
|
4445
|
+
const raw = await fsp.readFile(this.filePath, "utf8");
|
|
4446
|
+
const registry = JSON.parse(raw);
|
|
4447
|
+
const now = Date.now();
|
|
4448
|
+
let pruned = false;
|
|
4449
|
+
for (const [id, entry] of Object.entries(registry)) {
|
|
4450
|
+
const heartbeatAge = now - new Date(entry.lastHeartbeatAt).getTime();
|
|
4451
|
+
if (heartbeatAge > STALE_TIMEOUT_MS && !pidAlive(entry.pid)) {
|
|
4452
|
+
entry.status = "stale";
|
|
4453
|
+
const startedAge = now - new Date(entry.startedAt).getTime();
|
|
4454
|
+
if (startedAge > 5 * 6e4) {
|
|
4455
|
+
delete registry[id];
|
|
4456
|
+
pruned = true;
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4460
|
+
if (pruned) {
|
|
4461
|
+
await this.writeAtomic(registry).catch(() => void 0);
|
|
4462
|
+
}
|
|
4463
|
+
return registry;
|
|
4464
|
+
} catch {
|
|
4465
|
+
return {};
|
|
4466
|
+
}
|
|
4467
|
+
}
|
|
4468
|
+
async atomicUpdate(fn) {
|
|
4469
|
+
const lockPath = `${this.filePath}.lock`;
|
|
4470
|
+
const maxRetries = 5;
|
|
4471
|
+
const retryDelayMs = 20;
|
|
4472
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
4473
|
+
try {
|
|
4474
|
+
await fsp.mkdir(path13.dirname(this.filePath), { recursive: true });
|
|
4475
|
+
const lockHandle = await fsp.open(lockPath, "wx").catch(() => null);
|
|
4476
|
+
if (!lockHandle) {
|
|
4477
|
+
await new Promise((r) => setTimeout(r, retryDelayMs * (attempt + 1)));
|
|
4478
|
+
continue;
|
|
4479
|
+
}
|
|
4480
|
+
try {
|
|
4481
|
+
const raw = await fsp.readFile(this.filePath, "utf8").catch(() => "{}");
|
|
4482
|
+
const registry = JSON.parse(raw);
|
|
4483
|
+
fn(registry);
|
|
4484
|
+
await this.writeAtomicLocked(registry);
|
|
4485
|
+
return;
|
|
4486
|
+
} finally {
|
|
4487
|
+
await lockHandle.close();
|
|
4488
|
+
await fsp.unlink(lockPath).catch(() => void 0);
|
|
4489
|
+
}
|
|
4490
|
+
} catch {
|
|
4491
|
+
return;
|
|
4492
|
+
}
|
|
4493
|
+
}
|
|
4494
|
+
}
|
|
4495
|
+
async writeAtomicLocked(registry) {
|
|
4496
|
+
const tmp = `${this.filePath}.${randomUUID().slice(0, 8)}.tmp`;
|
|
4497
|
+
await fsp.writeFile(tmp, JSON.stringify(registry, null, 2), "utf8");
|
|
4498
|
+
await fsp.rename(tmp, this.filePath);
|
|
4499
|
+
}
|
|
4500
|
+
/** Legacy write without lock — used by heartbeat for performance. */
|
|
4501
|
+
async writeAtomic(registry) {
|
|
4502
|
+
const tmp = `${this.filePath}.${randomUUID().slice(0, 8)}.tmp`;
|
|
4503
|
+
await fsp.writeFile(tmp, JSON.stringify(registry, null, 2), "utf8");
|
|
4504
|
+
await fsp.rename(tmp, this.filePath);
|
|
4505
|
+
}
|
|
4506
|
+
};
|
|
4507
|
+
var _instance = null;
|
|
4508
|
+
function getSessionRegistry(globalRoot) {
|
|
4509
|
+
if (!_instance && globalRoot) {
|
|
4510
|
+
_instance = new SessionRegistry(globalRoot);
|
|
4511
|
+
}
|
|
4512
|
+
if (!_instance) {
|
|
4513
|
+
throw new Error("SessionRegistry not initialized. Call getSessionRegistry(globalRoot) first.");
|
|
4514
|
+
}
|
|
4515
|
+
return _instance;
|
|
4516
|
+
}
|
|
4517
|
+
function hasSessionRegistry() {
|
|
4518
|
+
return _instance !== null;
|
|
4519
|
+
}
|
|
4520
|
+
|
|
4521
|
+
// src/agent-status-tracker.ts
|
|
4522
|
+
var AgentStatusTracker = class {
|
|
4523
|
+
events;
|
|
4524
|
+
registry;
|
|
4525
|
+
leaderName;
|
|
4526
|
+
// Live agent map: agentId → AgentEntry
|
|
4527
|
+
agents = /* @__PURE__ */ new Map();
|
|
4528
|
+
// Leader tracking
|
|
4529
|
+
leaderStatus = "idle";
|
|
4530
|
+
leaderCurrentTool;
|
|
4531
|
+
leaderIterations = 0;
|
|
4532
|
+
leaderToolCalls = 0;
|
|
4533
|
+
unsubscribers = [];
|
|
4534
|
+
constructor(opts) {
|
|
4535
|
+
this.events = opts.events;
|
|
4536
|
+
this.registry = opts.registry;
|
|
4537
|
+
this.leaderName = opts.leaderName ?? "leader";
|
|
4538
|
+
}
|
|
4539
|
+
start() {
|
|
4540
|
+
this.unsubscribers.push(
|
|
4541
|
+
this.events.onPattern("agent.run.started", () => {
|
|
4542
|
+
this.leaderStatus = "running";
|
|
4543
|
+
this.leaderIterations++;
|
|
4544
|
+
this.flush();
|
|
4545
|
+
})
|
|
4546
|
+
);
|
|
4547
|
+
this.unsubscribers.push(
|
|
4548
|
+
this.events.onPattern("agent.run.completed", () => {
|
|
4549
|
+
this.leaderStatus = "idle";
|
|
4550
|
+
this.leaderCurrentTool = void 0;
|
|
4551
|
+
this.flush();
|
|
4552
|
+
})
|
|
4553
|
+
);
|
|
4554
|
+
this.unsubscribers.push(
|
|
4555
|
+
this.events.onPattern("agent.run.error", () => {
|
|
4556
|
+
this.leaderStatus = "error";
|
|
4557
|
+
this.leaderCurrentTool = void 0;
|
|
4558
|
+
this.flush();
|
|
4559
|
+
})
|
|
4560
|
+
);
|
|
4561
|
+
this.unsubscribers.push(
|
|
4562
|
+
this.events.onPattern("tool.started", (_event, payload) => {
|
|
4563
|
+
const p = payload;
|
|
4564
|
+
if (p?.name) {
|
|
4565
|
+
this.leaderCurrentTool = p.name;
|
|
4566
|
+
this.leaderToolCalls++;
|
|
4567
|
+
}
|
|
4568
|
+
this.leaderStatus = "running";
|
|
4569
|
+
this.flush();
|
|
4570
|
+
})
|
|
4571
|
+
);
|
|
4572
|
+
this.unsubscribers.push(
|
|
4573
|
+
this.events.onPattern("tool.executed", () => {
|
|
4574
|
+
this.leaderCurrentTool = void 0;
|
|
4575
|
+
this.flush();
|
|
4576
|
+
})
|
|
4577
|
+
);
|
|
4578
|
+
this.unsubscribers.push(
|
|
4579
|
+
this.events.onPattern("brain.ask_human", () => {
|
|
4580
|
+
this.leaderStatus = "waiting_user";
|
|
4581
|
+
this.flush();
|
|
4582
|
+
})
|
|
4583
|
+
);
|
|
4584
|
+
this.unsubscribers.push(
|
|
4585
|
+
this.events.onPattern("llm.stream_started", () => {
|
|
4586
|
+
this.leaderStatus = "streaming";
|
|
4587
|
+
this.flush();
|
|
4588
|
+
})
|
|
4589
|
+
);
|
|
4590
|
+
this.unsubscribers.push(
|
|
4591
|
+
this.events.onPattern("fleet.subagent.spawned", (_event, payload) => {
|
|
4592
|
+
const p = payload;
|
|
4593
|
+
if (p?.subagentId) {
|
|
4594
|
+
this.agents.set(p.subagentId, {
|
|
4595
|
+
id: p.subagentId,
|
|
4596
|
+
name: p.name ?? p.subagentId,
|
|
4597
|
+
status: "idle",
|
|
4598
|
+
iterations: 0,
|
|
4599
|
+
toolCalls: 0,
|
|
4600
|
+
lastActivityAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4601
|
+
});
|
|
4602
|
+
this.flush();
|
|
4603
|
+
}
|
|
4604
|
+
})
|
|
4605
|
+
);
|
|
4606
|
+
this.unsubscribers.push(
|
|
4607
|
+
this.events.onPattern("fleet.subagent.task_started", (_event, payload) => {
|
|
4608
|
+
const p = payload;
|
|
4609
|
+
if (p?.subagentId) {
|
|
4610
|
+
const entry = this.agents.get(p.subagentId);
|
|
4611
|
+
if (entry) {
|
|
4612
|
+
entry.status = "running";
|
|
4613
|
+
entry.iterations++;
|
|
4614
|
+
entry.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4615
|
+
this.flush();
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
})
|
|
4619
|
+
);
|
|
4620
|
+
this.unsubscribers.push(
|
|
4621
|
+
this.events.onPattern("fleet.subagent.task_completed", (_event, payload) => {
|
|
4622
|
+
const p = payload;
|
|
4623
|
+
if (p?.subagentId) {
|
|
4624
|
+
const entry = this.agents.get(p.subagentId);
|
|
4625
|
+
if (entry) {
|
|
4626
|
+
entry.status = "idle";
|
|
4627
|
+
entry.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4628
|
+
this.flush();
|
|
4629
|
+
}
|
|
4630
|
+
}
|
|
4631
|
+
})
|
|
4632
|
+
);
|
|
4633
|
+
this.unsubscribers.push(
|
|
4634
|
+
this.events.onPattern("fleet.subagent.error", (_event, payload) => {
|
|
4635
|
+
const p = payload;
|
|
4636
|
+
if (p?.subagentId) {
|
|
4637
|
+
const entry = this.agents.get(p.subagentId);
|
|
4638
|
+
if (entry) {
|
|
4639
|
+
entry.status = "error";
|
|
4640
|
+
entry.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4641
|
+
this.flush();
|
|
4642
|
+
}
|
|
4643
|
+
}
|
|
4644
|
+
})
|
|
4645
|
+
);
|
|
4646
|
+
this.unsubscribers.push(
|
|
4647
|
+
this.events.onPattern("fleet.subagent.stopped", (_event, payload) => {
|
|
4648
|
+
const p = payload;
|
|
4649
|
+
if (p?.subagentId) {
|
|
4650
|
+
this.agents.delete(p.subagentId);
|
|
4651
|
+
this.flush();
|
|
4652
|
+
}
|
|
4653
|
+
})
|
|
4654
|
+
);
|
|
4655
|
+
}
|
|
4656
|
+
stop() {
|
|
4657
|
+
for (const unsub of this.unsubscribers) {
|
|
4658
|
+
try {
|
|
4659
|
+
unsub();
|
|
4660
|
+
} catch {
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
this.unsubscribers = [];
|
|
4664
|
+
}
|
|
4665
|
+
flush() {
|
|
4666
|
+
const leaderEntry = {
|
|
4667
|
+
id: "leader",
|
|
4668
|
+
name: this.leaderName,
|
|
4669
|
+
status: this.leaderStatus,
|
|
4670
|
+
currentTool: this.leaderCurrentTool,
|
|
4671
|
+
iterations: this.leaderIterations,
|
|
4672
|
+
toolCalls: this.leaderToolCalls,
|
|
4673
|
+
lastActivityAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4674
|
+
};
|
|
4675
|
+
const allAgents = [leaderEntry, ...this.agents.values()];
|
|
4676
|
+
this.registry.updateAgents(allAgents).catch(() => void 0);
|
|
4677
|
+
}
|
|
4678
|
+
};
|
|
3848
4679
|
var DefaultSessionRewinder = class {
|
|
3849
4680
|
constructor(sessionsDir, projectRoot) {
|
|
3850
4681
|
this.sessionsDir = sessionsDir;
|
|
@@ -3893,7 +4724,11 @@ var DefaultSessionRewinder = class {
|
|
|
3893
4724
|
}
|
|
3894
4725
|
}
|
|
3895
4726
|
if (targetIdx === -1) {
|
|
3896
|
-
throw new
|
|
4727
|
+
throw new SessionError({
|
|
4728
|
+
message: `Checkpoint ${checkpointIndex} not found`,
|
|
4729
|
+
code: ERROR_CODES.SESSION_NOT_FOUND,
|
|
4730
|
+
context: { checkpointIndex }
|
|
4731
|
+
});
|
|
3897
4732
|
}
|
|
3898
4733
|
const snapshotsToRevert = [];
|
|
3899
4734
|
for (let i = targetIdx + 1; i < events.length; i++) {
|
|
@@ -4033,10 +4868,12 @@ async function saveTodosCheckpoint(filePath, sessionId, todos) {
|
|
|
4033
4868
|
try {
|
|
4034
4869
|
await atomicWrite(filePath, JSON.stringify(payload, null, 2), { mode: 384 });
|
|
4035
4870
|
} catch (err) {
|
|
4036
|
-
console.warn(
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4871
|
+
console.warn(JSON.stringify({
|
|
4872
|
+
level: "warn",
|
|
4873
|
+
event: "todos_checkpoint.save_failed",
|
|
4874
|
+
message: err instanceof Error ? err.message : String(err),
|
|
4875
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4876
|
+
}));
|
|
4040
4877
|
}
|
|
4041
4878
|
}
|
|
4042
4879
|
function attachTodosCheckpoint(state, filePath, sessionId) {
|
|
@@ -4046,7 +4883,13 @@ function attachTodosCheckpoint(state, filePath, sessionId) {
|
|
|
4046
4883
|
const enqueueWrite = (todos) => {
|
|
4047
4884
|
writeChain = writeChain.then(() => saveTodosCheckpoint(filePath, sessionId, todos)).catch((err) => {
|
|
4048
4885
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4049
|
-
console.error(
|
|
4886
|
+
console.error(JSON.stringify({
|
|
4887
|
+
level: "error",
|
|
4888
|
+
event: "todos_checkpoint.write_chain_failed",
|
|
4889
|
+
sessionId,
|
|
4890
|
+
message: msg,
|
|
4891
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4892
|
+
}));
|
|
4050
4893
|
});
|
|
4051
4894
|
return writeChain;
|
|
4052
4895
|
};
|
|
@@ -4182,19 +5025,29 @@ function deriveTodosFromPlanItem(plan, idOrIndex, subtasks) {
|
|
|
4182
5025
|
id: `todo_${Date.now()}_plan`,
|
|
4183
5026
|
content: item.title,
|
|
4184
5027
|
status: "in_progress",
|
|
4185
|
-
activeForm: item.title
|
|
5028
|
+
activeForm: item.title,
|
|
5029
|
+
promotedFromPlan: item.id
|
|
4186
5030
|
});
|
|
4187
5031
|
if (subtasks && subtasks.length > 0) {
|
|
4188
5032
|
for (const st of subtasks) {
|
|
4189
5033
|
todos.push({
|
|
4190
5034
|
id: `todo_${Date.now()}_${randomUUID().slice(0, 6)}`,
|
|
4191
5035
|
content: st,
|
|
4192
|
-
status: "pending"
|
|
5036
|
+
status: "pending",
|
|
5037
|
+
promotedFromPlan: item.id
|
|
4193
5038
|
});
|
|
4194
5039
|
}
|
|
4195
5040
|
}
|
|
4196
5041
|
return { plan: updatedPlan, todos };
|
|
4197
5042
|
}
|
|
5043
|
+
async function mutatePlan(filePath, sessionId, fn) {
|
|
5044
|
+
return withFileLock(filePath, async () => {
|
|
5045
|
+
const plan = await loadPlan(filePath) ?? emptyPlan(sessionId);
|
|
5046
|
+
const updated = await fn(plan);
|
|
5047
|
+
await savePlan(filePath, updated);
|
|
5048
|
+
return updated;
|
|
5049
|
+
});
|
|
5050
|
+
}
|
|
4198
5051
|
function attachPlanCheckpoint(_state, _filePath, _sessionId) {
|
|
4199
5052
|
return () => void 0;
|
|
4200
5053
|
}
|
|
@@ -4345,6 +5198,14 @@ async function saveTasks(filePath, tasks) {
|
|
|
4345
5198
|
);
|
|
4346
5199
|
}
|
|
4347
5200
|
}
|
|
5201
|
+
async function mutateTasks(filePath, sessionId, fn) {
|
|
5202
|
+
return withFileLock(filePath, async () => {
|
|
5203
|
+
const file = await loadTasks(filePath) ?? emptyTaskFile(sessionId);
|
|
5204
|
+
const updated = await fn(file);
|
|
5205
|
+
await saveTasks(filePath, updated);
|
|
5206
|
+
return updated;
|
|
5207
|
+
});
|
|
5208
|
+
}
|
|
4348
5209
|
async function loadDirectorState(filePath) {
|
|
4349
5210
|
let raw;
|
|
4350
5211
|
try {
|
|
@@ -4542,7 +5403,7 @@ function envFlag(value) {
|
|
|
4542
5403
|
return !/^(0|false|no|off)$/i.test(value.trim());
|
|
4543
5404
|
}
|
|
4544
5405
|
var COLOR = isColorTty();
|
|
4545
|
-
var wrap = (
|
|
5406
|
+
var wrap = (open6, close) => (s) => COLOR ? `\x1B[${open6}m${s}\x1B[${close}m` : s;
|
|
4546
5407
|
var color = {
|
|
4547
5408
|
reset: wrap("0", "0"),
|
|
4548
5409
|
bold: wrap("1", "22"),
|
|
@@ -4572,9 +5433,13 @@ function projectSlug(absRoot) {
|
|
|
4572
5433
|
function slugify(name) {
|
|
4573
5434
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
|
|
4574
5435
|
}
|
|
5436
|
+
function wstackGlobalRoot() {
|
|
5437
|
+
const fromEnv = process.env["WRONGSTACK_HOME"];
|
|
5438
|
+
if (fromEnv && fromEnv.trim().length > 0) return path13.resolve(fromEnv);
|
|
5439
|
+
return path13.join(os.homedir(), ".wrongstack");
|
|
5440
|
+
}
|
|
4575
5441
|
function resolveWstackPaths(opts) {
|
|
4576
|
-
const
|
|
4577
|
-
const globalRoot = opts.globalRoot ?? path13.join(home, ".wrongstack");
|
|
5442
|
+
const globalRoot = opts.globalRoot ?? (opts.userHome ? path13.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
|
|
4578
5443
|
const hash = projectHash(opts.projectRoot);
|
|
4579
5444
|
const slug = projectSlug(opts.projectRoot);
|
|
4580
5445
|
const projectDir = path13.join(globalRoot, "projects", slug);
|
|
@@ -4614,56 +5479,6 @@ function resolveWstackPaths(opts) {
|
|
|
4614
5479
|
};
|
|
4615
5480
|
}
|
|
4616
5481
|
|
|
4617
|
-
// src/types/errors.ts
|
|
4618
|
-
var ERROR_CODES = {
|
|
4619
|
-
// File system
|
|
4620
|
-
FS_READ_FAILED: "FS_READ_FAILED",
|
|
4621
|
-
FS_ATOMIC_WRITE_FAILED: "FS_ATOMIC_WRITE_FAILED"};
|
|
4622
|
-
var WrongStackError = class extends Error {
|
|
4623
|
-
code;
|
|
4624
|
-
subsystem;
|
|
4625
|
-
severity;
|
|
4626
|
-
recoverable;
|
|
4627
|
-
context;
|
|
4628
|
-
constructor(opts) {
|
|
4629
|
-
super(opts.message, { cause: opts.cause });
|
|
4630
|
-
this.name = "WrongStackError";
|
|
4631
|
-
this.code = opts.code;
|
|
4632
|
-
this.subsystem = opts.subsystem;
|
|
4633
|
-
this.severity = opts.severity ?? "error";
|
|
4634
|
-
this.recoverable = opts.recoverable ?? false;
|
|
4635
|
-
this.context = opts.context;
|
|
4636
|
-
}
|
|
4637
|
-
/**
|
|
4638
|
-
* Render a one-line user-facing description.
|
|
4639
|
-
* Subclasses should override for domain-specific formatting.
|
|
4640
|
-
*/
|
|
4641
|
-
describe() {
|
|
4642
|
-
const ctx = this.context ? ` ${formatContext(this.context)}` : "";
|
|
4643
|
-
return `${this.code}: ${this.message}${ctx}`;
|
|
4644
|
-
}
|
|
4645
|
-
};
|
|
4646
|
-
function formatContext(ctx) {
|
|
4647
|
-
const parts = Object.entries(ctx).filter(([, v]) => v !== void 0).slice(0, 3).map(([k, v]) => `${k}=${String(v)}`);
|
|
4648
|
-
return parts.length > 0 ? `[${parts.join(" ")}]` : "";
|
|
4649
|
-
}
|
|
4650
|
-
var FsError = class extends WrongStackError {
|
|
4651
|
-
path;
|
|
4652
|
-
constructor(opts) {
|
|
4653
|
-
super({
|
|
4654
|
-
message: opts.message,
|
|
4655
|
-
code: opts.code,
|
|
4656
|
-
subsystem: "fs",
|
|
4657
|
-
severity: "error",
|
|
4658
|
-
recoverable: opts.code !== ERROR_CODES.FS_READ_FAILED,
|
|
4659
|
-
context: { path: opts.path, ...opts.context },
|
|
4660
|
-
cause: opts.cause
|
|
4661
|
-
});
|
|
4662
|
-
this.name = "FsError";
|
|
4663
|
-
this.path = opts.path;
|
|
4664
|
-
}
|
|
4665
|
-
};
|
|
4666
|
-
|
|
4667
5482
|
// src/storage/goal-store.ts
|
|
4668
5483
|
var MAX_JOURNAL_ENTRIES = 500;
|
|
4669
5484
|
function goalFilePath(projectRoot) {
|
|
@@ -4681,12 +5496,24 @@ async function loadGoal(filePath) {
|
|
|
4681
5496
|
try {
|
|
4682
5497
|
const parsed = JSON.parse(raw);
|
|
4683
5498
|
if (parsed?.version !== 1 || typeof parsed.goal !== "string" || !Array.isArray(parsed.journal)) {
|
|
4684
|
-
console.warn(
|
|
5499
|
+
console.warn(JSON.stringify({
|
|
5500
|
+
level: "warn",
|
|
5501
|
+
event: "goal_store.invalid_schema",
|
|
5502
|
+
path: filePath,
|
|
5503
|
+
message: "invalid schema \u2014 consider deleting and re-creating",
|
|
5504
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5505
|
+
}));
|
|
4685
5506
|
return null;
|
|
4686
5507
|
}
|
|
4687
5508
|
return parsed;
|
|
4688
5509
|
} catch {
|
|
4689
|
-
console.warn(
|
|
5510
|
+
console.warn(JSON.stringify({
|
|
5511
|
+
level: "warn",
|
|
5512
|
+
event: "goal_store.parse_failed",
|
|
5513
|
+
path: filePath,
|
|
5514
|
+
message: "JSON parse failed \u2014 consider deleting and re-creating",
|
|
5515
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5516
|
+
}));
|
|
4690
5517
|
return null;
|
|
4691
5518
|
}
|
|
4692
5519
|
}
|
|
@@ -5078,7 +5905,12 @@ var CloudSync = class {
|
|
|
5078
5905
|
const res = await fetch(url, init);
|
|
5079
5906
|
if (!res.ok) {
|
|
5080
5907
|
const errText = await res.text();
|
|
5081
|
-
throw new
|
|
5908
|
+
throw new WrongStackError({
|
|
5909
|
+
message: `GitHub API ${method} ${pathSegment} failed (${res.status}): ${errText}`,
|
|
5910
|
+
code: ERROR_CODES.UNKNOWN,
|
|
5911
|
+
subsystem: "general",
|
|
5912
|
+
context: { method, pathSegment, status: res.status, repo: `${owner}/${repo}` }
|
|
5913
|
+
});
|
|
5082
5914
|
}
|
|
5083
5915
|
const text = await res.text();
|
|
5084
5916
|
return text ? JSON.parse(text) : {};
|
|
@@ -5203,20 +6035,35 @@ var CloudSync = class {
|
|
|
5203
6035
|
function resolvePulledCategoryPath(cat, localPath, rel, remotePath) {
|
|
5204
6036
|
const directoryBacked = cat === "skills" || cat === "prompts";
|
|
5205
6037
|
if (!directoryBacked) {
|
|
5206
|
-
if (rel) throw new
|
|
6038
|
+
if (rel) throw new FsError({
|
|
6039
|
+
message: `Refusing nested CloudSync path for file category: ${remotePath}`,
|
|
6040
|
+
code: ERROR_CODES.FS_DELETE_FAILED,
|
|
6041
|
+
path: remotePath,
|
|
6042
|
+
context: { reason: "nested_file_category", category: cat }
|
|
6043
|
+
});
|
|
5207
6044
|
return localPath;
|
|
5208
6045
|
}
|
|
5209
6046
|
if (!rel) return localPath;
|
|
5210
6047
|
const normalizedRel = path13.normalize(rel);
|
|
5211
6048
|
const traversesUp = normalizedRel === ".." || normalizedRel.startsWith(`..${path13.sep}`);
|
|
5212
6049
|
if (path13.isAbsolute(normalizedRel) || traversesUp) {
|
|
5213
|
-
throw new
|
|
6050
|
+
throw new FsError({
|
|
6051
|
+
message: `Refusing CloudSync path traversal: ${remotePath}`,
|
|
6052
|
+
code: ERROR_CODES.FS_DELETE_FAILED,
|
|
6053
|
+
path: remotePath,
|
|
6054
|
+
context: { reason: "path_traversal", normalizedRel }
|
|
6055
|
+
});
|
|
5214
6056
|
}
|
|
5215
6057
|
const dest = path13.resolve(localPath, normalizedRel);
|
|
5216
6058
|
const root = path13.resolve(localPath);
|
|
5217
|
-
const
|
|
5218
|
-
if (
|
|
5219
|
-
throw new
|
|
6059
|
+
const relative4 = path13.relative(root, dest);
|
|
6060
|
+
if (relative4.startsWith("..") || path13.isAbsolute(relative4)) {
|
|
6061
|
+
throw new FsError({
|
|
6062
|
+
message: `Refusing CloudSync path outside category root: ${remotePath}`,
|
|
6063
|
+
code: ERROR_CODES.FS_DELETE_FAILED,
|
|
6064
|
+
path: remotePath,
|
|
6065
|
+
context: { reason: "outside_category_root", category: cat }
|
|
6066
|
+
});
|
|
5220
6067
|
}
|
|
5221
6068
|
return dest;
|
|
5222
6069
|
}
|
|
@@ -5271,6 +6118,7 @@ function isAllowed(type, level) {
|
|
|
5271
6118
|
}
|
|
5272
6119
|
function createSessionEventBridge(writer, level = "standard", options = {}) {
|
|
5273
6120
|
const normalizedLevel = level ?? "standard";
|
|
6121
|
+
const resolveWriter = typeof writer === "function" ? writer : () => writer;
|
|
5274
6122
|
const progressCounters = /* @__PURE__ */ new Map();
|
|
5275
6123
|
const toolProgressConfig = options.sampling?.toolProgress ?? {};
|
|
5276
6124
|
const TOOL_PROGRESS_SAMPLE_RATE = toolProgressConfig.sampleRate ?? 8;
|
|
@@ -5295,13 +6143,26 @@ function createSessionEventBridge(writer, level = "standard", options = {}) {
|
|
|
5295
6143
|
return isAllowed(type, normalizedLevel);
|
|
5296
6144
|
},
|
|
5297
6145
|
async append(event) {
|
|
5298
|
-
|
|
6146
|
+
const target = resolveWriter();
|
|
6147
|
+
if (!target) return;
|
|
5299
6148
|
if (!isAllowed(event.type, normalizedLevel)) return;
|
|
5300
6149
|
if (!shouldSample(event)) return;
|
|
5301
6150
|
try {
|
|
5302
|
-
await
|
|
6151
|
+
await target.append(event);
|
|
5303
6152
|
} catch (err) {
|
|
5304
6153
|
}
|
|
6154
|
+
},
|
|
6155
|
+
async appendBatch(events) {
|
|
6156
|
+
const target = resolveWriter();
|
|
6157
|
+
if (!target || events.length === 0) return;
|
|
6158
|
+
const allowed = events.filter(
|
|
6159
|
+
(e) => isAllowed(e.type, normalizedLevel) && shouldSample(e)
|
|
6160
|
+
);
|
|
6161
|
+
if (allowed.length === 0) return;
|
|
6162
|
+
try {
|
|
6163
|
+
await target.appendBatch(allowed);
|
|
6164
|
+
} catch {
|
|
6165
|
+
}
|
|
5305
6166
|
}
|
|
5306
6167
|
};
|
|
5307
6168
|
}
|
|
@@ -5326,6 +6187,6 @@ function resolveSessionLoggingConfig(cfg) {
|
|
|
5326
6187
|
};
|
|
5327
6188
|
}
|
|
5328
6189
|
|
|
5329
|
-
export { ALL_SYNC_CATEGORIES, AnnotationsStore, CORE_RECONSTRUCT_EVENTS, CloudSync, ConfigMigrationError, DEFAULT_CONFIG_MIGRATIONS, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultMemoryStore, DefaultPromptStore, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DirectorStateCheckpoint, FileMemoryBackend, GraphMemoryBackend, MAX_JOURNAL_ENTRIES, MAX_PROGRESS_HISTORY, QueueStore, RecoveryLock, ReplayLogStore, STANDARD_AUDIT_EVENTS, SessionAnalyzer, SessionMemoryConsolidator, SessionRecovery, ToolAuditLog, addPlanItem, appendJournal, attachPlanCheckpoint, attachTodosCheckpoint, clearPlan, createSessionEventBridge, deriveTodosFromPlanItem, emptyGoal, emptyPlan, emptyTaskFile, formatGoal, formatPlan, formatPlanTemplates, getPlanTemplate, goalFilePath, listPlanTemplates, loadDirectorState, loadGoal, loadPlan, loadTasks, loadTodosCheckpoint, parseEntries, parseProgressFromText, recordProgress, removePlanItem, resolveAuditLevel, resolveSessionLoggingConfig, runConfigMigrations, saveGoal, savePlan, saveTasks, saveTodosCheckpoint, setPlanItemStatus, setProgress, summarizeUsage };
|
|
6190
|
+
export { ALL_SYNC_CATEGORIES, AgentStatusTracker, AnnotationsStore, CORE_RECONSTRUCT_EVENTS, CloudSync, ConfigMigrationError, DEFAULT_CONFIG_MIGRATIONS, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultMemoryStore, DefaultPromptStore, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DirectorStateCheckpoint, FileMemoryBackend, GraphMemoryBackend, MAX_JOURNAL_ENTRIES, MAX_PROGRESS_HISTORY, QueueStore, RecoveryLock, ReplayLogStore, STANDARD_AUDIT_EVENTS, SessionAnalyzer, SessionMemoryConsolidator, SessionRecovery, SessionRegistry, ToolAuditLog, addPlanItem, appendJournal, attachPlanCheckpoint, attachTodosCheckpoint, clearPlan, createSessionEventBridge, deriveTodosFromPlanItem, emptyGoal, emptyPlan, emptyTaskFile, formatGoal, formatPlan, formatPlanTemplates, getPlanTemplate, getSessionRegistry, goalFilePath, hasSessionRegistry, listPlanTemplates, loadDirectorState, loadGoal, loadPlan, loadTasks, loadTodosCheckpoint, mutatePlan, mutateTasks, parseEntries, parseProgressFromText, recordProgress, removePlanItem, resolveAuditLevel, resolveSessionLoggingConfig, runConfigMigrations, saveGoal, savePlan, saveTasks, saveTodosCheckpoint, setPlanItemStatus, setProgress, summarizeUsage };
|
|
5330
6191
|
//# sourceMappingURL=index.js.map
|
|
5331
6192
|
//# sourceMappingURL=index.js.map
|