@wrongstack/core 0.270.0 → 0.272.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 (76) hide show
  1. package/dist/{agent-bridge-PcHQl_UQ.d.ts → agent-bridge-jVSZiygR.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-SHJW7t8q.d.ts → agent-subagent-runner-DOLIwBRo.d.ts} +7 -7
  3. package/dist/{brain-BYcK__Ym.d.ts → brain-CdbbJWi3.d.ts} +71 -1
  4. package/dist/{compactor-C2RKEBtC.d.ts → compactor-72ug-ZRB.d.ts} +1 -1
  5. package/dist/{config-C_ae2k86.d.ts → config-D2DGoGSQ.d.ts} +29 -2
  6. package/dist/{context-Dp87Bcaq.d.ts → context-Dw55zZ_Q.d.ts} +110 -1
  7. package/dist/coordination/index.d.ts +121 -17
  8. package/dist/coordination/index.js +738 -74
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/defaults/index.d.ts +25 -25
  11. package/dist/defaults/index.js +599 -86
  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-CQj_C9Dp.d.ts} +139 -3
  21. package/dist/{goal-preamble-CA_4yiGQ.d.ts → goal-preamble-ZXDjjR1y.d.ts} +9 -9
  22. package/dist/{goal-store-DhuJoUNG.d.ts → goal-store-CcJBd-g1.d.ts} +1 -1
  23. package/dist/hq/index.d.ts +93 -6
  24. package/dist/hq/index.js +616 -46
  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-CZQ6Pwbs.d.ts → index-BL7BAx0p.d.ts} +8 -8
  28. package/dist/{index-W4VJCzHa.d.ts → index-Qo4kTzgw.d.ts} +5 -5
  29. package/dist/index.d.ts +96 -56
  30. package/dist/index.js +1938 -349
  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-DS-YUXvF.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-DP6pGHet.d.ts} +1 -1
  42. package/dist/{multi-agent-coordinator-CJSpTe5O.d.ts → multi-agent-coordinator-BvbdNQ14.d.ts} +1 -1
  43. package/dist/{null-fleet-bus-QVshIsDx.d.ts → null-fleet-bus-BxTfXBKo.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-Cf-GTegR.d.ts} +9 -9
  46. package/dist/{path-resolver-CnQ8SIfh.d.ts → path-resolver-DztfnFcv.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-sNIkhXeB.d.ts} +2 -2
  50. package/dist/{plan-templates-NtPgyeJA.d.ts → plan-templates-DYiKFmEb.d.ts} +11 -5
  51. package/dist/{provider-model-resolve-d5poT5y0.d.ts → provider-model-resolve-dYAbTs_i.d.ts} +3 -3
  52. package/dist/{provider-runner-gkctlQV_.d.ts → provider-runner-Dw8x0F7u.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 +45 -13
  65. package/dist/storage/index.js +374 -113
  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 = {}) {
@@ -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;
@@ -681,15 +683,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
681
683
  });
682
684
  return indexed.slice(0, limit);
683
685
  }
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);
686
+ return await this.listFromDirectoryScan(limit);
693
687
  } catch {
694
688
  return [];
695
689
  }
@@ -814,43 +808,102 @@ var DefaultSessionStore = class _DefaultSessionStore {
814
808
  this._indexCache = null;
815
809
  return valid.length;
816
810
  }
811
+ async listFromDirectoryScan(limit) {
812
+ const refs = await this.collectSessionFiles(this.dir);
813
+ const candidates = await mapWithConcurrency(
814
+ refs,
815
+ _DefaultSessionStore.LIST_SCAN_CONCURRENCY,
816
+ async (ref) => {
817
+ const manifest = await this.readSummaryManifest(ref.id);
818
+ if (manifest) return { summary: manifest, needsBackfill: false };
819
+ const summary = await this.summaryHeaderFor(ref);
820
+ return summary ? { summary, needsBackfill: true } : null;
821
+ }
822
+ );
823
+ const out = candidates.filter((s) => s !== null);
824
+ out.sort((a, b) => compareSessionSummaries(a.summary, b.summary));
825
+ const selected = out.slice(0, limit);
826
+ const summaries = await mapWithConcurrency(
827
+ selected,
828
+ Math.min(_DefaultSessionStore.LIST_SCAN_CONCURRENCY, Math.max(1, limit)),
829
+ async (candidate) => {
830
+ if (!candidate.needsBackfill) return candidate.summary;
831
+ return await this.summaryFor(candidate.summary.id).catch(() => candidate.summary);
832
+ }
833
+ );
834
+ return summaries.filter((s) => s !== null);
835
+ }
836
+ async collectSessionFiles(dir, prefix = "", depth = 0) {
837
+ let entries;
838
+ try {
839
+ entries = await fsp.readdir(dir, { withFileTypes: true });
840
+ } catch {
841
+ return [];
842
+ }
843
+ const dirEntries = [];
844
+ const files = [];
845
+ for (const entry of entries) {
846
+ if (entry.name.startsWith(".") && entry.name !== ".wrongstack") continue;
847
+ if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments")
848
+ continue;
849
+ if (entry.isDirectory()) {
850
+ dirEntries.push(entry);
851
+ } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
852
+ if (entry.name === "_index.jsonl") continue;
853
+ const base = entry.name.replace(/\.jsonl$/, "");
854
+ const id = prefix ? `${prefix}/${base}` : base;
855
+ files.push({ id, filePath: path2.join(dir, entry.name) });
856
+ }
857
+ }
858
+ const childFileArrays = await Promise.all(
859
+ dirEntries.map((entry) => {
860
+ const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
861
+ return this.collectSessionFiles(path2.join(dir, entry.name), childPrefix, depth + 1);
862
+ })
863
+ );
864
+ return [...childFileArrays.flat(), ...files];
865
+ }
817
866
  /** Recursively collect session IDs from date-shard subdirectories.
818
867
  * IDs include the date-prefix path (e.g. "2026-06-06/17-46-57Z_…").
819
868
  * Skips `.jsonl`/`.summary.json` root files, dot-files, and
820
869
  * sub-directories that belong to fleet/subagent sessions. */
821
870
  async collectSessionIds(dir, prefix = "", depth = 0) {
822
- const ids = [];
823
871
  let entries;
824
872
  try {
825
873
  entries = await fsp.readdir(dir, { withFileTypes: true });
826
874
  } catch {
827
- return ids;
875
+ return [];
828
876
  }
877
+ const dirEntries = [];
878
+ const fileIds = [];
829
879
  for (const entry of entries) {
830
880
  if (entry.name.startsWith(".") && entry.name !== ".wrongstack") continue;
831
881
  if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments")
832
882
  continue;
833
883
  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));
884
+ dirEntries.push(entry);
836
885
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
837
886
  if (entry.name === "_index.jsonl") continue;
838
887
  const base = entry.name.replace(/\.jsonl$/, "");
839
- ids.push(prefix ? `${prefix}/${base}` : base);
888
+ fileIds.push(prefix ? `${prefix}/${base}` : base);
840
889
  }
841
890
  }
842
- return ids;
891
+ const childIdArrays = await Promise.all(
892
+ dirEntries.map((entry) => {
893
+ const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
894
+ return this.collectSessionIds(path2.join(dir, entry.name), childPrefix, depth + 1);
895
+ })
896
+ );
897
+ return [...childIdArrays.flat(), ...fileIds];
843
898
  }
844
899
  async summaryFor(id) {
845
900
  const manifest = this.sessionPath(id, ".summary.json");
846
901
  const t0 = Date.now();
847
902
  let outcome = "success";
848
903
  let errorMsg;
904
+ const fromManifest = await this.readSummaryManifest(id, t0);
905
+ if (fromManifest) return fromManifest;
849
906
  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
907
  const full = this.sessionPath(id, ".jsonl");
855
908
  const stat7 = await fsp.stat(full);
856
909
  const summary = await this.summarize(id, stat7.mtime.toISOString());
@@ -869,6 +922,78 @@ var DefaultSessionStore = class _DefaultSessionStore {
869
922
  errorMsg = "summary fallback \u2014 manifest rebuilt";
870
923
  this.emitRead(id, manifest, "summary", outcome, Date.now() - t0, errorMsg);
871
924
  return summary;
925
+ } catch (err) {
926
+ outcome = "failure";
927
+ errorMsg = toErrorMessage(err);
928
+ this.emitRead(id, manifest, "summary", outcome, Date.now() - t0, errorMsg);
929
+ return {
930
+ id,
931
+ title: "(damaged)",
932
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
933
+ model: "unknown",
934
+ provider: "unknown",
935
+ tokenTotal: 0
936
+ };
937
+ }
938
+ }
939
+ async readSummaryManifest(id, startTime = Date.now()) {
940
+ const manifest = this.sessionPath(id, ".summary.json");
941
+ try {
942
+ const raw = await fsp.readFile(manifest, "utf8");
943
+ this.emitRead(id, manifest, "summary", "success", Date.now() - startTime);
944
+ return JSON.parse(raw);
945
+ } catch {
946
+ return null;
947
+ }
948
+ }
949
+ async summaryHeaderFor(ref) {
950
+ let mtime = (/* @__PURE__ */ new Date(0)).toISOString();
951
+ try {
952
+ const stat7 = await fsp.stat(ref.filePath);
953
+ if (!stat7.isFile()) {
954
+ return {
955
+ id: ref.id,
956
+ title: "(damaged)",
957
+ startedAt: stat7.mtime.toISOString(),
958
+ model: "unknown",
959
+ provider: "unknown",
960
+ tokenTotal: 0
961
+ };
962
+ }
963
+ mtime = stat7.mtime.toISOString();
964
+ } catch {
965
+ return null;
966
+ }
967
+ try {
968
+ for await (const event of this.iterSessionEvents(ref.filePath)) {
969
+ if (event.type === "session_start") {
970
+ return {
971
+ id: ref.id,
972
+ title: "(empty session)",
973
+ startedAt: event.ts,
974
+ model: event.model ?? "unknown",
975
+ provider: event.provider ?? "unknown",
976
+ tokenTotal: 0
977
+ };
978
+ }
979
+ }
980
+ return {
981
+ id: ref.id,
982
+ title: "(empty session)",
983
+ startedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
984
+ model: "unknown",
985
+ provider: "unknown",
986
+ tokenTotal: 0
987
+ };
988
+ } catch {
989
+ return {
990
+ id: ref.id,
991
+ title: "(damaged)",
992
+ startedAt: mtime,
993
+ model: "unknown",
994
+ provider: "unknown",
995
+ tokenTotal: 0
996
+ };
872
997
  }
873
998
  }
874
999
  /**
@@ -993,39 +1118,62 @@ var DefaultSessionStore = class _DefaultSessionStore {
993
1118
  }
994
1119
  async summarize(id, mtime) {
995
1120
  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)";
1121
+ const file = this.sessionPath(id, ".jsonl");
1122
+ let title = "(empty session)";
1123
+ let startedAt = (/* @__PURE__ */ new Date(0)).toISOString();
1124
+ let endedAt;
1125
+ let model = "unknown";
1126
+ let provider = "unknown";
1127
+ let tokenIn = 0;
1128
+ let tokenOut = 0;
999
1129
  let iterationCount = 0;
1000
1130
  let toolCallCount = 0;
1001
1131
  let toolErrorCount = 0;
1002
1132
  let fileChangeCount = 0;
1003
1133
  const toolBreakdown = {};
1004
1134
  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++;
1135
+ let lastEventType;
1136
+ let hasError = false;
1137
+ let sawStart = false;
1138
+ for await (const e of this.iterSessionEvents(file)) {
1139
+ lastEventType = e.type;
1140
+ if (e.type === "session_start") {
1141
+ if (!sawStart) {
1142
+ sawStart = true;
1143
+ startedAt = e.ts;
1144
+ model = e.model ?? "unknown";
1145
+ provider = e.provider ?? "unknown";
1146
+ }
1147
+ } else if (e.type === "session_end") {
1148
+ endedAt = e.ts;
1149
+ } else if (e.type === "user_input") {
1150
+ if (title === "(empty session)") title = userInputTitle(e.content);
1151
+ } else if (e.type === "llm_response") {
1152
+ tokenIn += e.usage.input ?? 0;
1153
+ tokenOut += e.usage.output ?? 0;
1154
+ } else if (e.type === "in_flight_start") iterationCount++;
1008
1155
  else if (e.type === "tool_call_start") {
1009
1156
  toolCallCount++;
1010
1157
  toolBreakdown[e.name] = (toolBreakdown[e.name] ?? 0) + 1;
1011
1158
  } else if (e.type === "tool_result" && e.isError) toolErrorCount++;
1012
1159
  else if (e.type === "file_snapshot") fileChangeCount += e.files.length;
1160
+ else if (e.type === "error" || e.type === "provider_error") hasError = true;
1013
1161
  }
1014
- if (lastEvent?.type === "session_end") {
1162
+ if (lastEventType === "session_end") {
1015
1163
  outcome = "completed";
1016
- } else if (lastEvent?.type === "in_flight_start") {
1164
+ } else if (lastEventType === "in_flight_start") {
1017
1165
  outcome = "aborted";
1018
- } else if (data.events.some((e) => e.type === "error")) {
1166
+ } else if (hasError) {
1019
1167
  outcome = "error";
1020
1168
  }
1021
1169
  return {
1022
1170
  id,
1023
1171
  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,
1172
+ startedAt,
1173
+ endedAt,
1174
+ model,
1175
+ provider,
1176
+ tokenTotal: tokenIn + tokenOut,
1029
1177
  iterationCount: iterationCount > 0 ? iterationCount : void 0,
1030
1178
  toolCallCount: toolCallCount > 0 ? toolCallCount : void 0,
1031
1179
  toolErrorCount: toolErrorCount > 0 ? toolErrorCount : void 0,
@@ -1044,6 +1192,25 @@ var DefaultSessionStore = class _DefaultSessionStore {
1044
1192
  };
1045
1193
  }
1046
1194
  }
1195
+ async *iterSessionEvents(file) {
1196
+ const stream = createReadStream(file, { encoding: "utf8" });
1197
+ const lines = createInterface({ input: stream, crlfDelay: Infinity });
1198
+ try {
1199
+ for await (const line of lines) {
1200
+ if (!line.trim()) continue;
1201
+ try {
1202
+ const parsed = JSON.parse(line);
1203
+ if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
1204
+ yield parsed;
1205
+ }
1206
+ } catch {
1207
+ }
1208
+ }
1209
+ } finally {
1210
+ lines.close();
1211
+ stream.destroy();
1212
+ }
1213
+ }
1047
1214
  metaFromEvents(id, events) {
1048
1215
  const start = events.find((e) => e.type === "session_start");
1049
1216
  const end = events.findLast((e) => e.type === "session_end");
@@ -1707,6 +1874,27 @@ function userInputTitle(content) {
1707
1874
  const text = typeof content === "string" ? content : content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
1708
1875
  return (text || "(non-text input)").slice(0, 60);
1709
1876
  }
1877
+ function compareSessionSummaries(a, b) {
1878
+ if (a.startedAt < b.startedAt) return 1;
1879
+ if (a.startedAt > b.startedAt) return -1;
1880
+ return a.id.localeCompare(b.id);
1881
+ }
1882
+ async function mapWithConcurrency(items, concurrency, fn) {
1883
+ if (items.length === 0) return [];
1884
+ const out = new Array(items.length);
1885
+ let next = 0;
1886
+ const workerCount = Math.min(Math.max(1, concurrency), items.length);
1887
+ const workers = Array.from({ length: workerCount }, async () => {
1888
+ for (; ; ) {
1889
+ const idx = next++;
1890
+ if (idx >= items.length) return;
1891
+ const item = items[idx];
1892
+ if (item !== void 0) out[idx] = await fn(item);
1893
+ }
1894
+ });
1895
+ await Promise.all(workers);
1896
+ return out;
1897
+ }
1710
1898
  var QueueStore = class {
1711
1899
  file;
1712
1900
  // Use `| undefined` (not `?`) so exactOptionalPropertyTypes doesn't
@@ -2767,6 +2955,12 @@ var GraphMemoryBackend = class {
2767
2955
  edges = [];
2768
2956
  loadedScope = null;
2769
2957
  loaded = false;
2958
+ /**
2959
+ * Promise that resolves when the current in-flight _saveGraph completes.
2960
+ * Tests call flush() to await this before deleting the backend or its temp dir.
2961
+ * Each save operation chains onto the previous one so concurrent saves are serialised.
2962
+ */
2963
+ _saveDone = Promise.resolve();
2770
2964
  constructor(opts) {
2771
2965
  this.file = new FileMemoryBackend({ paths: opts.paths });
2772
2966
  this.graphFile = opts.graphPath ?? `${opts.paths.projectDir}/memory-graph.json`;
@@ -2793,7 +2987,8 @@ var GraphMemoryBackend = class {
2793
2987
  tags: entry.tags,
2794
2988
  priority: entry.priority
2795
2989
  });
2796
- for (const [, other] of this.nodes) {
2990
+ const recentNodes = [...this.nodes.values()].slice(-100);
2991
+ for (const other of recentNodes) {
2797
2992
  if (other.id === nodeId) continue;
2798
2993
  const sim = wordOverlap(entry.text, other.entry.text);
2799
2994
  const tagSim = sharedTags(entry.tags ?? [], other.tags ?? []);
@@ -2809,7 +3004,8 @@ var GraphMemoryBackend = class {
2809
3004
  }
2810
3005
  }
2811
3006
  }
2812
- await this.saveGraph(scope);
3007
+ this._saveDone = this._saveGraph(scope);
3008
+ await this._saveDone;
2813
3009
  }
2814
3010
  async forget(scope, query, filePath) {
2815
3011
  const removed = await this.file.forget(scope, query, filePath);
@@ -2824,7 +3020,8 @@ var GraphMemoryBackend = class {
2824
3020
  }
2825
3021
  for (const id of toRemove) this.nodes.delete(id);
2826
3022
  this.edges = this.edges.filter((e) => !toRemove.includes(e.from) && !toRemove.includes(e.to));
2827
- await this.saveGraph(scope);
3023
+ this._saveDone = this._saveGraph(scope);
3024
+ await this._saveDone;
2828
3025
  }
2829
3026
  return removed;
2830
3027
  }
@@ -2936,7 +3133,8 @@ var GraphMemoryBackend = class {
2936
3133
  this.loadedScope = scope;
2937
3134
  this.loaded = true;
2938
3135
  }
2939
- async saveGraph(scope) {
3136
+ /** Fire-and-forget graph persistence. Named _saveGraph to signal it must not be awaited. */
3137
+ async _saveGraph(scope) {
2940
3138
  this.loadedScope = scope;
2941
3139
  this.loaded = true;
2942
3140
  try {
@@ -2944,16 +3142,21 @@ var GraphMemoryBackend = class {
2944
3142
  nodes: [...this.nodes.entries()],
2945
3143
  edges: this.edges
2946
3144
  };
2947
- await fsp.mkdir(
2948
- this.graphFile.substring(0, this.graphFile.lastIndexOf("/")),
2949
- { recursive: true }
2950
- );
3145
+ const dir = this.graphFile.substring(0, this.graphFile.lastIndexOf("/"));
3146
+ await fsp.mkdir(dir, { recursive: true });
2951
3147
  const tmp = `${this.graphFile}.tmp`;
2952
3148
  await fsp.writeFile(tmp, JSON.stringify(data));
2953
3149
  await fsp.rename(tmp, this.graphFile);
2954
3150
  } catch {
2955
3151
  }
2956
3152
  }
3153
+ /**
3154
+ * Wait for all in-flight _saveGraph operations to complete.
3155
+ * Call this before deleting the backend or its temp directory.
3156
+ */
3157
+ async flush() {
3158
+ await this._saveDone;
3159
+ }
2957
3160
  };
2958
3161
  function wordOverlap(a, b) {
2959
3162
  const wordsA = new Set(a.toLowerCase().split(/\s+/).filter((w) => w.length > 2));
@@ -3054,84 +3257,89 @@ var SessionMemoryConsolidator = class {
3054
3257
  this.minIterations = opts.minIterations ?? 2;
3055
3258
  this.maxExistingEntries = opts.maxExistingEntries ?? 15;
3056
3259
  }
3057
- afterRun = async (ctx, result) => {
3260
+ afterRun = (ctx, result) => {
3058
3261
  if (result.status !== "done") return;
3059
3262
  if (!result.finalText || result.finalText.trim().length < 20) return;
3060
3263
  if (result.iterations < this.minIterations) return;
3061
3264
  const provider = this.provider ?? ctx.provider;
3062
3265
  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++;
3266
+ const _finalText = result.finalText;
3267
+ const _iterations = result.iterations;
3268
+ const _model = this.model ?? ctx.model;
3269
+ void (async () => {
3270
+ try {
3271
+ const existingEntries = await this.memoryStore.list("project-memory", this.maxExistingEntries);
3272
+ const prompt = buildConsolidationPrompt(
3273
+ _finalText,
3274
+ _iterations,
3275
+ existingEntries
3276
+ );
3277
+ const signal = AbortSignal.timeout(15e3);
3278
+ const response = await provider.complete(
3279
+ {
3280
+ model: _model,
3281
+ system: [{ type: "text", text: prompt }],
3282
+ messages: [
3283
+ { role: "user", content: "Review the session and return memory operations as JSON." }
3284
+ ],
3285
+ maxTokens: 500
3286
+ },
3287
+ { signal }
3288
+ );
3289
+ const text = response.content.filter((b) => b.type === "text").map((b) => b.text).join("").trim();
3290
+ if (!text) return;
3291
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
3292
+ if (!jsonMatch) return;
3293
+ const parsed = JSON.parse(jsonMatch[0]);
3294
+ if (!Array.isArray(parsed.operations) || parsed.operations.length === 0) return;
3295
+ let added = 0;
3296
+ let edited = 0;
3297
+ let deleted = 0;
3298
+ for (const op of parsed.operations) {
3299
+ switch (op.action) {
3300
+ case "add": {
3301
+ if (op.text?.trim()) {
3302
+ await this.memoryStore.remember(op.text.trim(), void 0, {
3303
+ type: op.type,
3304
+ tags: op.tags,
3305
+ priority: op.priority
3306
+ });
3307
+ added++;
3308
+ }
3309
+ break;
3101
3310
  }
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++;
3311
+ case "edit": {
3312
+ if (op.query && op.text?.trim()) {
3313
+ await this.memoryStore.forget(op.query);
3314
+ await this.memoryStore.remember(op.text.trim(), void 0, {
3315
+ type: op.type,
3316
+ tags: op.tags,
3317
+ priority: op.priority
3318
+ });
3319
+ edited++;
3320
+ }
3321
+ break;
3113
3322
  }
3114
- break;
3115
- }
3116
- case "delete": {
3117
- if (op.query) {
3118
- const n = await this.memoryStore.forget(op.query);
3119
- deleted += n;
3323
+ case "delete": {
3324
+ if (op.query) {
3325
+ const n = await this.memoryStore.forget(op.query);
3326
+ deleted += n;
3327
+ }
3328
+ break;
3120
3329
  }
3121
- break;
3122
3330
  }
3123
3331
  }
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(", ")}
3332
+ if (added > 0 || edited > 0 || deleted > 0) {
3333
+ const parts = [];
3334
+ if (added) parts.push(`${added} added`);
3335
+ if (edited) parts.push(`${edited} edited`);
3336
+ if (deleted) parts.push(`${deleted} deleted`);
3337
+ process.stderr.write(`[memory] Session consolidation: ${parts.join(", ")}
3131
3338
  `);
3339
+ }
3340
+ } catch {
3132
3341
  }
3133
- } catch {
3134
- }
3342
+ })();
3135
3343
  };
3136
3344
  };
3137
3345
 
@@ -3297,8 +3505,13 @@ function deepFreeze(obj) {
3297
3505
  return Object.freeze(obj);
3298
3506
  }
3299
3507
  var KEY_BYTES = 32;
3508
+ var IV_BYTES = 12;
3509
+ var TAG_BYTES = 16;
3300
3510
  var KEY_FILE_MAGIC = Buffer.from("WSKV", "ascii");
3301
3511
  KEY_FILE_MAGIC.length + 1 + KEY_BYTES;
3512
+ var KEK_MAGIC = Buffer.from("WSKW", "ascii");
3513
+ var KEK_SALT_BYTES = 16;
3514
+ KEK_MAGIC.length + 1 + KEK_SALT_BYTES + IV_BYTES + TAG_BYTES + KEY_BYTES;
3302
3515
  function decryptConfigSecrets(cfg, vault, opts) {
3303
3516
  const warn = ((msg) => console.warn(msg));
3304
3517
  return walk(cfg, vault, (v, key) => {
@@ -3514,6 +3727,42 @@ var defaultIndexing = {
3514
3727
  watchExternal: true,
3515
3728
  debounceMs: 400
3516
3729
  };
3730
+ var IN_PROJECT_FORBIDDEN_KEYS = /* @__PURE__ */ new Set([
3731
+ "provider",
3732
+ "apiKey",
3733
+ "baseUrl",
3734
+ "providers",
3735
+ "mcpServers",
3736
+ "hooks",
3737
+ "plugins",
3738
+ "sync",
3739
+ "yolo",
3740
+ "extensions"
3741
+ ]);
3742
+ function stripUnsafeInProjectFields(inProject, sourcePath, warn = (msg) => console.warn(msg)) {
3743
+ const stripped = [];
3744
+ const out = {};
3745
+ for (const [k, v] of Object.entries(inProject)) {
3746
+ if (IN_PROJECT_FORBIDDEN_KEYS.has(k)) {
3747
+ stripped.push(k);
3748
+ continue;
3749
+ }
3750
+ out[k] = v;
3751
+ }
3752
+ if (stripped.length > 0) {
3753
+ warn(
3754
+ JSON.stringify({
3755
+ level: "warn",
3756
+ event: "config.in_project_unsafe_fields_ignored",
3757
+ path: sourcePath,
3758
+ ignoredKeys: stripped,
3759
+ 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.`,
3760
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3761
+ })
3762
+ );
3763
+ }
3764
+ return out;
3765
+ }
3517
3766
  function deepMerge2(base, patch) {
3518
3767
  const opts = { arrayMode: "concat-primitives" };
3519
3768
  if (envBoolOptional(process.env.WRONGSTACK_DEBUG_CONFIG)) {
@@ -3549,7 +3798,7 @@ var DefaultConfigLoader = class {
3549
3798
  ]);
3550
3799
  cfg = deepMerge2(cfg, global);
3551
3800
  cfg = deepMerge2(cfg, local);
3552
- cfg = deepMerge2(cfg, inProject);
3801
+ cfg = deepMerge2(cfg, stripUnsafeInProjectFields(inProject, this.paths.inProjectConfig));
3553
3802
  for (const [key, fn] of Object.entries(ENV_MAP)) {
3554
3803
  const v = process.env[key];
3555
3804
  if (v) fn(cfg, v);
@@ -5814,6 +6063,9 @@ var AgentStatusTracker = class {
5814
6063
  leaderName;
5815
6064
  // Live agent map: agentId → AgentEntry
5816
6065
  agents = /* @__PURE__ */ new Map();
6066
+ // Last full agent list flushed (leader + subagents). Lets external consumers
6067
+ // read the current state synchronously without re-deriving it.
6068
+ lastAgents = [];
5817
6069
  // Leader tracking
5818
6070
  leaderStatus = "idle";
5819
6071
  leaderCurrentTool;
@@ -5835,6 +6087,10 @@ var AgentStatusTracker = class {
5835
6087
  this.leaderName = opts.leaderName ?? "leader";
5836
6088
  this.onUpdate = opts.onUpdate;
5837
6089
  }
6090
+ /** Current full agent list (leader + subagents) as of the last flush. */
6091
+ getAgents() {
6092
+ return this.lastAgents.length > 0 ? [...this.lastAgents] : [];
6093
+ }
5838
6094
  start() {
5839
6095
  this.unsubscribers.push(
5840
6096
  this.events.onPattern("agent.run.started", () => {
@@ -6076,6 +6332,11 @@ var AgentStatusTracker = class {
6076
6332
  lastActivityAt: (/* @__PURE__ */ new Date()).toISOString()
6077
6333
  };
6078
6334
  const allAgents = [leaderEntry, ...this.agents.values()];
6335
+ this.lastAgents = allAgents;
6336
+ try {
6337
+ this.events.emit("session.agents_updated", { agents: allAgents });
6338
+ } catch {
6339
+ }
6079
6340
  this.registry.updateAgents(allAgents).then(() => {
6080
6341
  try {
6081
6342
  this.onUpdate?.();
@@ -7831,6 +8092,6 @@ function resolveSessionLoggingConfig(cfg) {
7831
8092
  };
7832
8093
  }
7833
8094
 
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 };
8095
+ 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
8096
  //# sourceMappingURL=index.js.map
7836
8097
  //# sourceMappingURL=index.js.map