@wrongstack/core 0.155.0 → 0.250.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 (81) hide show
  1. package/dist/{agent-bridge-BbZU5TPN.d.ts → agent-bridge-4gc0vfW2.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-Bsueu0J2.d.ts → agent-subagent-runner-Dz-9kiE6.d.ts} +9 -8
  3. package/dist/{brain-CS_B0vIE.d.ts → brain-sCZ3lCjq.d.ts} +26 -2
  4. package/dist/{compactor-BueGt7LG.d.ts → compactor-BRfg3QPd.d.ts} +1 -1
  5. package/dist/{config-BaVThgnT.d.ts → config-eSsrto5d.d.ts} +8 -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 +1986 -146
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/defaults/index.d.ts +26 -26
  11. package/dist/defaults/index.js +1110 -296
  12. package/dist/defaults/index.js.map +1 -1
  13. package/dist/execution/index.d.ts +45 -16
  14. package/dist/execution/index.js +229 -56
  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-CbV8pXLD.d.ts → goal-preamble-BjJpnLW4.d.ts} +19 -10
  23. package/dist/{index-CI1hRfPt.d.ts → index-Dy8OwfBD.d.ts} +8 -8
  24. package/dist/{index-B5wz-GXm.d.ts → index-IehiNryU.d.ts} +7 -5
  25. package/dist/index.d.ts +438 -128
  26. package/dist/index.js +4989 -849
  27. package/dist/index.js.map +1 -1
  28. package/dist/infrastructure/index.d.ts +7 -7
  29. package/dist/infrastructure/index.js +61 -13
  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-CPERR2De.d.ts → mcp-servers-DfXxCASH.d.ts} +3 -3
  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-DpanBg8D.d.ts} +1 -1
  41. package/dist/{multi-agent-coordinator-BSKSFNhv.d.ts → multi-agent-coordinator-CnbEqpv0.d.ts} +8 -8
  42. package/dist/{null-fleet-bus-CGOez8Le.d.ts → null-fleet-bus-Do1OLYpj.d.ts} +7 -7
  43. package/dist/observability/index.d.ts +2 -2
  44. package/dist/package-outdated-watcher-CA5GGB4C.d.ts +560 -0
  45. package/dist/{parallel-eternal-engine-CYoTKjsz.d.ts → parallel-eternal-engine-UZg1xOzE.d.ts} +13 -9
  46. package/dist/{path-resolver-DuhlmPil.d.ts → path-resolver-BaP06Owy.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-D1n-gQI-.d.ts +493 -0
  50. package/dist/{plan-templates-DbH7lg-t.d.ts → plan-templates-BUVRY0pU.d.ts} +18 -7
  51. package/dist/{provider-runner-Cocq0O9E.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 +221 -87
  55. package/dist/sdd/index.js.map +1 -1
  56. package/dist/{secret-vault-w8MbUe2Q.d.ts → secret-vault-CeVNiy_f.d.ts} +3 -2
  57. package/dist/security/index.d.ts +5 -4
  58. package/dist/security/index.js +155 -13
  59. package/dist/security/index.js.map +1 -1
  60. package/dist/{selector-4vDFZKt3.d.ts → selector-Cb4_9-hf.d.ts} +1 -1
  61. package/dist/{session-event-bridge-DWlvglC2.d.ts → session-event-bridge-BhtkkFFy.d.ts} +4 -2
  62. package/dist/{session-reader-BAtCxdaw.d.ts → session-reader-CCOssnBS.d.ts} +1 -1
  63. package/dist/skills/index.js +171 -21
  64. package/dist/skills/index.js.map +1 -1
  65. package/dist/storage/index.d.ts +150 -12
  66. package/dist/storage/index.js +1041 -214
  67. package/dist/storage/index.js.map +1 -1
  68. package/dist/types/index.d.ts +67 -20
  69. package/dist/types/index.js +562 -55
  70. package/dist/types/index.js.map +1 -1
  71. package/dist/utils/expect-defined.js +3 -1
  72. package/dist/utils/expect-defined.js.map +1 -1
  73. package/dist/utils/index.d.ts +25 -4
  74. package/dist/utils/index.js +45 -14
  75. package/dist/utils/index.js.map +1 -1
  76. package/dist/{wstack-paths-DD50Omgn.d.ts → wstack-paths-CJjEwPXn.d.ts} +14 -1
  77. package/package.json +7 -3
  78. package/skills/chimera/SKILL.md +105 -0
  79. package/skills/research-web/SKILL.md +342 -0
  80. package/dist/logger-B9J5puGM.d.ts +0 -32
  81. 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
  };
@@ -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;
@@ -2496,7 +2807,13 @@ var DefaultConfigLoader = class {
2496
2807
  cfg = deepMerge2(cfg, patch);
2497
2808
  }
2498
2809
  } catch (err) {
2499
- 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
+ }));
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("[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
+ }));
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(`[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
+ }));
2573
2901
  }
2574
2902
  return {};
2575
2903
  }
2576
2904
  const parsed = safeParse(raw);
2577
2905
  if (!parsed.ok || !parsed.value) {
2578
- console.warn(
2579
- `[config] Failed to parse "${file}": invalid JSON. Falling back to defaults for this layer.`
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 Error("Config: missing version field");
2587
- 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
+ });
2588
2928
  const c = cfg.context;
2589
- 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
+ });
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 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
+ });
2595
2943
  }
2596
2944
  }
2597
2945
  if (c.warnThreshold >= c.softThreshold || c.softThreshold >= c.hardThreshold) {
2598
- 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
+ });
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 Error(
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 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
+ });
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 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
+ );
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 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
+ });
3155
3542
  }
3156
3543
  if (text.length > MAX_TEXT_LENGTH) {
3157
- 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
+ });
3158
3550
  }
3159
3551
  if (!Number.isInteger(input.atEventIndex) || input.atEventIndex < 0) {
3160
- 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
+ });
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
- if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
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
- for (const name of entries) {
3377
- if (!name.endsWith(".replay.jsonl")) continue;
3378
- const sessionId = name.slice(0, -".replay.jsonl".length);
3379
- const all = await this.load(sessionId);
3380
- out.push({
3381
- sessionId,
3382
- entryCount: all.length,
3383
- path: path13.join(this.dir, name)
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
- if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
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
- for (const name of entries) {
3569
- if (!name.endsWith(".jsonl")) continue;
3570
- const sessionId = name.slice(0, -".jsonl".length);
3571
- if (sessionId.includes(".replay") || sessionId.includes(".annotations")) continue;
3572
- const stale = await this.detectStale(sessionId);
3573
- if (stale) out.push(stale);
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
- if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
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 = null;
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
- if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
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 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
+ });
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
- "[todos-checkpoint] save failed:",
4072
- err instanceof Error ? err.message : String(err)
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(`[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
+ }));
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 = (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;
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 home = opts.userHome ?? os.homedir();
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(`[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
+ }));
4719
5506
  return null;
4720
5507
  }
4721
5508
  return parsed;
4722
5509
  } catch {
4723
- 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
+ }));
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 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
+ });
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 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
+ });
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 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
+ });
5248
6056
  }
5249
6057
  const dest = path13.resolve(localPath, normalizedRel);
5250
6058
  const root = path13.resolve(localPath);
5251
- const relative3 = path13.relative(root, dest);
5252
- if (relative3.startsWith("..") || path13.isAbsolute(relative3)) {
5253
- 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
+ });
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
- if (!writer) return;
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 writer.append(event);
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