@wrongstack/core 0.270.0 → 0.272.1

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 (76) hide show
  1. package/dist/{agent-bridge-PcHQl_UQ.d.ts → agent-bridge-DFQYEeXf.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-SHJW7t8q.d.ts → agent-subagent-runner-BZa_IEcd.d.ts} +7 -7
  3. package/dist/{brain-BYcK__Ym.d.ts → brain-etbcbRwV.d.ts} +95 -2
  4. package/dist/{compactor-C2RKEBtC.d.ts → compactor-72ug-ZRB.d.ts} +1 -1
  5. package/dist/{config-C_ae2k86.d.ts → config-rRS8yorV.d.ts} +71 -2
  6. package/dist/{context-Dp87Bcaq.d.ts → context-Dw55zZ_Q.d.ts} +110 -1
  7. package/dist/coordination/index.d.ts +181 -17
  8. package/dist/coordination/index.js +1018 -166
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/defaults/index.d.ts +25 -25
  11. package/dist/defaults/index.js +804 -222
  12. package/dist/defaults/index.js.map +1 -1
  13. package/dist/execution/index.d.ts +23 -18
  14. package/dist/execution/index.js +136 -41
  15. package/dist/execution/index.js.map +1 -1
  16. package/dist/execution/prompt-enhancer.d.ts +36 -6
  17. package/dist/execution/prompt-enhancer.js +35 -9
  18. package/dist/execution/prompt-enhancer.js.map +1 -1
  19. package/dist/extension/index.d.ts +6 -6
  20. package/dist/{global-mailbox-Bvrz1P3f.d.ts → global-mailbox-DJ4EoRr0.d.ts} +145 -5
  21. package/dist/{goal-preamble-CA_4yiGQ.d.ts → goal-preamble-hM8BH7TK.d.ts} +9 -9
  22. package/dist/{goal-store-DhuJoUNG.d.ts → goal-store-CWlbT0TO.d.ts} +1 -1
  23. package/dist/hq/index.d.ts +95 -6
  24. package/dist/hq/index.js +628 -50
  25. package/dist/hq/index.js.map +1 -1
  26. package/dist/{index-whDfTANu.d.ts → index-2Lhk5v0o.d.ts} +2 -2
  27. package/dist/{index-W4VJCzHa.d.ts → index-DWm_PE9L.d.ts} +5 -5
  28. package/dist/{index-CZQ6Pwbs.d.ts → index-DqW4o62H.d.ts} +8 -8
  29. package/dist/index.d.ts +96 -56
  30. package/dist/index.js +2464 -519
  31. package/dist/index.js.map +1 -1
  32. package/dist/infrastructure/index.d.ts +6 -6
  33. package/dist/infrastructure/index.js +5 -3
  34. package/dist/infrastructure/index.js.map +1 -1
  35. package/dist/kernel/index.d.ts +9 -9
  36. package/dist/kernel/index.js.map +1 -1
  37. package/dist/{mcp-servers-DJdZiRcv.d.ts → mcp-servers-BpWHTKlE.d.ts} +3 -3
  38. package/dist/models/index.d.ts +5 -5
  39. package/dist/models/index.js +28 -5
  40. package/dist/models/index.js.map +1 -1
  41. package/dist/{models-registry-C3a-2-Yd.d.ts → models-registry-CXQFUn5t.d.ts} +1 -1
  42. package/dist/{multi-agent-coordinator-CJSpTe5O.d.ts → multi-agent-coordinator-jyimfo7D.d.ts} +1 -1
  43. package/dist/{null-fleet-bus-QVshIsDx.d.ts → null-fleet-bus-DOGQcvrY.d.ts} +6 -6
  44. package/dist/observability/index.d.ts +2 -2
  45. package/dist/{parallel-eternal-engine-D9y5Pkcc.d.ts → parallel-eternal-engine-rItJBYp9.d.ts} +9 -9
  46. package/dist/{path-resolver-CnQ8SIfh.d.ts → path-resolver-DrpF5MGK.d.ts} +3 -3
  47. package/dist/{permission-CvYQNUqZ.d.ts → permission-CC7XFYWG.d.ts} +1 -1
  48. package/dist/{permission-policy-D5Ss8j4B.d.ts → permission-policy-cYR4RJmw.d.ts} +2 -2
  49. package/dist/{pipeline-l_zzFRh3.d.ts → pipeline-Ckkn3AOA.d.ts} +2 -2
  50. package/dist/{plan-templates-NtPgyeJA.d.ts → plan-templates-BvHw5Znw.d.ts} +33 -9
  51. package/dist/{provider-model-resolve-d5poT5y0.d.ts → provider-model-resolve-nZqnCeaR.d.ts} +3 -3
  52. package/dist/{provider-runner-gkctlQV_.d.ts → provider-runner-zVOn1p67.d.ts} +3 -3
  53. package/dist/{retry-policy-CtFhfwa8.d.ts → retry-policy-BV7nzeAd.d.ts} +1 -1
  54. package/dist/sdd/index.d.ts +8 -8
  55. package/dist/sdd/index.js +2 -0
  56. package/dist/sdd/index.js.map +1 -1
  57. package/dist/{secret-vault-BLsVmTIK.d.ts → secret-vault-eMBKfheR.d.ts} +9 -1
  58. package/dist/security/index.d.ts +5 -5
  59. package/dist/security/index.js +137 -10
  60. package/dist/security/index.js.map +1 -1
  61. package/dist/{selector-CXl2_y9W.d.ts → selector-C4ORTOid.d.ts} +1 -1
  62. package/dist/{session-event-bridge-Ccud20CC.d.ts → session-event-bridge-CeNpUL9w.d.ts} +1 -1
  63. package/dist/{session-reader-ZeXQmsmE.d.ts → session-reader-BepLSnGL.d.ts} +1 -1
  64. package/dist/storage/index.d.ts +50 -13
  65. package/dist/storage/index.js +620 -220
  66. package/dist/storage/index.js.map +1 -1
  67. package/dist/tools/index.d.ts +2 -2
  68. package/dist/tools/index.js +9 -2
  69. package/dist/tools/index.js.map +1 -1
  70. package/dist/types/index.d.ts +19 -19
  71. package/dist/types/index.js +202 -41
  72. package/dist/types/index.js.map +1 -1
  73. package/dist/utils/index.d.ts +17 -4
  74. package/dist/utils/index.js +48 -9
  75. package/dist/utils/index.js.map +1 -1
  76. package/package.json +1 -1
@@ -1,9 +1,10 @@
1
- import { randomBytes, randomUUID, createHash } from 'crypto';
1
+ import { createReadStream } from 'fs';
2
2
  import * as fsp from 'fs/promises';
3
3
  import * as path2 from 'path';
4
+ import { createInterface } from 'readline';
5
+ import { randomBytes, randomUUID, createHash } from 'crypto';
4
6
  import * as os from 'os';
5
7
  import { hostname } from 'os';
6
- import 'fs';
7
8
 
8
9
  // src/storage/session-store.ts
9
10
  async function atomicWrite(targetPath, content, opts = {}) {
@@ -79,7 +80,7 @@ async function withFileLock(targetPath, fn, opts = {}) {
79
80
  if (Date.now() - started >= timeoutMs) {
80
81
  throw new Error(`Timed out waiting for file lock: ${targetPath}`);
81
82
  }
82
- await new Promise((resolve6) => setTimeout(resolve6, 25));
83
+ await new Promise((resolve7) => setTimeout(resolve7, 25));
83
84
  }
84
85
  }
85
86
  try {
@@ -113,7 +114,7 @@ async function renameWithRetry(from, to) {
113
114
  if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
114
115
  throw err;
115
116
  }
116
- await new Promise((resolve6) => setTimeout(resolve6, delays[i]));
117
+ await new Promise((resolve7) => setTimeout(resolve7, delays[i]));
117
118
  }
118
119
  }
119
120
  throw lastErr;
@@ -430,8 +431,6 @@ function resolveWstackPaths(opts) {
430
431
  projectStatus: (projectHash2) => path2.join(globalRoot, "projects", projectHash2, "status.json")
431
432
  };
432
433
  }
433
-
434
- // src/storage/session-store.ts
435
434
  function sanitizeModel(model) {
436
435
  return model.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
437
436
  }
@@ -442,6 +441,8 @@ function generateSessionId(startedAt, model) {
442
441
  const modelPart = model ? `_${sanitizeModel(model)}` : "";
443
442
  return `${date}/${time}Z${modelPart}_${suffix}`;
444
443
  }
444
+
445
+ // src/storage/session-store.ts
445
446
  var DefaultSessionStore = class _DefaultSessionStore {
446
447
  dir;
447
448
  events;
@@ -459,6 +460,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
459
460
  _loadCache = /* @__PURE__ */ new Map();
460
461
  _indexCache = null;
461
462
  static LOAD_CACHE_MAX_ENTRIES = 50;
463
+ static LIST_SCAN_CONCURRENCY = 32;
462
464
  constructor(opts) {
463
465
  this.dir = opts.dir;
464
466
  this.events = opts.events;
@@ -475,7 +477,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
475
477
  this._loadCache.clear();
476
478
  }
477
479
  }
478
- // ── Storage event helpers ───────────────────────────────────────────────────
480
+ // ── Storage event helpers ───────────────────────────────────────────────────
479
481
  emitRead(sessionId, filePath, operation, outcome, durationMs, error) {
480
482
  this.events?.emit("storage.read", {
481
483
  sessionId,
@@ -590,7 +592,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
590
592
  this.events,
591
593
  {
592
594
  resumed: true,
593
- // Shard directory (sessions/<date>/) must match create() so the
595
+ // Shard directory (sessions/<date>/) — must match create() so the
594
596
  // .summary.json sidecar lands next to the JSONL instead of the
595
597
  // sessions root (where summaryFor() would never find it).
596
598
  dir: path2.dirname(file),
@@ -631,19 +633,93 @@ var DefaultSessionStore = class _DefaultSessionStore {
631
633
  const raw = await fsp.readFile(file, "utf8");
632
634
  const lines = raw.split("\n").filter((l) => l.trim());
633
635
  const events = [];
636
+ let sessionStartEvent;
637
+ let sessionEndEvent;
638
+ let sessionModel;
639
+ let sessionProvider;
640
+ let sessionPendingToolUses;
641
+ const messages = [];
642
+ const openToolUses = /* @__PURE__ */ new Set();
643
+ let usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
634
644
  for (const line of lines) {
635
645
  try {
636
646
  const parsed = JSON.parse(line);
637
647
  if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
638
- events.push(parsed);
648
+ const ev = parsed;
649
+ events.push(ev);
650
+ if (ev.type === "session_start" && !sessionStartEvent) {
651
+ sessionStartEvent = ev;
652
+ sessionModel = ev.model;
653
+ sessionProvider = ev.provider;
654
+ }
655
+ if (ev.type === "session_end") {
656
+ sessionEndEvent = ev;
657
+ sessionPendingToolUses = ev.pendingToolUses;
658
+ }
659
+ if (ev.type === "user_input") {
660
+ openToolUses.clear();
661
+ messages.push({ role: "user", content: ev.content, ts: ev.ts });
662
+ } else if (ev.type === "llm_response") {
663
+ messages.push({ role: "assistant", content: ev.content, ts: ev.ts });
664
+ for (const b of ev.content) {
665
+ if (b.type === "tool_use") openToolUses.add(b.id);
666
+ }
667
+ usage = {
668
+ input: usage.input + (ev.usage.input ?? 0),
669
+ output: usage.output + (ev.usage.output ?? 0),
670
+ cacheRead: (usage.cacheRead ?? 0) + (ev.usage.cacheRead ?? 0),
671
+ cacheWrite: (usage.cacheWrite ?? 0) + (ev.usage.cacheWrite ?? 0)
672
+ };
673
+ } else if (ev.type === "tool_result") {
674
+ if (!openToolUses.has(ev.id)) {
675
+ this.events?.emit("session.damaged", {
676
+ sessionId: id,
677
+ detail: `Orphan tool_result "${ev.id}" has no matching tool_use`
678
+ });
679
+ continue;
680
+ }
681
+ openToolUses.delete(ev.id);
682
+ const resultBlock = {
683
+ type: "tool_result",
684
+ tool_use_id: ev.id,
685
+ content: typeof ev.content === "string" ? ev.content : JSON.stringify(ev.content),
686
+ is_error: ev.isError
687
+ };
688
+ const last = messages[messages.length - 1];
689
+ const lastIsToolResultUser = last?.role === "user" && Array.isArray(last.content) && last.content.every((b) => b.type === "tool_result");
690
+ if (lastIsToolResultUser && Array.isArray(last.content)) {
691
+ last.content.push(resultBlock);
692
+ } else {
693
+ messages.push({ role: "user", content: [resultBlock], ts: ev.ts });
694
+ }
695
+ }
639
696
  }
640
697
  } catch {
641
698
  }
642
699
  }
643
- const meta = this.metaFromEvents(id, events);
644
- const { messages, usage } = this.replay(events, id);
700
+ if (openToolUses.size > 0) {
701
+ this.events?.emit("session.damaged", {
702
+ sessionId: id,
703
+ detail: `${openToolUses.size} tool_use blocks without matching results - replay repaired`
704
+ });
705
+ }
706
+ const repaired = repairToolUseAdjacency(messages);
707
+ if (repaired.report.changed) {
708
+ this.events?.emit("session.damaged", {
709
+ sessionId: id,
710
+ detail: `Repaired replay adjacency: removed ${repaired.report.removedToolUses.length} tool_use, ${repaired.report.removedToolResults.length} tool_result, ${repaired.report.removedMessages} empty messages`
711
+ });
712
+ }
713
+ const meta = {
714
+ id,
715
+ startedAt: sessionStartEvent?.ts ?? (/* @__PURE__ */ new Date(0)).toISOString(),
716
+ endedAt: sessionEndEvent?.ts,
717
+ model: sessionModel,
718
+ provider: sessionProvider,
719
+ pendingToolUses: sessionPendingToolUses
720
+ };
645
721
  const toolCallEnds = extractToolCallEnds(events);
646
- const data = { metadata: meta, events, messages, usage, toolCallEnds };
722
+ const data = { metadata: meta, events, messages: repaired.messages, usage, toolCallEnds };
647
723
  if (this._loadCache.size >= _DefaultSessionStore.LOAD_CACHE_MAX_ENTRIES) {
648
724
  const oldest = this._loadCache.keys().next().value;
649
725
  if (oldest !== void 0) {
@@ -681,20 +757,12 @@ var DefaultSessionStore = class _DefaultSessionStore {
681
757
  });
682
758
  return indexed.slice(0, limit);
683
759
  }
684
- const ids = await this.collectSessionIds(this.dir);
685
- const sessions = await Promise.all(ids.map((id) => this.summaryFor(id).catch(() => null)));
686
- const out = sessions.filter((s) => s !== null);
687
- out.sort((a, b) => {
688
- if (a.startedAt < b.startedAt) return 1;
689
- if (a.startedAt > b.startedAt) return -1;
690
- return a.id.localeCompare(b.id);
691
- });
692
- return out.slice(0, limit);
760
+ return await this.listFromDirectoryScan(limit);
693
761
  } catch {
694
762
  return [];
695
763
  }
696
764
  }
697
- // ── Session index (_index.jsonl) ─────────────────────────────────────────
765
+ // ── Session index (_index.jsonl) ─────────────────────────────────────────
698
766
  //
699
767
  // One JSON line per closed session, appended atomically on close().
700
768
  // When a session is deleted, a tombstone {action:"delete",id:"..."} is
@@ -814,43 +882,102 @@ var DefaultSessionStore = class _DefaultSessionStore {
814
882
  this._indexCache = null;
815
883
  return valid.length;
816
884
  }
885
+ async listFromDirectoryScan(limit) {
886
+ const refs = await this.collectSessionFiles(this.dir);
887
+ const candidates = await mapWithConcurrency(
888
+ refs,
889
+ _DefaultSessionStore.LIST_SCAN_CONCURRENCY,
890
+ async (ref) => {
891
+ const manifest = await this.readSummaryManifest(ref.id);
892
+ if (manifest) return { summary: manifest, needsBackfill: false };
893
+ const summary = await this.summaryHeaderFor(ref);
894
+ return summary ? { summary, needsBackfill: true } : null;
895
+ }
896
+ );
897
+ const out = candidates.filter((s) => s !== null);
898
+ out.sort((a, b) => compareSessionSummaries(a.summary, b.summary));
899
+ const selected = out.slice(0, limit);
900
+ const summaries = await mapWithConcurrency(
901
+ selected,
902
+ Math.min(_DefaultSessionStore.LIST_SCAN_CONCURRENCY, Math.max(1, limit)),
903
+ async (candidate) => {
904
+ if (!candidate.needsBackfill) return candidate.summary;
905
+ return await this.summaryFor(candidate.summary.id).catch(() => candidate.summary);
906
+ }
907
+ );
908
+ return summaries.filter((s) => s !== null);
909
+ }
910
+ async collectSessionFiles(dir, prefix = "", depth = 0) {
911
+ let entries;
912
+ try {
913
+ entries = await fsp.readdir(dir, { withFileTypes: true });
914
+ } catch {
915
+ return [];
916
+ }
917
+ const dirEntries = [];
918
+ const files = [];
919
+ for (const entry of entries) {
920
+ if (entry.name.startsWith(".") && entry.name !== ".wrongstack") continue;
921
+ if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments")
922
+ continue;
923
+ if (entry.isDirectory()) {
924
+ dirEntries.push(entry);
925
+ } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
926
+ if (entry.name === "_index.jsonl") continue;
927
+ const base = entry.name.replace(/\.jsonl$/, "");
928
+ const id = prefix ? `${prefix}/${base}` : base;
929
+ files.push({ id, filePath: path2.join(dir, entry.name) });
930
+ }
931
+ }
932
+ const childFileArrays = await Promise.all(
933
+ dirEntries.map((entry) => {
934
+ const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
935
+ return this.collectSessionFiles(path2.join(dir, entry.name), childPrefix, depth + 1);
936
+ })
937
+ );
938
+ return [...childFileArrays.flat(), ...files];
939
+ }
817
940
  /** Recursively collect session IDs from date-shard subdirectories.
818
- * IDs include the date-prefix path (e.g. "2026-06-06/17-46-57Z_").
941
+ * IDs include the date-prefix path (e.g. "2026-06-06/17-46-57Z_…").
819
942
  * Skips `.jsonl`/`.summary.json` root files, dot-files, and
820
943
  * sub-directories that belong to fleet/subagent sessions. */
821
944
  async collectSessionIds(dir, prefix = "", depth = 0) {
822
- const ids = [];
823
945
  let entries;
824
946
  try {
825
947
  entries = await fsp.readdir(dir, { withFileTypes: true });
826
948
  } catch {
827
- return ids;
949
+ return [];
828
950
  }
951
+ const dirEntries = [];
952
+ const fileIds = [];
829
953
  for (const entry of entries) {
830
954
  if (entry.name.startsWith(".") && entry.name !== ".wrongstack") continue;
831
955
  if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments")
832
956
  continue;
833
957
  if (entry.isDirectory()) {
834
- const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
835
- ids.push(...await this.collectSessionIds(path2.join(dir, entry.name), childPrefix, depth + 1));
958
+ dirEntries.push(entry);
836
959
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
837
960
  if (entry.name === "_index.jsonl") continue;
838
961
  const base = entry.name.replace(/\.jsonl$/, "");
839
- ids.push(prefix ? `${prefix}/${base}` : base);
962
+ fileIds.push(prefix ? `${prefix}/${base}` : base);
840
963
  }
841
964
  }
842
- return ids;
965
+ const childIdArrays = await Promise.all(
966
+ dirEntries.map((entry) => {
967
+ const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
968
+ return this.collectSessionIds(path2.join(dir, entry.name), childPrefix, depth + 1);
969
+ })
970
+ );
971
+ return [...childIdArrays.flat(), ...fileIds];
843
972
  }
844
973
  async summaryFor(id) {
845
974
  const manifest = this.sessionPath(id, ".summary.json");
846
975
  const t0 = Date.now();
847
976
  let outcome = "success";
848
977
  let errorMsg;
978
+ const fromManifest = await this.readSummaryManifest(id, t0);
979
+ if (fromManifest) return fromManifest;
849
980
  try {
850
- const raw = await fsp.readFile(manifest, "utf8");
851
- this.emitRead(id, manifest, "summary", "success", Date.now() - t0);
852
- return JSON.parse(raw);
853
- } catch {
854
981
  const full = this.sessionPath(id, ".jsonl");
855
982
  const stat7 = await fsp.stat(full);
856
983
  const summary = await this.summarize(id, stat7.mtime.toISOString());
@@ -866,9 +993,81 @@ var DefaultSessionStore = class _DefaultSessionStore {
866
993
  }));
867
994
  });
868
995
  outcome = "failure";
869
- errorMsg = "summary fallback \u2014 manifest rebuilt";
996
+ errorMsg = "summary fallback \xE2\u20AC\u201D manifest rebuilt";
870
997
  this.emitRead(id, manifest, "summary", outcome, Date.now() - t0, errorMsg);
871
998
  return summary;
999
+ } catch (err) {
1000
+ outcome = "failure";
1001
+ errorMsg = toErrorMessage(err);
1002
+ this.emitRead(id, manifest, "summary", outcome, Date.now() - t0, errorMsg);
1003
+ return {
1004
+ id,
1005
+ title: "(damaged)",
1006
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1007
+ model: "unknown",
1008
+ provider: "unknown",
1009
+ tokenTotal: 0
1010
+ };
1011
+ }
1012
+ }
1013
+ async readSummaryManifest(id, startTime = Date.now()) {
1014
+ const manifest = this.sessionPath(id, ".summary.json");
1015
+ try {
1016
+ const raw = await fsp.readFile(manifest, "utf8");
1017
+ this.emitRead(id, manifest, "summary", "success", Date.now() - startTime);
1018
+ return JSON.parse(raw);
1019
+ } catch {
1020
+ return null;
1021
+ }
1022
+ }
1023
+ async summaryHeaderFor(ref) {
1024
+ let mtime = (/* @__PURE__ */ new Date(0)).toISOString();
1025
+ try {
1026
+ const stat7 = await fsp.stat(ref.filePath);
1027
+ if (!stat7.isFile()) {
1028
+ return {
1029
+ id: ref.id,
1030
+ title: "(damaged)",
1031
+ startedAt: stat7.mtime.toISOString(),
1032
+ model: "unknown",
1033
+ provider: "unknown",
1034
+ tokenTotal: 0
1035
+ };
1036
+ }
1037
+ mtime = stat7.mtime.toISOString();
1038
+ } catch {
1039
+ return null;
1040
+ }
1041
+ try {
1042
+ for await (const event of this.iterSessionEvents(ref.filePath)) {
1043
+ if (event.type === "session_start") {
1044
+ return {
1045
+ id: ref.id,
1046
+ title: "(empty session)",
1047
+ startedAt: event.ts,
1048
+ model: event.model ?? "unknown",
1049
+ provider: event.provider ?? "unknown",
1050
+ tokenTotal: 0
1051
+ };
1052
+ }
1053
+ }
1054
+ return {
1055
+ id: ref.id,
1056
+ title: "(empty session)",
1057
+ startedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
1058
+ model: "unknown",
1059
+ provider: "unknown",
1060
+ tokenTotal: 0
1061
+ };
1062
+ } catch {
1063
+ return {
1064
+ id: ref.id,
1065
+ title: "(damaged)",
1066
+ startedAt: mtime,
1067
+ model: "unknown",
1068
+ provider: "unknown",
1069
+ tokenTotal: 0
1070
+ };
872
1071
  }
873
1072
  }
874
1073
  /**
@@ -993,39 +1192,62 @@ var DefaultSessionStore = class _DefaultSessionStore {
993
1192
  }
994
1193
  async summarize(id, mtime) {
995
1194
  try {
996
- const data = await this.load(id);
997
- const firstUser = data.events.find((e) => e.type === "user_input");
998
- const title = firstUser && firstUser.type === "user_input" ? userInputTitle(firstUser.content) : "(empty session)";
1195
+ const file = this.sessionPath(id, ".jsonl");
1196
+ let title = "(empty session)";
1197
+ let startedAt = (/* @__PURE__ */ new Date(0)).toISOString();
1198
+ let endedAt;
1199
+ let model = "unknown";
1200
+ let provider = "unknown";
1201
+ let tokenIn = 0;
1202
+ let tokenOut = 0;
999
1203
  let iterationCount = 0;
1000
1204
  let toolCallCount = 0;
1001
1205
  let toolErrorCount = 0;
1002
1206
  let fileChangeCount = 0;
1003
1207
  const toolBreakdown = {};
1004
1208
  let outcome;
1005
- const lastEvent = data.events[data.events.length - 1];
1006
- for (const e of data.events) {
1007
- if (e.type === "in_flight_start") iterationCount++;
1209
+ let lastEventType;
1210
+ let hasError = false;
1211
+ let sawStart = false;
1212
+ for await (const e of this.iterSessionEvents(file)) {
1213
+ lastEventType = e.type;
1214
+ if (e.type === "session_start") {
1215
+ if (!sawStart) {
1216
+ sawStart = true;
1217
+ startedAt = e.ts;
1218
+ model = e.model ?? "unknown";
1219
+ provider = e.provider ?? "unknown";
1220
+ }
1221
+ } else if (e.type === "session_end") {
1222
+ endedAt = e.ts;
1223
+ } else if (e.type === "user_input") {
1224
+ if (title === "(empty session)") title = userInputTitle(e.content);
1225
+ } else if (e.type === "llm_response") {
1226
+ tokenIn += e.usage.input ?? 0;
1227
+ tokenOut += e.usage.output ?? 0;
1228
+ } else if (e.type === "in_flight_start") iterationCount++;
1008
1229
  else if (e.type === "tool_call_start") {
1009
1230
  toolCallCount++;
1010
1231
  toolBreakdown[e.name] = (toolBreakdown[e.name] ?? 0) + 1;
1011
1232
  } else if (e.type === "tool_result" && e.isError) toolErrorCount++;
1012
1233
  else if (e.type === "file_snapshot") fileChangeCount += e.files.length;
1234
+ else if (e.type === "error" || e.type === "provider_error") hasError = true;
1013
1235
  }
1014
- if (lastEvent?.type === "session_end") {
1236
+ if (lastEventType === "session_end") {
1015
1237
  outcome = "completed";
1016
- } else if (lastEvent?.type === "in_flight_start") {
1238
+ } else if (lastEventType === "in_flight_start") {
1017
1239
  outcome = "aborted";
1018
- } else if (data.events.some((e) => e.type === "error")) {
1240
+ } else if (hasError) {
1019
1241
  outcome = "error";
1020
1242
  }
1021
1243
  return {
1022
1244
  id,
1023
1245
  title,
1024
- startedAt: data.metadata.startedAt,
1025
- endedAt: data.metadata.endedAt,
1026
- model: data.metadata.model ?? "unknown",
1027
- provider: data.metadata.provider ?? "unknown",
1028
- tokenTotal: data.usage.input + data.usage.output,
1246
+ startedAt,
1247
+ endedAt,
1248
+ model,
1249
+ provider,
1250
+ tokenTotal: tokenIn + tokenOut,
1029
1251
  iterationCount: iterationCount > 0 ? iterationCount : void 0,
1030
1252
  toolCallCount: toolCallCount > 0 ? toolCallCount : void 0,
1031
1253
  toolErrorCount: toolErrorCount > 0 ? toolErrorCount : void 0,
@@ -1044,75 +1266,24 @@ var DefaultSessionStore = class _DefaultSessionStore {
1044
1266
  };
1045
1267
  }
1046
1268
  }
1047
- metaFromEvents(id, events) {
1048
- const start = events.find((e) => e.type === "session_start");
1049
- const end = events.findLast((e) => e.type === "session_end");
1050
- return {
1051
- id,
1052
- startedAt: start?.ts ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1053
- endedAt: end?.ts,
1054
- model: start?.model,
1055
- provider: start?.provider,
1056
- pendingToolUses: end?.pendingToolUses
1057
- };
1058
- }
1059
- replay(events, sessionId = "unknown") {
1060
- const messages = [];
1061
- let usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
1062
- const openToolUses = /* @__PURE__ */ new Set();
1063
- for (const e of events) {
1064
- if (e.type === "user_input") {
1065
- openToolUses.clear();
1066
- messages.push({ role: "user", content: e.content, ts: e.ts });
1067
- } else if (e.type === "llm_response") {
1068
- messages.push({ role: "assistant", content: e.content, ts: e.ts });
1069
- for (const b of e.content) {
1070
- if (b.type === "tool_use") openToolUses.add(b.id);
1071
- }
1072
- usage = {
1073
- input: usage.input + (e.usage.input ?? 0),
1074
- output: usage.output + (e.usage.output ?? 0),
1075
- cacheRead: (usage.cacheRead ?? 0) + (e.usage.cacheRead ?? 0),
1076
- cacheWrite: (usage.cacheWrite ?? 0) + (e.usage.cacheWrite ?? 0)
1077
- };
1078
- } else if (e.type === "tool_result") {
1079
- if (!openToolUses.has(e.id)) {
1080
- this.events?.emit("session.damaged", {
1081
- sessionId,
1082
- detail: `Orphan tool_result "${e.id}" has no matching tool_use`
1083
- });
1084
- continue;
1085
- }
1086
- openToolUses.delete(e.id);
1087
- const resultBlock = {
1088
- type: "tool_result",
1089
- tool_use_id: e.id,
1090
- content: typeof e.content === "string" ? e.content : JSON.stringify(e.content),
1091
- is_error: e.isError
1092
- };
1093
- const last = messages[messages.length - 1];
1094
- const lastIsToolResultUser = last?.role === "user" && Array.isArray(last.content) && last.content.every((b) => b.type === "tool_result");
1095
- if (lastIsToolResultUser && Array.isArray(last.content)) {
1096
- last.content.push(resultBlock);
1097
- } else {
1098
- messages.push({ role: "user", content: [resultBlock], ts: e.ts });
1269
+ async *iterSessionEvents(file) {
1270
+ const stream = createReadStream(file, { encoding: "utf8" });
1271
+ const lines = createInterface({ input: stream, crlfDelay: Infinity });
1272
+ try {
1273
+ for await (const line of lines) {
1274
+ if (!line.trim()) continue;
1275
+ try {
1276
+ const parsed = JSON.parse(line);
1277
+ if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
1278
+ yield parsed;
1279
+ }
1280
+ } catch {
1099
1281
  }
1100
1282
  }
1283
+ } finally {
1284
+ lines.close();
1285
+ stream.destroy();
1101
1286
  }
1102
- if (openToolUses.size > 0) {
1103
- this.events?.emit("session.damaged", {
1104
- sessionId,
1105
- detail: `${openToolUses.size} tool_use blocks without matching results - replay repaired`
1106
- });
1107
- }
1108
- const repaired = repairToolUseAdjacency(messages);
1109
- if (repaired.report.changed) {
1110
- this.events?.emit("session.damaged", {
1111
- sessionId,
1112
- detail: `Repaired replay adjacency: removed ${repaired.report.removedToolUses.length} tool_use, ${repaired.report.removedToolResults.length} tool_result, ${repaired.report.removedMessages} empty messages`
1113
- });
1114
- }
1115
- return { messages: repaired.messages, usage };
1116
1287
  }
1117
1288
  };
1118
1289
  function extractToolCallEnds(events) {
@@ -1172,7 +1343,7 @@ var FileSessionWriter = class _FileSessionWriter {
1172
1343
  /**
1173
1344
  * Lazy session_start/session_resumed init, shared by all appenders.
1174
1345
  * A single promise (not a boolean) so a second append racing the first
1175
- * can't push its event into the buffer BEFORE the first append's event
1346
+ * can't push its event into the buffer BEFORE the first append's event —
1176
1347
  * every appender awaits the same init and resumes in FIFO call order.
1177
1348
  */
1178
1349
  initPromise = null;
@@ -1185,24 +1356,24 @@ var FileSessionWriter = class _FileSessionWriter {
1185
1356
  lastAppendWarnAt = 0;
1186
1357
  secretScrubber;
1187
1358
  onCloseCb;
1188
- /** Implements SessionWriter.traceId propagated from ContextInit.traceId. */
1359
+ /** Implements SessionWriter.traceId — propagated from ContextInit.traceId. */
1189
1360
  traceId;
1190
- // ── Write buffer batches events to reduce per-event disk I/O ─────────
1361
+ // ── Write buffer — batches events to reduce per-event disk I/O ─────────
1191
1362
  //
1192
1363
  // Every append() pushes the scrubbed event into an in-memory buffer instead
1193
1364
  // of calling handle.appendFile() synchronously. The buffer flushes to disk
1194
1365
  // when it reaches FLUSH_SIZE events OR after FLUSH_INTERVAL_MS of inactivity.
1195
1366
  // This cuts the number of disk writes by ~95% without changing the on-disk
1196
- // format the JSONL is still one JSON object per line.
1367
+ // format — the JSONL is still one JSON object per line.
1197
1368
  writeBuffer = [];
1198
1369
  flushTimer = null;
1199
1370
  static FLUSH_INTERVAL_MS = 500;
1200
1371
  static FLUSH_SIZE = 50;
1201
- // ── Write serialization ─────────────────────────────────────────────────
1372
+ // ── Write serialization ─────────────────────────────────────────────────
1202
1373
  //
1203
1374
  // All disk writes are funneled through a FIFO promise chain. Without it,
1204
1375
  // a timer-driven flush racing an explicit flush()/close() issues two
1205
- // concurrent appendFile() calls on the shared O_APPEND handle the kernel
1376
+ // concurrent appendFile() calls on the shared O_APPEND handle — the kernel
1206
1377
  // may complete them out of order (chronology breaks) or, for large
1207
1378
  // batches, interleave partial writes (torn JSONL lines). The chain keeps
1208
1379
  // exactly one write in flight; failures don't break the chain.
@@ -1216,7 +1387,7 @@ var FileSessionWriter = class _FileSessionWriter {
1216
1387
  );
1217
1388
  return write;
1218
1389
  }
1219
- // ── Enriched summary tracking ──────────────────────────────────────────
1390
+ // ── Enriched summary tracking ──────────────────────────────────────────
1220
1391
  iterationCount = 0;
1221
1392
  toolCallCount = 0;
1222
1393
  toolErrorCount = 0;
@@ -1308,7 +1479,7 @@ var FileSessionWriter = class _FileSessionWriter {
1308
1479
  * (user_input, llm_response) call this so they survive SIGKILL/crash
1309
1480
  * instead of sitting in the in-memory buffer for up to 500ms.
1310
1481
  *
1311
- * Idempotent cancels any pending timer and writes whatever has
1482
+ * Idempotent — cancels any pending timer and writes whatever has
1312
1483
  * accumulated in the buffer. Safe to call even when the buffer
1313
1484
  * is empty (no-op).
1314
1485
  */
@@ -1331,7 +1502,7 @@ var FileSessionWriter = class _FileSessionWriter {
1331
1502
  /**
1332
1503
  * Flush all buffered events to disk as a single appendFile call.
1333
1504
  * Errors use the same throttled-warning pattern the old per-event
1334
- * append path used one warning every 5s with a suppressed count.
1505
+ * append path used — one warning every 5s with a suppressed count.
1335
1506
  * On failure the buffer is cleared (events are best-effort, same as
1336
1507
  * the old per-event path where a failed write was silently dropped).
1337
1508
  */
@@ -1514,7 +1685,7 @@ var FileSessionWriter = class _FileSessionWriter {
1514
1685
  /**
1515
1686
  * Truncate the session file to the checkpoint with the given promptIndex,
1516
1687
  * removing all events that follow it. Uses a single-pass byte-offset scan
1517
- * so post-checkpoint content is never read or parsed O(1) memory instead
1688
+ * so post-checkpoint content is never read or parsed — O(1) memory instead
1518
1689
  * of O(N) JSON.parse calls over the full file.
1519
1690
  */
1520
1691
  async truncateToCheckpoint(targetPromptIndex) {
@@ -1671,7 +1842,7 @@ var FileSessionWriter = class _FileSessionWriter {
1671
1842
  await fsp.writeFile(this.filePath, record, "utf8");
1672
1843
  }
1673
1844
  /**
1674
- * Idea #1 write an in-flight marker. The agent loop should call
1845
+ * Idea #1 — write an in-flight marker. The agent loop should call
1675
1846
  * this at the start of each long-running operation; a matching
1676
1847
  * `clearInFlightMarker` follows on clean exit. A stale marker
1677
1848
  * (no end) is what `SessionRecovery.detectStale` looks for.
@@ -1688,9 +1859,9 @@ var FileSessionWriter = class _FileSessionWriter {
1688
1859
  this.events?.emit("in_flight.started", { context, ts: (/* @__PURE__ */ new Date()).toISOString() });
1689
1860
  }
1690
1861
  /**
1691
- * Idea #1 close the in-flight marker. Idempotent in spirit
1862
+ * Idea #1 — close the in-flight marker. Idempotent in spirit
1692
1863
  * (you can call it after a successful iteration even if you
1693
- * didn't open one this round) but the session log records
1864
+ * didn't open one this round) — but the session log records
1694
1865
  * every call so postmortem tooling can see "the agent finished
1695
1866
  * cleanly X times, then died without finishing Y".
1696
1867
  */
@@ -1707,6 +1878,27 @@ function userInputTitle(content) {
1707
1878
  const text = typeof content === "string" ? content : content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
1708
1879
  return (text || "(non-text input)").slice(0, 60);
1709
1880
  }
1881
+ function compareSessionSummaries(a, b) {
1882
+ if (a.startedAt < b.startedAt) return 1;
1883
+ if (a.startedAt > b.startedAt) return -1;
1884
+ return a.id.localeCompare(b.id);
1885
+ }
1886
+ async function mapWithConcurrency(items, concurrency, fn) {
1887
+ if (items.length === 0) return [];
1888
+ const out = new Array(items.length);
1889
+ let next = 0;
1890
+ const workerCount = Math.min(Math.max(1, concurrency), items.length);
1891
+ const workers = Array.from({ length: workerCount }, async () => {
1892
+ for (; ; ) {
1893
+ const idx = next++;
1894
+ if (idx >= items.length) return;
1895
+ const item = items[idx];
1896
+ if (item !== void 0) out[idx] = await fn(item);
1897
+ }
1898
+ });
1899
+ await Promise.all(workers);
1900
+ return out;
1901
+ }
1710
1902
  var QueueStore = class {
1711
1903
  file;
1712
1904
  // Use `| undefined` (not `?`) so exactOptionalPropertyTypes doesn't
@@ -2287,6 +2479,20 @@ var DefaultMemoryStore = class {
2287
2479
  */
2288
2480
  persistBackup;
2289
2481
  backupDir;
2482
+ /**
2483
+ * Per-scope tracked byte sizes — incremented on `remember()`, decremented on
2484
+ * `forget()`, recalculated after `consolidate()`. Eliminates the redundant
2485
+ * readAll() call that previously checked the file size after every write.
2486
+ */
2487
+ _trackedByteSizes = {};
2488
+ /** Result cache for scoreRelevant() — keyed by scope + context hash, TTL 30s. */
2489
+ _scoreCache = /* @__PURE__ */ new Map();
2490
+ /**
2491
+ * Per-entry cached lowercase strings — computed once per scoreRelevant() call,
2492
+ * stored here so repeated scoring of the same entries avoids re-computation.
2493
+ * Cleared on every mutation (remember/forget/consolidate/clear).
2494
+ */
2495
+ _cachedLower = null;
2290
2496
  constructor(opts) {
2291
2497
  this.files = {
2292
2498
  "project-agents": opts.paths.inProjectAgentsFile,
@@ -2320,6 +2526,20 @@ var DefaultMemoryStore = class {
2320
2526
  }
2321
2527
  }
2322
2528
  }
2529
+ /**
2530
+ * Recalculate the tracked byte size for a scope by re-reading the file and
2531
+ * summing the serialized byte length of each line. Called after consolidate()
2532
+ * (which modifies the file) to keep the tracker accurate.
2533
+ */
2534
+ async _recalcTrackedByteSize(scope) {
2535
+ const raw = await this.backend.readAll(scope, this.files[scope]);
2536
+ let total = 0;
2537
+ for (const line of raw.split("\n")) {
2538
+ if (line.trim()) total += Buffer.byteLength(line, "utf8");
2539
+ }
2540
+ this._trackedByteSizes[scope] = total;
2541
+ return total;
2542
+ }
2323
2543
  async readAll() {
2324
2544
  const parts = [];
2325
2545
  for (const scope of ["project-agents", "project-memory", "user-memory"]) {
@@ -2420,6 +2640,7 @@ ${body.trim()}`);
2420
2640
  const t0 = Date.now();
2421
2641
  try {
2422
2642
  await this.backend.remember(scope, entry, filePath);
2643
+ this._scoreCache.clear();
2423
2644
  const dur = Date.now() - t0;
2424
2645
  this.events?.emit("storage.write", {
2425
2646
  sessionId: "~memory~",
@@ -2444,16 +2665,21 @@ ${body.trim()}`);
2444
2665
  });
2445
2666
  throw err;
2446
2667
  }
2447
- const raw = await this.backend.readAll(scope, this.files[scope]);
2448
- if (Buffer.byteLength(raw, "utf8") > MAX_BYTES_TOTAL) {
2668
+ let trackedSize = this._trackedByteSizes[scope];
2669
+ if (trackedSize === void 0) {
2670
+ trackedSize = await this._recalcTrackedByteSize(scope);
2671
+ }
2672
+ if (trackedSize > MAX_BYTES_TOTAL) {
2449
2673
  const removed = await this.backend.consolidate(scope, this.files[scope]);
2450
2674
  if (removed > 0) {
2451
2675
  this.events?.emit("memory.consolidated", {
2452
2676
  scope,
2453
2677
  removed
2454
2678
  });
2679
+ await this._recalcTrackedByteSize(scope);
2455
2680
  }
2456
2681
  }
2682
+ this._trackedByteSizes[scope] = (this._trackedByteSizes[scope] ?? 0) + Buffer.byteLength(JSON.stringify(entry), "utf8");
2457
2683
  await this.mirrorBackup(scope);
2458
2684
  this.events?.emit("memory.remembered", {
2459
2685
  scope,
@@ -2472,16 +2698,30 @@ ${body.trim()}`);
2472
2698
  async scoreRelevant(ctx, scope = "project-memory", limit = 8) {
2473
2699
  const all = await this.list(scope);
2474
2700
  if (all.length === 0) return [];
2701
+ const ctxHash = `${scope}|${ctx.currentTask}|${(ctx.activeSkills ?? []).join(",")}|${(ctx.toolNames ?? []).join(",")}`;
2702
+ const now = Date.now();
2703
+ const TTL_MS = 3e4;
2704
+ const cached = this._scoreCache.get(ctxHash);
2705
+ if (cached && cached.expiresAt > now && cached.entries === all) {
2706
+ return cached.scored.slice(0, Math.min(limit, 15));
2707
+ }
2475
2708
  const taskWords = ctx.currentTask.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
2476
2709
  const skillWords = (ctx.activeSkills ?? []).flatMap((s) => s.split("-"));
2477
2710
  const toolWords = (ctx.toolNames ?? []).flatMap((t) => t.toLowerCase().split("_"));
2478
- const now = Date.now();
2711
+ this._cachedLower = /* @__PURE__ */ new WeakMap();
2479
2712
  const scored = [];
2480
2713
  for (const entry of all) {
2481
2714
  let score = 0;
2482
2715
  const reasons = [];
2483
- const textLower = entry.text.toLowerCase();
2484
- const tagsLower = (entry.tags ?? []).map((t) => t.toLowerCase());
2716
+ let cachedLower = this._cachedLower.get(entry);
2717
+ if (!cachedLower) {
2718
+ cachedLower = {
2719
+ textLower: entry.text.toLowerCase(),
2720
+ tagsLower: (entry.tags ?? []).map((t) => t.toLowerCase())
2721
+ };
2722
+ this._cachedLower.set(entry, cachedLower);
2723
+ }
2724
+ const { textLower, tagsLower } = cachedLower;
2485
2725
  let taskHits = 0;
2486
2726
  for (const w of taskWords) {
2487
2727
  if (textLower.includes(w)) {
@@ -2567,6 +2807,7 @@ ${body.trim()}`);
2567
2807
  const relevant = scored.filter(
2568
2808
  (s) => s.score >= threshold || s.priority === "critical" || s.priority === "high"
2569
2809
  );
2810
+ this._scoreCache.set(ctxHash, { entries: all, scored: relevant, expiresAt: now + TTL_MS });
2570
2811
  return relevant.slice(0, Math.min(limit, 15));
2571
2812
  }
2572
2813
  async forget(query, scope = "project-memory") {
@@ -2576,6 +2817,7 @@ ${body.trim()}`);
2576
2817
  let removed = 0;
2577
2818
  try {
2578
2819
  removed = await this.backend.forget(scope, query, filePath);
2820
+ this._scoreCache.clear();
2579
2821
  const dur = Date.now() - t0;
2580
2822
  this.events?.emit("storage.write", {
2581
2823
  sessionId: "~memory~",
@@ -2607,6 +2849,7 @@ ${body.trim()}`);
2607
2849
  removed
2608
2850
  });
2609
2851
  await this.mirrorBackup(scope);
2852
+ await this._recalcTrackedByteSize(scope);
2610
2853
  }
2611
2854
  return removed;
2612
2855
  });
@@ -2618,6 +2861,7 @@ ${body.trim()}`);
2618
2861
  let removed = 0;
2619
2862
  try {
2620
2863
  removed = await this.backend.consolidate(scope, filePath);
2864
+ this._scoreCache.clear();
2621
2865
  const dur = Date.now() - t0;
2622
2866
  this.events?.emit("storage.write", {
2623
2867
  sessionId: "~memory~",
@@ -2648,6 +2892,7 @@ ${body.trim()}`);
2648
2892
  removed
2649
2893
  });
2650
2894
  await this.mirrorBackup(scope);
2895
+ await this._recalcTrackedByteSize(scope);
2651
2896
  }
2652
2897
  });
2653
2898
  }
@@ -2658,6 +2903,7 @@ ${body.trim()}`);
2658
2903
  const t0 = Date.now();
2659
2904
  try {
2660
2905
  await this.backend.clear(scope, filePath);
2906
+ this._scoreCache.clear();
2661
2907
  const dur = Date.now() - t0;
2662
2908
  this.events?.emit("storage.write", {
2663
2909
  sessionId: "~memory~",
@@ -2684,6 +2930,7 @@ ${body.trim()}`);
2684
2930
  }
2685
2931
  this.events?.emit("memory.cleared", { scope });
2686
2932
  await this.mirrorBackup(scope);
2933
+ this._trackedByteSizes[scope] = 0;
2687
2934
  });
2688
2935
  return;
2689
2936
  }
@@ -2767,6 +3014,12 @@ var GraphMemoryBackend = class {
2767
3014
  edges = [];
2768
3015
  loadedScope = null;
2769
3016
  loaded = false;
3017
+ /**
3018
+ * Promise that resolves when the current in-flight _saveGraph completes.
3019
+ * Tests call flush() to await this before deleting the backend or its temp dir.
3020
+ * Each save operation chains onto the previous one so concurrent saves are serialised.
3021
+ */
3022
+ _saveDone = Promise.resolve();
2770
3023
  constructor(opts) {
2771
3024
  this.file = new FileMemoryBackend({ paths: opts.paths });
2772
3025
  this.graphFile = opts.graphPath ?? `${opts.paths.projectDir}/memory-graph.json`;
@@ -2793,7 +3046,8 @@ var GraphMemoryBackend = class {
2793
3046
  tags: entry.tags,
2794
3047
  priority: entry.priority
2795
3048
  });
2796
- for (const [, other] of this.nodes) {
3049
+ const recentNodes = [...this.nodes.values()].slice(-100);
3050
+ for (const other of recentNodes) {
2797
3051
  if (other.id === nodeId) continue;
2798
3052
  const sim = wordOverlap(entry.text, other.entry.text);
2799
3053
  const tagSim = sharedTags(entry.tags ?? [], other.tags ?? []);
@@ -2809,7 +3063,8 @@ var GraphMemoryBackend = class {
2809
3063
  }
2810
3064
  }
2811
3065
  }
2812
- await this.saveGraph(scope);
3066
+ this._saveDone = this._saveGraph(scope);
3067
+ await this._saveDone;
2813
3068
  }
2814
3069
  async forget(scope, query, filePath) {
2815
3070
  const removed = await this.file.forget(scope, query, filePath);
@@ -2824,7 +3079,8 @@ var GraphMemoryBackend = class {
2824
3079
  }
2825
3080
  for (const id of toRemove) this.nodes.delete(id);
2826
3081
  this.edges = this.edges.filter((e) => !toRemove.includes(e.from) && !toRemove.includes(e.to));
2827
- await this.saveGraph(scope);
3082
+ this._saveDone = this._saveGraph(scope);
3083
+ await this._saveDone;
2828
3084
  }
2829
3085
  return removed;
2830
3086
  }
@@ -2936,7 +3192,8 @@ var GraphMemoryBackend = class {
2936
3192
  this.loadedScope = scope;
2937
3193
  this.loaded = true;
2938
3194
  }
2939
- async saveGraph(scope) {
3195
+ /** Fire-and-forget graph persistence. Named _saveGraph to signal it must not be awaited. */
3196
+ async _saveGraph(scope) {
2940
3197
  this.loadedScope = scope;
2941
3198
  this.loaded = true;
2942
3199
  try {
@@ -2944,16 +3201,21 @@ var GraphMemoryBackend = class {
2944
3201
  nodes: [...this.nodes.entries()],
2945
3202
  edges: this.edges
2946
3203
  };
2947
- await fsp.mkdir(
2948
- this.graphFile.substring(0, this.graphFile.lastIndexOf("/")),
2949
- { recursive: true }
2950
- );
3204
+ const dir = this.graphFile.substring(0, this.graphFile.lastIndexOf("/"));
3205
+ await fsp.mkdir(dir, { recursive: true });
2951
3206
  const tmp = `${this.graphFile}.tmp`;
2952
3207
  await fsp.writeFile(tmp, JSON.stringify(data));
2953
3208
  await fsp.rename(tmp, this.graphFile);
2954
3209
  } catch {
2955
3210
  }
2956
3211
  }
3212
+ /**
3213
+ * Wait for all in-flight _saveGraph operations to complete.
3214
+ * Call this before deleting the backend or its temp directory.
3215
+ */
3216
+ async flush() {
3217
+ await this._saveDone;
3218
+ }
2957
3219
  };
2958
3220
  function wordOverlap(a, b) {
2959
3221
  const wordsA = new Set(a.toLowerCase().split(/\s+/).filter((w) => w.length > 2));
@@ -3054,84 +3316,89 @@ var SessionMemoryConsolidator = class {
3054
3316
  this.minIterations = opts.minIterations ?? 2;
3055
3317
  this.maxExistingEntries = opts.maxExistingEntries ?? 15;
3056
3318
  }
3057
- afterRun = async (ctx, result) => {
3319
+ afterRun = (ctx, result) => {
3058
3320
  if (result.status !== "done") return;
3059
3321
  if (!result.finalText || result.finalText.trim().length < 20) return;
3060
3322
  if (result.iterations < this.minIterations) return;
3061
3323
  const provider = this.provider ?? ctx.provider;
3062
3324
  if (!provider?.complete) return;
3063
- try {
3064
- const existingEntries = await this.memoryStore.list("project-memory", this.maxExistingEntries);
3065
- const prompt = buildConsolidationPrompt(
3066
- result.finalText,
3067
- result.iterations,
3068
- existingEntries
3069
- );
3070
- const signal = AbortSignal.timeout(15e3);
3071
- const response = await provider.complete(
3072
- {
3073
- model: this.model ?? ctx.model,
3074
- system: [{ type: "text", text: prompt }],
3075
- messages: [
3076
- { role: "user", content: "Review the session and return memory operations as JSON." }
3077
- ],
3078
- maxTokens: 500
3079
- },
3080
- { signal }
3081
- );
3082
- const text = response.content.filter((b) => b.type === "text").map((b) => b.text).join("").trim();
3083
- if (!text) return;
3084
- const jsonMatch = text.match(/\{[\s\S]*\}/);
3085
- if (!jsonMatch) return;
3086
- const parsed = JSON.parse(jsonMatch[0]);
3087
- if (!Array.isArray(parsed.operations) || parsed.operations.length === 0) return;
3088
- let added = 0;
3089
- let edited = 0;
3090
- let deleted = 0;
3091
- for (const op of parsed.operations) {
3092
- switch (op.action) {
3093
- case "add": {
3094
- if (op.text?.trim()) {
3095
- await this.memoryStore.remember(op.text.trim(), void 0, {
3096
- type: op.type,
3097
- tags: op.tags,
3098
- priority: op.priority
3099
- });
3100
- added++;
3325
+ const _finalText = result.finalText;
3326
+ const _iterations = result.iterations;
3327
+ const _model = this.model ?? ctx.model;
3328
+ void (async () => {
3329
+ try {
3330
+ const existingEntries = await this.memoryStore.list("project-memory", this.maxExistingEntries);
3331
+ const prompt = buildConsolidationPrompt(
3332
+ _finalText,
3333
+ _iterations,
3334
+ existingEntries
3335
+ );
3336
+ const signal = AbortSignal.timeout(15e3);
3337
+ const response = await provider.complete(
3338
+ {
3339
+ model: _model,
3340
+ system: [{ type: "text", text: prompt }],
3341
+ messages: [
3342
+ { role: "user", content: "Review the session and return memory operations as JSON." }
3343
+ ],
3344
+ maxTokens: 500
3345
+ },
3346
+ { signal }
3347
+ );
3348
+ const text = response.content.filter((b) => b.type === "text").map((b) => b.text).join("").trim();
3349
+ if (!text) return;
3350
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
3351
+ if (!jsonMatch) return;
3352
+ const parsed = JSON.parse(jsonMatch[0]);
3353
+ if (!Array.isArray(parsed.operations) || parsed.operations.length === 0) return;
3354
+ let added = 0;
3355
+ let edited = 0;
3356
+ let deleted = 0;
3357
+ for (const op of parsed.operations) {
3358
+ switch (op.action) {
3359
+ case "add": {
3360
+ if (op.text?.trim()) {
3361
+ await this.memoryStore.remember(op.text.trim(), void 0, {
3362
+ type: op.type,
3363
+ tags: op.tags,
3364
+ priority: op.priority
3365
+ });
3366
+ added++;
3367
+ }
3368
+ break;
3101
3369
  }
3102
- break;
3103
- }
3104
- case "edit": {
3105
- if (op.query && op.text?.trim()) {
3106
- await this.memoryStore.forget(op.query);
3107
- await this.memoryStore.remember(op.text.trim(), void 0, {
3108
- type: op.type,
3109
- tags: op.tags,
3110
- priority: op.priority
3111
- });
3112
- edited++;
3370
+ case "edit": {
3371
+ if (op.query && op.text?.trim()) {
3372
+ await this.memoryStore.forget(op.query);
3373
+ await this.memoryStore.remember(op.text.trim(), void 0, {
3374
+ type: op.type,
3375
+ tags: op.tags,
3376
+ priority: op.priority
3377
+ });
3378
+ edited++;
3379
+ }
3380
+ break;
3113
3381
  }
3114
- break;
3115
- }
3116
- case "delete": {
3117
- if (op.query) {
3118
- const n = await this.memoryStore.forget(op.query);
3119
- deleted += n;
3382
+ case "delete": {
3383
+ if (op.query) {
3384
+ const n = await this.memoryStore.forget(op.query);
3385
+ deleted += n;
3386
+ }
3387
+ break;
3120
3388
  }
3121
- break;
3122
3389
  }
3123
3390
  }
3124
- }
3125
- if (added > 0 || edited > 0 || deleted > 0) {
3126
- const parts = [];
3127
- if (added) parts.push(`${added} added`);
3128
- if (edited) parts.push(`${edited} edited`);
3129
- if (deleted) parts.push(`${deleted} deleted`);
3130
- process.stderr.write(`[memory] Session consolidation: ${parts.join(", ")}
3391
+ if (added > 0 || edited > 0 || deleted > 0) {
3392
+ const parts = [];
3393
+ if (added) parts.push(`${added} added`);
3394
+ if (edited) parts.push(`${edited} edited`);
3395
+ if (deleted) parts.push(`${deleted} deleted`);
3396
+ process.stderr.write(`[memory] Session consolidation: ${parts.join(", ")}
3131
3397
  `);
3398
+ }
3399
+ } catch {
3132
3400
  }
3133
- } catch {
3134
- }
3401
+ })();
3135
3402
  };
3136
3403
  };
3137
3404
 
@@ -3297,8 +3564,13 @@ function deepFreeze(obj) {
3297
3564
  return Object.freeze(obj);
3298
3565
  }
3299
3566
  var KEY_BYTES = 32;
3567
+ var IV_BYTES = 12;
3568
+ var TAG_BYTES = 16;
3300
3569
  var KEY_FILE_MAGIC = Buffer.from("WSKV", "ascii");
3301
3570
  KEY_FILE_MAGIC.length + 1 + KEY_BYTES;
3571
+ var KEK_MAGIC = Buffer.from("WSKW", "ascii");
3572
+ var KEK_SALT_BYTES = 16;
3573
+ KEK_MAGIC.length + 1 + KEK_SALT_BYTES + IV_BYTES + TAG_BYTES + KEY_BYTES;
3302
3574
  function decryptConfigSecrets(cfg, vault, opts) {
3303
3575
  const warn = ((msg) => console.warn(msg));
3304
3576
  return walk(cfg, vault, (v, key) => {
@@ -3514,6 +3786,51 @@ var defaultIndexing = {
3514
3786
  watchExternal: true,
3515
3787
  debounceMs: 400
3516
3788
  };
3789
+ var IN_PROJECT_FORBIDDEN_KEYS = /* @__PURE__ */ new Set([
3790
+ "provider",
3791
+ "apiKey",
3792
+ "baseUrl",
3793
+ "providers",
3794
+ "mcpServers",
3795
+ "hooks",
3796
+ "plugins",
3797
+ "sync",
3798
+ "yolo",
3799
+ "extensions"
3800
+ ]);
3801
+ function stripUnsafeInProjectFields(inProject, sourcePath, warn = (msg) => console.warn(msg)) {
3802
+ const stripped = [];
3803
+ const out = {};
3804
+ for (const [k, v] of Object.entries(inProject)) {
3805
+ if (IN_PROJECT_FORBIDDEN_KEYS.has(k)) {
3806
+ stripped.push(k);
3807
+ continue;
3808
+ }
3809
+ out[k] = v;
3810
+ }
3811
+ if (stripped.length > 0) {
3812
+ warn(
3813
+ JSON.stringify({
3814
+ level: "warn",
3815
+ event: "config.in_project_unsafe_fields_ignored",
3816
+ path: sourcePath,
3817
+ ignoredKeys: stripped,
3818
+ message: `Ignored ${stripped.length} unsafe field(s) from the repo-committed config "${sourcePath}": ${stripped.join(", ")}. These can only be set in your personal ~/.wrongstack/config.json, not in a project-committed file.`,
3819
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3820
+ })
3821
+ );
3822
+ }
3823
+ return out;
3824
+ }
3825
+ function samePath(a, b) {
3826
+ let ra = path2.resolve(a);
3827
+ let rb = path2.resolve(b);
3828
+ if (process.platform === "win32" || process.platform === "darwin") {
3829
+ ra = ra.toLowerCase();
3830
+ rb = rb.toLowerCase();
3831
+ }
3832
+ return ra === rb;
3833
+ }
3517
3834
  function deepMerge2(base, patch) {
3518
3835
  const opts = { arrayMode: "concat-primitives" };
3519
3836
  if (envBoolOptional(process.env.WRONGSTACK_DEBUG_CONFIG)) {
@@ -3542,14 +3859,15 @@ var DefaultConfigLoader = class {
3542
3859
  }
3543
3860
  async load(opts = {}) {
3544
3861
  let cfg = { ...BEHAVIOR_DEFAULTS };
3862
+ const inProjectCollides = samePath(this.paths.inProjectConfig, this.paths.globalConfig) || samePath(this.paths.inProjectConfig, this.paths.projectLocalConfig);
3545
3863
  const [global, local, inProject] = await Promise.all([
3546
3864
  this.readJson(this.paths.globalConfig),
3547
3865
  this.readJson(this.paths.projectLocalConfig),
3548
- this.readJson(this.paths.inProjectConfig)
3866
+ inProjectCollides ? Promise.resolve({}) : this.readJson(this.paths.inProjectConfig)
3549
3867
  ]);
3550
3868
  cfg = deepMerge2(cfg, global);
3551
3869
  cfg = deepMerge2(cfg, local);
3552
- cfg = deepMerge2(cfg, inProject);
3870
+ cfg = deepMerge2(cfg, stripUnsafeInProjectFields(inProject, this.paths.inProjectConfig));
3553
3871
  for (const [key, fn] of Object.entries(ENV_MAP)) {
3554
3872
  const v = process.env[key];
3555
3873
  if (v) fn(cfg, v);
@@ -5814,6 +6132,9 @@ var AgentStatusTracker = class {
5814
6132
  leaderName;
5815
6133
  // Live agent map: agentId → AgentEntry
5816
6134
  agents = /* @__PURE__ */ new Map();
6135
+ // Last full agent list flushed (leader + subagents). Lets external consumers
6136
+ // read the current state synchronously without re-deriving it.
6137
+ lastAgents = [];
5817
6138
  // Leader tracking
5818
6139
  leaderStatus = "idle";
5819
6140
  leaderCurrentTool;
@@ -5825,6 +6146,7 @@ var AgentStatusTracker = class {
5825
6146
  leaderCtxPct;
5826
6147
  leaderModel;
5827
6148
  leaderPartialText = "";
6149
+ leaderStartedAt;
5828
6150
  unsubscribers = [];
5829
6151
  onUpdate;
5830
6152
  sweepTimer = null;
@@ -5835,9 +6157,17 @@ var AgentStatusTracker = class {
5835
6157
  this.leaderName = opts.leaderName ?? "leader";
5836
6158
  this.onUpdate = opts.onUpdate;
5837
6159
  }
6160
+ /** Current full agent list (leader + subagents) as of the last flush. */
6161
+ getAgents() {
6162
+ return this.lastAgents.length > 0 ? [...this.lastAgents] : [];
6163
+ }
5838
6164
  start() {
5839
6165
  this.unsubscribers.push(
5840
- this.events.onPattern("agent.run.started", () => {
6166
+ this.events.onPattern("agent.run.started", (_event, payload) => {
6167
+ const p = payload;
6168
+ this.markLeaderStarted(p?.at);
6169
+ this.captureLeaderContext(p?.ctx);
6170
+ if (p?.model) this.leaderModel = p.model;
5841
6171
  this.leaderStatus = "running";
5842
6172
  this.leaderIterations++;
5843
6173
  this.flush();
@@ -5845,25 +6175,36 @@ var AgentStatusTracker = class {
5845
6175
  );
5846
6176
  this.unsubscribers.push(
5847
6177
  this.events.onPattern("iteration.started", (_e, payload) => {
5848
- const ctx = payload?.ctx;
5849
- if (!ctx) return;
5850
- if (ctx.model) this.leaderModel = ctx.model;
5851
- if (typeof ctx.tokenCount === "number" && typeof ctx.maxContext === "number" && ctx.maxContext > 0) {
5852
- this.leaderCtxPct = Math.round(ctx.tokenCount / ctx.maxContext * 100);
6178
+ const p = payload;
6179
+ const ctx = p?.ctx;
6180
+ this.markLeaderStarted();
6181
+ this.leaderStatus = "running";
6182
+ if (typeof p?.index === "number") {
6183
+ this.leaderIterations = Math.max(this.leaderIterations, p.index + 1);
5853
6184
  }
6185
+ if (!ctx) {
6186
+ this.flush();
6187
+ return;
6188
+ }
6189
+ this.captureLeaderContext(ctx);
5854
6190
  this.flush();
5855
6191
  })
5856
6192
  );
5857
6193
  this.unsubscribers.push(
5858
- this.events.onPattern("agent.run.completed", () => {
5859
- this.leaderStatus = "idle";
6194
+ this.events.onPattern("agent.run.completed", (_event, payload) => {
6195
+ const p = payload;
6196
+ this.captureLeaderContext(p?.ctx);
6197
+ this.leaderStatus = p?.status === "failed" ? "error" : "idle";
5860
6198
  this.leaderCurrentTool = void 0;
5861
6199
  this.leaderPartialText = "";
6200
+ if (this.leaderStatus === "idle") this.leaderStartedAt = void 0;
5862
6201
  this.flush();
5863
6202
  })
5864
6203
  );
5865
6204
  this.unsubscribers.push(
5866
- this.events.onPattern("agent.run.error", () => {
6205
+ this.events.onPattern("agent.run.error", (_event, payload) => {
6206
+ const p = payload;
6207
+ this.captureLeaderContext(p?.ctx);
5867
6208
  this.leaderStatus = "error";
5868
6209
  this.leaderCurrentTool = void 0;
5869
6210
  this.leaderPartialText = "";
@@ -5874,6 +6215,7 @@ var AgentStatusTracker = class {
5874
6215
  this.events.onPattern("tool.started", (_event, payload) => {
5875
6216
  const p = payload;
5876
6217
  if (p?.name) {
6218
+ this.markLeaderStarted();
5877
6219
  this.leaderCurrentTool = p.name;
5878
6220
  this.leaderToolCalls++;
5879
6221
  }
@@ -5889,12 +6231,14 @@ var AgentStatusTracker = class {
5889
6231
  );
5890
6232
  this.unsubscribers.push(
5891
6233
  this.events.onPattern("brain.ask_human", () => {
6234
+ this.markLeaderStarted();
5892
6235
  this.leaderStatus = "waiting_user";
5893
6236
  this.flush();
5894
6237
  })
5895
6238
  );
5896
6239
  this.unsubscribers.push(
5897
6240
  this.events.onPattern("llm.stream_started", () => {
6241
+ this.markLeaderStarted();
5898
6242
  this.leaderStatus = "streaming";
5899
6243
  this.leaderPartialText = "";
5900
6244
  this.flush();
@@ -5902,14 +6246,42 @@ var AgentStatusTracker = class {
5902
6246
  );
5903
6247
  this.unsubscribers.push(
5904
6248
  this.events.onPattern("provider.text_delta", (_e, payload) => {
5905
- const text = payload?.text;
6249
+ const p = payload;
6250
+ const text = p?.text;
5906
6251
  if (!text) return;
6252
+ this.markLeaderStarted();
6253
+ this.captureLeaderContext(p?.ctx);
5907
6254
  this.leaderStatus = "streaming";
5908
6255
  const next = this.leaderPartialText + text;
5909
6256
  this.leaderPartialText = next.length > PARTIAL_TEXT_CAP ? next.slice(next.length - PARTIAL_TEXT_CAP) : next;
5910
6257
  this.schedulePartialFlush();
5911
6258
  })
5912
6259
  );
6260
+ this.unsubscribers.push(
6261
+ this.events.onPattern("provider.response", (_e, payload) => {
6262
+ const p = payload;
6263
+ this.captureLeaderContext(p?.ctx);
6264
+ this.flush();
6265
+ })
6266
+ );
6267
+ this.unsubscribers.push(
6268
+ this.events.onPattern("provider.fallback", (_e, payload) => {
6269
+ const p = payload;
6270
+ if (p?.to?.model) {
6271
+ this.leaderModel = p.to.providerId ? `${p.to.providerId}/${p.to.model}` : p.to.model;
6272
+ this.flush();
6273
+ }
6274
+ })
6275
+ );
6276
+ this.unsubscribers.push(
6277
+ this.events.onPattern("ctx.pct", (_e, payload) => {
6278
+ const p = payload;
6279
+ if (typeof p?.load === "number" && Number.isFinite(p.load)) {
6280
+ this.leaderCtxPct = Math.round(p.load * 100);
6281
+ this.flush();
6282
+ }
6283
+ })
6284
+ );
5913
6285
  this.unsubscribers.push(
5914
6286
  this.events.onPattern("token.accounted", (_e, payload) => {
5915
6287
  const p = payload;
@@ -5923,7 +6295,8 @@ var AgentStatusTracker = class {
5923
6295
  const touch = (id) => {
5924
6296
  let entry = this.agents.get(id);
5925
6297
  if (!entry) {
5926
- entry = { id, name: id, status: "idle", iterations: 0, toolCalls: 0, lastActivityAt: (/* @__PURE__ */ new Date()).toISOString() };
6298
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6299
+ entry = { id, name: id, status: "idle", iterations: 0, toolCalls: 0, startedAt: now, lastActivityAt: now };
5927
6300
  this.agents.set(id, entry);
5928
6301
  }
5929
6302
  entry.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -5936,6 +6309,7 @@ var AgentStatusTracker = class {
5936
6309
  const entry = touch(p.subagentId);
5937
6310
  entry.name = p.name?.trim() || entry.name;
5938
6311
  if (p.model) entry.model = p.model;
6312
+ if (!entry.startedAt) entry.startedAt = (/* @__PURE__ */ new Date()).toISOString();
5939
6313
  entry.status = "running";
5940
6314
  this.flush();
5941
6315
  })
@@ -5955,6 +6329,7 @@ var AgentStatusTracker = class {
5955
6329
  if (!p?.subagentId) return;
5956
6330
  const entry = touch(p.subagentId);
5957
6331
  entry.status = "running";
6332
+ if (!entry.startedAt) entry.startedAt = (/* @__PURE__ */ new Date()).toISOString();
5958
6333
  entry.iterations++;
5959
6334
  this.flush();
5960
6335
  })
@@ -5965,6 +6340,7 @@ var AgentStatusTracker = class {
5965
6340
  if (!p?.subagentId) return;
5966
6341
  const entry = touch(p.subagentId);
5967
6342
  entry.status = "running";
6343
+ if (!entry.startedAt) entry.startedAt = (/* @__PURE__ */ new Date()).toISOString();
5968
6344
  entry.currentTool = p.name;
5969
6345
  entry.toolCalls++;
5970
6346
  this.flush();
@@ -5976,6 +6352,7 @@ var AgentStatusTracker = class {
5976
6352
  if (!p?.subagentId) return;
5977
6353
  const entry = touch(p.subagentId);
5978
6354
  entry.status = "running";
6355
+ if (!entry.startedAt) entry.startedAt = (/* @__PURE__ */ new Date()).toISOString();
5979
6356
  if (typeof p.iteration === "number") entry.iterations = p.iteration;
5980
6357
  if (typeof p.toolCalls === "number") entry.toolCalls = p.toolCalls;
5981
6358
  if (typeof p.costUsd === "number") entry.costUsd = p.costUsd;
@@ -6063,6 +6440,7 @@ var AgentStatusTracker = class {
6063
6440
  const leaderEntry = {
6064
6441
  id: "leader",
6065
6442
  name: this.leaderName,
6443
+ startedAt: this.leaderStartedAt,
6066
6444
  status: this.leaderStatus,
6067
6445
  currentTool: this.leaderCurrentTool,
6068
6446
  iterations: this.leaderIterations,
@@ -6076,6 +6454,11 @@ var AgentStatusTracker = class {
6076
6454
  lastActivityAt: (/* @__PURE__ */ new Date()).toISOString()
6077
6455
  };
6078
6456
  const allAgents = [leaderEntry, ...this.agents.values()];
6457
+ this.lastAgents = allAgents;
6458
+ try {
6459
+ this.events.emit("session.agents_updated", { agents: allAgents });
6460
+ } catch {
6461
+ }
6079
6462
  this.registry.updateAgents(allAgents).then(() => {
6080
6463
  try {
6081
6464
  this.onUpdate?.();
@@ -6083,6 +6466,23 @@ var AgentStatusTracker = class {
6083
6466
  }
6084
6467
  }).catch(() => void 0);
6085
6468
  }
6469
+ markLeaderStarted(startedAt) {
6470
+ if (this.leaderStartedAt && (this.leaderStatus === "running" || this.leaderStatus === "streaming" || this.leaderStatus === "waiting_user")) {
6471
+ return;
6472
+ }
6473
+ this.leaderStartedAt = startedAt ?? (/* @__PURE__ */ new Date()).toISOString();
6474
+ }
6475
+ captureLeaderContext(ctx) {
6476
+ if (typeof ctx !== "object" || ctx === null) return;
6477
+ const c = ctx;
6478
+ if (typeof c.model === "string" && c.model.length > 0) this.leaderModel = c.model;
6479
+ const metaLimit = c.meta?.["effectiveMaxContext"];
6480
+ const providerMax = c.provider?.capabilities?.maxContext;
6481
+ const maxContext = typeof metaLimit === "number" && metaLimit > 0 ? metaLimit : typeof providerMax === "number" && providerMax > 0 ? providerMax : void 0;
6482
+ if (typeof c.lastRequestTokens === "number" && c.lastRequestTokens > 0 && maxContext !== void 0) {
6483
+ this.leaderCtxPct = Math.round(c.lastRequestTokens / maxContext * 100);
6484
+ }
6485
+ }
6086
6486
  };
6087
6487
  var INSTANCES_FILE = "webui-instances.json";
6088
6488
  var DISCOVERY_TTL_MS = 2500;
@@ -7831,6 +8231,6 @@ function resolveSessionLoggingConfig(cfg) {
7831
8231
  };
7832
8232
  }
7833
8233
 
7834
- export { ALL_SYNC_CATEGORIES, AgentStatusTracker, AnnotationsStore, CORE_RECONSTRUCT_EVENTS, CloudSync, ConfigMigrationError, DEFAULT_CONFIG_MIGRATIONS, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultMemoryStore, DefaultPromptStore, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DirectorStateCheckpoint, FileMemoryBackend, FleetNotifier, 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 };
8234
+ export { ALL_SYNC_CATEGORIES, AgentStatusTracker, AnnotationsStore, CORE_RECONSTRUCT_EVENTS, CloudSync, ConfigMigrationError, DEFAULT_CONFIG_MIGRATIONS, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultMemoryStore, DefaultPromptStore, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DirectorStateCheckpoint, FileMemoryBackend, FleetNotifier, 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, generateSessionId, getPlanTemplate, getSessionRegistry, goalFilePath, hasSessionRegistry, listPlanTemplates, loadDirectorState, loadGoal, loadPlan, loadTasks, loadTodosCheckpoint, mutatePlan, mutateTasks, parseEntries, parseProgressFromText, recordProgress, removePlanItem, resolveAuditLevel, resolveSessionLoggingConfig, runConfigMigrations, sanitizeModel, saveGoal, savePlan, saveTasks, saveTodosCheckpoint, setPlanItemStatus, setProgress, summarizeUsage };
7835
8235
  //# sourceMappingURL=index.js.map
7836
8236
  //# sourceMappingURL=index.js.map