@wrongstack/core 0.1.4 → 0.1.7

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.
@@ -180,9 +180,11 @@ var DefaultPathResolver = class {
180
180
  ensureInsideRoot(absPath) {
181
181
  const resolved = this.resolve(absPath);
182
182
  if (!this.isInsideRoot(resolved)) {
183
- throw new Error(
184
- `Path "${absPath}" resolves outside the project root (${this.projectRoot})`
185
- );
183
+ const display = path2.isAbsolute(absPath) ? path2.basename(absPath) : absPath;
184
+ const err = new Error(`Path "${display}" resolves outside the project root`);
185
+ err.fullPath = absPath;
186
+ err.projectRoot = this.projectRoot;
187
+ throw err;
186
188
  }
187
189
  return resolved;
188
190
  }
@@ -352,7 +354,9 @@ var DefaultSessionStore = class {
352
354
  try {
353
355
  handle = await fsp.open(file, "a", 384);
354
356
  } catch (err) {
355
- throw new Error(`Failed to open session file: ${err instanceof Error ? err.message : String(err)}`);
357
+ throw new Error(`Failed to open session file: ${err instanceof Error ? err.message : String(err)}`, {
358
+ cause: err
359
+ });
356
360
  }
357
361
  try {
358
362
  return new FileSessionWriter(id, handle, startedAt, meta, { dir: this.dir, filePath: file });
@@ -370,7 +374,8 @@ var DefaultSessionStore = class {
370
374
  handle = await fsp.open(file, "a", 384);
371
375
  } catch (err) {
372
376
  throw new Error(
373
- `Failed to open session "${id}" for append: ${err instanceof Error ? err.message : String(err)}`
377
+ `Failed to open session "${id}" for append: ${err instanceof Error ? err.message : String(err)}`,
378
+ { cause: err }
374
379
  );
375
380
  }
376
381
  const writer = new FileSessionWriter(
@@ -393,7 +398,10 @@ var DefaultSessionStore = class {
393
398
  const events = [];
394
399
  for (const line of lines) {
395
400
  try {
396
- events.push(JSON.parse(line));
401
+ const parsed = JSON.parse(line);
402
+ if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
403
+ events.push(parsed);
404
+ }
397
405
  } catch {
398
406
  }
399
407
  }
@@ -410,7 +418,11 @@ var DefaultSessionStore = class {
410
418
  ids.map((id) => this.summaryFor(id).catch(() => null))
411
419
  );
412
420
  const out = sessions.filter((s) => s !== null);
413
- out.sort((a, b) => a.startedAt < b.startedAt ? 1 : -1);
421
+ out.sort((a, b) => {
422
+ if (a.startedAt < b.startedAt) return 1;
423
+ if (a.startedAt > b.startedAt) return -1;
424
+ return a.id.localeCompare(b.id);
425
+ });
414
426
  return out.slice(0, limit);
415
427
  } catch {
416
428
  return [];
@@ -556,6 +568,8 @@ var FileSessionWriter = class {
556
568
  filePath;
557
569
  initDone = false;
558
570
  resumed;
571
+ appendFailCount = 0;
572
+ lastAppendWarnAt = 0;
559
573
  async writeSessionStart() {
560
574
  if (this.initDone || this.closed) return;
561
575
  this.initDone = true;
@@ -584,7 +598,19 @@ var FileSessionWriter = class {
584
598
  await this.handle.appendFile(`${JSON.stringify(event)}
585
599
  `, "utf8");
586
600
  } catch (err) {
587
- console.warn("[session] append failed:", err instanceof Error ? err.message : String(err));
601
+ this.appendFailCount++;
602
+ const now = Date.now();
603
+ if (now - this.lastAppendWarnAt > 5e3) {
604
+ const suppressed = this.appendFailCount - 1;
605
+ const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
606
+ console.warn(
607
+ "[session] append failed:",
608
+ err instanceof Error ? err.message : String(err),
609
+ tail
610
+ );
611
+ this.lastAppendWarnAt = now;
612
+ this.appendFailCount = 0;
613
+ }
588
614
  }
589
615
  }
590
616
  /**
@@ -790,6 +816,15 @@ function mergeAdjacentText(blocks) {
790
816
  var MAX_BYTES_TOTAL = 32e3;
791
817
  var DefaultMemoryStore = class {
792
818
  files;
819
+ /**
820
+ * Per-scope serialization queue. `remember` / `forget` / `consolidate` /
821
+ * `clear` are read-modify-write against a single file; without a lock,
822
+ * two concurrent calls on the same scope can read the same baseline and
823
+ * the later write silently drops the earlier entry. We chain each
824
+ * mutation onto the prior promise for the same scope so they run in
825
+ * issue order. Different scopes still proceed in parallel.
826
+ */
827
+ writeChain = /* @__PURE__ */ new Map();
793
828
  constructor(opts) {
794
829
  this.files = {
795
830
  "project-agents": opts.paths.inProjectAgentsFile,
@@ -797,6 +832,18 @@ var DefaultMemoryStore = class {
797
832
  "user-memory": opts.paths.globalMemory
798
833
  };
799
834
  }
835
+ async runSerialized(scope, work) {
836
+ const prior = this.writeChain.get(scope) ?? Promise.resolve();
837
+ const next = prior.catch(() => void 0).then(work);
838
+ this.writeChain.set(scope, next);
839
+ try {
840
+ return await next;
841
+ } finally {
842
+ if (this.writeChain.get(scope) === next) {
843
+ this.writeChain.delete(scope);
844
+ }
845
+ }
846
+ }
800
847
  async readAll() {
801
848
  const parts = [];
802
849
  for (const scope of ["project-agents", "project-memory", "user-memory"]) {
@@ -815,27 +862,32 @@ ${body.trim()}`);
815
862
  }
816
863
  }
817
864
  async remember(text, scope = "project-memory") {
818
- const file = this.files[scope];
819
- await ensureDir(path2.dirname(file));
820
- let existing = "";
821
- try {
822
- existing = await fsp.readFile(file, "utf8");
823
- } catch {
824
- }
825
- const ts = (/* @__PURE__ */ new Date()).toISOString();
826
- const id = `mem_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
827
- const entry = `
865
+ return this.runSerialized(scope, async () => {
866
+ const file = this.files[scope];
867
+ await ensureDir(path2.dirname(file));
868
+ let existing = "";
869
+ try {
870
+ existing = await fsp.readFile(file, "utf8");
871
+ } catch {
872
+ }
873
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
874
+ const id = `mem_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
875
+ const entry = `
828
876
  - [${ts}] ${id} ${text.replace(/\n/g, " ")}
829
877
  `;
830
- const next = existing.trim() ? existing.replace(/\n+$/, "") + entry : `# WrongStack Memory
878
+ const next = existing.trim() ? existing.replace(/\n+$/, "") + entry : `# WrongStack Memory
831
879
  ${entry}`;
832
- await atomicWrite(file, next);
833
- const buf = Buffer.byteLength(next, "utf8");
834
- if (buf > MAX_BYTES_TOTAL) {
835
- await this.consolidate(scope);
836
- }
880
+ await atomicWrite(file, next);
881
+ const buf = Buffer.byteLength(next, "utf8");
882
+ if (buf > MAX_BYTES_TOTAL) {
883
+ await this.consolidateUnsafe(scope);
884
+ }
885
+ });
837
886
  }
838
887
  async forget(query, scope = "project-memory") {
888
+ return this.runSerialized(scope, async () => this.forgetUnsafe(query, scope));
889
+ }
890
+ async forgetUnsafe(query, scope) {
839
891
  const file = this.files[scope];
840
892
  let existing;
841
893
  try {
@@ -872,6 +924,9 @@ ${entry}`;
872
924
  return removed;
873
925
  }
874
926
  async consolidate(scope) {
927
+ return this.runSerialized(scope, async () => this.consolidateUnsafe(scope));
928
+ }
929
+ async consolidateUnsafe(scope) {
875
930
  const file = this.files[scope];
876
931
  let existing;
877
932
  try {
@@ -902,12 +957,14 @@ ${entry}`;
902
957
  }
903
958
  async clear(scope) {
904
959
  if (scope) {
905
- await atomicWrite(this.files[scope], "");
906
- } else {
907
- for (const s of ["project-agents", "project-memory", "user-memory"]) {
908
- await atomicWrite(this.files[s], "");
909
- }
960
+ await this.runSerialized(scope, async () => atomicWrite(this.files[scope], ""));
961
+ return;
910
962
  }
963
+ await Promise.all(
964
+ ["project-agents", "project-memory", "user-memory"].map(
965
+ (s) => this.runSerialized(s, async () => atomicWrite(this.files[s], ""))
966
+ )
967
+ );
911
968
  }
912
969
  };
913
970
  function labelOf(scope) {
@@ -955,9 +1012,27 @@ var PATTERNS = [
955
1012
  regex: /\b([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,})['"]?(?!\s*[A-Za-z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))/g
956
1013
  }
957
1014
  ];
1015
+ var SCRUB_CHUNK_BYTES = 64 * 1024;
958
1016
  var DefaultSecretScrubber = class {
959
1017
  scrub(text) {
960
1018
  if (!text) return text;
1019
+ if (text.length <= SCRUB_CHUNK_BYTES) {
1020
+ return this.scrubOne(text);
1021
+ }
1022
+ const out = [];
1023
+ let i = 0;
1024
+ while (i < text.length) {
1025
+ let end = Math.min(i + SCRUB_CHUNK_BYTES, text.length);
1026
+ if (end < text.length) {
1027
+ const nl = text.lastIndexOf("\n", end);
1028
+ if (nl > i + SCRUB_CHUNK_BYTES / 2) end = nl + 1;
1029
+ }
1030
+ out.push(this.scrubOne(text.slice(i, end)));
1031
+ i = end;
1032
+ }
1033
+ return out.join("");
1034
+ }
1035
+ scrubOne(text) {
961
1036
  let out = text;
962
1037
  for (const p of PATTERNS) {
963
1038
  out = out.replace(p.regex, (_match, group1, group2) => {
@@ -1062,7 +1137,17 @@ var DefaultSecretVault = class {
1062
1137
  }
1063
1138
  };
1064
1139
  function decryptConfigSecrets(cfg, vault) {
1065
- return walk(cfg, vault, (v) => vault.decrypt(v));
1140
+ return walk(cfg, vault, (v, key) => {
1141
+ try {
1142
+ return vault.decrypt(v);
1143
+ } catch (err) {
1144
+ console.warn(
1145
+ `[secret-vault] Failed to decrypt "${key}":`,
1146
+ err instanceof Error ? err.message : err
1147
+ );
1148
+ return "";
1149
+ }
1150
+ });
1066
1151
  }
1067
1152
  function encryptConfigSecrets(cfg, vault) {
1068
1153
  return walk(cfg, vault, (v) => vault.encrypt(v));
@@ -1076,7 +1161,7 @@ function walk(node, vault, transform) {
1076
1161
  const out = {};
1077
1162
  for (const [k, v] of Object.entries(node)) {
1078
1163
  if (typeof v === "string" && isSecretField(k)) {
1079
- out[k] = transform(v);
1164
+ out[k] = transform(v, k);
1080
1165
  } else if (typeof v === "object" && v !== null) {
1081
1166
  out[k] = walk(v, vault, transform);
1082
1167
  } else {
@@ -1150,9 +1235,11 @@ function walkCount(node, vault, counter) {
1150
1235
  }
1151
1236
  return out;
1152
1237
  }
1238
+ var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1153
1239
  function deepMerge(a, b) {
1154
1240
  const out = { ...a };
1155
1241
  for (const [k, v] of Object.entries(b)) {
1242
+ if (FORBIDDEN_PROTO_KEYS.has(k)) continue;
1156
1243
  const existing = out[k];
1157
1244
  if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
1158
1245
  out[k] = deepMerge(existing, v);
@@ -1165,7 +1252,22 @@ function deepMerge(a, b) {
1165
1252
 
1166
1253
  // src/utils/glob-match.ts
1167
1254
  function escapeRegex(s) {
1168
- return s.replace(/[.+^${}()|\\/]/g, "\\$&");
1255
+ return s.replace(/[.+^${}()|\\]/g, "\\$&");
1256
+ }
1257
+ var COMPILED_GLOB_CACHE = /* @__PURE__ */ new Map();
1258
+ var CACHE_MAX_SIZE = 2e3;
1259
+ function getCachedGlob(pattern) {
1260
+ const cached = COMPILED_GLOB_CACHE.get(pattern);
1261
+ if (cached) return cached;
1262
+ if (COMPILED_GLOB_CACHE.size >= CACHE_MAX_SIZE) {
1263
+ const keys = [...COMPILED_GLOB_CACHE.keys()];
1264
+ for (let i = 0; i < Math.floor(CACHE_MAX_SIZE / 4); i++) {
1265
+ COMPILED_GLOB_CACHE.delete(keys[i]);
1266
+ }
1267
+ }
1268
+ const re = compileGlob(pattern);
1269
+ COMPILED_GLOB_CACHE.set(pattern, re);
1270
+ return re;
1169
1271
  }
1170
1272
  function compileGlob(pattern) {
1171
1273
  let i = 0;
@@ -1214,7 +1316,7 @@ function compileGlob(pattern) {
1214
1316
  return new RegExp(re);
1215
1317
  }
1216
1318
  function matchGlob(pattern, input) {
1217
- return compileGlob(pattern).test(input);
1319
+ return getCachedGlob(pattern).test(input);
1218
1320
  }
1219
1321
  function matchAny(patterns, input) {
1220
1322
  return patterns.some((p) => matchGlob(p, input));
@@ -1261,7 +1363,7 @@ var DefaultPermissionPolicy = class {
1261
1363
  if (!this.loaded) await this.reload();
1262
1364
  const namespaceEntry = this.findNamespaceEntry(tool.name);
1263
1365
  const entry = this.policy[tool.name] ?? namespaceEntry;
1264
- const subject = this.subjectFor(tool.name, input);
1366
+ const subject = this.subjectFor(tool.name, input, tool.subjectKey);
1265
1367
  if (entry?.deny && subject && matchAny(entry.deny, subject)) {
1266
1368
  return { permission: "deny", source: "deny", reason: "matched deny pattern" };
1267
1369
  }
@@ -1309,16 +1411,23 @@ var DefaultPermissionPolicy = class {
1309
1411
  throw err;
1310
1412
  }
1311
1413
  }
1312
- subjectFor(toolName, input) {
1414
+ subjectFor(toolName, input, subjectKey) {
1313
1415
  if (!input || typeof input !== "object") return void 0;
1314
1416
  const obj = input;
1315
1417
  const globChars = /[*?\[\]]/g;
1316
1418
  const escapeGlob = (s) => s.replace(globChars, (c) => `\\${c}`);
1419
+ const normalizePath = (s) => escapeGlob(s.replace(/\\/g, "/"));
1420
+ if (subjectKey) {
1421
+ const v = obj[subjectKey];
1422
+ if (typeof v === "string") {
1423
+ return subjectKey === "path" || subjectKey === "file" || subjectKey === "files" ? normalizePath(v) : escapeGlob(v);
1424
+ }
1425
+ }
1317
1426
  if (toolName === "bash" && typeof obj.command === "string") {
1318
1427
  return escapeGlob(obj.command);
1319
1428
  }
1320
1429
  if (typeof obj.path === "string") {
1321
- return escapeGlob(obj.path.replace(/\\/g, "/"));
1430
+ return normalizePath(obj.path);
1322
1431
  }
1323
1432
  if (typeof obj.url === "string") {
1324
1433
  return escapeGlob(obj.url);
@@ -1464,14 +1573,15 @@ function providerStatusToCode(status, type) {
1464
1573
  }
1465
1574
 
1466
1575
  // src/defaults/retry-policy.ts
1467
- var DefaultRetryPolicy = class {
1576
+ var DefaultRetryPolicy = class _DefaultRetryPolicy {
1577
+ static NETWORK_ERR_RE = /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i;
1468
1578
  shouldRetry(err, attempt) {
1469
1579
  if (err instanceof ProviderError) {
1470
1580
  if (!err.retryable) return false;
1471
1581
  return attempt < this.maxAttempts(err);
1472
1582
  }
1473
1583
  const msg = err.message ?? "";
1474
- const isNetwork = /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(msg);
1584
+ const isNetwork = _DefaultRetryPolicy.NETWORK_ERR_RE.test(msg);
1475
1585
  if (isNetwork) return attempt < 2;
1476
1586
  return false;
1477
1587
  }
@@ -1493,55 +1603,43 @@ var DefaultRetryPolicy = class {
1493
1603
  };
1494
1604
 
1495
1605
  // src/defaults/error-handler.ts
1606
+ var CONTEXT_OVERFLOW_RE = /context|too long|tokens/i;
1607
+ var NETWORK_ERR_RE = /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i;
1496
1608
  function buildRecoveryStrategies(opts) {
1497
1609
  return [
1498
1610
  {
1499
1611
  label: "context_overflow_reduce",
1500
1612
  compactor: opts?.compactor,
1501
1613
  async attempt(err, ctx) {
1502
- if (err instanceof ProviderError && (err.status === 413 || /context|too long|tokens/i.test(err.message))) {
1503
- if (this.compactor) {
1504
- try {
1505
- const report = await this.compactor.compact(ctx, { aggressive: true });
1506
- if (report.after < report.before) {
1507
- return {
1508
- content: [{ type: "text", text: "[context compacted automatically \u2014 please retry]" }],
1509
- stopReason: "end_turn",
1510
- usage: { input: 0, output: 0 },
1511
- model: ctx.model
1512
- };
1513
- }
1514
- } catch {
1614
+ if (!(err instanceof ProviderError)) return null;
1615
+ if (err.status !== 413 && !CONTEXT_OVERFLOW_RE.test(err.message)) return null;
1616
+ if (this.compactor) {
1617
+ try {
1618
+ const report = await this.compactor.compact(ctx, { aggressive: true });
1619
+ if (report.after < report.before) {
1620
+ return { action: "retry", reason: "context_compacted" };
1515
1621
  }
1622
+ } catch {
1516
1623
  }
1517
- return null;
1518
1624
  }
1519
1625
  return null;
1520
1626
  }
1521
1627
  },
1522
1628
  {
1523
1629
  label: "rate_limit_backoff",
1524
- async attempt(err, ctx) {
1525
- if (err instanceof ProviderError && err.status === 429) {
1526
- const delayMs = err.body?.retryAfterMs ?? 5e3;
1527
- const delay = Math.max(1e3, Math.min(delayMs, 6e4));
1528
- await new Promise((r) => setTimeout(r, delay));
1529
- return {
1530
- content: [{ type: "text", text: "[rate limit backoff applied \u2014 please retry]" }],
1531
- stopReason: "end_turn",
1532
- usage: { input: 0, output: 0 },
1533
- model: ctx.model
1534
- };
1535
- }
1536
- return null;
1630
+ async attempt(err) {
1631
+ if (!(err instanceof ProviderError) || err.status !== 429) return null;
1632
+ const delayMs = err.body?.retryAfterMs ?? 5e3;
1633
+ const delay = Math.max(1e3, Math.min(delayMs, 6e4));
1634
+ await new Promise((r) => setTimeout(r, delay));
1635
+ return { action: "retry", reason: "rate_limit_backoff" };
1537
1636
  }
1538
1637
  },
1539
1638
  {
1540
1639
  label: "downgrade_model",
1541
1640
  async attempt(err, ctx) {
1542
- if (err instanceof ProviderError && (err.status === 429 || err.status === 529 || err.status >= 500)) {
1543
- return null;
1544
- }
1641
+ if (!(err instanceof ProviderError)) return null;
1642
+ if (err.status !== 429 && err.status !== 529 && err.status < 500) return null;
1545
1643
  return null;
1546
1644
  }
1547
1645
  }
@@ -1564,12 +1662,12 @@ var DefaultErrorHandler = class {
1564
1662
  if (err.status === 429) return { kind: "rate_limit", retryable: true };
1565
1663
  if (err.status === 529) return { kind: "overloaded", retryable: true };
1566
1664
  if (err.status >= 500) return { kind: "server", retryable: true };
1567
- if (err.status === 413 || /context|too long|tokens/i.test(err.message)) {
1665
+ if (err.status === 413 || CONTEXT_OVERFLOW_RE.test(err.message)) {
1568
1666
  return { kind: "context_overflow", retryable: false };
1569
1667
  }
1570
1668
  if (err.status >= 400) return { kind: "client", retryable: false };
1571
1669
  }
1572
- if (err instanceof Error && /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(err.message)) {
1670
+ if (err instanceof Error && NETWORK_ERR_RE.test(err.message)) {
1573
1671
  return { kind: "network", retryable: true };
1574
1672
  }
1575
1673
  return { kind: "unknown", retryable: false };
@@ -1633,13 +1731,28 @@ var DefaultSkillLoader = class {
1633
1731
  async manifestText() {
1634
1732
  const skills = await this.list();
1635
1733
  if (skills.length === 0) return "";
1734
+ const entries = await this.listEntries();
1636
1735
  const lines = ["## Available skills"];
1637
- for (const s of skills) {
1638
- lines.push(`- **${s.name}** \u2014 ${s.description.replace(/\n/g, " ").trim()}`);
1639
- lines.push(` Path: ${s.path}`);
1736
+ for (const e of entries) {
1737
+ const scopeTag = e.scope.length > 0 ? ` \u2014 ${e.scope.slice(0, 3).join(", ")}` : "";
1738
+ lines.push(`- **${e.name}**${scopeTag}`);
1739
+ lines.push(` Use when: ${e.trigger}`);
1640
1740
  }
1641
1741
  return lines.join("\n");
1642
1742
  }
1743
+ async listEntries() {
1744
+ const skills = await this.list();
1745
+ const entries = [];
1746
+ for (const s of skills) {
1747
+ try {
1748
+ const raw = await fsp.readFile(s.path, "utf8");
1749
+ const { trigger, scope } = parseDescription(raw);
1750
+ entries.push({ name: s.name, trigger, scope, source: s.source, path: s.path });
1751
+ } catch {
1752
+ }
1753
+ }
1754
+ return entries;
1755
+ }
1643
1756
  async readBody(name) {
1644
1757
  const m = await this.find(name);
1645
1758
  if (!m) throw new Error(`Skill "${name}" not found`);
@@ -1682,6 +1795,19 @@ function parseFrontmatter(raw) {
1682
1795
  flush();
1683
1796
  return out;
1684
1797
  }
1798
+ function parseDescription(raw) {
1799
+ const fm = parseFrontmatter(raw);
1800
+ const desc = fm.description ?? "";
1801
+ const firstSentenceEnd = desc.indexOf(". ");
1802
+ const trigger = firstSentenceEnd !== -1 ? desc.slice(0, firstSentenceEnd + 1).trim() : desc.trim().split("\n")[0] ?? "";
1803
+ const scope = [];
1804
+ const coversMatch = /(?:covers|for|including)\s+([^.]+)/i.exec(desc);
1805
+ if (coversMatch) {
1806
+ const items = coversMatch[1].replace(/[·•]/g, ",").split(",").map((s) => s.trim()).filter(Boolean);
1807
+ scope.push(...items);
1808
+ }
1809
+ return { trigger, scope };
1810
+ }
1685
1811
  var BEHAVIOR_DEFAULTS = {
1686
1812
  version: 1,
1687
1813
  context: {
@@ -1730,11 +1856,13 @@ var ENV_MAP = {
1730
1856
  function isPrimitiveArray(a) {
1731
1857
  return a.every((v) => v === null || typeof v !== "object");
1732
1858
  }
1859
+ var FORBIDDEN_PROTO_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1733
1860
  function deepMerge2(base, patch) {
1734
1861
  if (typeof base !== "object" || base === null) return patch ?? base;
1735
1862
  if (typeof patch !== "object" || patch === null) return base;
1736
1863
  const out = { ...base };
1737
1864
  for (const [k, v] of Object.entries(patch)) {
1865
+ if (FORBIDDEN_PROTO_KEYS2.has(k)) continue;
1738
1866
  const existing = out[k];
1739
1867
  if (Array.isArray(v)) {
1740
1868
  if (Array.isArray(existing) && isPrimitiveArray(v) && isPrimitiveArray(existing)) {
@@ -1802,8 +1930,12 @@ var DefaultConfigLoader = class {
1802
1930
  if (cfg.providers) {
1803
1931
  for (const pcfg of Object.values(cfg.providers)) {
1804
1932
  if (!pcfg || typeof pcfg !== "object") continue;
1805
- const keys = pcfg.apiKeys;
1806
- if (!Array.isArray(keys) || keys.length === 0) continue;
1933
+ const rawKeys = pcfg.apiKeys;
1934
+ if (!Array.isArray(rawKeys) || rawKeys.length === 0) continue;
1935
+ const keys = rawKeys.filter(
1936
+ (k) => !!k && typeof k === "object" && typeof k.label === "string" && typeof k.apiKey === "string"
1937
+ );
1938
+ if (keys.length === 0) continue;
1807
1939
  const existing = pcfg.apiKey;
1808
1940
  if (existing && existing.length > 0) continue;
1809
1941
  const activeLabel = pcfg.activeKey;
@@ -1814,23 +1946,42 @@ var DefaultConfigLoader = class {
1814
1946
  }
1815
1947
  }
1816
1948
  this.validateBehavior(cfg);
1817
- if (this.strict) this.validateIdentity(cfg);
1949
+ if (this.strict) {
1950
+ this.validateIdentity(cfg);
1951
+ }
1818
1952
  return Object.freeze(cfg);
1819
1953
  }
1820
1954
  async readJson(file) {
1955
+ let raw;
1821
1956
  try {
1822
- const raw = await fsp.readFile(file, "utf8");
1823
- const parsed = safeParse(raw);
1824
- if (parsed.ok && parsed.value) return parsed.value;
1825
- } catch {
1957
+ raw = await fsp.readFile(file, "utf8");
1958
+ } catch (err) {
1959
+ if (err.code !== "ENOENT") {
1960
+ console.warn(`[config] Failed to read "${file}":`, err);
1961
+ }
1962
+ return {};
1826
1963
  }
1827
- return {};
1964
+ const parsed = safeParse(raw);
1965
+ if (!parsed.ok || !parsed.value) {
1966
+ console.warn(
1967
+ `[config] Failed to parse "${file}": invalid JSON. Falling back to defaults for this layer.`
1968
+ );
1969
+ return {};
1970
+ }
1971
+ return parsed.value;
1828
1972
  }
1829
1973
  validateBehavior(cfg) {
1830
1974
  if (cfg.version === void 0) throw new Error("Config: missing version field");
1831
1975
  if (cfg.version !== 1) throw new Error(`Config: unsupported version ${cfg.version}`);
1832
1976
  const c = cfg.context;
1833
1977
  if (!c) throw new Error("Config: missing context section");
1978
+ const fields = ["warnThreshold", "softThreshold", "hardThreshold"];
1979
+ for (const f of fields) {
1980
+ const v = c[f];
1981
+ if (typeof v !== "number" || !Number.isFinite(v)) {
1982
+ throw new Error(`Config: context.${String(f)} must be a finite number (got ${typeof v})`);
1983
+ }
1984
+ }
1834
1985
  if (c.warnThreshold >= c.softThreshold || c.softThreshold >= c.hardThreshold) {
1835
1986
  throw new Error("Config: context thresholds must satisfy warn < soft < hard");
1836
1987
  }
@@ -1878,7 +2029,8 @@ var DefaultConfigStore = class {
1878
2029
  for (const w of this.watchers) {
1879
2030
  try {
1880
2031
  w(next, prev);
1881
- } catch {
2032
+ } catch (err) {
2033
+ console.error("[config-store] watcher threw:", err);
1882
2034
  }
1883
2035
  }
1884
2036
  return next;
@@ -1955,21 +2107,33 @@ var DEFAULT_CONFIG_MIGRATIONS = [];
1955
2107
 
1956
2108
  // src/utils/token-estimate.ts
1957
2109
  var RoughTokenEstimate = (text) => Math.max(1, Math.ceil(text.length / 4));
2110
+ var ESTIMATE_CACHE = /* @__PURE__ */ new Map();
2111
+ var ESTIMATE_CACHE_MAX_SIZE = 1e4;
2112
+ function getCachedEstimate(key, compute) {
2113
+ const existing = ESTIMATE_CACHE.get(key);
2114
+ if (existing !== void 0) return existing;
2115
+ if (ESTIMATE_CACHE.size >= ESTIMATE_CACHE_MAX_SIZE) {
2116
+ const keys = [...ESTIMATE_CACHE.keys()];
2117
+ for (let i = 0; i < Math.floor(ESTIMATE_CACHE_MAX_SIZE / 4); i++) {
2118
+ ESTIMATE_CACHE.delete(keys[i]);
2119
+ }
2120
+ }
2121
+ const estimate = compute();
2122
+ ESTIMATE_CACHE.set(key, estimate);
2123
+ return estimate;
2124
+ }
1958
2125
  function estimateToolInputTokens(input) {
1959
2126
  if (typeof input === "string") return RoughTokenEstimate(input);
1960
- if (input !== null && typeof input === "object" && "__tokenEstimate" in input) {
1961
- return input.__tokenEstimate;
1962
- }
1963
- const str = typeof input === "object" ? JSON.stringify(input) : String(input);
1964
- const estimate = RoughTokenEstimate(str);
1965
- if (input !== null && typeof input === "object" && !Array.isArray(input)) {
1966
- input.__tokenEstimate = estimate;
2127
+ if (input === null || typeof input !== "object") {
2128
+ return RoughTokenEstimate(String(input));
1967
2129
  }
1968
- return estimate;
2130
+ const key = JSON.stringify(input);
2131
+ return getCachedEstimate(key, () => RoughTokenEstimate(key));
1969
2132
  }
1970
2133
  function estimateToolResultTokens(content) {
1971
2134
  if (typeof content === "string") return RoughTokenEstimate(content);
1972
- return RoughTokenEstimate(JSON.stringify(content));
2135
+ const key = JSON.stringify(content);
2136
+ return getCachedEstimate(key, () => RoughTokenEstimate(key));
1973
2137
  }
1974
2138
  function estimateTextTokens(text) {
1975
2139
  return RoughTokenEstimate(text);
@@ -2010,9 +2174,18 @@ var HybridCompactor = class {
2010
2174
  }
2011
2175
  }
2012
2176
  let saved = 0;
2013
- for (let i = 0; i < preserveStart; i++) {
2177
+ let changed = false;
2178
+ const nextMessages = new Array(messages.length);
2179
+ for (let i = 0; i < messages.length; i++) {
2014
2180
  const msg = messages[i];
2015
- if (!msg || !Array.isArray(msg.content)) continue;
2181
+ if (i >= preserveStart) {
2182
+ nextMessages[i] = msg;
2183
+ continue;
2184
+ }
2185
+ if (!msg || !Array.isArray(msg.content)) {
2186
+ nextMessages[i] = msg;
2187
+ continue;
2188
+ }
2016
2189
  const newContent = msg.content.map((b) => {
2017
2190
  if (b.type !== "tool_result") return b;
2018
2191
  const tokens = estimateToolResultTokens(b.content);
@@ -2026,8 +2199,14 @@ var HybridCompactor = class {
2026
2199
  };
2027
2200
  return elided;
2028
2201
  });
2029
- messages[i] = { ...msg, content: newContent };
2202
+ if (newContent.length === msg.content.length && newContent.every((b, idx) => b === msg.content[idx])) {
2203
+ nextMessages[i] = msg;
2204
+ } else {
2205
+ nextMessages[i] = { ...msg, content: newContent };
2206
+ changed = true;
2207
+ }
2030
2208
  }
2209
+ if (changed) ctx.state.replaceMessages(nextMessages);
2031
2210
  return saved;
2032
2211
  }
2033
2212
  collapseAncientTurns(ctx) {
@@ -2061,10 +2240,10 @@ var HybridCompactor = class {
2061
2240
  let total = 0;
2062
2241
  for (const m of messages) {
2063
2242
  if (typeof m.content === "string") {
2064
- total += estimateTextTokens(m.content);
2243
+ total += this.estimator(m.content);
2065
2244
  } else {
2066
2245
  for (const b of m.content) {
2067
- if (b.type === "text") total += estimateTextTokens(b.text);
2246
+ if (b.type === "text") total += this.estimator(b.text);
2068
2247
  else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
2069
2248
  else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
2070
2249
  }
@@ -2109,7 +2288,7 @@ var IntelligentCompactor = class {
2109
2288
  const beforeTokens = this.estimateTokens(ctx.messages);
2110
2289
  const reductions = [];
2111
2290
  const load = beforeTokens / this.maxContext;
2112
- const aggressive = opts.aggressive ?? load >= this.softThreshold;
2291
+ const aggressive = load >= this.hardThreshold ? true : opts.aggressive ?? load >= this.softThreshold;
2113
2292
  const saved1 = this.eliseOldToolResults(ctx);
2114
2293
  if (saved1 > 0) reductions.push({ phase: "elision", saved: saved1 });
2115
2294
  if (aggressive) {
@@ -2218,9 +2397,18 @@ var IntelligentCompactor = class {
2218
2397
  }
2219
2398
  }
2220
2399
  let saved = 0;
2221
- for (let i = 0; i < preserveStart; i++) {
2400
+ let changed = false;
2401
+ const nextMessages = new Array(messages.length);
2402
+ for (let i = 0; i < messages.length; i++) {
2222
2403
  const msg = messages[i];
2223
- if (!msg || !Array.isArray(msg.content)) continue;
2404
+ if (i >= preserveStart) {
2405
+ nextMessages[i] = msg;
2406
+ continue;
2407
+ }
2408
+ if (!msg || !Array.isArray(msg.content)) {
2409
+ nextMessages[i] = msg;
2410
+ continue;
2411
+ }
2224
2412
  const newContent = msg.content.map((b) => {
2225
2413
  if (b.type !== "tool_result") return b;
2226
2414
  const tokens = estimateToolResultTokens(b.content);
@@ -2233,8 +2421,14 @@ var IntelligentCompactor = class {
2233
2421
  is_error: b.is_error
2234
2422
  };
2235
2423
  });
2236
- messages[i] = { ...msg, content: newContent };
2424
+ if (newContent.length === msg.content.length && newContent.every((b, idx) => b === msg.content[idx])) {
2425
+ nextMessages[i] = msg;
2426
+ } else {
2427
+ nextMessages[i] = { ...msg, content: newContent };
2428
+ changed = true;
2429
+ }
2237
2430
  }
2431
+ if (changed) ctx.state.replaceMessages(nextMessages);
2238
2432
  return saved;
2239
2433
  }
2240
2434
  hasTextContent(m) {
@@ -2387,7 +2581,7 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
2387
2581
  const jsonEnd = raw.lastIndexOf("}");
2388
2582
  if (jsonStart === -1 || jsonEnd === -1) {
2389
2583
  return this.fallbackSelect(
2390
- Array.from({ length: messageCount }, (_, i) => ({ role: "user", content: "" })),
2584
+ Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
2391
2585
  this.maxContextTokens
2392
2586
  );
2393
2587
  }
@@ -2396,7 +2590,7 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
2396
2590
  parsed = JSON.parse(raw.slice(jsonStart, jsonEnd + 1));
2397
2591
  } catch {
2398
2592
  return this.fallbackSelect(
2399
- Array.from({ length: messageCount }, (_, i) => ({ role: "user", content: "" })),
2593
+ Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
2400
2594
  this.maxContextTokens
2401
2595
  );
2402
2596
  }
@@ -2449,7 +2643,7 @@ var SelectiveCompactor = class {
2449
2643
  const savedElision = this.eliseOldToolResults(ctx);
2450
2644
  if (savedElision > 0) reductions.push({ phase: "elision", saved: savedElision });
2451
2645
  const afterPhase1 = this.estimateTokens(ctx.messages);
2452
- const targetBudget = this.computeTargetBudget(load, opts.aggressive ?? false);
2646
+ const targetBudget = this.computeTargetBudget(load);
2453
2647
  if (afterPhase1 > targetBudget) {
2454
2648
  const savedSelective = await this.runSelector(ctx, targetBudget);
2455
2649
  if (savedSelective > 0) reductions.push({ phase: "selective", saved: savedSelective });
@@ -2467,7 +2661,7 @@ var SelectiveCompactor = class {
2467
2661
  try {
2468
2662
  result = await this.selector.select(ctx.messages, targetBudget);
2469
2663
  } catch {
2470
- return this.aggressiveRecencyTrim(ctx, targetBudget);
2664
+ return this.aggressiveRecencyTrim(ctx);
2471
2665
  }
2472
2666
  await this.executePlan(ctx, result);
2473
2667
  const after = this.estimateTokens(ctx.messages);
@@ -2522,9 +2716,8 @@ Summarize the following message range:`;
2522
2716
  * Fallback when selector fails: aggressively trim from the oldest end
2523
2717
  * until we hit targetBudget.
2524
2718
  */
2525
- aggressiveRecencyTrim(ctx, targetBudget) {
2719
+ aggressiveRecencyTrim(ctx) {
2526
2720
  const messages = ctx.messages;
2527
- this.estimateTokens(messages);
2528
2721
  const preserveIdx = Math.max(0, messages.length - this.preserveK * 2);
2529
2722
  if (preserveIdx <= 0) return 0;
2530
2723
  let boundary = preserveIdx;
@@ -2545,7 +2738,7 @@ Summarize the following message range:`;
2545
2738
  ctx.state.replaceMessages([summaryMsg, ...tail]);
2546
2739
  return Math.max(0, removedTokens - this.estimateTokens([summaryMsg]));
2547
2740
  }
2548
- computeTargetBudget(load, aggressive) {
2741
+ computeTargetBudget(load) {
2549
2742
  if (load >= this.hardThreshold) {
2550
2743
  return Math.floor(this.maxContext * 0.5);
2551
2744
  }
@@ -2567,9 +2760,18 @@ Summarize the following message range:`;
2567
2760
  }
2568
2761
  }
2569
2762
  let saved = 0;
2570
- for (let i = 0; i < preserveStart; i++) {
2763
+ let changed = false;
2764
+ const nextMessages = new Array(messages.length);
2765
+ for (let i = 0; i < messages.length; i++) {
2571
2766
  const msg = messages[i];
2572
- if (!msg || !Array.isArray(msg.content)) continue;
2767
+ if (i >= preserveStart) {
2768
+ nextMessages[i] = msg;
2769
+ continue;
2770
+ }
2771
+ if (!msg || !Array.isArray(msg.content)) {
2772
+ nextMessages[i] = msg;
2773
+ continue;
2774
+ }
2573
2775
  const newContent = msg.content.map((b) => {
2574
2776
  if (b.type !== "tool_result") return b;
2575
2777
  const text = typeof b.content === "string" ? b.content : JSON.stringify(b.content);
@@ -2583,8 +2785,14 @@ Summarize the following message range:`;
2583
2785
  is_error: b.is_error
2584
2786
  };
2585
2787
  });
2586
- messages[i] = { ...msg, content: newContent };
2788
+ if (newContent.every((b, idx) => b === msg.content[idx])) {
2789
+ nextMessages[i] = msg;
2790
+ } else {
2791
+ nextMessages[i] = { ...msg, content: newContent };
2792
+ changed = true;
2793
+ }
2587
2794
  }
2795
+ if (changed) ctx.state.replaceMessages(nextMessages);
2588
2796
  return saved;
2589
2797
  }
2590
2798
  hasTextContent(m) {
@@ -2620,46 +2828,78 @@ var AutoCompactionMiddleware = class {
2620
2828
  name = "AutoCompaction";
2621
2829
  compactor;
2622
2830
  warnThreshold;
2623
- // fraction of maxContext (0-1)
2624
2831
  softThreshold;
2625
2832
  hardThreshold;
2626
2833
  maxContext;
2627
2834
  estimator;
2628
2835
  aggressiveOn;
2836
+ events;
2837
+ failureMode;
2629
2838
  /**
2630
- * @param compactor Compactor to use for compaction
2631
- * @param maxContext Provider's max context window in tokens
2632
- * @param estimator Token estimation function (ctx → token count)
2633
- * @param thresholds Threshold fractions (0-1) of maxContext
2634
- * @param aggressiveOn Which threshold triggers aggressive (full LLM summarization)
2839
+ * @param compactor Compactor to use for compaction.
2840
+ * @param maxContext Provider's max context window in tokens.
2841
+ * @param estimator Token estimation function.
2842
+ * @param thresholds Threshold fractions (0-1) of maxContext.
2843
+ * @param opts Optional behavior. By default, failures at the
2844
+ * hard threshold throw AGENT_CONTEXT_OVERFLOW so
2845
+ * the agent does not continue into a likely
2846
+ * provider context overflow. Warn/soft failures
2847
+ * still emit compaction.failed and continue.
2635
2848
  */
2636
- constructor(compactor, maxContext, estimator, thresholds, aggressiveOn = "soft") {
2849
+ constructor(compactor, maxContext, estimator, thresholds, optsOrAggressiveOn = {}, events) {
2850
+ const opts = typeof optsOrAggressiveOn === "string" ? { aggressiveOn: optsOrAggressiveOn, events } : optsOrAggressiveOn;
2637
2851
  this.compactor = compactor;
2638
2852
  this.maxContext = maxContext;
2639
2853
  this.estimator = estimator;
2640
2854
  this.warnThreshold = thresholds.warn;
2641
2855
  this.softThreshold = thresholds.soft;
2642
2856
  this.hardThreshold = thresholds.hard;
2643
- this.aggressiveOn = aggressiveOn;
2857
+ this.aggressiveOn = opts.aggressiveOn ?? "soft";
2858
+ this.events = opts.events;
2859
+ this.failureMode = opts.failureMode ?? "throw_on_hard";
2644
2860
  }
2645
2861
  handler() {
2646
2862
  return async (ctx, next) => {
2647
2863
  const tokens = this.estimator(ctx);
2648
2864
  const load = tokens / this.maxContext;
2649
2865
  if (load >= this.hardThreshold) {
2650
- await this.compact(ctx, true);
2866
+ await this.compact(ctx, true, { level: "hard", tokens, load });
2651
2867
  } else if (load >= this.softThreshold) {
2652
- await this.compact(ctx, this.aggressiveOn !== "hard");
2868
+ await this.compact(ctx, this.aggressiveOn !== "hard", { level: "soft", tokens, load });
2653
2869
  } else if (load >= this.warnThreshold) {
2654
- await this.compact(ctx, false);
2870
+ await this.compact(ctx, false, { level: "warn", tokens, load });
2655
2871
  }
2656
2872
  return next(ctx);
2657
2873
  };
2658
2874
  }
2659
- async compact(ctx, aggressive) {
2875
+ async compact(ctx, aggressive, pressure) {
2660
2876
  try {
2661
2877
  await this.compactor.compact(ctx, { aggressive });
2662
- } catch {
2878
+ } catch (err) {
2879
+ const error = err instanceof Error ? err : new Error(String(err));
2880
+ const fatal = this.failureMode === "throw" || this.failureMode === "throw_on_hard" && pressure.level === "hard";
2881
+ this.events?.emit("compaction.failed", {
2882
+ err: error,
2883
+ aggressive,
2884
+ level: pressure.level,
2885
+ tokens: pressure.tokens,
2886
+ maxContext: this.maxContext,
2887
+ load: pressure.load,
2888
+ fatal
2889
+ });
2890
+ if (fatal) {
2891
+ throw new AgentError({
2892
+ message: `Auto-compaction failed at ${pressure.level} threshold`,
2893
+ code: "AGENT_CONTEXT_OVERFLOW",
2894
+ recoverable: true,
2895
+ context: {
2896
+ level: pressure.level,
2897
+ tokens: pressure.tokens,
2898
+ maxContext: this.maxContext
2899
+ },
2900
+ cause: err
2901
+ });
2902
+ }
2663
2903
  }
2664
2904
  }
2665
2905
  };
@@ -3173,8 +3413,9 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3173
3413
  const context = {
3174
3414
  subagentId: id,
3175
3415
  tasks: [],
3176
- // parentBridge: wired by the caller via setSubagentBridge() once the
3177
- // bidirectional bridge is created. Reads gated by hasParentBridge().
3416
+ // Wired later by the caller via setSubagentBridge() once the
3417
+ // bidirectional bridge is created. Readers must null-check / use
3418
+ // hasParentBridge() — the type now reflects this.
3178
3419
  parentBridge: null,
3179
3420
  doneCondition: this.config.doneCondition,
3180
3421
  maxConcurrent: this.config.maxConcurrent ?? 4
@@ -3219,9 +3460,7 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3219
3460
  this.emit("subagent.stopped", { subagentId, reason: "stopped by coordinator" });
3220
3461
  }
3221
3462
  async stopAll() {
3222
- for (const id of this.subagents.keys()) {
3223
- await this.stop(id);
3224
- }
3463
+ await Promise.allSettled([...this.subagents.keys()].map((id) => this.stop(id)));
3225
3464
  }
3226
3465
  getStatus() {
3227
3466
  return {
@@ -3257,7 +3496,17 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3257
3496
  if (!subagentId) return;
3258
3497
  const task = this.pendingTasks.shift();
3259
3498
  if (!task) return;
3260
- void this.runDispatched(subagentId, task);
3499
+ this.runDispatched(subagentId, task).catch((err) => {
3500
+ this.recordCompletion({
3501
+ subagentId,
3502
+ taskId: task.id,
3503
+ status: "failed",
3504
+ error: err instanceof Error ? err.message : String(err),
3505
+ iterations: 0,
3506
+ toolCalls: 0,
3507
+ durationMs: 0
3508
+ });
3509
+ });
3261
3510
  }
3262
3511
  }
3263
3512
  canDispatch() {
@@ -3277,7 +3526,6 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3277
3526
  subagent.currentTask = task.id;
3278
3527
  task.subagentId = subagentId;
3279
3528
  subagent.context.tasks.push(task);
3280
- this.inFlight++;
3281
3529
  this.emit("task.assigned", { task, subagentId });
3282
3530
  const budget = new SubagentBudget({
3283
3531
  maxIterations: subagent.config.maxIterations ?? this.config.defaultBudget?.maxIterations,
@@ -3287,6 +3535,10 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3287
3535
  timeoutMs: task.timeoutMs ?? subagent.config.timeoutMs ?? this.config.defaultBudget?.timeoutMs
3288
3536
  });
3289
3537
  subagent.activeBudget = budget;
3538
+ if (!this.runner) {
3539
+ return;
3540
+ }
3541
+ this.inFlight++;
3290
3542
  const startTime = Date.now();
3291
3543
  const runCtx = {
3292
3544
  subagentId,
@@ -3296,9 +3548,6 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3296
3548
  bridge: subagent.context.parentBridge || null
3297
3549
  };
3298
3550
  let result;
3299
- if (!this.runner) {
3300
- return;
3301
- }
3302
3551
  budget.start();
3303
3552
  try {
3304
3553
  const outcome = await this.executeWithTimeout(this.runner, task, runCtx, budget);
@@ -3313,13 +3562,14 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3313
3562
  };
3314
3563
  } catch (err) {
3315
3564
  const status = err instanceof BudgetExceededError && err.kind === "timeout" ? "timeout" : subagent.abortController.signal.aborted ? "stopped" : "failed";
3565
+ const usage = budget.usage();
3316
3566
  result = {
3317
3567
  subagentId,
3318
3568
  taskId: task.id,
3319
3569
  status,
3320
3570
  error: err instanceof Error ? err.message : String(err),
3321
- iterations: budget.usage().iterations,
3322
- toolCalls: budget.usage().toolCalls,
3571
+ iterations: usage.iterations,
3572
+ toolCalls: usage.toolCalls,
3323
3573
  durationMs: Date.now() - startTime
3324
3574
  };
3325
3575
  }
@@ -3344,11 +3594,24 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3344
3594
  recordCompletion(result) {
3345
3595
  this.completedResults.push(result);
3346
3596
  this.totalIterations += result.iterations;
3347
- this.inFlight = Math.max(0, this.inFlight - 1);
3597
+ if (this.inFlight > 0) {
3598
+ this.inFlight--;
3599
+ } else if (this.runner) {
3600
+ this.emit("warning", {
3601
+ type: "inFlight_underflow",
3602
+ taskId: result.taskId,
3603
+ subagentId: result.subagentId
3604
+ });
3605
+ return;
3606
+ }
3348
3607
  const subagent = this.subagents.get(result.subagentId);
3349
3608
  if (subagent && subagent.status !== "stopped") {
3350
- subagent.status = result.status === "failed" || result.status === "timeout" ? "error" : "idle";
3609
+ const failed = result.status === "failed" || result.status === "timeout";
3610
+ subagent.status = failed ? "error" : "idle";
3351
3611
  subagent.currentTask = void 0;
3612
+ if (subagent.abortController.signal.aborted) {
3613
+ subagent.abortController = new AbortController();
3614
+ }
3352
3615
  if (subagent.status === "error") {
3353
3616
  queueMicrotask(() => {
3354
3617
  if (subagent.status === "error") subagent.status = "idle";
@@ -3536,6 +3799,11 @@ var InMemoryAgentBridge = class {
3536
3799
  if (this.stopped) throw new Error("Bridge is stopped");
3537
3800
  const timeout = timeoutMs ?? this.timeoutMs;
3538
3801
  const correlationId = msg.id;
3802
+ if (this.pendingRequests.has(correlationId)) {
3803
+ throw new Error(
3804
+ `Bridge request id "${correlationId}" collides with an in-flight request \u2014 caller is reusing message ids`
3805
+ );
3806
+ }
3539
3807
  return new Promise((resolve3, reject) => {
3540
3808
  const timer = setTimeout(() => {
3541
3809
  this.pendingRequests.delete(correlationId);
@@ -3576,8 +3844,10 @@ function createMessage(type, from, payload, to) {
3576
3844
  var DoneConditionChecker = class {
3577
3845
  constructor(condition) {
3578
3846
  this.condition = condition;
3847
+ this.compiledRegex = condition.type === "output_match" && condition.pattern ? new RegExp(condition.pattern) : null;
3579
3848
  }
3580
3849
  condition;
3850
+ compiledRegex;
3581
3851
  check(state) {
3582
3852
  switch (this.condition.type) {
3583
3853
  case "iterations":
@@ -3591,11 +3861,8 @@ var DoneConditionChecker = class {
3591
3861
  }
3592
3862
  break;
3593
3863
  case "output_match":
3594
- if (this.condition.pattern && state.lastOutput) {
3595
- const regex = new RegExp(this.condition.pattern);
3596
- if (regex.test(state.lastOutput)) {
3597
- return { done: true, reason: `output matched pattern "${this.condition.pattern}"`, ...state };
3598
- }
3864
+ if (this.compiledRegex && state.lastOutput && this.compiledRegex.test(state.lastOutput)) {
3865
+ return { done: true, reason: `output matched pattern "${this.condition.pattern}"`, ...state };
3599
3866
  }
3600
3867
  break;
3601
3868
  }
@@ -3652,7 +3919,8 @@ var AutonomousRunner = class {
3652
3919
  return failedResult;
3653
3920
  }
3654
3921
  } catch (e) {
3655
- if (e.message.includes("timeout")) {
3922
+ const msg = e instanceof Error ? e.message : String(e);
3923
+ if (msg.includes("timeout")) {
3656
3924
  const timeoutResult = {
3657
3925
  status: "failed",
3658
3926
  error: toWrongStackError(e),
@@ -3681,14 +3949,11 @@ var AutonomousRunner = class {
3681
3949
 
3682
3950
  // src/defaults/spec-parser.ts
3683
3951
  var SpecParser = class {
3684
- constructor(opts = {}) {
3685
- this.opts = opts;
3686
- }
3687
- opts;
3688
3952
  parse(content) {
3689
3953
  const lines = content.split("\n");
3690
3954
  const sections = this.extractSections(lines);
3691
3955
  const requirements = this.extractRequirements(lines);
3956
+ const now = Date.now();
3692
3957
  return {
3693
3958
  id: crypto.randomUUID(),
3694
3959
  title: this.extractTitle(lines),
@@ -3697,8 +3962,8 @@ var SpecParser = class {
3697
3962
  overview: this.extractOverview(lines),
3698
3963
  sections,
3699
3964
  requirements,
3700
- createdAt: Date.now(),
3701
- updatedAt: Date.now()
3965
+ createdAt: now,
3966
+ updatedAt: now
3702
3967
  };
3703
3968
  }
3704
3969
  extractTitle(lines) {
@@ -3790,20 +4055,13 @@ var SpecParser = class {
3790
4055
  parseRequirementLine(line, id) {
3791
4056
  const trimmed = line.trim();
3792
4057
  if (!trimmed || trimmed.startsWith("#")) return null;
3793
- const typeMap = {
3794
- "functional": "functional",
3795
- "non-functional": "non-functional",
3796
- "security": "security",
3797
- "performance": "performance",
3798
- "ux": "ux"
3799
- };
4058
+ const lower = trimmed.toLowerCase();
4059
+ const types = ["functional", "non-functional", "security", "performance", "ux"];
3800
4060
  let type = "functional";
3801
- let priority = "medium";
3802
- for (const [key, val] of Object.entries(typeMap)) {
3803
- if (trimmed.toLowerCase().includes(`[${key}]`)) {
3804
- type = val;
3805
- }
4061
+ for (const t of types) {
4062
+ if (lower.includes(`[${t}]`)) type = t;
3806
4063
  }
4064
+ let priority = "medium";
3807
4065
  if (trimmed.includes("[critical]") || trimmed.includes("[prio:high]")) {
3808
4066
  priority = "critical";
3809
4067
  } else if (trimmed.includes("[high]")) {
@@ -3893,9 +4151,10 @@ var SpecParser = class {
3893
4151
  warnings.push({ path: `requirement.${req.id}`, message: "No acceptance criteria defined" });
3894
4152
  }
3895
4153
  }
4154
+ const reqIds = new Set(spec.requirements.map((r) => r.id));
3896
4155
  const blockedByIds = new Set(spec.requirements.flatMap((r) => r.blockedBy ?? []));
3897
4156
  for (const id of blockedByIds) {
3898
- if (!spec.requirements.find((r) => r.id === id)) {
4157
+ if (!reqIds.has(id)) {
3899
4158
  errors.push({ path: "requirements", message: `BlockedBy references non-existent requirement: ${id}` });
3900
4159
  }
3901
4160
  }
@@ -3925,25 +4184,21 @@ var TaskGenerator = class {
3925
4184
  status: "pending"
3926
4185
  });
3927
4186
  }
3928
- const criticalReqs = spec.requirements.filter((r) => r.priority === "critical");
3929
- const highReqs = spec.requirements.filter((r) => r.priority === "high");
3930
- const mediumReqs = spec.requirements.filter((r) => r.priority === "medium");
3931
- const lowReqs = spec.requirements.filter((r) => r.priority === "low");
3932
- for (const req of criticalReqs) {
3933
- const task = this.createTaskFromRequirement(req, spec.title);
3934
- this.opts.taskTracker.addNode(task);
3935
- }
3936
- for (const req of highReqs) {
3937
- const task = this.createTaskFromRequirement(req, spec.title);
3938
- this.opts.taskTracker.addNode(task);
3939
- }
3940
- for (const req of mediumReqs) {
3941
- const task = this.createTaskFromRequirement(req, spec.title);
3942
- this.opts.taskTracker.addNode(task);
4187
+ const byPriority = {
4188
+ critical: [],
4189
+ high: [],
4190
+ medium: [],
4191
+ low: []
4192
+ };
4193
+ for (const req of spec.requirements) {
4194
+ const bucket = byPriority[req.priority] ?? byPriority.medium;
4195
+ bucket.push(req);
3943
4196
  }
3944
- for (const req of lowReqs) {
3945
- const task = this.createTaskFromRequirement(req, spec.title);
3946
- this.opts.taskTracker.addNode(task);
4197
+ const order = ["critical", "high", "medium", "low"];
4198
+ for (const p of order) {
4199
+ for (const req of byPriority[p]) {
4200
+ this.opts.taskTracker.addNode(this.createTaskFromRequirement(req));
4201
+ }
3947
4202
  }
3948
4203
  if (spec.apiEndpoints && spec.apiEndpoints.length > 0) {
3949
4204
  const apiParent = this.opts.taskTracker.addNode({
@@ -3977,17 +4232,15 @@ var TaskGenerator = class {
3977
4232
  });
3978
4233
  return graph;
3979
4234
  }
3980
- createTaskFromRequirement(req, specTitle) {
3981
- const type = this.mapRequirementType(req.type);
3982
- const tags = [req.type, req.priority];
4235
+ createTaskFromRequirement(req) {
3983
4236
  return {
3984
4237
  title: req.description,
3985
- description: this.buildDescription(req, specTitle),
3986
- type,
3987
- priority: this.mapPriority(req.priority),
4238
+ description: this.buildDescription(req),
4239
+ type: this.mapRequirementType(req.type),
4240
+ priority: req.priority,
3988
4241
  status: "pending",
3989
4242
  specRequirementId: req.id,
3990
- tags,
4243
+ tags: [req.type, req.priority],
3991
4244
  estimateHours: this.estimateHours(req)
3992
4245
  };
3993
4246
  }
@@ -4002,7 +4255,7 @@ var TaskGenerator = class {
4002
4255
  estimateHours: this.estimateForEndpoint(endpoint)
4003
4256
  };
4004
4257
  }
4005
- buildDescription(req, specTitle) {
4258
+ buildDescription(req) {
4006
4259
  const lines = [
4007
4260
  req.description,
4008
4261
  "",
@@ -4036,20 +4289,6 @@ var TaskGenerator = class {
4036
4289
  return "feature";
4037
4290
  }
4038
4291
  }
4039
- mapPriority(priority) {
4040
- switch (priority) {
4041
- case "critical":
4042
- return "critical";
4043
- case "high":
4044
- return "high";
4045
- case "medium":
4046
- return "medium";
4047
- case "low":
4048
- return "low";
4049
- default:
4050
- return "medium";
4051
- }
4052
- }
4053
4292
  estimateHours(req) {
4054
4293
  switch (req.priority) {
4055
4294
  case "critical":
@@ -4120,16 +4359,33 @@ var DefaultTaskStore = class {
4120
4359
 
4121
4360
  // src/types/task-graph.ts
4122
4361
  function computeTaskProgress(graph) {
4123
- const nodes = Array.from(graph.nodes.values());
4124
- const total = nodes.length;
4125
- const completed = nodes.filter((n) => n.status === "completed").length;
4126
- const pending = nodes.filter((n) => n.status === "pending").length;
4127
- const inProgress = nodes.filter((n) => n.status === "in_progress").length;
4128
- const blocked = nodes.filter((n) => n.status === "blocked").length;
4129
- const failed = nodes.filter((n) => n.status === "failed").length;
4130
- const review = nodes.filter((n) => n.status === "review").length;
4131
- const estimatedHours = nodes.reduce((sum, n) => sum + (n.estimateHours ?? 0), 0);
4132
- const actualHours = nodes.reduce((sum, n) => sum + (n.actualHours ?? 0), 0);
4362
+ let completed = 0, pending = 0, inProgress = 0, blocked = 0, failed = 0, review = 0;
4363
+ let estimatedHours = 0, actualHours = 0;
4364
+ for (const n of graph.nodes.values()) {
4365
+ switch (n.status) {
4366
+ case "completed":
4367
+ completed++;
4368
+ break;
4369
+ case "pending":
4370
+ pending++;
4371
+ break;
4372
+ case "in_progress":
4373
+ inProgress++;
4374
+ break;
4375
+ case "blocked":
4376
+ blocked++;
4377
+ break;
4378
+ case "failed":
4379
+ failed++;
4380
+ break;
4381
+ case "review":
4382
+ review++;
4383
+ break;
4384
+ }
4385
+ estimatedHours += n.estimateHours ?? 0;
4386
+ actualHours += n.actualHours ?? 0;
4387
+ }
4388
+ const total = graph.nodes.size;
4133
4389
  return {
4134
4390
  total,
4135
4391
  pending,
@@ -4185,40 +4441,40 @@ var TaskTracker = class {
4185
4441
  this.graph.rootNodes.push(newNode.id);
4186
4442
  }
4187
4443
  this.graph.updatedAt = now;
4188
- this.opts.store.saveGraph(this.graph);
4444
+ this.persist();
4189
4445
  return newNode;
4190
4446
  }
4191
4447
  addEdge(from, to, type = "depends_on") {
4192
4448
  if (!this.graph) throw new Error("No graph loaded");
4193
- const edge = {
4449
+ this.graph.edges.push({
4194
4450
  id: crypto.randomUUID(),
4195
4451
  from,
4196
4452
  to,
4197
4453
  type
4198
- };
4199
- this.graph.edges.push(edge);
4454
+ });
4200
4455
  this.graph.updatedAt = Date.now();
4201
- this.opts.store.saveGraph(this.graph);
4456
+ this.persist();
4202
4457
  }
4203
4458
  updateNodeStatus(id, status, reason) {
4204
4459
  if (!this.graph) throw new Error("No graph loaded");
4205
4460
  const node = this.graph.nodes.get(id);
4206
4461
  if (!node) throw new Error(`Node ${id} not found`);
4207
4462
  const from = node.status;
4463
+ const now = Date.now();
4208
4464
  node.status = status;
4209
- node.updatedAt = Date.now();
4465
+ node.updatedAt = now;
4210
4466
  if (status === "completed") {
4211
- node.completedAt = Date.now();
4467
+ node.completedAt = now;
4212
4468
  }
4213
- this.transitions.push({ from, to: status, timestamp: Date.now(), reason });
4469
+ this.transitions.push({ from, to: status, timestamp: now, reason });
4214
4470
  if (status === "completed") {
4215
4471
  this.unblockDependents(id);
4216
4472
  }
4217
4473
  if (status === "in_progress") {
4218
4474
  this.checkAndBlockIfNeeded(id);
4219
4475
  }
4220
- this.graph.updatedAt = Date.now();
4221
- this.opts.store.saveGraph(this.graph);
4476
+ this.graph.updatedAt = now;
4477
+ this.persist();
4222
4478
  }
4223
4479
  getNode(id) {
4224
4480
  return this.graph?.nodes.get(id);
@@ -4239,9 +4495,7 @@ var TaskTracker = class {
4239
4495
  }
4240
4496
  if (sort) {
4241
4497
  nodes.sort((a, b) => {
4242
- const aVal = a[sort.field] ?? "";
4243
- const bVal = b[sort.field] ?? "";
4244
- const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
4498
+ const cmp = compareByField(a, b, sort.field);
4245
4499
  return sort.direction === "asc" ? cmp : -cmp;
4246
4500
  });
4247
4501
  }
@@ -4320,7 +4574,47 @@ var TaskTracker = class {
4320
4574
  }
4321
4575
  }
4322
4576
  }
4577
+ /**
4578
+ * Fire-and-forget persistence with attached error handler.
4579
+ * Synchronous mutators (addNode/addEdge/updateNodeStatus) use this to
4580
+ * avoid forcing an async cascade through every caller; if the store
4581
+ * rejects, the configured `onPersistError` is invoked so failures are
4582
+ * surfaced instead of swallowed by an unhandled promise rejection.
4583
+ */
4584
+ persist() {
4585
+ if (!this.graph) return;
4586
+ this.opts.store.saveGraph(this.graph).catch((err) => {
4587
+ if (this.opts.onPersistError) this.opts.onPersistError(err);
4588
+ else console.warn("[task-tracker] saveGraph failed:", err instanceof Error ? err.message : String(err));
4589
+ });
4590
+ }
4591
+ };
4592
+ var PRIORITY_RANK = {
4593
+ critical: 0,
4594
+ high: 1,
4595
+ medium: 2,
4596
+ low: 3
4597
+ };
4598
+ var STATUS_RANK = {
4599
+ in_progress: 0,
4600
+ pending: 1,
4601
+ review: 2,
4602
+ blocked: 3,
4603
+ failed: 4,
4604
+ completed: 5
4323
4605
  };
4606
+ function compareByField(a, b, field) {
4607
+ switch (field) {
4608
+ case "priority":
4609
+ return PRIORITY_RANK[a.priority] - PRIORITY_RANK[b.priority];
4610
+ case "status":
4611
+ return STATUS_RANK[a.status] - STATUS_RANK[b.status];
4612
+ case "createdAt":
4613
+ return a.createdAt - b.createdAt;
4614
+ case "updatedAt":
4615
+ return a.updatedAt - b.updatedAt;
4616
+ }
4617
+ }
4324
4618
 
4325
4619
  // src/defaults/task-flow.ts
4326
4620
  var TaskFlow = class {
@@ -4372,9 +4666,10 @@ var TaskFlow = class {
4372
4666
  const task = batch[i];
4373
4667
  if (!result || !task) continue;
4374
4668
  if (result.status === "rejected") {
4375
- this.opts.tracker.updateNodeStatus(task.id, "failed", result.reason?.message);
4376
- this.emit("task.failed", { taskId: task.id, error: result.reason?.message ?? "unknown" });
4377
- ctx.onTaskFail?.(task, result.reason);
4669
+ const reason = result.reason;
4670
+ this.opts.tracker.updateNodeStatus(task.id, "failed", reason?.message);
4671
+ this.emit("task.failed", { taskId: task.id, error: reason?.message ?? "unknown" });
4672
+ ctx.onTaskFail?.(task, reason);
4378
4673
  } else {
4379
4674
  this.opts.tracker.updateNodeStatus(task.id, "completed");
4380
4675
  this.emit("task.completed", { taskId: task.id, result: result.value });
@@ -4657,7 +4952,6 @@ function createToolOutputSerializer(opts = {}) {
4657
4952
  }
4658
4953
  const half = Math.floor(available / 2);
4659
4954
  const first = text.slice(0, half);
4660
- Buffer.byteLength(first, "utf8");
4661
4955
  const second = text.slice(text.length - half);
4662
4956
  return { text: `${first}${marker}${second}`, newBudget: 0 };
4663
4957
  }
@@ -4707,7 +5001,7 @@ var ToolExecutor = class {
4707
5001
  return { result, tool, durationMs: Date.now() - start };
4708
5002
  }
4709
5003
  } else {
4710
- const suggestedPattern = this.subjectFor(tool.name, use.input) ?? tool.name;
5004
+ const suggestedPattern = this.subjectFor(tool.name, use.input, tool.subjectKey) ?? tool.name;
4711
5005
  const pending = { type: "tool_confirm_pending", toolUseId: use.id, toolName: tool.name, input: use.input, suggestedPattern };
4712
5006
  return { result: pending, tool, durationMs: Date.now() - start };
4713
5007
  }
@@ -4739,15 +5033,31 @@ var ToolExecutor = class {
4739
5033
  span?.end();
4740
5034
  }
4741
5035
  };
5036
+ const safeRun = async (use) => {
5037
+ try {
5038
+ return await runOne(use);
5039
+ } catch (err) {
5040
+ const msg = err instanceof Error ? err.message : String(err);
5041
+ const scrubbed = this.opts.secretScrubber.scrub(msg);
5042
+ const result = {
5043
+ type: "tool_result",
5044
+ tool_use_id: use.id,
5045
+ content: `Tool "${use.name}" execution failed: ${scrubbed}`,
5046
+ is_error: true
5047
+ };
5048
+ budget = this.decrementBudget(result, budget);
5049
+ return { result, tool: this.registry.get(use.name), durationMs: 0 };
5050
+ }
5051
+ };
4742
5052
  if (strategy === "sequential") {
4743
5053
  const outputs = [];
4744
5054
  for (const use of toolUses) {
4745
- if (use) outputs.push(await runOne(use));
5055
+ if (use) outputs.push(await safeRun(use));
4746
5056
  }
4747
5057
  return { outputs, remainingBudget: budget };
4748
5058
  }
4749
5059
  if (strategy === "parallel") {
4750
- const outputs = await Promise.all(toolUses.map((use) => runOne(use)));
5060
+ const outputs = await Promise.all(toolUses.map((use) => safeRun(use)));
4751
5061
  return { outputs, remainingBudget: budget };
4752
5062
  }
4753
5063
  const nonMutating = [];
@@ -4758,10 +5068,10 @@ var ToolExecutor = class {
4758
5068
  if (tool?.mutating) mutating.push(use);
4759
5069
  else nonMutating.push(use);
4760
5070
  }
4761
- const firstPass = await Promise.all(nonMutating.map((use) => runOne(use)));
5071
+ const firstPass = await Promise.all(nonMutating.map((use) => safeRun(use)));
4762
5072
  const secondPass = [];
4763
5073
  for (const use of mutating) {
4764
- secondPass.push(await runOne(use));
5074
+ secondPass.push(await safeRun(use));
4765
5075
  }
4766
5076
  return {
4767
5077
  outputs: [...firstPass, ...secondPass],
@@ -4796,7 +5106,8 @@ var ToolExecutor = class {
4796
5106
  }
4797
5107
  async runWithTimeout(tool, input, parentSignal, ctx, toolUseId) {
4798
5108
  if (parentSignal.aborted) {
4799
- throw parentSignal.reason instanceof Error ? parentSignal.reason : new Error(typeof parentSignal.reason === "string" ? parentSignal.reason : "aborted");
5109
+ if (parentSignal.reason instanceof Error) throw parentSignal.reason;
5110
+ throw new Error(typeof parentSignal.reason === "string" ? parentSignal.reason : "aborted");
4800
5111
  }
4801
5112
  const timeoutMs = tool.timeoutMs ?? this.iterationTimeoutMs;
4802
5113
  const ctrl = new AbortController();
@@ -4865,16 +5176,23 @@ var ToolExecutor = class {
4865
5176
  * Matches the logic in DefaultPermissionPolicy so the TUI shows the
4866
5177
  * same subject that the trust file would use.
4867
5178
  */
4868
- subjectFor(toolName, input) {
5179
+ subjectFor(toolName, input, subjectKey) {
4869
5180
  if (!input || typeof input !== "object") return void 0;
4870
5181
  const obj = input;
4871
5182
  const globChars = /[*?\[\]]/g;
4872
5183
  const escapeGlob = (s) => s.replace(globChars, (c) => `\\${c}`);
5184
+ const normalizePath = (s) => escapeGlob(s.replace(/\\/g, "/"));
5185
+ if (subjectKey) {
5186
+ const v = obj[subjectKey];
5187
+ if (typeof v === "string") {
5188
+ return subjectKey === "path" || subjectKey === "file" || subjectKey === "files" ? normalizePath(v) : escapeGlob(v);
5189
+ }
5190
+ }
4873
5191
  if (toolName === "bash" && typeof obj.command === "string") {
4874
5192
  return escapeGlob(obj.command);
4875
5193
  }
4876
5194
  if (typeof obj.path === "string") {
4877
- return escapeGlob(obj.path.replace(/\\/g, "/"));
5195
+ return normalizePath(obj.path);
4878
5196
  }
4879
5197
  if (typeof obj.url === "string") {
4880
5198
  return escapeGlob(obj.url);
@@ -5298,8 +5616,9 @@ var DefaultHealthRegistry = class {
5298
5616
  return { status, timestamp: Date.now(), checks: results };
5299
5617
  }
5300
5618
  async runOne(check) {
5619
+ let timer = null;
5301
5620
  const timeout = new Promise((resolve3) => {
5302
- setTimeout(
5621
+ timer = setTimeout(
5303
5622
  () => resolve3({ status: "unhealthy", detail: `timeout after ${this.timeoutMs}ms` }),
5304
5623
  this.timeoutMs
5305
5624
  );
@@ -5308,6 +5627,8 @@ var DefaultHealthRegistry = class {
5308
5627
  return await Promise.race([check.check(), timeout]);
5309
5628
  } catch (err) {
5310
5629
  return { status: "unhealthy", detail: err instanceof Error ? err.message : String(err) };
5630
+ } finally {
5631
+ if (timer) clearTimeout(timer);
5311
5632
  }
5312
5633
  }
5313
5634
  };
@@ -5946,7 +6267,6 @@ function createContextManagerTool(opts = {}) {
5946
6267
  notes: `Invalid range [${from}, ${to}] for ${messages.length} messages.`
5947
6268
  };
5948
6269
  }
5949
- messages.slice(from, to + 1);
5950
6270
  const summaryText = input.text ?? '[summary placeholder \u2014 provide "text" to record the summary]';
5951
6271
  const summaryMsg = {
5952
6272
  role: "system",