@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.
package/dist/index.js CHANGED
@@ -467,7 +467,8 @@ var TOKENS = {
467
467
  SkillLoader: t("SkillLoader"),
468
468
  SystemPromptBuilder: t("SystemPromptBuilder"),
469
469
  SecretScrubber: t("SecretScrubber"),
470
- ModelsRegistry: t("ModelsRegistry")
470
+ ModelsRegistry: t("ModelsRegistry"),
471
+ ModeStore: t("ModeStore")
471
472
  };
472
473
 
473
474
  // src/kernel/run-controller.ts
@@ -800,16 +801,33 @@ var DEFAULT_SPEC_TEMPLATE = {
800
801
 
801
802
  // src/types/task-graph.ts
802
803
  function computeTaskProgress(graph) {
803
- const nodes = Array.from(graph.nodes.values());
804
- const total = nodes.length;
805
- const completed = nodes.filter((n) => n.status === "completed").length;
806
- const pending = nodes.filter((n) => n.status === "pending").length;
807
- const inProgress = nodes.filter((n) => n.status === "in_progress").length;
808
- const blocked = nodes.filter((n) => n.status === "blocked").length;
809
- const failed = nodes.filter((n) => n.status === "failed").length;
810
- const review = nodes.filter((n) => n.status === "review").length;
811
- const estimatedHours = nodes.reduce((sum, n) => sum + (n.estimateHours ?? 0), 0);
812
- const actualHours = nodes.reduce((sum, n) => sum + (n.actualHours ?? 0), 0);
804
+ let completed = 0, pending = 0, inProgress = 0, blocked = 0, failed = 0, review = 0;
805
+ let estimatedHours = 0, actualHours = 0;
806
+ for (const n of graph.nodes.values()) {
807
+ switch (n.status) {
808
+ case "completed":
809
+ completed++;
810
+ break;
811
+ case "pending":
812
+ pending++;
813
+ break;
814
+ case "in_progress":
815
+ inProgress++;
816
+ break;
817
+ case "blocked":
818
+ blocked++;
819
+ break;
820
+ case "failed":
821
+ failed++;
822
+ break;
823
+ case "review":
824
+ review++;
825
+ break;
826
+ }
827
+ estimatedHours += n.estimateHours ?? 0;
828
+ actualHours += n.actualHours ?? 0;
829
+ }
830
+ const total = graph.nodes.size;
813
831
  return {
814
832
  total,
815
833
  pending,
@@ -836,16 +854,18 @@ function findCriticalPath(graph) {
836
854
  }
837
855
  function topologicalSort(graph) {
838
856
  const visited = /* @__PURE__ */ new Set();
857
+ const inStack = /* @__PURE__ */ new Set();
839
858
  const result = [];
840
859
  function visit(id) {
860
+ if (inStack.has(id)) return;
841
861
  if (visited.has(id)) return;
862
+ if (!graph.nodes.has(id)) return;
842
863
  visited.add(id);
843
- const node = graph.nodes.get(id);
844
- if (!node) return;
845
- const outgoing = graph.edges.filter((e) => e.from === id);
846
- for (const edge of outgoing) {
847
- visit(edge.to);
864
+ inStack.add(id);
865
+ for (const edge of graph.edges) {
866
+ if (edge.from === id) visit(edge.to);
848
867
  }
868
+ inStack.delete(id);
849
869
  result.push(id);
850
870
  }
851
871
  for (const rootId of graph.rootNodes) {
@@ -937,7 +957,7 @@ function sanitizeJsonString(s) {
937
957
  JSON.parse(out);
938
958
  return out;
939
959
  } catch {
940
- return out;
960
+ return null;
941
961
  }
942
962
  }
943
963
  function stripSingleLineComments(s) {
@@ -1027,7 +1047,22 @@ function stripAnsi(s) {
1027
1047
 
1028
1048
  // src/utils/glob-match.ts
1029
1049
  function escapeRegex(s) {
1030
- return s.replace(/[.+^${}()|\\/]/g, "\\$&");
1050
+ return s.replace(/[.+^${}()|\\]/g, "\\$&");
1051
+ }
1052
+ var COMPILED_GLOB_CACHE = /* @__PURE__ */ new Map();
1053
+ var CACHE_MAX_SIZE = 2e3;
1054
+ function getCachedGlob(pattern) {
1055
+ const cached = COMPILED_GLOB_CACHE.get(pattern);
1056
+ if (cached) return cached;
1057
+ if (COMPILED_GLOB_CACHE.size >= CACHE_MAX_SIZE) {
1058
+ const keys = [...COMPILED_GLOB_CACHE.keys()];
1059
+ for (let i = 0; i < Math.floor(CACHE_MAX_SIZE / 4); i++) {
1060
+ COMPILED_GLOB_CACHE.delete(keys[i]);
1061
+ }
1062
+ }
1063
+ const re = compileGlob(pattern);
1064
+ COMPILED_GLOB_CACHE.set(pattern, re);
1065
+ return re;
1031
1066
  }
1032
1067
  function compileGlob(pattern) {
1033
1068
  let i = 0;
@@ -1076,7 +1111,7 @@ function compileGlob(pattern) {
1076
1111
  return new RegExp(re);
1077
1112
  }
1078
1113
  function matchGlob(pattern, input) {
1079
- return compileGlob(pattern).test(input);
1114
+ return getCachedGlob(pattern).test(input);
1080
1115
  }
1081
1116
  function matchAny(patterns, input) {
1082
1117
  return patterns.some((p) => matchGlob(p, input));
@@ -1243,6 +1278,7 @@ function resolveWstackPaths(opts) {
1243
1278
  const projectDir = path2.join(globalRoot, "projects", hash);
1244
1279
  return {
1245
1280
  globalRoot,
1281
+ configDir: globalRoot,
1246
1282
  globalConfig: path2.join(globalRoot, "config.json"),
1247
1283
  secretsKey: path2.join(globalRoot, ".key"),
1248
1284
  globalMemory: path2.join(globalRoot, "memory.md"),
@@ -1301,7 +1337,6 @@ function createToolOutputSerializer(opts = {}) {
1301
1337
  }
1302
1338
  const half = Math.floor(available / 2);
1303
1339
  const first = text.slice(0, half);
1304
- Buffer.byteLength(first, "utf8");
1305
1340
  const second = text.slice(text.length - half);
1306
1341
  return { text: `${first}${marker}${second}`, newBudget: 0 };
1307
1342
  }
@@ -1310,21 +1345,33 @@ function createToolOutputSerializer(opts = {}) {
1310
1345
 
1311
1346
  // src/utils/token-estimate.ts
1312
1347
  var RoughTokenEstimate = (text) => Math.max(1, Math.ceil(text.length / 4));
1348
+ var ESTIMATE_CACHE = /* @__PURE__ */ new Map();
1349
+ var ESTIMATE_CACHE_MAX_SIZE = 1e4;
1350
+ function getCachedEstimate(key, compute) {
1351
+ const existing = ESTIMATE_CACHE.get(key);
1352
+ if (existing !== void 0) return existing;
1353
+ if (ESTIMATE_CACHE.size >= ESTIMATE_CACHE_MAX_SIZE) {
1354
+ const keys = [...ESTIMATE_CACHE.keys()];
1355
+ for (let i = 0; i < Math.floor(ESTIMATE_CACHE_MAX_SIZE / 4); i++) {
1356
+ ESTIMATE_CACHE.delete(keys[i]);
1357
+ }
1358
+ }
1359
+ const estimate = compute();
1360
+ ESTIMATE_CACHE.set(key, estimate);
1361
+ return estimate;
1362
+ }
1313
1363
  function estimateToolInputTokens(input) {
1314
1364
  if (typeof input === "string") return RoughTokenEstimate(input);
1315
- if (input !== null && typeof input === "object" && "__tokenEstimate" in input) {
1316
- return input.__tokenEstimate;
1317
- }
1318
- const str = typeof input === "object" ? JSON.stringify(input) : String(input);
1319
- const estimate = RoughTokenEstimate(str);
1320
- if (input !== null && typeof input === "object" && !Array.isArray(input)) {
1321
- input.__tokenEstimate = estimate;
1365
+ if (input === null || typeof input !== "object") {
1366
+ return RoughTokenEstimate(String(input));
1322
1367
  }
1323
- return estimate;
1368
+ const key = JSON.stringify(input);
1369
+ return getCachedEstimate(key, () => RoughTokenEstimate(key));
1324
1370
  }
1325
1371
  function estimateToolResultTokens(content) {
1326
1372
  if (typeof content === "string") return RoughTokenEstimate(content);
1327
- return RoughTokenEstimate(JSON.stringify(content));
1373
+ const key = JSON.stringify(content);
1374
+ return getCachedEstimate(key, () => RoughTokenEstimate(key));
1328
1375
  }
1329
1376
  function estimateTextTokens(text) {
1330
1377
  return RoughTokenEstimate(text);
@@ -1567,9 +1614,11 @@ var DefaultPathResolver = class {
1567
1614
  ensureInsideRoot(absPath) {
1568
1615
  const resolved = this.resolve(absPath);
1569
1616
  if (!this.isInsideRoot(resolved)) {
1570
- throw new Error(
1571
- `Path "${absPath}" resolves outside the project root (${this.projectRoot})`
1572
- );
1617
+ const display = path2.isAbsolute(absPath) ? path2.basename(absPath) : absPath;
1618
+ const err = new Error(`Path "${display}" resolves outside the project root`);
1619
+ err.fullPath = absPath;
1620
+ err.projectRoot = this.projectRoot;
1621
+ throw err;
1573
1622
  }
1574
1623
  return resolved;
1575
1624
  }
@@ -1697,7 +1746,9 @@ var DefaultSessionStore = class {
1697
1746
  try {
1698
1747
  handle = await fsp.open(file, "a", 384);
1699
1748
  } catch (err) {
1700
- throw new Error(`Failed to open session file: ${err instanceof Error ? err.message : String(err)}`);
1749
+ throw new Error(`Failed to open session file: ${err instanceof Error ? err.message : String(err)}`, {
1750
+ cause: err
1751
+ });
1701
1752
  }
1702
1753
  try {
1703
1754
  return new FileSessionWriter(id, handle, startedAt, meta, { dir: this.dir, filePath: file });
@@ -1715,7 +1766,8 @@ var DefaultSessionStore = class {
1715
1766
  handle = await fsp.open(file, "a", 384);
1716
1767
  } catch (err) {
1717
1768
  throw new Error(
1718
- `Failed to open session "${id}" for append: ${err instanceof Error ? err.message : String(err)}`
1769
+ `Failed to open session "${id}" for append: ${err instanceof Error ? err.message : String(err)}`,
1770
+ { cause: err }
1719
1771
  );
1720
1772
  }
1721
1773
  const writer = new FileSessionWriter(
@@ -1738,7 +1790,10 @@ var DefaultSessionStore = class {
1738
1790
  const events = [];
1739
1791
  for (const line of lines) {
1740
1792
  try {
1741
- events.push(JSON.parse(line));
1793
+ const parsed = JSON.parse(line);
1794
+ if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
1795
+ events.push(parsed);
1796
+ }
1742
1797
  } catch {
1743
1798
  }
1744
1799
  }
@@ -1755,7 +1810,11 @@ var DefaultSessionStore = class {
1755
1810
  ids.map((id) => this.summaryFor(id).catch(() => null))
1756
1811
  );
1757
1812
  const out = sessions.filter((s) => s !== null);
1758
- out.sort((a, b) => a.startedAt < b.startedAt ? 1 : -1);
1813
+ out.sort((a, b) => {
1814
+ if (a.startedAt < b.startedAt) return 1;
1815
+ if (a.startedAt > b.startedAt) return -1;
1816
+ return a.id.localeCompare(b.id);
1817
+ });
1759
1818
  return out.slice(0, limit);
1760
1819
  } catch {
1761
1820
  return [];
@@ -1901,6 +1960,8 @@ var FileSessionWriter = class {
1901
1960
  filePath;
1902
1961
  initDone = false;
1903
1962
  resumed;
1963
+ appendFailCount = 0;
1964
+ lastAppendWarnAt = 0;
1904
1965
  async writeSessionStart() {
1905
1966
  if (this.initDone || this.closed) return;
1906
1967
  this.initDone = true;
@@ -1929,7 +1990,19 @@ var FileSessionWriter = class {
1929
1990
  await this.handle.appendFile(`${JSON.stringify(event)}
1930
1991
  `, "utf8");
1931
1992
  } catch (err) {
1932
- console.warn("[session] append failed:", err instanceof Error ? err.message : String(err));
1993
+ this.appendFailCount++;
1994
+ const now = Date.now();
1995
+ if (now - this.lastAppendWarnAt > 5e3) {
1996
+ const suppressed = this.appendFailCount - 1;
1997
+ const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
1998
+ console.warn(
1999
+ "[session] append failed:",
2000
+ err instanceof Error ? err.message : String(err),
2001
+ tail
2002
+ );
2003
+ this.lastAppendWarnAt = now;
2004
+ this.appendFailCount = 0;
2005
+ }
1933
2006
  }
1934
2007
  }
1935
2008
  /**
@@ -2135,6 +2208,15 @@ function mergeAdjacentText(blocks) {
2135
2208
  var MAX_BYTES_TOTAL = 32e3;
2136
2209
  var DefaultMemoryStore = class {
2137
2210
  files;
2211
+ /**
2212
+ * Per-scope serialization queue. `remember` / `forget` / `consolidate` /
2213
+ * `clear` are read-modify-write against a single file; without a lock,
2214
+ * two concurrent calls on the same scope can read the same baseline and
2215
+ * the later write silently drops the earlier entry. We chain each
2216
+ * mutation onto the prior promise for the same scope so they run in
2217
+ * issue order. Different scopes still proceed in parallel.
2218
+ */
2219
+ writeChain = /* @__PURE__ */ new Map();
2138
2220
  constructor(opts) {
2139
2221
  this.files = {
2140
2222
  "project-agents": opts.paths.inProjectAgentsFile,
@@ -2142,6 +2224,18 @@ var DefaultMemoryStore = class {
2142
2224
  "user-memory": opts.paths.globalMemory
2143
2225
  };
2144
2226
  }
2227
+ async runSerialized(scope, work) {
2228
+ const prior = this.writeChain.get(scope) ?? Promise.resolve();
2229
+ const next = prior.catch(() => void 0).then(work);
2230
+ this.writeChain.set(scope, next);
2231
+ try {
2232
+ return await next;
2233
+ } finally {
2234
+ if (this.writeChain.get(scope) === next) {
2235
+ this.writeChain.delete(scope);
2236
+ }
2237
+ }
2238
+ }
2145
2239
  async readAll() {
2146
2240
  const parts = [];
2147
2241
  for (const scope of ["project-agents", "project-memory", "user-memory"]) {
@@ -2160,27 +2254,32 @@ ${body.trim()}`);
2160
2254
  }
2161
2255
  }
2162
2256
  async remember(text, scope = "project-memory") {
2163
- const file = this.files[scope];
2164
- await ensureDir(path2.dirname(file));
2165
- let existing = "";
2166
- try {
2167
- existing = await fsp.readFile(file, "utf8");
2168
- } catch {
2169
- }
2170
- const ts = (/* @__PURE__ */ new Date()).toISOString();
2171
- const id = `mem_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
2172
- const entry = `
2257
+ return this.runSerialized(scope, async () => {
2258
+ const file = this.files[scope];
2259
+ await ensureDir(path2.dirname(file));
2260
+ let existing = "";
2261
+ try {
2262
+ existing = await fsp.readFile(file, "utf8");
2263
+ } catch {
2264
+ }
2265
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
2266
+ const id = `mem_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
2267
+ const entry = `
2173
2268
  - [${ts}] ${id} ${text.replace(/\n/g, " ")}
2174
2269
  `;
2175
- const next = existing.trim() ? existing.replace(/\n+$/, "") + entry : `# WrongStack Memory
2270
+ const next = existing.trim() ? existing.replace(/\n+$/, "") + entry : `# WrongStack Memory
2176
2271
  ${entry}`;
2177
- await atomicWrite(file, next);
2178
- const buf = Buffer.byteLength(next, "utf8");
2179
- if (buf > MAX_BYTES_TOTAL) {
2180
- await this.consolidate(scope);
2181
- }
2272
+ await atomicWrite(file, next);
2273
+ const buf = Buffer.byteLength(next, "utf8");
2274
+ if (buf > MAX_BYTES_TOTAL) {
2275
+ await this.consolidateUnsafe(scope);
2276
+ }
2277
+ });
2182
2278
  }
2183
2279
  async forget(query, scope = "project-memory") {
2280
+ return this.runSerialized(scope, async () => this.forgetUnsafe(query, scope));
2281
+ }
2282
+ async forgetUnsafe(query, scope) {
2184
2283
  const file = this.files[scope];
2185
2284
  let existing;
2186
2285
  try {
@@ -2217,6 +2316,9 @@ ${entry}`;
2217
2316
  return removed;
2218
2317
  }
2219
2318
  async consolidate(scope) {
2319
+ return this.runSerialized(scope, async () => this.consolidateUnsafe(scope));
2320
+ }
2321
+ async consolidateUnsafe(scope) {
2220
2322
  const file = this.files[scope];
2221
2323
  let existing;
2222
2324
  try {
@@ -2247,12 +2349,14 @@ ${entry}`;
2247
2349
  }
2248
2350
  async clear(scope) {
2249
2351
  if (scope) {
2250
- await atomicWrite(this.files[scope], "");
2251
- } else {
2252
- for (const s of ["project-agents", "project-memory", "user-memory"]) {
2253
- await atomicWrite(this.files[s], "");
2254
- }
2352
+ await this.runSerialized(scope, async () => atomicWrite(this.files[scope], ""));
2353
+ return;
2255
2354
  }
2355
+ await Promise.all(
2356
+ ["project-agents", "project-memory", "user-memory"].map(
2357
+ (s) => this.runSerialized(s, async () => atomicWrite(this.files[s], ""))
2358
+ )
2359
+ );
2256
2360
  }
2257
2361
  };
2258
2362
  function labelOf(scope) {
@@ -2300,9 +2404,27 @@ var PATTERNS = [
2300
2404
  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
2301
2405
  }
2302
2406
  ];
2407
+ var SCRUB_CHUNK_BYTES = 64 * 1024;
2303
2408
  var DefaultSecretScrubber = class {
2304
2409
  scrub(text) {
2305
2410
  if (!text) return text;
2411
+ if (text.length <= SCRUB_CHUNK_BYTES) {
2412
+ return this.scrubOne(text);
2413
+ }
2414
+ const out = [];
2415
+ let i = 0;
2416
+ while (i < text.length) {
2417
+ let end = Math.min(i + SCRUB_CHUNK_BYTES, text.length);
2418
+ if (end < text.length) {
2419
+ const nl = text.lastIndexOf("\n", end);
2420
+ if (nl > i + SCRUB_CHUNK_BYTES / 2) end = nl + 1;
2421
+ }
2422
+ out.push(this.scrubOne(text.slice(i, end)));
2423
+ i = end;
2424
+ }
2425
+ return out.join("");
2426
+ }
2427
+ scrubOne(text) {
2306
2428
  let out = text;
2307
2429
  for (const p of PATTERNS) {
2308
2430
  out = out.replace(p.regex, (_match, group1, group2) => {
@@ -2402,7 +2524,17 @@ var DefaultSecretVault = class {
2402
2524
  }
2403
2525
  };
2404
2526
  function decryptConfigSecrets(cfg, vault) {
2405
- return walk2(cfg, vault, (v) => vault.decrypt(v));
2527
+ return walk2(cfg, vault, (v, key) => {
2528
+ try {
2529
+ return vault.decrypt(v);
2530
+ } catch (err) {
2531
+ console.warn(
2532
+ `[secret-vault] Failed to decrypt "${key}":`,
2533
+ err instanceof Error ? err.message : err
2534
+ );
2535
+ return "";
2536
+ }
2537
+ });
2406
2538
  }
2407
2539
  function encryptConfigSecrets(cfg, vault) {
2408
2540
  return walk2(cfg, vault, (v) => vault.encrypt(v));
@@ -2416,7 +2548,7 @@ function walk2(node, vault, transform) {
2416
2548
  const out = {};
2417
2549
  for (const [k, v] of Object.entries(node)) {
2418
2550
  if (typeof v === "string" && isSecretField(k)) {
2419
- out[k] = transform(v);
2551
+ out[k] = transform(v, k);
2420
2552
  } else if (typeof v === "object" && v !== null) {
2421
2553
  out[k] = walk2(v, vault, transform);
2422
2554
  } else {
@@ -2490,9 +2622,11 @@ function walkCount(node, vault, counter) {
2490
2622
  }
2491
2623
  return out;
2492
2624
  }
2625
+ var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
2493
2626
  function deepMerge(a, b) {
2494
2627
  const out = { ...a };
2495
2628
  for (const [k, v] of Object.entries(b)) {
2629
+ if (FORBIDDEN_PROTO_KEYS.has(k)) continue;
2496
2630
  const existing = out[k];
2497
2631
  if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
2498
2632
  out[k] = deepMerge(existing, v);
@@ -2527,7 +2661,7 @@ var DefaultPermissionPolicy = class {
2527
2661
  if (!this.loaded) await this.reload();
2528
2662
  const namespaceEntry = this.findNamespaceEntry(tool.name);
2529
2663
  const entry = this.policy[tool.name] ?? namespaceEntry;
2530
- const subject = this.subjectFor(tool.name, input);
2664
+ const subject = this.subjectFor(tool.name, input, tool.subjectKey);
2531
2665
  if (entry?.deny && subject && matchAny(entry.deny, subject)) {
2532
2666
  return { permission: "deny", source: "deny", reason: "matched deny pattern" };
2533
2667
  }
@@ -2575,16 +2709,23 @@ var DefaultPermissionPolicy = class {
2575
2709
  throw err;
2576
2710
  }
2577
2711
  }
2578
- subjectFor(toolName, input) {
2712
+ subjectFor(toolName, input, subjectKey) {
2579
2713
  if (!input || typeof input !== "object") return void 0;
2580
2714
  const obj = input;
2581
2715
  const globChars = /[*?\[\]]/g;
2582
2716
  const escapeGlob = (s) => s.replace(globChars, (c) => `\\${c}`);
2717
+ const normalizePath = (s) => escapeGlob(s.replace(/\\/g, "/"));
2718
+ if (subjectKey) {
2719
+ const v = obj[subjectKey];
2720
+ if (typeof v === "string") {
2721
+ return subjectKey === "path" || subjectKey === "file" || subjectKey === "files" ? normalizePath(v) : escapeGlob(v);
2722
+ }
2723
+ }
2583
2724
  if (toolName === "bash" && typeof obj.command === "string") {
2584
2725
  return escapeGlob(obj.command);
2585
2726
  }
2586
2727
  if (typeof obj.path === "string") {
2587
- return escapeGlob(obj.path.replace(/\\/g, "/"));
2728
+ return normalizePath(obj.path);
2588
2729
  }
2589
2730
  if (typeof obj.url === "string") {
2590
2731
  return escapeGlob(obj.url);
@@ -2605,14 +2746,15 @@ var DefaultPermissionPolicy = class {
2605
2746
  };
2606
2747
 
2607
2748
  // src/defaults/retry-policy.ts
2608
- var DefaultRetryPolicy = class {
2749
+ var DefaultRetryPolicy = class _DefaultRetryPolicy {
2750
+ static NETWORK_ERR_RE = /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i;
2609
2751
  shouldRetry(err, attempt) {
2610
2752
  if (err instanceof ProviderError) {
2611
2753
  if (!err.retryable) return false;
2612
2754
  return attempt < this.maxAttempts(err);
2613
2755
  }
2614
2756
  const msg = err.message ?? "";
2615
- const isNetwork = /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(msg);
2757
+ const isNetwork = _DefaultRetryPolicy.NETWORK_ERR_RE.test(msg);
2616
2758
  if (isNetwork) return attempt < 2;
2617
2759
  return false;
2618
2760
  }
@@ -2634,55 +2776,43 @@ var DefaultRetryPolicy = class {
2634
2776
  };
2635
2777
 
2636
2778
  // src/defaults/error-handler.ts
2779
+ var CONTEXT_OVERFLOW_RE = /context|too long|tokens/i;
2780
+ var NETWORK_ERR_RE = /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i;
2637
2781
  function buildRecoveryStrategies(opts) {
2638
2782
  return [
2639
2783
  {
2640
2784
  label: "context_overflow_reduce",
2641
2785
  compactor: opts?.compactor,
2642
2786
  async attempt(err, ctx) {
2643
- if (err instanceof ProviderError && (err.status === 413 || /context|too long|tokens/i.test(err.message))) {
2644
- if (this.compactor) {
2645
- try {
2646
- const report = await this.compactor.compact(ctx, { aggressive: true });
2647
- if (report.after < report.before) {
2648
- return {
2649
- content: [{ type: "text", text: "[context compacted automatically \u2014 please retry]" }],
2650
- stopReason: "end_turn",
2651
- usage: { input: 0, output: 0 },
2652
- model: ctx.model
2653
- };
2654
- }
2655
- } catch {
2787
+ if (!(err instanceof ProviderError)) return null;
2788
+ if (err.status !== 413 && !CONTEXT_OVERFLOW_RE.test(err.message)) return null;
2789
+ if (this.compactor) {
2790
+ try {
2791
+ const report = await this.compactor.compact(ctx, { aggressive: true });
2792
+ if (report.after < report.before) {
2793
+ return { action: "retry", reason: "context_compacted" };
2656
2794
  }
2795
+ } catch {
2657
2796
  }
2658
- return null;
2659
2797
  }
2660
2798
  return null;
2661
2799
  }
2662
2800
  },
2663
2801
  {
2664
2802
  label: "rate_limit_backoff",
2665
- async attempt(err, ctx) {
2666
- if (err instanceof ProviderError && err.status === 429) {
2667
- const delayMs = err.body?.retryAfterMs ?? 5e3;
2668
- const delay = Math.max(1e3, Math.min(delayMs, 6e4));
2669
- await new Promise((r) => setTimeout(r, delay));
2670
- return {
2671
- content: [{ type: "text", text: "[rate limit backoff applied \u2014 please retry]" }],
2672
- stopReason: "end_turn",
2673
- usage: { input: 0, output: 0 },
2674
- model: ctx.model
2675
- };
2676
- }
2677
- return null;
2803
+ async attempt(err) {
2804
+ if (!(err instanceof ProviderError) || err.status !== 429) return null;
2805
+ const delayMs = err.body?.retryAfterMs ?? 5e3;
2806
+ const delay = Math.max(1e3, Math.min(delayMs, 6e4));
2807
+ await new Promise((r) => setTimeout(r, delay));
2808
+ return { action: "retry", reason: "rate_limit_backoff" };
2678
2809
  }
2679
2810
  },
2680
2811
  {
2681
2812
  label: "downgrade_model",
2682
2813
  async attempt(err, ctx) {
2683
- if (err instanceof ProviderError && (err.status === 429 || err.status === 529 || err.status >= 500)) {
2684
- return null;
2685
- }
2814
+ if (!(err instanceof ProviderError)) return null;
2815
+ if (err.status !== 429 && err.status !== 529 && err.status < 500) return null;
2686
2816
  return null;
2687
2817
  }
2688
2818
  }
@@ -2705,12 +2835,12 @@ var DefaultErrorHandler = class {
2705
2835
  if (err.status === 429) return { kind: "rate_limit", retryable: true };
2706
2836
  if (err.status === 529) return { kind: "overloaded", retryable: true };
2707
2837
  if (err.status >= 500) return { kind: "server", retryable: true };
2708
- if (err.status === 413 || /context|too long|tokens/i.test(err.message)) {
2838
+ if (err.status === 413 || CONTEXT_OVERFLOW_RE.test(err.message)) {
2709
2839
  return { kind: "context_overflow", retryable: false };
2710
2840
  }
2711
2841
  if (err.status >= 400) return { kind: "client", retryable: false };
2712
2842
  }
2713
- if (err instanceof Error && /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(err.message)) {
2843
+ if (err instanceof Error && NETWORK_ERR_RE.test(err.message)) {
2714
2844
  return { kind: "network", retryable: true };
2715
2845
  }
2716
2846
  return { kind: "unknown", retryable: false };
@@ -2774,13 +2904,28 @@ var DefaultSkillLoader = class {
2774
2904
  async manifestText() {
2775
2905
  const skills = await this.list();
2776
2906
  if (skills.length === 0) return "";
2907
+ const entries = await this.listEntries();
2777
2908
  const lines = ["## Available skills"];
2778
- for (const s of skills) {
2779
- lines.push(`- **${s.name}** \u2014 ${s.description.replace(/\n/g, " ").trim()}`);
2780
- lines.push(` Path: ${s.path}`);
2909
+ for (const e of entries) {
2910
+ const scopeTag = e.scope.length > 0 ? ` \u2014 ${e.scope.slice(0, 3).join(", ")}` : "";
2911
+ lines.push(`- **${e.name}**${scopeTag}`);
2912
+ lines.push(` Use when: ${e.trigger}`);
2781
2913
  }
2782
2914
  return lines.join("\n");
2783
2915
  }
2916
+ async listEntries() {
2917
+ const skills = await this.list();
2918
+ const entries = [];
2919
+ for (const s of skills) {
2920
+ try {
2921
+ const raw = await fsp.readFile(s.path, "utf8");
2922
+ const { trigger, scope } = parseDescription(raw);
2923
+ entries.push({ name: s.name, trigger, scope, source: s.source, path: s.path });
2924
+ } catch {
2925
+ }
2926
+ }
2927
+ return entries;
2928
+ }
2784
2929
  async readBody(name) {
2785
2930
  const m = await this.find(name);
2786
2931
  if (!m) throw new Error(`Skill "${name}" not found`);
@@ -2823,6 +2968,19 @@ function parseFrontmatter(raw) {
2823
2968
  flush();
2824
2969
  return out;
2825
2970
  }
2971
+ function parseDescription(raw) {
2972
+ const fm = parseFrontmatter(raw);
2973
+ const desc = fm.description ?? "";
2974
+ const firstSentenceEnd = desc.indexOf(". ");
2975
+ const trigger = firstSentenceEnd !== -1 ? desc.slice(0, firstSentenceEnd + 1).trim() : desc.trim().split("\n")[0] ?? "";
2976
+ const scope = [];
2977
+ const coversMatch = /(?:covers|for|including)\s+([^.]+)/i.exec(desc);
2978
+ if (coversMatch) {
2979
+ const items = coversMatch[1].replace(/[·•]/g, ",").split(",").map((s) => s.trim()).filter(Boolean);
2980
+ scope.push(...items);
2981
+ }
2982
+ return { trigger, scope };
2983
+ }
2826
2984
  var BEHAVIOR_DEFAULTS = {
2827
2985
  version: 1,
2828
2986
  context: {
@@ -2871,11 +3029,13 @@ var ENV_MAP = {
2871
3029
  function isPrimitiveArray(a) {
2872
3030
  return a.every((v) => v === null || typeof v !== "object");
2873
3031
  }
3032
+ var FORBIDDEN_PROTO_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
2874
3033
  function deepMerge2(base, patch) {
2875
3034
  if (typeof base !== "object" || base === null) return patch ?? base;
2876
3035
  if (typeof patch !== "object" || patch === null) return base;
2877
3036
  const out = { ...base };
2878
3037
  for (const [k, v] of Object.entries(patch)) {
3038
+ if (FORBIDDEN_PROTO_KEYS2.has(k)) continue;
2879
3039
  const existing = out[k];
2880
3040
  if (Array.isArray(v)) {
2881
3041
  if (Array.isArray(existing) && isPrimitiveArray(v) && isPrimitiveArray(existing)) {
@@ -2943,8 +3103,12 @@ var DefaultConfigLoader = class {
2943
3103
  if (cfg.providers) {
2944
3104
  for (const pcfg of Object.values(cfg.providers)) {
2945
3105
  if (!pcfg || typeof pcfg !== "object") continue;
2946
- const keys = pcfg.apiKeys;
2947
- if (!Array.isArray(keys) || keys.length === 0) continue;
3106
+ const rawKeys = pcfg.apiKeys;
3107
+ if (!Array.isArray(rawKeys) || rawKeys.length === 0) continue;
3108
+ const keys = rawKeys.filter(
3109
+ (k) => !!k && typeof k === "object" && typeof k.label === "string" && typeof k.apiKey === "string"
3110
+ );
3111
+ if (keys.length === 0) continue;
2948
3112
  const existing = pcfg.apiKey;
2949
3113
  if (existing && existing.length > 0) continue;
2950
3114
  const activeLabel = pcfg.activeKey;
@@ -2955,23 +3119,42 @@ var DefaultConfigLoader = class {
2955
3119
  }
2956
3120
  }
2957
3121
  this.validateBehavior(cfg);
2958
- if (this.strict) this.validateIdentity(cfg);
3122
+ if (this.strict) {
3123
+ this.validateIdentity(cfg);
3124
+ }
2959
3125
  return Object.freeze(cfg);
2960
3126
  }
2961
3127
  async readJson(file) {
3128
+ let raw;
2962
3129
  try {
2963
- const raw = await fsp.readFile(file, "utf8");
2964
- const parsed = safeParse(raw);
2965
- if (parsed.ok && parsed.value) return parsed.value;
2966
- } catch {
3130
+ raw = await fsp.readFile(file, "utf8");
3131
+ } catch (err) {
3132
+ if (err.code !== "ENOENT") {
3133
+ console.warn(`[config] Failed to read "${file}":`, err);
3134
+ }
3135
+ return {};
3136
+ }
3137
+ const parsed = safeParse(raw);
3138
+ if (!parsed.ok || !parsed.value) {
3139
+ console.warn(
3140
+ `[config] Failed to parse "${file}": invalid JSON. Falling back to defaults for this layer.`
3141
+ );
3142
+ return {};
2967
3143
  }
2968
- return {};
3144
+ return parsed.value;
2969
3145
  }
2970
3146
  validateBehavior(cfg) {
2971
3147
  if (cfg.version === void 0) throw new Error("Config: missing version field");
2972
3148
  if (cfg.version !== 1) throw new Error(`Config: unsupported version ${cfg.version}`);
2973
3149
  const c = cfg.context;
2974
3150
  if (!c) throw new Error("Config: missing context section");
3151
+ const fields = ["warnThreshold", "softThreshold", "hardThreshold"];
3152
+ for (const f of fields) {
3153
+ const v = c[f];
3154
+ if (typeof v !== "number" || !Number.isFinite(v)) {
3155
+ throw new Error(`Config: context.${String(f)} must be a finite number (got ${typeof v})`);
3156
+ }
3157
+ }
2975
3158
  if (c.warnThreshold >= c.softThreshold || c.softThreshold >= c.hardThreshold) {
2976
3159
  throw new Error("Config: context thresholds must satisfy warn < soft < hard");
2977
3160
  }
@@ -3019,7 +3202,8 @@ var DefaultConfigStore = class {
3019
3202
  for (const w of this.watchers) {
3020
3203
  try {
3021
3204
  w(next, prev);
3022
- } catch {
3205
+ } catch (err) {
3206
+ console.error("[config-store] watcher threw:", err);
3023
3207
  }
3024
3208
  }
3025
3209
  return next;
@@ -3129,9 +3313,18 @@ var HybridCompactor = class {
3129
3313
  }
3130
3314
  }
3131
3315
  let saved = 0;
3132
- for (let i = 0; i < preserveStart; i++) {
3316
+ let changed = false;
3317
+ const nextMessages = new Array(messages.length);
3318
+ for (let i = 0; i < messages.length; i++) {
3133
3319
  const msg = messages[i];
3134
- if (!msg || !Array.isArray(msg.content)) continue;
3320
+ if (i >= preserveStart) {
3321
+ nextMessages[i] = msg;
3322
+ continue;
3323
+ }
3324
+ if (!msg || !Array.isArray(msg.content)) {
3325
+ nextMessages[i] = msg;
3326
+ continue;
3327
+ }
3135
3328
  const newContent = msg.content.map((b) => {
3136
3329
  if (b.type !== "tool_result") return b;
3137
3330
  const tokens = estimateToolResultTokens(b.content);
@@ -3145,8 +3338,14 @@ var HybridCompactor = class {
3145
3338
  };
3146
3339
  return elided;
3147
3340
  });
3148
- messages[i] = { ...msg, content: newContent };
3341
+ if (newContent.length === msg.content.length && newContent.every((b, idx) => b === msg.content[idx])) {
3342
+ nextMessages[i] = msg;
3343
+ } else {
3344
+ nextMessages[i] = { ...msg, content: newContent };
3345
+ changed = true;
3346
+ }
3149
3347
  }
3348
+ if (changed) ctx.state.replaceMessages(nextMessages);
3150
3349
  return saved;
3151
3350
  }
3152
3351
  collapseAncientTurns(ctx) {
@@ -3180,10 +3379,10 @@ var HybridCompactor = class {
3180
3379
  let total = 0;
3181
3380
  for (const m of messages) {
3182
3381
  if (typeof m.content === "string") {
3183
- total += estimateTextTokens(m.content);
3382
+ total += this.estimator(m.content);
3184
3383
  } else {
3185
3384
  for (const b of m.content) {
3186
- if (b.type === "text") total += estimateTextTokens(b.text);
3385
+ if (b.type === "text") total += this.estimator(b.text);
3187
3386
  else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
3188
3387
  else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
3189
3388
  }
@@ -3223,7 +3422,7 @@ var IntelligentCompactor = class {
3223
3422
  const beforeTokens = this.estimateTokens(ctx.messages);
3224
3423
  const reductions = [];
3225
3424
  const load = beforeTokens / this.maxContext;
3226
- const aggressive = opts.aggressive ?? load >= this.softThreshold;
3425
+ const aggressive = load >= this.hardThreshold ? true : opts.aggressive ?? load >= this.softThreshold;
3227
3426
  const saved1 = this.eliseOldToolResults(ctx);
3228
3427
  if (saved1 > 0) reductions.push({ phase: "elision", saved: saved1 });
3229
3428
  if (aggressive) {
@@ -3332,9 +3531,18 @@ var IntelligentCompactor = class {
3332
3531
  }
3333
3532
  }
3334
3533
  let saved = 0;
3335
- for (let i = 0; i < preserveStart; i++) {
3534
+ let changed = false;
3535
+ const nextMessages = new Array(messages.length);
3536
+ for (let i = 0; i < messages.length; i++) {
3336
3537
  const msg = messages[i];
3337
- if (!msg || !Array.isArray(msg.content)) continue;
3538
+ if (i >= preserveStart) {
3539
+ nextMessages[i] = msg;
3540
+ continue;
3541
+ }
3542
+ if (!msg || !Array.isArray(msg.content)) {
3543
+ nextMessages[i] = msg;
3544
+ continue;
3545
+ }
3338
3546
  const newContent = msg.content.map((b) => {
3339
3547
  if (b.type !== "tool_result") return b;
3340
3548
  const tokens = estimateToolResultTokens(b.content);
@@ -3347,8 +3555,14 @@ var IntelligentCompactor = class {
3347
3555
  is_error: b.is_error
3348
3556
  };
3349
3557
  });
3350
- messages[i] = { ...msg, content: newContent };
3558
+ if (newContent.length === msg.content.length && newContent.every((b, idx) => b === msg.content[idx])) {
3559
+ nextMessages[i] = msg;
3560
+ } else {
3561
+ nextMessages[i] = { ...msg, content: newContent };
3562
+ changed = true;
3563
+ }
3351
3564
  }
3565
+ if (changed) ctx.state.replaceMessages(nextMessages);
3352
3566
  return saved;
3353
3567
  }
3354
3568
  hasTextContent(m) {
@@ -3501,7 +3715,7 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
3501
3715
  const jsonEnd = raw.lastIndexOf("}");
3502
3716
  if (jsonStart === -1 || jsonEnd === -1) {
3503
3717
  return this.fallbackSelect(
3504
- Array.from({ length: messageCount }, (_, i) => ({ role: "user", content: "" })),
3718
+ Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
3505
3719
  this.maxContextTokens
3506
3720
  );
3507
3721
  }
@@ -3510,7 +3724,7 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
3510
3724
  parsed = JSON.parse(raw.slice(jsonStart, jsonEnd + 1));
3511
3725
  } catch {
3512
3726
  return this.fallbackSelect(
3513
- Array.from({ length: messageCount }, (_, i) => ({ role: "user", content: "" })),
3727
+ Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
3514
3728
  this.maxContextTokens
3515
3729
  );
3516
3730
  }
@@ -3563,7 +3777,7 @@ var SelectiveCompactor = class {
3563
3777
  const savedElision = this.eliseOldToolResults(ctx);
3564
3778
  if (savedElision > 0) reductions.push({ phase: "elision", saved: savedElision });
3565
3779
  const afterPhase1 = this.estimateTokens(ctx.messages);
3566
- const targetBudget = this.computeTargetBudget(load, opts.aggressive ?? false);
3780
+ const targetBudget = this.computeTargetBudget(load);
3567
3781
  if (afterPhase1 > targetBudget) {
3568
3782
  const savedSelective = await this.runSelector(ctx, targetBudget);
3569
3783
  if (savedSelective > 0) reductions.push({ phase: "selective", saved: savedSelective });
@@ -3581,7 +3795,7 @@ var SelectiveCompactor = class {
3581
3795
  try {
3582
3796
  result = await this.selector.select(ctx.messages, targetBudget);
3583
3797
  } catch {
3584
- return this.aggressiveRecencyTrim(ctx, targetBudget);
3798
+ return this.aggressiveRecencyTrim(ctx);
3585
3799
  }
3586
3800
  await this.executePlan(ctx, result);
3587
3801
  const after = this.estimateTokens(ctx.messages);
@@ -3636,9 +3850,8 @@ Summarize the following message range:`;
3636
3850
  * Fallback when selector fails: aggressively trim from the oldest end
3637
3851
  * until we hit targetBudget.
3638
3852
  */
3639
- aggressiveRecencyTrim(ctx, targetBudget) {
3853
+ aggressiveRecencyTrim(ctx) {
3640
3854
  const messages = ctx.messages;
3641
- this.estimateTokens(messages);
3642
3855
  const preserveIdx = Math.max(0, messages.length - this.preserveK * 2);
3643
3856
  if (preserveIdx <= 0) return 0;
3644
3857
  let boundary = preserveIdx;
@@ -3659,7 +3872,7 @@ Summarize the following message range:`;
3659
3872
  ctx.state.replaceMessages([summaryMsg, ...tail]);
3660
3873
  return Math.max(0, removedTokens - this.estimateTokens([summaryMsg]));
3661
3874
  }
3662
- computeTargetBudget(load, aggressive) {
3875
+ computeTargetBudget(load) {
3663
3876
  if (load >= this.hardThreshold) {
3664
3877
  return Math.floor(this.maxContext * 0.5);
3665
3878
  }
@@ -3681,9 +3894,18 @@ Summarize the following message range:`;
3681
3894
  }
3682
3895
  }
3683
3896
  let saved = 0;
3684
- for (let i = 0; i < preserveStart; i++) {
3897
+ let changed = false;
3898
+ const nextMessages = new Array(messages.length);
3899
+ for (let i = 0; i < messages.length; i++) {
3685
3900
  const msg = messages[i];
3686
- if (!msg || !Array.isArray(msg.content)) continue;
3901
+ if (i >= preserveStart) {
3902
+ nextMessages[i] = msg;
3903
+ continue;
3904
+ }
3905
+ if (!msg || !Array.isArray(msg.content)) {
3906
+ nextMessages[i] = msg;
3907
+ continue;
3908
+ }
3687
3909
  const newContent = msg.content.map((b) => {
3688
3910
  if (b.type !== "tool_result") return b;
3689
3911
  const text = typeof b.content === "string" ? b.content : JSON.stringify(b.content);
@@ -3697,8 +3919,14 @@ Summarize the following message range:`;
3697
3919
  is_error: b.is_error
3698
3920
  };
3699
3921
  });
3700
- messages[i] = { ...msg, content: newContent };
3922
+ if (newContent.every((b, idx) => b === msg.content[idx])) {
3923
+ nextMessages[i] = msg;
3924
+ } else {
3925
+ nextMessages[i] = { ...msg, content: newContent };
3926
+ changed = true;
3927
+ }
3701
3928
  }
3929
+ if (changed) ctx.state.replaceMessages(nextMessages);
3702
3930
  return saved;
3703
3931
  }
3704
3932
  hasTextContent(m) {
@@ -3734,46 +3962,78 @@ var AutoCompactionMiddleware = class {
3734
3962
  name = "AutoCompaction";
3735
3963
  compactor;
3736
3964
  warnThreshold;
3737
- // fraction of maxContext (0-1)
3738
3965
  softThreshold;
3739
3966
  hardThreshold;
3740
3967
  maxContext;
3741
3968
  estimator;
3742
3969
  aggressiveOn;
3970
+ events;
3971
+ failureMode;
3743
3972
  /**
3744
- * @param compactor Compactor to use for compaction
3745
- * @param maxContext Provider's max context window in tokens
3746
- * @param estimator Token estimation function (ctx → token count)
3747
- * @param thresholds Threshold fractions (0-1) of maxContext
3748
- * @param aggressiveOn Which threshold triggers aggressive (full LLM summarization)
3973
+ * @param compactor Compactor to use for compaction.
3974
+ * @param maxContext Provider's max context window in tokens.
3975
+ * @param estimator Token estimation function.
3976
+ * @param thresholds Threshold fractions (0-1) of maxContext.
3977
+ * @param opts Optional behavior. By default, failures at the
3978
+ * hard threshold throw AGENT_CONTEXT_OVERFLOW so
3979
+ * the agent does not continue into a likely
3980
+ * provider context overflow. Warn/soft failures
3981
+ * still emit compaction.failed and continue.
3749
3982
  */
3750
- constructor(compactor, maxContext, estimator, thresholds, aggressiveOn = "soft") {
3983
+ constructor(compactor, maxContext, estimator, thresholds, optsOrAggressiveOn = {}, events) {
3984
+ const opts = typeof optsOrAggressiveOn === "string" ? { aggressiveOn: optsOrAggressiveOn, events } : optsOrAggressiveOn;
3751
3985
  this.compactor = compactor;
3752
3986
  this.maxContext = maxContext;
3753
3987
  this.estimator = estimator;
3754
3988
  this.warnThreshold = thresholds.warn;
3755
3989
  this.softThreshold = thresholds.soft;
3756
3990
  this.hardThreshold = thresholds.hard;
3757
- this.aggressiveOn = aggressiveOn;
3991
+ this.aggressiveOn = opts.aggressiveOn ?? "soft";
3992
+ this.events = opts.events;
3993
+ this.failureMode = opts.failureMode ?? "throw_on_hard";
3758
3994
  }
3759
3995
  handler() {
3760
3996
  return async (ctx, next) => {
3761
3997
  const tokens = this.estimator(ctx);
3762
3998
  const load = tokens / this.maxContext;
3763
3999
  if (load >= this.hardThreshold) {
3764
- await this.compact(ctx, true);
4000
+ await this.compact(ctx, true, { level: "hard", tokens, load });
3765
4001
  } else if (load >= this.softThreshold) {
3766
- await this.compact(ctx, this.aggressiveOn !== "hard");
4002
+ await this.compact(ctx, this.aggressiveOn !== "hard", { level: "soft", tokens, load });
3767
4003
  } else if (load >= this.warnThreshold) {
3768
- await this.compact(ctx, false);
4004
+ await this.compact(ctx, false, { level: "warn", tokens, load });
3769
4005
  }
3770
4006
  return next(ctx);
3771
4007
  };
3772
4008
  }
3773
- async compact(ctx, aggressive) {
4009
+ async compact(ctx, aggressive, pressure) {
3774
4010
  try {
3775
4011
  await this.compactor.compact(ctx, { aggressive });
3776
- } catch {
4012
+ } catch (err) {
4013
+ const error = err instanceof Error ? err : new Error(String(err));
4014
+ const fatal = this.failureMode === "throw" || this.failureMode === "throw_on_hard" && pressure.level === "hard";
4015
+ this.events?.emit("compaction.failed", {
4016
+ err: error,
4017
+ aggressive,
4018
+ level: pressure.level,
4019
+ tokens: pressure.tokens,
4020
+ maxContext: this.maxContext,
4021
+ load: pressure.load,
4022
+ fatal
4023
+ });
4024
+ if (fatal) {
4025
+ throw new AgentError({
4026
+ message: `Auto-compaction failed at ${pressure.level} threshold`,
4027
+ code: "AGENT_CONTEXT_OVERFLOW",
4028
+ recoverable: true,
4029
+ context: {
4030
+ level: pressure.level,
4031
+ tokens: pressure.tokens,
4032
+ maxContext: this.maxContext
4033
+ },
4034
+ cause: err
4035
+ });
4036
+ }
3777
4037
  }
3778
4038
  }
3779
4039
  };
@@ -4150,8 +4410,9 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
4150
4410
  const context = {
4151
4411
  subagentId: id,
4152
4412
  tasks: [],
4153
- // parentBridge: wired by the caller via setSubagentBridge() once the
4154
- // bidirectional bridge is created. Reads gated by hasParentBridge().
4413
+ // Wired later by the caller via setSubagentBridge() once the
4414
+ // bidirectional bridge is created. Readers must null-check / use
4415
+ // hasParentBridge() — the type now reflects this.
4155
4416
  parentBridge: null,
4156
4417
  doneCondition: this.config.doneCondition,
4157
4418
  maxConcurrent: this.config.maxConcurrent ?? 4
@@ -4196,9 +4457,7 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
4196
4457
  this.emit("subagent.stopped", { subagentId, reason: "stopped by coordinator" });
4197
4458
  }
4198
4459
  async stopAll() {
4199
- for (const id of this.subagents.keys()) {
4200
- await this.stop(id);
4201
- }
4460
+ await Promise.allSettled([...this.subagents.keys()].map((id) => this.stop(id)));
4202
4461
  }
4203
4462
  getStatus() {
4204
4463
  return {
@@ -4234,7 +4493,17 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
4234
4493
  if (!subagentId) return;
4235
4494
  const task = this.pendingTasks.shift();
4236
4495
  if (!task) return;
4237
- void this.runDispatched(subagentId, task);
4496
+ this.runDispatched(subagentId, task).catch((err) => {
4497
+ this.recordCompletion({
4498
+ subagentId,
4499
+ taskId: task.id,
4500
+ status: "failed",
4501
+ error: err instanceof Error ? err.message : String(err),
4502
+ iterations: 0,
4503
+ toolCalls: 0,
4504
+ durationMs: 0
4505
+ });
4506
+ });
4238
4507
  }
4239
4508
  }
4240
4509
  canDispatch() {
@@ -4254,7 +4523,6 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
4254
4523
  subagent.currentTask = task.id;
4255
4524
  task.subagentId = subagentId;
4256
4525
  subagent.context.tasks.push(task);
4257
- this.inFlight++;
4258
4526
  this.emit("task.assigned", { task, subagentId });
4259
4527
  const budget = new SubagentBudget({
4260
4528
  maxIterations: subagent.config.maxIterations ?? this.config.defaultBudget?.maxIterations,
@@ -4264,6 +4532,10 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
4264
4532
  timeoutMs: task.timeoutMs ?? subagent.config.timeoutMs ?? this.config.defaultBudget?.timeoutMs
4265
4533
  });
4266
4534
  subagent.activeBudget = budget;
4535
+ if (!this.runner) {
4536
+ return;
4537
+ }
4538
+ this.inFlight++;
4267
4539
  const startTime = Date.now();
4268
4540
  const runCtx = {
4269
4541
  subagentId,
@@ -4273,9 +4545,6 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
4273
4545
  bridge: subagent.context.parentBridge || null
4274
4546
  };
4275
4547
  let result;
4276
- if (!this.runner) {
4277
- return;
4278
- }
4279
4548
  budget.start();
4280
4549
  try {
4281
4550
  const outcome = await this.executeWithTimeout(this.runner, task, runCtx, budget);
@@ -4290,13 +4559,14 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
4290
4559
  };
4291
4560
  } catch (err) {
4292
4561
  const status = err instanceof BudgetExceededError && err.kind === "timeout" ? "timeout" : subagent.abortController.signal.aborted ? "stopped" : "failed";
4562
+ const usage = budget.usage();
4293
4563
  result = {
4294
4564
  subagentId,
4295
4565
  taskId: task.id,
4296
4566
  status,
4297
4567
  error: err instanceof Error ? err.message : String(err),
4298
- iterations: budget.usage().iterations,
4299
- toolCalls: budget.usage().toolCalls,
4568
+ iterations: usage.iterations,
4569
+ toolCalls: usage.toolCalls,
4300
4570
  durationMs: Date.now() - startTime
4301
4571
  };
4302
4572
  }
@@ -4321,11 +4591,24 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
4321
4591
  recordCompletion(result) {
4322
4592
  this.completedResults.push(result);
4323
4593
  this.totalIterations += result.iterations;
4324
- this.inFlight = Math.max(0, this.inFlight - 1);
4594
+ if (this.inFlight > 0) {
4595
+ this.inFlight--;
4596
+ } else if (this.runner) {
4597
+ this.emit("warning", {
4598
+ type: "inFlight_underflow",
4599
+ taskId: result.taskId,
4600
+ subagentId: result.subagentId
4601
+ });
4602
+ return;
4603
+ }
4325
4604
  const subagent = this.subagents.get(result.subagentId);
4326
4605
  if (subagent && subagent.status !== "stopped") {
4327
- subagent.status = result.status === "failed" || result.status === "timeout" ? "error" : "idle";
4606
+ const failed = result.status === "failed" || result.status === "timeout";
4607
+ subagent.status = failed ? "error" : "idle";
4328
4608
  subagent.currentTask = void 0;
4609
+ if (subagent.abortController.signal.aborted) {
4610
+ subagent.abortController = new AbortController();
4611
+ }
4329
4612
  if (subagent.status === "error") {
4330
4613
  queueMicrotask(() => {
4331
4614
  if (subagent.status === "error") subagent.status = "idle";
@@ -4513,6 +4796,11 @@ var InMemoryAgentBridge = class {
4513
4796
  if (this.stopped) throw new Error("Bridge is stopped");
4514
4797
  const timeout = timeoutMs ?? this.timeoutMs;
4515
4798
  const correlationId = msg.id;
4799
+ if (this.pendingRequests.has(correlationId)) {
4800
+ throw new Error(
4801
+ `Bridge request id "${correlationId}" collides with an in-flight request \u2014 caller is reusing message ids`
4802
+ );
4803
+ }
4516
4804
  return new Promise((resolve4, reject) => {
4517
4805
  const timer = setTimeout(() => {
4518
4806
  this.pendingRequests.delete(correlationId);
@@ -4553,8 +4841,10 @@ function createMessage(type, from, payload, to) {
4553
4841
  var DoneConditionChecker = class {
4554
4842
  constructor(condition) {
4555
4843
  this.condition = condition;
4844
+ this.compiledRegex = condition.type === "output_match" && condition.pattern ? new RegExp(condition.pattern) : null;
4556
4845
  }
4557
4846
  condition;
4847
+ compiledRegex;
4558
4848
  check(state) {
4559
4849
  switch (this.condition.type) {
4560
4850
  case "iterations":
@@ -4568,11 +4858,8 @@ var DoneConditionChecker = class {
4568
4858
  }
4569
4859
  break;
4570
4860
  case "output_match":
4571
- if (this.condition.pattern && state.lastOutput) {
4572
- const regex = new RegExp(this.condition.pattern);
4573
- if (regex.test(state.lastOutput)) {
4574
- return { done: true, reason: `output matched pattern "${this.condition.pattern}"`, ...state };
4575
- }
4861
+ if (this.compiledRegex && state.lastOutput && this.compiledRegex.test(state.lastOutput)) {
4862
+ return { done: true, reason: `output matched pattern "${this.condition.pattern}"`, ...state };
4576
4863
  }
4577
4864
  break;
4578
4865
  }
@@ -4629,7 +4916,8 @@ var AutonomousRunner = class {
4629
4916
  return failedResult;
4630
4917
  }
4631
4918
  } catch (e) {
4632
- if (e.message.includes("timeout")) {
4919
+ const msg = e instanceof Error ? e.message : String(e);
4920
+ if (msg.includes("timeout")) {
4633
4921
  const timeoutResult = {
4634
4922
  status: "failed",
4635
4923
  error: toWrongStackError(e),
@@ -4658,14 +4946,11 @@ var AutonomousRunner = class {
4658
4946
 
4659
4947
  // src/defaults/spec-parser.ts
4660
4948
  var SpecParser = class {
4661
- constructor(opts = {}) {
4662
- this.opts = opts;
4663
- }
4664
- opts;
4665
4949
  parse(content) {
4666
4950
  const lines = content.split("\n");
4667
4951
  const sections = this.extractSections(lines);
4668
4952
  const requirements = this.extractRequirements(lines);
4953
+ const now = Date.now();
4669
4954
  return {
4670
4955
  id: crypto.randomUUID(),
4671
4956
  title: this.extractTitle(lines),
@@ -4674,8 +4959,8 @@ var SpecParser = class {
4674
4959
  overview: this.extractOverview(lines),
4675
4960
  sections,
4676
4961
  requirements,
4677
- createdAt: Date.now(),
4678
- updatedAt: Date.now()
4962
+ createdAt: now,
4963
+ updatedAt: now
4679
4964
  };
4680
4965
  }
4681
4966
  extractTitle(lines) {
@@ -4767,20 +5052,13 @@ var SpecParser = class {
4767
5052
  parseRequirementLine(line, id) {
4768
5053
  const trimmed = line.trim();
4769
5054
  if (!trimmed || trimmed.startsWith("#")) return null;
4770
- const typeMap = {
4771
- "functional": "functional",
4772
- "non-functional": "non-functional",
4773
- "security": "security",
4774
- "performance": "performance",
4775
- "ux": "ux"
4776
- };
5055
+ const lower = trimmed.toLowerCase();
5056
+ const types = ["functional", "non-functional", "security", "performance", "ux"];
4777
5057
  let type = "functional";
4778
- let priority = "medium";
4779
- for (const [key, val] of Object.entries(typeMap)) {
4780
- if (trimmed.toLowerCase().includes(`[${key}]`)) {
4781
- type = val;
4782
- }
5058
+ for (const t2 of types) {
5059
+ if (lower.includes(`[${t2}]`)) type = t2;
4783
5060
  }
5061
+ let priority = "medium";
4784
5062
  if (trimmed.includes("[critical]") || trimmed.includes("[prio:high]")) {
4785
5063
  priority = "critical";
4786
5064
  } else if (trimmed.includes("[high]")) {
@@ -4870,9 +5148,10 @@ var SpecParser = class {
4870
5148
  warnings.push({ path: `requirement.${req.id}`, message: "No acceptance criteria defined" });
4871
5149
  }
4872
5150
  }
5151
+ const reqIds = new Set(spec.requirements.map((r) => r.id));
4873
5152
  const blockedByIds = new Set(spec.requirements.flatMap((r) => r.blockedBy ?? []));
4874
5153
  for (const id of blockedByIds) {
4875
- if (!spec.requirements.find((r) => r.id === id)) {
5154
+ if (!reqIds.has(id)) {
4876
5155
  errors.push({ path: "requirements", message: `BlockedBy references non-existent requirement: ${id}` });
4877
5156
  }
4878
5157
  }
@@ -4902,25 +5181,21 @@ var TaskGenerator = class {
4902
5181
  status: "pending"
4903
5182
  });
4904
5183
  }
4905
- const criticalReqs = spec.requirements.filter((r) => r.priority === "critical");
4906
- const highReqs = spec.requirements.filter((r) => r.priority === "high");
4907
- const mediumReqs = spec.requirements.filter((r) => r.priority === "medium");
4908
- const lowReqs = spec.requirements.filter((r) => r.priority === "low");
4909
- for (const req of criticalReqs) {
4910
- const task = this.createTaskFromRequirement(req, spec.title);
4911
- this.opts.taskTracker.addNode(task);
4912
- }
4913
- for (const req of highReqs) {
4914
- const task = this.createTaskFromRequirement(req, spec.title);
4915
- this.opts.taskTracker.addNode(task);
4916
- }
4917
- for (const req of mediumReqs) {
4918
- const task = this.createTaskFromRequirement(req, spec.title);
4919
- this.opts.taskTracker.addNode(task);
5184
+ const byPriority = {
5185
+ critical: [],
5186
+ high: [],
5187
+ medium: [],
5188
+ low: []
5189
+ };
5190
+ for (const req of spec.requirements) {
5191
+ const bucket = byPriority[req.priority] ?? byPriority.medium;
5192
+ bucket.push(req);
4920
5193
  }
4921
- for (const req of lowReqs) {
4922
- const task = this.createTaskFromRequirement(req, spec.title);
4923
- this.opts.taskTracker.addNode(task);
5194
+ const order = ["critical", "high", "medium", "low"];
5195
+ for (const p of order) {
5196
+ for (const req of byPriority[p]) {
5197
+ this.opts.taskTracker.addNode(this.createTaskFromRequirement(req));
5198
+ }
4924
5199
  }
4925
5200
  if (spec.apiEndpoints && spec.apiEndpoints.length > 0) {
4926
5201
  const apiParent = this.opts.taskTracker.addNode({
@@ -4954,17 +5229,15 @@ var TaskGenerator = class {
4954
5229
  });
4955
5230
  return graph;
4956
5231
  }
4957
- createTaskFromRequirement(req, specTitle) {
4958
- const type = this.mapRequirementType(req.type);
4959
- const tags = [req.type, req.priority];
5232
+ createTaskFromRequirement(req) {
4960
5233
  return {
4961
5234
  title: req.description,
4962
- description: this.buildDescription(req, specTitle),
4963
- type,
4964
- priority: this.mapPriority(req.priority),
5235
+ description: this.buildDescription(req),
5236
+ type: this.mapRequirementType(req.type),
5237
+ priority: req.priority,
4965
5238
  status: "pending",
4966
5239
  specRequirementId: req.id,
4967
- tags,
5240
+ tags: [req.type, req.priority],
4968
5241
  estimateHours: this.estimateHours(req)
4969
5242
  };
4970
5243
  }
@@ -4979,7 +5252,7 @@ var TaskGenerator = class {
4979
5252
  estimateHours: this.estimateForEndpoint(endpoint)
4980
5253
  };
4981
5254
  }
4982
- buildDescription(req, specTitle) {
5255
+ buildDescription(req) {
4983
5256
  const lines = [
4984
5257
  req.description,
4985
5258
  "",
@@ -5013,20 +5286,6 @@ var TaskGenerator = class {
5013
5286
  return "feature";
5014
5287
  }
5015
5288
  }
5016
- mapPriority(priority) {
5017
- switch (priority) {
5018
- case "critical":
5019
- return "critical";
5020
- case "high":
5021
- return "high";
5022
- case "medium":
5023
- return "medium";
5024
- case "low":
5025
- return "low";
5026
- default:
5027
- return "medium";
5028
- }
5029
- }
5030
5289
  estimateHours(req) {
5031
5290
  switch (req.priority) {
5032
5291
  case "critical":
@@ -5136,40 +5395,40 @@ var TaskTracker = class {
5136
5395
  this.graph.rootNodes.push(newNode.id);
5137
5396
  }
5138
5397
  this.graph.updatedAt = now;
5139
- this.opts.store.saveGraph(this.graph);
5398
+ this.persist();
5140
5399
  return newNode;
5141
5400
  }
5142
5401
  addEdge(from, to, type = "depends_on") {
5143
5402
  if (!this.graph) throw new Error("No graph loaded");
5144
- const edge = {
5403
+ this.graph.edges.push({
5145
5404
  id: crypto.randomUUID(),
5146
5405
  from,
5147
5406
  to,
5148
5407
  type
5149
- };
5150
- this.graph.edges.push(edge);
5408
+ });
5151
5409
  this.graph.updatedAt = Date.now();
5152
- this.opts.store.saveGraph(this.graph);
5410
+ this.persist();
5153
5411
  }
5154
5412
  updateNodeStatus(id, status, reason) {
5155
5413
  if (!this.graph) throw new Error("No graph loaded");
5156
5414
  const node = this.graph.nodes.get(id);
5157
5415
  if (!node) throw new Error(`Node ${id} not found`);
5158
5416
  const from = node.status;
5417
+ const now = Date.now();
5159
5418
  node.status = status;
5160
- node.updatedAt = Date.now();
5419
+ node.updatedAt = now;
5161
5420
  if (status === "completed") {
5162
- node.completedAt = Date.now();
5421
+ node.completedAt = now;
5163
5422
  }
5164
- this.transitions.push({ from, to: status, timestamp: Date.now(), reason });
5423
+ this.transitions.push({ from, to: status, timestamp: now, reason });
5165
5424
  if (status === "completed") {
5166
5425
  this.unblockDependents(id);
5167
5426
  }
5168
5427
  if (status === "in_progress") {
5169
5428
  this.checkAndBlockIfNeeded(id);
5170
5429
  }
5171
- this.graph.updatedAt = Date.now();
5172
- this.opts.store.saveGraph(this.graph);
5430
+ this.graph.updatedAt = now;
5431
+ this.persist();
5173
5432
  }
5174
5433
  getNode(id) {
5175
5434
  return this.graph?.nodes.get(id);
@@ -5190,9 +5449,7 @@ var TaskTracker = class {
5190
5449
  }
5191
5450
  if (sort) {
5192
5451
  nodes.sort((a, b) => {
5193
- const aVal = a[sort.field] ?? "";
5194
- const bVal = b[sort.field] ?? "";
5195
- const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
5452
+ const cmp = compareByField(a, b, sort.field);
5196
5453
  return sort.direction === "asc" ? cmp : -cmp;
5197
5454
  });
5198
5455
  }
@@ -5271,7 +5528,47 @@ var TaskTracker = class {
5271
5528
  }
5272
5529
  }
5273
5530
  }
5531
+ /**
5532
+ * Fire-and-forget persistence with attached error handler.
5533
+ * Synchronous mutators (addNode/addEdge/updateNodeStatus) use this to
5534
+ * avoid forcing an async cascade through every caller; if the store
5535
+ * rejects, the configured `onPersistError` is invoked so failures are
5536
+ * surfaced instead of swallowed by an unhandled promise rejection.
5537
+ */
5538
+ persist() {
5539
+ if (!this.graph) return;
5540
+ this.opts.store.saveGraph(this.graph).catch((err) => {
5541
+ if (this.opts.onPersistError) this.opts.onPersistError(err);
5542
+ else console.warn("[task-tracker] saveGraph failed:", err instanceof Error ? err.message : String(err));
5543
+ });
5544
+ }
5545
+ };
5546
+ var PRIORITY_RANK = {
5547
+ critical: 0,
5548
+ high: 1,
5549
+ medium: 2,
5550
+ low: 3
5274
5551
  };
5552
+ var STATUS_RANK = {
5553
+ in_progress: 0,
5554
+ pending: 1,
5555
+ review: 2,
5556
+ blocked: 3,
5557
+ failed: 4,
5558
+ completed: 5
5559
+ };
5560
+ function compareByField(a, b, field) {
5561
+ switch (field) {
5562
+ case "priority":
5563
+ return PRIORITY_RANK[a.priority] - PRIORITY_RANK[b.priority];
5564
+ case "status":
5565
+ return STATUS_RANK[a.status] - STATUS_RANK[b.status];
5566
+ case "createdAt":
5567
+ return a.createdAt - b.createdAt;
5568
+ case "updatedAt":
5569
+ return a.updatedAt - b.updatedAt;
5570
+ }
5571
+ }
5275
5572
 
5276
5573
  // src/defaults/task-flow.ts
5277
5574
  var TaskFlow = class {
@@ -5323,9 +5620,10 @@ var TaskFlow = class {
5323
5620
  const task = batch[i];
5324
5621
  if (!result || !task) continue;
5325
5622
  if (result.status === "rejected") {
5326
- this.opts.tracker.updateNodeStatus(task.id, "failed", result.reason?.message);
5327
- this.emit("task.failed", { taskId: task.id, error: result.reason?.message ?? "unknown" });
5328
- ctx.onTaskFail?.(task, result.reason);
5623
+ const reason = result.reason;
5624
+ this.opts.tracker.updateNodeStatus(task.id, "failed", reason?.message);
5625
+ this.emit("task.failed", { taskId: task.id, error: reason?.message ?? "unknown" });
5626
+ ctx.onTaskFail?.(task, reason);
5329
5627
  } else {
5330
5628
  this.opts.tracker.updateNodeStatus(task.id, "completed");
5331
5629
  this.emit("task.completed", { taskId: task.id, result: result.value });
@@ -5613,7 +5911,7 @@ var ToolExecutor = class {
5613
5911
  return { result, tool, durationMs: Date.now() - start };
5614
5912
  }
5615
5913
  } else {
5616
- const suggestedPattern = this.subjectFor(tool.name, use.input) ?? tool.name;
5914
+ const suggestedPattern = this.subjectFor(tool.name, use.input, tool.subjectKey) ?? tool.name;
5617
5915
  const pending = { type: "tool_confirm_pending", toolUseId: use.id, toolName: tool.name, input: use.input, suggestedPattern };
5618
5916
  return { result: pending, tool, durationMs: Date.now() - start };
5619
5917
  }
@@ -5645,15 +5943,31 @@ var ToolExecutor = class {
5645
5943
  span?.end();
5646
5944
  }
5647
5945
  };
5946
+ const safeRun = async (use) => {
5947
+ try {
5948
+ return await runOne(use);
5949
+ } catch (err) {
5950
+ const msg = err instanceof Error ? err.message : String(err);
5951
+ const scrubbed = this.opts.secretScrubber.scrub(msg);
5952
+ const result = {
5953
+ type: "tool_result",
5954
+ tool_use_id: use.id,
5955
+ content: `Tool "${use.name}" execution failed: ${scrubbed}`,
5956
+ is_error: true
5957
+ };
5958
+ budget = this.decrementBudget(result, budget);
5959
+ return { result, tool: this.registry.get(use.name), durationMs: 0 };
5960
+ }
5961
+ };
5648
5962
  if (strategy === "sequential") {
5649
5963
  const outputs = [];
5650
5964
  for (const use of toolUses) {
5651
- if (use) outputs.push(await runOne(use));
5965
+ if (use) outputs.push(await safeRun(use));
5652
5966
  }
5653
5967
  return { outputs, remainingBudget: budget };
5654
5968
  }
5655
5969
  if (strategy === "parallel") {
5656
- const outputs = await Promise.all(toolUses.map((use) => runOne(use)));
5970
+ const outputs = await Promise.all(toolUses.map((use) => safeRun(use)));
5657
5971
  return { outputs, remainingBudget: budget };
5658
5972
  }
5659
5973
  const nonMutating = [];
@@ -5664,10 +5978,10 @@ var ToolExecutor = class {
5664
5978
  if (tool?.mutating) mutating.push(use);
5665
5979
  else nonMutating.push(use);
5666
5980
  }
5667
- const firstPass = await Promise.all(nonMutating.map((use) => runOne(use)));
5981
+ const firstPass = await Promise.all(nonMutating.map((use) => safeRun(use)));
5668
5982
  const secondPass = [];
5669
5983
  for (const use of mutating) {
5670
- secondPass.push(await runOne(use));
5984
+ secondPass.push(await safeRun(use));
5671
5985
  }
5672
5986
  return {
5673
5987
  outputs: [...firstPass, ...secondPass],
@@ -5702,7 +6016,8 @@ var ToolExecutor = class {
5702
6016
  }
5703
6017
  async runWithTimeout(tool, input, parentSignal, ctx, toolUseId) {
5704
6018
  if (parentSignal.aborted) {
5705
- throw parentSignal.reason instanceof Error ? parentSignal.reason : new Error(typeof parentSignal.reason === "string" ? parentSignal.reason : "aborted");
6019
+ if (parentSignal.reason instanceof Error) throw parentSignal.reason;
6020
+ throw new Error(typeof parentSignal.reason === "string" ? parentSignal.reason : "aborted");
5706
6021
  }
5707
6022
  const timeoutMs = tool.timeoutMs ?? this.iterationTimeoutMs;
5708
6023
  const ctrl = new AbortController();
@@ -5771,16 +6086,23 @@ var ToolExecutor = class {
5771
6086
  * Matches the logic in DefaultPermissionPolicy so the TUI shows the
5772
6087
  * same subject that the trust file would use.
5773
6088
  */
5774
- subjectFor(toolName, input) {
6089
+ subjectFor(toolName, input, subjectKey) {
5775
6090
  if (!input || typeof input !== "object") return void 0;
5776
6091
  const obj = input;
5777
6092
  const globChars = /[*?\[\]]/g;
5778
6093
  const escapeGlob = (s) => s.replace(globChars, (c) => `\\${c}`);
6094
+ const normalizePath = (s) => escapeGlob(s.replace(/\\/g, "/"));
6095
+ if (subjectKey) {
6096
+ const v = obj[subjectKey];
6097
+ if (typeof v === "string") {
6098
+ return subjectKey === "path" || subjectKey === "file" || subjectKey === "files" ? normalizePath(v) : escapeGlob(v);
6099
+ }
6100
+ }
5779
6101
  if (toolName === "bash" && typeof obj.command === "string") {
5780
6102
  return escapeGlob(obj.command);
5781
6103
  }
5782
6104
  if (typeof obj.path === "string") {
5783
- return escapeGlob(obj.path.replace(/\\/g, "/"));
6105
+ return normalizePath(obj.path);
5784
6106
  }
5785
6107
  if (typeof obj.url === "string") {
5786
6108
  return escapeGlob(obj.url);
@@ -6204,8 +6526,9 @@ var DefaultHealthRegistry = class {
6204
6526
  return { status, timestamp: Date.now(), checks: results };
6205
6527
  }
6206
6528
  async runOne(check) {
6529
+ let timer = null;
6207
6530
  const timeout = new Promise((resolve4) => {
6208
- setTimeout(
6531
+ timer = setTimeout(
6209
6532
  () => resolve4({ status: "unhealthy", detail: `timeout after ${this.timeoutMs}ms` }),
6210
6533
  this.timeoutMs
6211
6534
  );
@@ -6214,6 +6537,8 @@ var DefaultHealthRegistry = class {
6214
6537
  return await Promise.race([check.check(), timeout]);
6215
6538
  } catch (err) {
6216
6539
  return { status: "unhealthy", detail: err instanceof Error ? err.message : String(err) };
6540
+ } finally {
6541
+ if (timer) clearTimeout(timer);
6217
6542
  }
6218
6543
  }
6219
6544
  };
@@ -6852,7 +7177,6 @@ function createContextManagerTool(opts = {}) {
6852
7177
  notes: `Invalid range [${from}, ${to}] for ${messages.length} messages.`
6853
7178
  };
6854
7179
  }
6855
- messages.slice(from, to + 1);
6856
7180
  const summaryText = input.text ?? '[summary placeholder \u2014 provide "text" to record the summary]';
6857
7181
  const summaryMsg = {
6858
7182
  role: "system",
@@ -7114,13 +7438,23 @@ async function streamProviderToResponse(provider, req, signal, ctx, events) {
7114
7438
  }
7115
7439
  } catch (err) {
7116
7440
  if (signal.aborted) {
7117
- state.stopReason = "max_tokens";
7441
+ state.stopReason = "end_turn";
7118
7442
  return buildResponse(state);
7119
7443
  }
7120
7444
  throw err;
7121
7445
  } finally {
7122
7446
  try {
7123
- await iter.return?.();
7447
+ let drainTimer = null;
7448
+ try {
7449
+ await Promise.race([
7450
+ Promise.resolve(iter.return?.()),
7451
+ new Promise((resolve4) => {
7452
+ drainTimer = setTimeout(resolve4, 500);
7453
+ })
7454
+ ]);
7455
+ } finally {
7456
+ if (drainTimer) clearTimeout(drainTimer);
7457
+ }
7124
7458
  } catch {
7125
7459
  }
7126
7460
  }
@@ -7177,12 +7511,24 @@ async function runProviderWithRetry(opts) {
7177
7511
  });
7178
7512
  }
7179
7513
  await new Promise((resolve4, reject) => {
7180
- const t2 = setTimeout(resolve4, delay);
7181
- const onAbort = () => {
7514
+ let settled = false;
7515
+ const cleanup = () => {
7516
+ if (settled) return;
7517
+ settled = true;
7182
7518
  clearTimeout(t2);
7519
+ };
7520
+ const onAbort = () => {
7521
+ cleanup();
7183
7522
  reject(new Error("aborted"));
7184
7523
  };
7185
- if (signal.aborted) onAbort();
7524
+ const t2 = setTimeout(() => {
7525
+ cleanup();
7526
+ resolve4();
7527
+ }, delay);
7528
+ if (signal.aborted) {
7529
+ onAbort();
7530
+ return;
7531
+ }
7186
7532
  signal.addEventListener("abort", onAbort, { once: true });
7187
7533
  });
7188
7534
  attempt++;
@@ -7306,9 +7652,6 @@ var Agent = class {
7306
7652
  get scrubber() {
7307
7653
  return this.container.resolve(TOKENS.SecretScrubber);
7308
7654
  }
7309
- get compactor() {
7310
- return this.container.has(TOKENS.Compactor) ? this.container.resolve(TOKENS.Compactor) : void 0;
7311
- }
7312
7655
  get renderer() {
7313
7656
  return this.container.has(TOKENS.Renderer) ? this.container.resolve(TOKENS.Renderer) : void 0;
7314
7657
  }
@@ -7354,6 +7697,7 @@ var Agent = class {
7354
7697
  let iterations = 0;
7355
7698
  let effectiveLimit = opts.maxIterations ?? this.maxIterations;
7356
7699
  const hasHardLimit = effectiveLimit > 0 && Number.isFinite(effectiveLimit);
7700
+ let recoveryRetries = 0;
7357
7701
  for (let i = 0; ; i++) {
7358
7702
  iterations = i + 1;
7359
7703
  if (controller.signal.aborted) {
@@ -7383,17 +7727,33 @@ var Agent = class {
7383
7727
  logger: this.logger,
7384
7728
  tracer: this.tracer
7385
7729
  });
7730
+ recoveryRetries = 0;
7386
7731
  } catch (err) {
7387
7732
  if (controller.signal.aborted) {
7388
7733
  this.events.emit("error", { err: toError(err), phase: "provider" });
7389
7734
  return { status: "aborted", iterations, error: toWrongStackError(err, "AGENT_ABORTED") };
7390
7735
  }
7391
7736
  const recovered = await this.errorHandler.recover(err, this.ctx);
7392
- if (!recovered) {
7737
+ if (!recovered || recovered.action === "fail") {
7393
7738
  this.events.emit("error", { err: toError(err), phase: "provider" });
7394
- return { status: "failed", iterations, error: toWrongStackError(err) };
7739
+ return {
7740
+ status: "failed",
7741
+ iterations,
7742
+ error: toWrongStackError(recovered?.error ?? err)
7743
+ };
7744
+ }
7745
+ if (recovered.action === "retry") {
7746
+ recoveryRetries++;
7747
+ if (recoveryRetries > 2) {
7748
+ this.events.emit("error", { err: toError(err), phase: "provider" });
7749
+ return { status: "failed", iterations, error: toWrongStackError(err) };
7750
+ }
7751
+ if (recovered.model) this.ctx.model = recovered.model;
7752
+ this.logger.info(`Recovered provider error via ${recovered.reason}; retrying turn`);
7753
+ continue;
7395
7754
  }
7396
- res = recovered;
7755
+ recoveryRetries = 0;
7756
+ res = recovered.response;
7397
7757
  }
7398
7758
  const responseResult = await this.processResponse(res, req);
7399
7759
  if (responseResult.aborted) {
@@ -7467,7 +7827,7 @@ var Agent = class {
7467
7827
  * update session, render text, handle abort.
7468
7828
  */
7469
7829
  async processResponse(res, req) {
7470
- await this.pipelines.response.run(res);
7830
+ res = await this.pipelines.response.run(res);
7471
7831
  this.events.emit("provider.response", {
7472
7832
  ctx: this.ctx,
7473
7833
  usage: res.usage,
@@ -7538,6 +7898,7 @@ var Agent = class {
7538
7898
  isError: !!reRunResult.result.is_error
7539
7899
  });
7540
7900
  this.events.emit("tool.executed", {
7901
+ id: reRunResult.result.tool_use_id,
7541
7902
  name: tool.name,
7542
7903
  durationMs: reRunResult.durationMs,
7543
7904
  ok: !reRunResult.result.is_error,
@@ -7545,7 +7906,6 @@ var Agent = class {
7545
7906
  output: truncateForEvent(reRunResult.result.content)
7546
7907
  });
7547
7908
  }
7548
- this.ctx.state.appendMessage({ role: "user", content: [reRunResult.result] });
7549
7909
  continue;
7550
7910
  }
7551
7911
  const use = useById.get(result.tool_use_id);
@@ -7564,6 +7924,7 @@ var Agent = class {
7564
7924
  isError: !!result.is_error
7565
7925
  });
7566
7926
  this.events.emit("tool.executed", {
7927
+ id: result.tool_use_id,
7567
7928
  name: use.name,
7568
7929
  durationMs,
7569
7930
  ok: !result.is_error,
@@ -7604,12 +7965,11 @@ var Agent = class {
7604
7965
  }
7605
7966
  }
7606
7967
  /**
7607
- * Run context window pipeline if compactor is present.
7968
+ * Run context window pipeline. The pipeline may be empty, or it may contain
7969
+ * middleware with its own injected dependencies.
7608
7970
  */
7609
7971
  async compactContextIfNeeded() {
7610
- if (this.compactor) {
7611
- await this.pipelines.contextWindow.run(this.ctx);
7612
- }
7972
+ await this.pipelines.contextWindow.run(this.ctx);
7613
7973
  }
7614
7974
  };
7615
7975
  function toError(err) {
@@ -7627,7 +7987,6 @@ var ConversationState = class {
7627
7987
  constructor(ctx) {
7628
7988
  this.ctx = ctx;
7629
7989
  }
7630
- // ─── Read API ───────────────────────────────────────────────────────
7631
7990
  get messages() {
7632
7991
  return this.ctx.messages;
7633
7992
  }
@@ -7642,25 +8001,24 @@ var ConversationState = class {
7642
8001
  * that need a stable view across an async boundary.
7643
8002
  */
7644
8003
  snapshot() {
7645
- return {
7646
- messages: [...this.ctx.messages],
7647
- todos: [...this.ctx.todos],
7648
- meta: { ...this.ctx.meta }
7649
- };
8004
+ return Object.freeze({
8005
+ messages: Object.freeze([...this.ctx.messages]),
8006
+ todos: Object.freeze([...this.ctx.todos]),
8007
+ meta: Object.freeze({ ...this.ctx.meta })
8008
+ });
7650
8009
  }
7651
- // ─── Write API (preferred — fires onChange) ─────────────────────────
7652
8010
  appendMessage(message) {
7653
- this.ctx.messages.push(message);
8011
+ this.ctx.messages.splice(this.ctx.messages.length, 0, message);
7654
8012
  this.emit({ kind: "message_appended", message });
7655
8013
  }
7656
8014
  replaceMessages(messages) {
7657
8015
  this.ctx.messages.length = 0;
7658
- this.ctx.messages.push(...messages);
8016
+ this.ctx.messages.splice(0, 0, ...messages);
7659
8017
  this.emit({ kind: "messages_replaced", messages: [...messages] });
7660
8018
  }
7661
8019
  replaceTodos(todos) {
7662
8020
  this.ctx.todos.length = 0;
7663
- this.ctx.todos.push(...todos);
8021
+ this.ctx.todos.splice(0, 0, ...todos);
7664
8022
  this.emit({ kind: "todos_replaced", todos: [...todos] });
7665
8023
  }
7666
8024
  setMeta(key, value) {
@@ -7672,13 +8030,15 @@ var ConversationState = class {
7672
8030
  delete this.ctx.meta[key];
7673
8031
  this.emit({ kind: "meta_deleted", key });
7674
8032
  }
7675
- // ─── Subscription ───────────────────────────────────────────────────
8033
+ clearMeta() {
8034
+ const keys = Object.keys(this.ctx.meta);
8035
+ if (keys.length === 0) return;
8036
+ for (const key of keys) delete this.ctx.meta[key];
8037
+ this.emit({ kind: "meta_cleared" });
8038
+ }
7676
8039
  /**
7677
- * Subscribe to mutations that go through this wrapper. Note: mutations
7678
- * that bypass the wrapper (e.g. `ctx.messages.push(...)` directly) are
7679
- * NOT observed — by design during migration, since we don't want to
7680
- * monkey-patch arrays. Migrating call sites to use this API is the
7681
- * dev-plan #1 work.
8040
+ * Subscribe to mutations that go through this wrapper. Direct mutations of
8041
+ * the compatibility arrays are intentionally not observed.
7682
8042
  */
7683
8043
  onChange(listener) {
7684
8044
  this.listeners.add(listener);
@@ -7895,34 +8255,60 @@ You operate inside the user's terminal with direct read and write access to thei
7895
8255
 
7896
8256
  ## Core principles
7897
8257
 
7898
- 1. Read before you write. Always inspect the relevant files before proposing changes. Assumptions about code you haven't read are bugs in waiting.
7899
-
7900
- 2. Prefer surgical edits over rewrites. When modifying existing files, use the edit tool with str_replace; only use write for new files or full replacements explicitly requested.
7901
-
7902
- 3. Show your work. Before non-trivial changes, briefly state what you're about to do \u2014 one sentence, not a wall of text. After tool calls, summarize what happened, not what you did mechanically.
8258
+ 1. **Read before you write.** Always inspect the relevant files before proposing changes. Assumptions about code you haven't read are bugs in waiting.
8259
+ 2. **Prefer surgical edits over rewrites.** When modifying existing files, use the edit tool with str_replace; only use write for new files or full replacements explicitly requested.
8260
+ 3. **Show your work.** Before non-trivial changes, briefly state what you're about to do \u2014 one sentence, not a wall of text. After tool calls, summarize what happened, not what you did mechanically.
8261
+ 4. **Be honest about limits.** If you don't know, say so. If something failed, say what failed and what you'll try next. Never fabricate file contents, API responses, or test results.
8262
+ 5. **Be concise.** The user is a developer in a terminal. No marketing language, no "great question!", no bullet-point lists when prose works. If a one-liner answers, a one-liner is the answer.
8263
+ 6. **Ask when blocked, proceed when not.** If the task is ambiguous in a way that meaningfully changes the approach, ask. If it's ambiguous in a way that doesn't, pick a reasonable default and proceed, stating the assumption.
8264
+ 7. **Trust the tools.** If a permission prompt is shown, the user will answer. Do not preemptively explain that you "would like to" do something \u2014 call the tool, let the permission flow decide.
8265
+ 8. **Format for scanability.** Use code blocks for code, backticks for file paths, bold for key terms. One-liners stay one line. Paragraphs max 3 sentences.
8266
+ 9. **Recover explicitly.** When a tool fails, state: (1) what failed, (2) what you tried, (3) what you'll attempt next. Never silently skip.
7903
8267
 
7904
- 4. Honest about limits. If you don't know, say so. If something failed, say what failed and what you'll try next. Never fabricate file contents, API responses, or test results.
8268
+ ## Decision heuristics
7905
8269
 
7906
- 5. Concise output. The user is a developer in a terminal. No marketing language, no "great question!", no bullet-point lists when prose works. If a one-liner answers, a one-liner is the answer.
8270
+ - **Task is ambiguous** (unclear which file, conflicting requirements) \u2192 ask before proceeding
8271
+ - **Task is clear, approach is unknown** \u2192 try one approach, report what happened
8272
+ - **Tool fails** \u2192 retry once with adjusted params, then report failure
8273
+ - **Permission prompt shown** \u2192 wait for user, do not act unilaterally
8274
+ - **Context window filling up** \u2192 use context_manager proactively; don't wait to be told
7907
8275
 
7908
- 6. Ask when blocked, proceed when not. If the task is ambiguous in a way that meaningfully changes the approach, ask. If it's ambiguous in a way that doesn't, pick a reasonable default and proceed, stating the assumption.
8276
+ ## How you work
7909
8277
 
7910
- 7. Trust the tools. If a permission prompt is shown, the user will answer. Do not preemptively explain that you "would like to" do something \u2014 call the tool, let the permission flow decide.
7911
-
7912
- ## What you do not do
7913
-
7914
- - You do not lecture about software engineering principles unless asked.
7915
- - You do not add comments to code unless they materially help or were requested.
7916
- - You do not refactor adjacent code while fixing a bug, unless asked.
7917
- - You do not claim work is "production-ready" or "fully tested" \u2014 the user decides that.
7918
- - You do not apologize for failures. You report them and proceed.`;
8278
+ - **Stay focused.** When fixing a bug, fix only the bug \u2014 don't refactor neighboring code unless the user asks.
8279
+ - **Comment with purpose.** Add comments only when they explain why, not what. The code already says what.
8280
+ - **Own your output.** Never call work "production-ready" or "fully tested" \u2014 the user makes that call.
8281
+ - **Move on from mistakes.** When something fails, report what happened and what you'll do next. No apologies, no hand-wringing.
8282
+ - **Stay in your lane.** Don't lecture about software engineering principles unless explicitly asked \u2014 the user is the expert on their codebase.`;
7919
8283
  var DefaultSystemPromptBuilder = class {
7920
8284
  constructor(opts = {}) {
7921
8285
  this.opts = opts;
7922
8286
  }
7923
8287
  opts;
7924
- envCache;
8288
+ /**
8289
+ * Cached environment block, keyed by projectRoot. A single builder
8290
+ * instance is normally reused across turns of the same agent run, but
8291
+ * tests and library consumers may reuse it across runs with different
8292
+ * roots; keying the cache prevents leaking the first call's project
8293
+ * state into a later call against an unrelated project.
8294
+ */
8295
+ envCacheByRoot = /* @__PURE__ */ new Map();
8296
+ skillCache;
7925
8297
  async build(ctx) {
8298
+ if (this.opts.skillLoader && !this.skillCache) {
8299
+ try {
8300
+ const entries = await this.opts.skillLoader.listEntries();
8301
+ if (entries.length > 0) {
8302
+ const lines = [];
8303
+ for (const e of entries) {
8304
+ const scopeTag = e.scope.length > 0 ? ` \u2014 ${e.scope.slice(0, 4).join(", ")}` : "";
8305
+ lines.push(`- **${e.name}**${scopeTag} (${e.trigger})`);
8306
+ }
8307
+ this.skillCache = lines.join("\n");
8308
+ }
8309
+ } catch {
8310
+ }
8311
+ }
7926
8312
  const layer1 = LAYER_1_IDENTITY;
7927
8313
  const layer2 = this.buildToolUsage(ctx.tools);
7928
8314
  const layer3 = await this.buildEnvironment(ctx);
@@ -7958,8 +8344,20 @@ var DefaultSystemPromptBuilder = class {
7958
8344
  ### ${t2.name}
7959
8345
  ${hint.trim()}`);
7960
8346
  }
8347
+ lines.push(`
8348
+ ## Common patterns
8349
+
8350
+ - **Inspect before edit:** \`read\`/\`glob\`/\`grep\` \u2192 locate target \u2192 \`edit\`
8351
+ - **Search then operate:** \`grep\`/\`glob\` \u2192 identify targets \u2192 \`batch_tool_use\` or iterative \`edit\`
8352
+ - **Verify after mutate:** \`write\`/\`edit\`/\`patch\` \u2192 \`read\` back to confirm \u2192 report outcome
8353
+ - **Explore project:** \`glob\` for structure \u2192 \`read\` key files \u2192 \`grep\` for patterns
8354
+ - **Batch ops:** Use \`replace\` with glob patterns for multi-file surgical changes
8355
+
8356
+ When unsure about a file's current state, read it first rather than assuming.`);
7961
8357
  const hasContextManager = tools.some((t2) => t2.name === "context_manager");
7962
8358
  if (hasContextManager) {
8359
+ const maxCtx = this.opts.modelCapabilities?.maxContextTokens ?? 128e3;
8360
+ const threshold = maxCtx <= 32e3 ? "50" : "70";
7963
8361
  lines.push(`
7964
8362
  ## Context management
7965
8363
 
@@ -7967,7 +8365,7 @@ When the conversation grows long and context window usage exceeds what you can t
7967
8365
  use the context_manager tool proactively \u2014 do NOT wait to be told:
7968
8366
 
7969
8367
  - Call \`context_manager\` with \`{"action":"check"}\` to see current token budget and message counts.
7970
- - When the conversation exceeds ~70% of your context window, call \`{"action":"summary"}\` or \`{"action":"compact"}\` to reclaim space.
8368
+ - When the conversation exceeds ~${threshold}% of your context window, call \`{"action":"summary"}\` or \`{"action":"compact"}\` to reclaim space.
7971
8369
  - Use \`{"action":"prune"}\` to surgically remove specific irrelevant message ranges (e.g. old debug output).
7972
8370
  - Use \`{"action":"add_note"}\` to inject a summary note at a specific point after a complex operation.
7973
8371
 
@@ -7977,14 +8375,17 @@ summarize it, and let the tool result hold only the summary.`);
7977
8375
  return lines.join("\n");
7978
8376
  }
7979
8377
  async buildEnvironment(ctx) {
7980
- if (this.envCache) return this.envCache;
8378
+ const cached = this.envCacheByRoot.get(ctx.projectRoot);
8379
+ if (cached) return cached;
7981
8380
  const today = this.opts.todayIso ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
7982
8381
  const platform2 = `${os3.platform()} ${os3.release()}`;
7983
8382
  const shell = process.env.SHELL ?? process.env.ComSpec ?? "unknown";
7984
8383
  const node = process.version;
7985
8384
  const isGit = await this.dirExists(path2.join(ctx.projectRoot, ".git"));
7986
- const git = isGit ? await this.gitStatus(ctx.projectRoot) : "not a git repo";
7987
- const langs = await this.detectLanguages(ctx.projectRoot);
8385
+ const [git, langs] = await Promise.all([
8386
+ isGit ? this.gitStatus(ctx.projectRoot) : Promise.resolve("not a git repo"),
8387
+ this.detectLanguages(ctx.projectRoot)
8388
+ ]);
7988
8389
  const lines = [
7989
8390
  "## Environment",
7990
8391
  "",
@@ -8002,8 +8403,17 @@ summarize it, and let the tool result hold only the summary.`);
8002
8403
  `- Running on: ${ctx.provider ?? "<unknown provider>"}/${ctx.model ?? "<unknown model>"}`
8003
8404
  );
8004
8405
  }
8406
+ if (this.opts.modeId && this.opts.modeId !== "default") {
8407
+ lines.push(`- Mode: ${this.opts.modeId}`);
8408
+ }
8409
+ if (this.opts.modelCapabilities) {
8410
+ lines.push(`- Context window: ${this.opts.modelCapabilities.maxContextTokens.toLocaleString()} tokens max`);
8411
+ }
8412
+ if (this.skillCache) {
8413
+ lines.push("", "## Skills in scope for this session", this.skillCache);
8414
+ }
8005
8415
  const text = lines.join("\n");
8006
- this.envCache = text;
8416
+ this.envCacheByRoot.set(ctx.projectRoot, text);
8007
8417
  return text;
8008
8418
  }
8009
8419
  async buildMemoryAndSkills() {
@@ -8017,16 +8427,10 @@ ${mem}`);
8017
8427
  } catch {
8018
8428
  }
8019
8429
  }
8020
- if (this.opts.skillLoader) {
8021
- try {
8022
- const manifest = await this.opts.skillLoader.manifestText();
8023
- if (manifest.trim()) parts.push(manifest);
8024
- } catch {
8025
- }
8026
- }
8027
8430
  return parts.join("\n\n");
8028
8431
  }
8029
8432
  async buildMode() {
8433
+ if (this.opts.modePrompt) return this.opts.modePrompt;
8030
8434
  if (!this.opts.modeStore) return "";
8031
8435
  const mode = await this.opts.modeStore.getActiveMode();
8032
8436
  if (!mode?.prompt) return "";
@@ -8042,8 +8446,19 @@ ${mem}`);
8042
8446
  }
8043
8447
  async gitStatus(root) {
8044
8448
  return new Promise((resolve4) => {
8449
+ let settled = false;
8450
+ const finish = (s) => {
8451
+ if (settled) return;
8452
+ settled = true;
8453
+ resolve4(s);
8454
+ };
8455
+ let proc;
8456
+ const timer = setTimeout(() => {
8457
+ proc?.kill("SIGKILL");
8458
+ finish("git timeout");
8459
+ }, 2e3);
8045
8460
  try {
8046
- const proc = spawn("git", ["status", "--porcelain=v1", "--branch"], {
8461
+ proc = spawn("git", ["status", "--porcelain=v1", "--branch"], {
8047
8462
  cwd: root,
8048
8463
  stdio: ["ignore", "pipe", "ignore"]
8049
8464
  });
@@ -8051,19 +8466,24 @@ ${mem}`);
8051
8466
  proc.stdout?.on("data", (c) => {
8052
8467
  buf += c.toString();
8053
8468
  });
8054
- proc.on("error", () => resolve4("git error"));
8469
+ proc.on("error", () => {
8470
+ clearTimeout(timer);
8471
+ finish("git error");
8472
+ });
8055
8473
  proc.on("close", () => {
8474
+ clearTimeout(timer);
8056
8475
  const lines = buf.split("\n").filter(Boolean);
8057
8476
  const branchLine = lines[0] ?? "";
8058
- const branchMatch = /## ([^\s.]+)/.exec(branchLine);
8477
+ const branchMatch = branchLine.match(/## ([^\s.]+)/);
8059
8478
  const branch = branchMatch?.[1] ?? "detached";
8060
8479
  const dirty = lines.slice(1);
8061
8480
  const staged = dirty.filter((l) => /^[MARCD]/.test(l)).length;
8062
8481
  const modified = dirty.length - staged;
8063
- resolve4(`branch=${branch}, ${modified} modified, ${staged} staged`);
8482
+ finish(`branch=${branch}, ${modified} modified, ${staged} staged`);
8064
8483
  });
8065
8484
  } catch {
8066
- resolve4("git unavailable");
8485
+ clearTimeout(timer);
8486
+ finish("git unavailable");
8067
8487
  }
8068
8488
  });
8069
8489
  }
@@ -8081,14 +8501,17 @@ ${mem}`);
8081
8501
  ["composer.json", "PHP"],
8082
8502
  ["mix.exs", "Elixir"]
8083
8503
  ];
8084
- const langs = /* @__PURE__ */ new Set();
8085
- for (const [marker, lang] of checks) {
8086
- try {
8087
- await fsp.access(path2.join(root, marker));
8088
- langs.add(lang);
8089
- } catch {
8090
- }
8091
- }
8504
+ const hits = await Promise.all(
8505
+ checks.map(async ([marker, lang]) => {
8506
+ try {
8507
+ await fsp.access(path2.join(root, marker));
8508
+ return lang;
8509
+ } catch {
8510
+ return null;
8511
+ }
8512
+ })
8513
+ );
8514
+ const langs = new Set(hits.filter((l) => l !== null));
8092
8515
  return langs.size === 0 ? "unknown" : Array.from(langs).join(", ");
8093
8516
  }
8094
8517
  };