@wrongstack/core 0.155.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-BbZU5TPN.d.ts → agent-bridge-Cimv7bK7.d.ts} +1 -1
- package/dist/{agent-subagent-runner-Bsueu0J2.d.ts → agent-subagent-runner-C658wj_c.d.ts} +9 -8
- package/dist/{brain-CS_B0vIE.d.ts → brain-sCZ3lCjq.d.ts} +26 -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 +1983 -145
- package/dist/coordination/index.js.map +1 -1
- package/dist/defaults/index.d.ts +26 -26
- package/dist/defaults/index.js +1105 -289
- package/dist/defaults/index.js.map +1 -1
- package/dist/execution/index.d.ts +45 -16
- package/dist/execution/index.js +224 -53
- 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-CbV8pXLD.d.ts → goal-preamble-CnbzyVvl.d.ts} +19 -10
- package/dist/{index-CI1hRfPt.d.ts → index-BlMqh5GO.d.ts} +8 -8
- package/dist/{index-B5wz-GXm.d.ts → index-C2eSNPsB.d.ts} +7 -5
- package/dist/index.d.ts +438 -128
- package/dist/index.js +4974 -836
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/index.d.ts +7 -7
- package/dist/infrastructure/index.js +61 -13
- 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-CPERR2De.d.ts → mcp-servers-DFbirBv6.d.ts} +3 -3
- 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-BSKSFNhv.d.ts → multi-agent-coordinator-60weDZoA.d.ts} +8 -8
- package/dist/{null-fleet-bus-CGOez8Le.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-CYoTKjsz.d.ts → parallel-eternal-engine-DtG1fjc9.d.ts} +13 -9
- package/dist/{path-resolver-DuhlmPil.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-DbH7lg-t.d.ts → plan-templates-DPABrDvy.d.ts} +18 -7
- package/dist/{provider-runner-Cocq0O9E.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 +215 -79
- package/dist/sdd/index.js.map +1 -1
- package/dist/{secret-vault-w8MbUe2Q.d.ts → secret-vault-CeVNiy_f.d.ts} +3 -2
- package/dist/security/index.d.ts +5 -4
- package/dist/security/index.js +155 -13
- 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 +150 -12
- package/dist/storage/index.js +1041 -214
- package/dist/storage/index.js.map +1 -1
- package/dist/types/index.d.ts +67 -20
- package/dist/types/index.js +557 -52
- 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 +16 -4
- package/dist/utils/index.js +40 -14
- 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
|
};
|
|
@@ -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;
|
|
@@ -2496,7 +2807,13 @@ var DefaultConfigLoader = class {
|
|
|
2496
2807
|
cfg = deepMerge2(cfg, patch);
|
|
2497
2808
|
}
|
|
2498
2809
|
} catch (err) {
|
|
2499
|
-
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
|
+
}));
|
|
2500
2817
|
}
|
|
2501
2818
|
}
|
|
2502
2819
|
if (opts.cliFlags) {
|
|
@@ -2559,7 +2876,12 @@ var DefaultConfigLoader = class {
|
|
|
2559
2876
|
return parsed.value;
|
|
2560
2877
|
} catch (err) {
|
|
2561
2878
|
if (err.code === "ENOENT") return null;
|
|
2562
|
-
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
|
+
}));
|
|
2563
2885
|
return null;
|
|
2564
2886
|
}
|
|
2565
2887
|
}
|
|
@@ -2569,33 +2891,63 @@ var DefaultConfigLoader = class {
|
|
|
2569
2891
|
raw = await fsp.readFile(file, "utf8");
|
|
2570
2892
|
} catch (err) {
|
|
2571
2893
|
if (err.code !== "ENOENT") {
|
|
2572
|
-
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
|
+
}));
|
|
2573
2901
|
}
|
|
2574
2902
|
return {};
|
|
2575
2903
|
}
|
|
2576
2904
|
const parsed = safeParse(raw);
|
|
2577
2905
|
if (!parsed.ok || !parsed.value) {
|
|
2578
|
-
console.warn(
|
|
2579
|
-
|
|
2580
|
-
|
|
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
|
+
}));
|
|
2581
2913
|
return {};
|
|
2582
2914
|
}
|
|
2583
2915
|
return parsed.value;
|
|
2584
2916
|
}
|
|
2585
2917
|
validateBehavior(cfg) {
|
|
2586
|
-
if (cfg.version === void 0) throw new
|
|
2587
|
-
|
|
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
|
+
});
|
|
2588
2928
|
const c = cfg.context;
|
|
2589
|
-
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
|
+
});
|
|
2590
2934
|
const fields = ["warnThreshold", "softThreshold", "hardThreshold"];
|
|
2591
2935
|
for (const f of fields) {
|
|
2592
2936
|
const v = c[f];
|
|
2593
2937
|
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
2594
|
-
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
|
+
});
|
|
2595
2943
|
}
|
|
2596
2944
|
}
|
|
2597
2945
|
if (c.warnThreshold >= c.softThreshold || c.softThreshold >= c.hardThreshold) {
|
|
2598
|
-
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
|
+
});
|
|
2599
2951
|
}
|
|
2600
2952
|
if (c.mode !== void 0 && !isContextWindowModeId(c.mode)) {
|
|
2601
2953
|
const known = listContextWindowModes().map((m) => m.id).join(", ");
|
|
@@ -2607,12 +2959,18 @@ var DefaultConfigLoader = class {
|
|
|
2607
2959
|
}
|
|
2608
2960
|
validateIdentity(cfg) {
|
|
2609
2961
|
if (!cfg.provider) {
|
|
2610
|
-
throw new
|
|
2611
|
-
"Config: no provider configured. Run `wstack init` or set WRONGSTACK_PROVIDER."
|
|
2612
|
-
|
|
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
|
+
});
|
|
2613
2967
|
}
|
|
2614
2968
|
if (!cfg.model) {
|
|
2615
|
-
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
|
+
});
|
|
2616
2974
|
}
|
|
2617
2975
|
}
|
|
2618
2976
|
};
|
|
@@ -2710,7 +3068,10 @@ var RecoveryLock = class {
|
|
|
2710
3068
|
if (this.sessionStore) {
|
|
2711
3069
|
try {
|
|
2712
3070
|
const data = await this.sessionStore.load(lock.sessionId);
|
|
2713
|
-
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
|
+
);
|
|
2714
3075
|
if (closed) return null;
|
|
2715
3076
|
messageCount = data.messages.length;
|
|
2716
3077
|
} catch {
|
|
@@ -3114,6 +3475,27 @@ function renderPlainText(meta, events) {
|
|
|
3114
3475
|
}
|
|
3115
3476
|
return lines.join("\n");
|
|
3116
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
|
|
3117
3499
|
var FILE_VERSION = 1;
|
|
3118
3500
|
var MAX_TEXT_LENGTH = 2e3;
|
|
3119
3501
|
var MAX_ANNOTATIONS = 1e3;
|
|
@@ -3151,13 +3533,28 @@ var AnnotationsStore = class {
|
|
|
3151
3533
|
async add(input) {
|
|
3152
3534
|
const text = input.text.trim();
|
|
3153
3535
|
if (text.length === 0) {
|
|
3154
|
-
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
|
+
});
|
|
3155
3542
|
}
|
|
3156
3543
|
if (text.length > MAX_TEXT_LENGTH) {
|
|
3157
|
-
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
|
+
});
|
|
3158
3550
|
}
|
|
3159
3551
|
if (!Number.isInteger(input.atEventIndex) || input.atEventIndex < 0) {
|
|
3160
|
-
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
|
+
});
|
|
3161
3558
|
}
|
|
3162
3559
|
const annotation = {
|
|
3163
3560
|
id: randomUUID(),
|
|
@@ -3220,10 +3617,7 @@ var AnnotationsStore = class {
|
|
|
3220
3617
|
}
|
|
3221
3618
|
// ── Internals ──────────────────────────────────────────────────────────
|
|
3222
3619
|
filePath(sessionId) {
|
|
3223
|
-
|
|
3224
|
-
throw new Error(`Invalid sessionId: ${sessionId}`);
|
|
3225
|
-
}
|
|
3226
|
-
return path13.join(this.dir, `${sessionId}.annotations.json`);
|
|
3620
|
+
return sessionScopedPath(this.dir, sessionId, ".annotations.json");
|
|
3227
3621
|
}
|
|
3228
3622
|
async readFile(sessionId) {
|
|
3229
3623
|
const fp = this.filePath(sessionId);
|
|
@@ -3365,32 +3759,46 @@ var ReplayLogStore = class {
|
|
|
3365
3759
|
* by sessionId for stable output. Used by `wstack replay --list`.
|
|
3366
3760
|
*/
|
|
3367
3761
|
async list() {
|
|
3368
|
-
let entries;
|
|
3369
|
-
try {
|
|
3370
|
-
entries = await fsp.readdir(this.dir);
|
|
3371
|
-
} catch (err) {
|
|
3372
|
-
if (err.code === "ENOENT") return [];
|
|
3373
|
-
return [];
|
|
3374
|
-
}
|
|
3375
3762
|
const out = [];
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
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);
|
|
3386
3797
|
return out.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
|
|
3387
3798
|
}
|
|
3388
3799
|
// ── Internals ───────────────────────────────────────────────────────────
|
|
3389
3800
|
filePath(sessionId) {
|
|
3390
|
-
|
|
3391
|
-
throw new Error(`Invalid sessionId: ${sessionId}`);
|
|
3392
|
-
}
|
|
3393
|
-
return path13.join(this.dir, `${sessionId}.replay.jsonl`);
|
|
3801
|
+
return sessionScopedPath(this.dir, sessionId, ".replay.jsonl");
|
|
3394
3802
|
}
|
|
3395
3803
|
async readAll(sessionId) {
|
|
3396
3804
|
const fp = this.filePath(sessionId);
|
|
@@ -3557,29 +3965,40 @@ var SessionRecovery = class {
|
|
|
3557
3965
|
* recent crash first.
|
|
3558
3966
|
*/
|
|
3559
3967
|
async listResumable() {
|
|
3560
|
-
let entries;
|
|
3561
|
-
try {
|
|
3562
|
-
entries = await fsp.readdir(this.dir);
|
|
3563
|
-
} catch (err) {
|
|
3564
|
-
if (err.code === "ENOENT") return [];
|
|
3565
|
-
return [];
|
|
3566
|
-
}
|
|
3567
3968
|
const out = [];
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
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);
|
|
3575
3997
|
return out.sort((a, b) => b.lastEventTs.localeCompare(a.lastEventTs));
|
|
3576
3998
|
}
|
|
3577
3999
|
// ── Internals ──────────────────────────────────────────────────────────
|
|
3578
4000
|
filePath(sessionId) {
|
|
3579
|
-
|
|
3580
|
-
throw new Error(`Invalid sessionId: ${sessionId}`);
|
|
3581
|
-
}
|
|
3582
|
-
return path13.join(this.dir, `${sessionId}.jsonl`);
|
|
4001
|
+
return sessionScopedPath(this.dir, sessionId, ".jsonl");
|
|
3583
4002
|
}
|
|
3584
4003
|
};
|
|
3585
4004
|
var GENESIS_PREV = "0".repeat(64);
|
|
@@ -3605,7 +4024,7 @@ var ToolAuditLog = class {
|
|
|
3605
4024
|
* intentional: the audit log is a record, not a cache.
|
|
3606
4025
|
*/
|
|
3607
4026
|
async record(input) {
|
|
3608
|
-
let entry
|
|
4027
|
+
let entry;
|
|
3609
4028
|
await this.enqueue(input.sessionId, async () => {
|
|
3610
4029
|
await withFileLock(this.filePath(input.sessionId), async () => {
|
|
3611
4030
|
const entries = await this.readAll(input.sessionId);
|
|
@@ -3699,10 +4118,7 @@ var ToolAuditLog = class {
|
|
|
3699
4118
|
}
|
|
3700
4119
|
// ── Internals ────────────────────────────────────────────────────────────
|
|
3701
4120
|
filePath(sessionId) {
|
|
3702
|
-
|
|
3703
|
-
throw new Error(`Invalid sessionId: ${sessionId}`);
|
|
3704
|
-
}
|
|
3705
|
-
return path13.join(this.dir, `${sessionId}.audit.jsonl`);
|
|
4121
|
+
return sessionScopedPath(this.dir, sessionId, ".audit.jsonl");
|
|
3706
4122
|
}
|
|
3707
4123
|
async readAll(sessionId) {
|
|
3708
4124
|
const fp = this.filePath(sessionId);
|
|
@@ -3879,6 +4295,387 @@ var SessionAnalyzer = class {
|
|
|
3879
4295
|
return last - first;
|
|
3880
4296
|
}
|
|
3881
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
|
+
};
|
|
3882
4679
|
var DefaultSessionRewinder = class {
|
|
3883
4680
|
constructor(sessionsDir, projectRoot) {
|
|
3884
4681
|
this.sessionsDir = sessionsDir;
|
|
@@ -3927,7 +4724,11 @@ var DefaultSessionRewinder = class {
|
|
|
3927
4724
|
}
|
|
3928
4725
|
}
|
|
3929
4726
|
if (targetIdx === -1) {
|
|
3930
|
-
throw new
|
|
4727
|
+
throw new SessionError({
|
|
4728
|
+
message: `Checkpoint ${checkpointIndex} not found`,
|
|
4729
|
+
code: ERROR_CODES.SESSION_NOT_FOUND,
|
|
4730
|
+
context: { checkpointIndex }
|
|
4731
|
+
});
|
|
3931
4732
|
}
|
|
3932
4733
|
const snapshotsToRevert = [];
|
|
3933
4734
|
for (let i = targetIdx + 1; i < events.length; i++) {
|
|
@@ -4067,10 +4868,12 @@ async function saveTodosCheckpoint(filePath, sessionId, todos) {
|
|
|
4067
4868
|
try {
|
|
4068
4869
|
await atomicWrite(filePath, JSON.stringify(payload, null, 2), { mode: 384 });
|
|
4069
4870
|
} catch (err) {
|
|
4070
|
-
console.warn(
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
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
|
+
}));
|
|
4074
4877
|
}
|
|
4075
4878
|
}
|
|
4076
4879
|
function attachTodosCheckpoint(state, filePath, sessionId) {
|
|
@@ -4080,7 +4883,13 @@ function attachTodosCheckpoint(state, filePath, sessionId) {
|
|
|
4080
4883
|
const enqueueWrite = (todos) => {
|
|
4081
4884
|
writeChain = writeChain.then(() => saveTodosCheckpoint(filePath, sessionId, todos)).catch((err) => {
|
|
4082
4885
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4083
|
-
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
|
+
}));
|
|
4084
4893
|
});
|
|
4085
4894
|
return writeChain;
|
|
4086
4895
|
};
|
|
@@ -4216,19 +5025,29 @@ function deriveTodosFromPlanItem(plan, idOrIndex, subtasks) {
|
|
|
4216
5025
|
id: `todo_${Date.now()}_plan`,
|
|
4217
5026
|
content: item.title,
|
|
4218
5027
|
status: "in_progress",
|
|
4219
|
-
activeForm: item.title
|
|
5028
|
+
activeForm: item.title,
|
|
5029
|
+
promotedFromPlan: item.id
|
|
4220
5030
|
});
|
|
4221
5031
|
if (subtasks && subtasks.length > 0) {
|
|
4222
5032
|
for (const st of subtasks) {
|
|
4223
5033
|
todos.push({
|
|
4224
5034
|
id: `todo_${Date.now()}_${randomUUID().slice(0, 6)}`,
|
|
4225
5035
|
content: st,
|
|
4226
|
-
status: "pending"
|
|
5036
|
+
status: "pending",
|
|
5037
|
+
promotedFromPlan: item.id
|
|
4227
5038
|
});
|
|
4228
5039
|
}
|
|
4229
5040
|
}
|
|
4230
5041
|
return { plan: updatedPlan, todos };
|
|
4231
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
|
+
}
|
|
4232
5051
|
function attachPlanCheckpoint(_state, _filePath, _sessionId) {
|
|
4233
5052
|
return () => void 0;
|
|
4234
5053
|
}
|
|
@@ -4379,6 +5198,14 @@ async function saveTasks(filePath, tasks) {
|
|
|
4379
5198
|
);
|
|
4380
5199
|
}
|
|
4381
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
|
+
}
|
|
4382
5209
|
async function loadDirectorState(filePath) {
|
|
4383
5210
|
let raw;
|
|
4384
5211
|
try {
|
|
@@ -4576,7 +5403,7 @@ function envFlag(value) {
|
|
|
4576
5403
|
return !/^(0|false|no|off)$/i.test(value.trim());
|
|
4577
5404
|
}
|
|
4578
5405
|
var COLOR = isColorTty();
|
|
4579
|
-
var wrap = (
|
|
5406
|
+
var wrap = (open6, close) => (s) => COLOR ? `\x1B[${open6}m${s}\x1B[${close}m` : s;
|
|
4580
5407
|
var color = {
|
|
4581
5408
|
reset: wrap("0", "0"),
|
|
4582
5409
|
bold: wrap("1", "22"),
|
|
@@ -4606,9 +5433,13 @@ function projectSlug(absRoot) {
|
|
|
4606
5433
|
function slugify(name) {
|
|
4607
5434
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
|
|
4608
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
|
+
}
|
|
4609
5441
|
function resolveWstackPaths(opts) {
|
|
4610
|
-
const
|
|
4611
|
-
const globalRoot = opts.globalRoot ?? path13.join(home, ".wrongstack");
|
|
5442
|
+
const globalRoot = opts.globalRoot ?? (opts.userHome ? path13.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
|
|
4612
5443
|
const hash = projectHash(opts.projectRoot);
|
|
4613
5444
|
const slug = projectSlug(opts.projectRoot);
|
|
4614
5445
|
const projectDir = path13.join(globalRoot, "projects", slug);
|
|
@@ -4648,56 +5479,6 @@ function resolveWstackPaths(opts) {
|
|
|
4648
5479
|
};
|
|
4649
5480
|
}
|
|
4650
5481
|
|
|
4651
|
-
// src/types/errors.ts
|
|
4652
|
-
var ERROR_CODES = {
|
|
4653
|
-
// File system
|
|
4654
|
-
FS_READ_FAILED: "FS_READ_FAILED",
|
|
4655
|
-
FS_ATOMIC_WRITE_FAILED: "FS_ATOMIC_WRITE_FAILED"};
|
|
4656
|
-
var WrongStackError = class extends Error {
|
|
4657
|
-
code;
|
|
4658
|
-
subsystem;
|
|
4659
|
-
severity;
|
|
4660
|
-
recoverable;
|
|
4661
|
-
context;
|
|
4662
|
-
constructor(opts) {
|
|
4663
|
-
super(opts.message, { cause: opts.cause });
|
|
4664
|
-
this.name = "WrongStackError";
|
|
4665
|
-
this.code = opts.code;
|
|
4666
|
-
this.subsystem = opts.subsystem;
|
|
4667
|
-
this.severity = opts.severity ?? "error";
|
|
4668
|
-
this.recoverable = opts.recoverable ?? false;
|
|
4669
|
-
this.context = opts.context;
|
|
4670
|
-
}
|
|
4671
|
-
/**
|
|
4672
|
-
* Render a one-line user-facing description.
|
|
4673
|
-
* Subclasses should override for domain-specific formatting.
|
|
4674
|
-
*/
|
|
4675
|
-
describe() {
|
|
4676
|
-
const ctx = this.context ? ` ${formatContext(this.context)}` : "";
|
|
4677
|
-
return `${this.code}: ${this.message}${ctx}`;
|
|
4678
|
-
}
|
|
4679
|
-
};
|
|
4680
|
-
function formatContext(ctx) {
|
|
4681
|
-
const parts = Object.entries(ctx).filter(([, v]) => v !== void 0).slice(0, 3).map(([k, v]) => `${k}=${String(v)}`);
|
|
4682
|
-
return parts.length > 0 ? `[${parts.join(" ")}]` : "";
|
|
4683
|
-
}
|
|
4684
|
-
var FsError = class extends WrongStackError {
|
|
4685
|
-
path;
|
|
4686
|
-
constructor(opts) {
|
|
4687
|
-
super({
|
|
4688
|
-
message: opts.message,
|
|
4689
|
-
code: opts.code,
|
|
4690
|
-
subsystem: "fs",
|
|
4691
|
-
severity: "error",
|
|
4692
|
-
recoverable: opts.code !== ERROR_CODES.FS_READ_FAILED,
|
|
4693
|
-
context: { path: opts.path, ...opts.context },
|
|
4694
|
-
cause: opts.cause
|
|
4695
|
-
});
|
|
4696
|
-
this.name = "FsError";
|
|
4697
|
-
this.path = opts.path;
|
|
4698
|
-
}
|
|
4699
|
-
};
|
|
4700
|
-
|
|
4701
5482
|
// src/storage/goal-store.ts
|
|
4702
5483
|
var MAX_JOURNAL_ENTRIES = 500;
|
|
4703
5484
|
function goalFilePath(projectRoot) {
|
|
@@ -4715,12 +5496,24 @@ async function loadGoal(filePath) {
|
|
|
4715
5496
|
try {
|
|
4716
5497
|
const parsed = JSON.parse(raw);
|
|
4717
5498
|
if (parsed?.version !== 1 || typeof parsed.goal !== "string" || !Array.isArray(parsed.journal)) {
|
|
4718
|
-
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
|
+
}));
|
|
4719
5506
|
return null;
|
|
4720
5507
|
}
|
|
4721
5508
|
return parsed;
|
|
4722
5509
|
} catch {
|
|
4723
|
-
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
|
+
}));
|
|
4724
5517
|
return null;
|
|
4725
5518
|
}
|
|
4726
5519
|
}
|
|
@@ -5112,7 +5905,12 @@ var CloudSync = class {
|
|
|
5112
5905
|
const res = await fetch(url, init);
|
|
5113
5906
|
if (!res.ok) {
|
|
5114
5907
|
const errText = await res.text();
|
|
5115
|
-
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
|
+
});
|
|
5116
5914
|
}
|
|
5117
5915
|
const text = await res.text();
|
|
5118
5916
|
return text ? JSON.parse(text) : {};
|
|
@@ -5237,20 +6035,35 @@ var CloudSync = class {
|
|
|
5237
6035
|
function resolvePulledCategoryPath(cat, localPath, rel, remotePath) {
|
|
5238
6036
|
const directoryBacked = cat === "skills" || cat === "prompts";
|
|
5239
6037
|
if (!directoryBacked) {
|
|
5240
|
-
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
|
+
});
|
|
5241
6044
|
return localPath;
|
|
5242
6045
|
}
|
|
5243
6046
|
if (!rel) return localPath;
|
|
5244
6047
|
const normalizedRel = path13.normalize(rel);
|
|
5245
6048
|
const traversesUp = normalizedRel === ".." || normalizedRel.startsWith(`..${path13.sep}`);
|
|
5246
6049
|
if (path13.isAbsolute(normalizedRel) || traversesUp) {
|
|
5247
|
-
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
|
+
});
|
|
5248
6056
|
}
|
|
5249
6057
|
const dest = path13.resolve(localPath, normalizedRel);
|
|
5250
6058
|
const root = path13.resolve(localPath);
|
|
5251
|
-
const
|
|
5252
|
-
if (
|
|
5253
|
-
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
|
+
});
|
|
5254
6067
|
}
|
|
5255
6068
|
return dest;
|
|
5256
6069
|
}
|
|
@@ -5305,6 +6118,7 @@ function isAllowed(type, level) {
|
|
|
5305
6118
|
}
|
|
5306
6119
|
function createSessionEventBridge(writer, level = "standard", options = {}) {
|
|
5307
6120
|
const normalizedLevel = level ?? "standard";
|
|
6121
|
+
const resolveWriter = typeof writer === "function" ? writer : () => writer;
|
|
5308
6122
|
const progressCounters = /* @__PURE__ */ new Map();
|
|
5309
6123
|
const toolProgressConfig = options.sampling?.toolProgress ?? {};
|
|
5310
6124
|
const TOOL_PROGRESS_SAMPLE_RATE = toolProgressConfig.sampleRate ?? 8;
|
|
@@ -5329,13 +6143,26 @@ function createSessionEventBridge(writer, level = "standard", options = {}) {
|
|
|
5329
6143
|
return isAllowed(type, normalizedLevel);
|
|
5330
6144
|
},
|
|
5331
6145
|
async append(event) {
|
|
5332
|
-
|
|
6146
|
+
const target = resolveWriter();
|
|
6147
|
+
if (!target) return;
|
|
5333
6148
|
if (!isAllowed(event.type, normalizedLevel)) return;
|
|
5334
6149
|
if (!shouldSample(event)) return;
|
|
5335
6150
|
try {
|
|
5336
|
-
await
|
|
6151
|
+
await target.append(event);
|
|
5337
6152
|
} catch (err) {
|
|
5338
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
|
+
}
|
|
5339
6166
|
}
|
|
5340
6167
|
};
|
|
5341
6168
|
}
|
|
@@ -5360,6 +6187,6 @@ function resolveSessionLoggingConfig(cfg) {
|
|
|
5360
6187
|
};
|
|
5361
6188
|
}
|
|
5362
6189
|
|
|
5363
|
-
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 };
|
|
5364
6191
|
//# sourceMappingURL=index.js.map
|
|
5365
6192
|
//# sourceMappingURL=index.js.map
|