@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.
Files changed (82) hide show
  1. package/dist/{agent-bridge-r9y6gdn4.d.ts → agent-bridge-Cimv7bK7.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-1GeQE_L0.d.ts → agent-subagent-runner-C658wj_c.d.ts} +9 -8
  3. package/dist/{brain-Cp_3GIS2.d.ts → brain-sCZ3lCjq.d.ts} +28 -2
  4. package/dist/{compactor-BueGt7LG.d.ts → compactor-BRfg3QPd.d.ts} +1 -1
  5. package/dist/{config-BaVThgnT.d.ts → config-Koq6f3fs.d.ts} +2 -2
  6. package/dist/{context-C7G_MtLV.d.ts → context-CLz3z_E8.d.ts} +126 -2
  7. package/dist/coordination/index.d.ts +70 -13
  8. package/dist/coordination/index.js +2126 -151
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/defaults/index.d.ts +27 -27
  11. package/dist/defaults/index.js +1328 -354
  12. package/dist/defaults/index.js.map +1 -1
  13. package/dist/execution/index.d.ts +45 -16
  14. package/dist/execution/index.js +367 -59
  15. package/dist/execution/index.js.map +1 -1
  16. package/dist/execution/prompt-enhancer.d.ts +86 -0
  17. package/dist/execution/prompt-enhancer.js +125 -0
  18. package/dist/execution/prompt-enhancer.js.map +1 -0
  19. package/dist/extension/index.d.ts +6 -6
  20. package/dist/extension/index.js +3 -1
  21. package/dist/extension/index.js.map +1 -1
  22. package/dist/{goal-preamble-CYJLg0wk.d.ts → goal-preamble-CnbzyVvl.d.ts} +19 -10
  23. package/dist/{index-BZdezm3g.d.ts → index-BlMqh5GO.d.ts} +8 -8
  24. package/dist/{index-CPweVoFM.d.ts → index-C2eSNPsB.d.ts} +7 -5
  25. package/dist/index.d.ts +439 -129
  26. package/dist/index.js +5206 -905
  27. package/dist/index.js.map +1 -1
  28. package/dist/infrastructure/index.d.ts +7 -7
  29. package/dist/infrastructure/index.js +72 -15
  30. package/dist/infrastructure/index.js.map +1 -1
  31. package/dist/kernel/index.d.ts +9 -9
  32. package/dist/kernel/index.js +7 -1
  33. package/dist/kernel/index.js.map +1 -1
  34. package/dist/{llm-selector-CP72f1lC.d.ts → llm-selector-D22R4AFz.d.ts} +2 -2
  35. package/dist/logger-DmmQhf4P.d.ts +65 -0
  36. package/dist/{mcp-servers-Bl5LTvQg.d.ts → mcp-servers-DFbirBv6.d.ts} +11 -4
  37. package/dist/models/index.d.ts +5 -5
  38. package/dist/models/index.js +89 -9
  39. package/dist/models/index.js.map +1 -1
  40. package/dist/{models-registry-D90K9UnM.d.ts → models-registry-CnJRjTXc.d.ts} +1 -1
  41. package/dist/{multi-agent-coordinator-QWEzJDlm.d.ts → multi-agent-coordinator-60weDZoA.d.ts} +8 -8
  42. package/dist/{null-fleet-bus-BUyfqh23.d.ts → null-fleet-bus-1068dEnr.d.ts} +7 -7
  43. package/dist/observability/index.d.ts +2 -2
  44. package/dist/package-outdated-watcher-pzJ5w7y8.d.ts +560 -0
  45. package/dist/{parallel-eternal-engine-C75QuhAI.d.ts → parallel-eternal-engine-DtG1fjc9.d.ts} +13 -9
  46. package/dist/{path-resolver-DRjQBkoO.d.ts → path-resolver-CA1ULU0J.d.ts} +3 -3
  47. package/dist/{permission-B7nKnEvQ.d.ts → permission-DbWPbuoA.d.ts} +1 -1
  48. package/dist/{permission-policy-8-6zBmfA.d.ts → permission-policy-AOk0LVsV.d.ts} +2 -2
  49. package/dist/pipeline-DsmlwTXu.d.ts +493 -0
  50. package/dist/{plan-templates-CkKNPU3I.d.ts → plan-templates-DPABrDvy.d.ts} +19 -8
  51. package/dist/{provider-runner-BNpuIyOL.d.ts → provider-runner-D0HgUqwV.d.ts} +3 -3
  52. package/dist/{retry-policy-rutAfVeR.d.ts → retry-policy-BVnkbMET.d.ts} +1 -1
  53. package/dist/sdd/index.d.ts +8 -8
  54. package/dist/sdd/index.js +358 -85
  55. package/dist/sdd/index.js.map +1 -1
  56. package/dist/{secret-vault-DoISxaKO.d.ts → secret-vault-BJDY28ev.d.ts} +7 -1
  57. package/dist/{secret-vault-BTcC_T5v.d.ts → secret-vault-CeVNiy_f.d.ts} +4 -3
  58. package/dist/security/index.d.ts +6 -5
  59. package/dist/security/index.js +214 -35
  60. package/dist/security/index.js.map +1 -1
  61. package/dist/{selector-4vDFZKt3.d.ts → selector-Cb4_9-hf.d.ts} +1 -1
  62. package/dist/{session-event-bridge-DWlvglC2.d.ts → session-event-bridge-BhtkkFFy.d.ts} +4 -2
  63. package/dist/{session-reader-BAtCxdaw.d.ts → session-reader-CCOssnBS.d.ts} +1 -1
  64. package/dist/skills/index.js +171 -21
  65. package/dist/skills/index.js.map +1 -1
  66. package/dist/storage/index.d.ts +151 -13
  67. package/dist/storage/index.js +1117 -256
  68. package/dist/storage/index.js.map +1 -1
  69. package/dist/types/index.d.ts +68 -21
  70. package/dist/types/index.js +616 -74
  71. package/dist/types/index.js.map +1 -1
  72. package/dist/utils/expect-defined.js +3 -1
  73. package/dist/utils/expect-defined.js.map +1 -1
  74. package/dist/utils/index.d.ts +80 -4
  75. package/dist/utils/index.js +100 -15
  76. package/dist/utils/index.js.map +1 -1
  77. package/dist/{wstack-paths-DD50Omgn.d.ts → wstack-paths-CJjEwPXn.d.ts} +14 -1
  78. package/package.json +7 -3
  79. package/skills/chimera/SKILL.md +105 -0
  80. package/skills/research-web/SKILL.md +342 -0
  81. package/dist/logger-B9J5puGM.d.ts +0 -32
  82. package/dist/pipeline-BG7UgbDc.d.ts +0 -239
@@ -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
- throw new Error("Expected value to be defined");
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((resolve4) => setTimeout(resolve4, 25));
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((resolve4) => setTimeout(resolve4, delays[i]));
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(`[session-store] handle.close() failed: ${e}`));
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
- { resumed: true, dir: this.dir, filePath: file, secretScrubber: this.secretScrubber, onClose: (s) => this.appendToIndex(s) }
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(`[session-store] handle.close() failed: ${e}`));
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
- return { metadata: meta, events, messages, usage };
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
- `[session-store] Failed to write manifest for "${id}":`,
484
- err instanceof Error ? err.message : String(err)
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
- await fsp.unlink(this.sessionPath(id, ".jsonl")).catch((err) => console.warn(`[session-store] delete .jsonl failed: ${err}`));
496
- await fsp.unlink(this.sessionPath(id, ".summary.json")).catch((err) => console.warn(`[session-store] delete .summary.json failed: ${err}`));
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
- await fsp.rm(sessDir, { recursive: true, force: true }).catch((err) => console.warn(`[session-store] delete session dir failed: ${err}`));
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.endsWith(".jsonl")) continue;
526
- const jsonlPath = path13.join(dateDir, file.name);
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.find((e) => e.type === "session_end");
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 content = [
664
- {
665
- type: "tool_result",
666
- tool_use_id: e.id,
667
- content: typeof e.content === "string" ? e.content : JSON.stringify(e.content),
668
- is_error: e.isError
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
- if (last && last.role === "user") {
673
- if (Array.isArray(last.content)) {
674
- last.content.push(...content);
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
- var FileSessionWriter = class {
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
- closing = false;
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
- initDone = false;
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
- if (this.filePath) {
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
- if (!this.initDone) {
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.handle.appendFile(`${JSON.stringify(scrubbed)}
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] append failed:",
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.closing) return;
860
- this.closing = true;
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(`[QueueStore] failed to read queue file "${this.file}":`, err instanceof Error ? err.message : String(err));
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(`QueueStore.clear() failed for ${this.file}: ${err.message}`);
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 : `# WrongStack Memory
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: writeFile6, mkdir: mkdir6 } = await import('fs/promises');
1720
- await mkdir6(this.backupDir, { recursive: true });
1721
- await writeFile6(`${this.backupDir}/${scope}.md`, content, "utf8");
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 Error(`ConfigStore.update: version must remain 1, got ${String(next.version)}`);
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("[config-store] watcher threw:", err);
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 isPrimitiveArray(a) {
2391
- return a.every((v) => v === null || typeof v !== "object");
2392
- }
2393
- var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set([
2394
- "__proto__",
2395
- "constructor",
2396
- "prototype",
2397
- "__defineGetter__",
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 out;
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 = deepMerge(cfg, global);
2447
- cfg = deepMerge(cfg, local);
2448
- cfg = deepMerge(cfg, inProject);
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 = deepMerge(cfg, patch);
2807
+ cfg = deepMerge2(cfg, patch);
2463
2808
  }
2464
2809
  } catch (err) {
2465
- console.warn(`Config source "${src.name}" failed`, err);
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 = deepMerge(cfg, opts.cliFlags);
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("[config] Failed to load sync config:", err);
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(`[config] Failed to read "${file}":`, err);
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
- `[config] Failed to parse "${file}": invalid JSON. Falling back to defaults for this layer.`
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 Error("Config: missing version field");
2553
- if (cfg.version !== 1) throw new Error(`Config: unsupported version ${cfg.version}`);
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 Error("Config: missing context section");
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 Error(`Config: context.${String(f)} must be a finite number (got ${typeof v})`);
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 Error("Config: context thresholds must satisfy warn < soft < hard");
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 Error(
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 Error("Config: no model configured. Run `wstack init` or set WRONGSTACK_MODEL.");
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 closed = data.events.some((e) => e.type === "session_end");
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 Error("Annotation text must be non-empty");
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 Error(`Annotation text exceeds ${MAX_TEXT_LENGTH} chars (got ${text.length})`);
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 Error("atEventIndex must be a non-negative integer");
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
- if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
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
- for (const name of entries) {
3343
- if (!name.endsWith(".replay.jsonl")) continue;
3344
- const sessionId = name.slice(0, -".replay.jsonl".length);
3345
- const all = await this.load(sessionId);
3346
- out.push({
3347
- sessionId,
3348
- entryCount: all.length,
3349
- path: path13.join(this.dir, name)
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
- if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
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
- for (const name of entries) {
3535
- if (!name.endsWith(".jsonl")) continue;
3536
- const sessionId = name.slice(0, -".jsonl".length);
3537
- if (sessionId.includes(".replay") || sessionId.includes(".annotations")) continue;
3538
- const stale = await this.detectStale(sessionId);
3539
- if (stale) out.push(stale);
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
- if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
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 = null;
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
- if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
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 Error(`Checkpoint ${checkpointIndex} not found`);
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
- "[todos-checkpoint] save failed:",
4038
- err instanceof Error ? err.message : String(err)
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(`[TodosCheckpoint] save failed for session ${sessionId}: ${msg}`);
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 = (open5, close) => (s) => COLOR ? `\x1B[${open5}m${s}\x1B[${close}m` : s;
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 home = opts.userHome ?? os.homedir();
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(`[goal-store] Corrupt goal.json at ${filePath} \u2014 invalid schema. Consider deleting it and re-creating.`);
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(`[goal-store] Corrupt goal.json at ${filePath} \u2014 JSON parse failed. Consider deleting it and re-creating.`);
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 Error(`GitHub API ${method} ${pathSegment} failed (${res.status}): ${errText}`);
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 Error(`Refusing nested CloudSync path for file category: ${remotePath}`);
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 Error(`Refusing CloudSync path traversal: ${remotePath}`);
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 relative3 = path13.relative(root, dest);
5218
- if (relative3.startsWith("..") || path13.isAbsolute(relative3)) {
5219
- throw new Error(`Refusing CloudSync path outside category root: ${remotePath}`);
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
- if (!writer) return;
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 writer.append(event);
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