@wrongstack/core 0.275.1 → 0.276.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/{agent-bridge-D9JkPvJ0.d.ts → agent-bridge-D7A-eu3C.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-CArSFKFl.d.ts → agent-subagent-runner-CEuw4ATz.d.ts} +16 -10
  3. package/dist/{brain-DCkB5_e7.d.ts → brain-BLOyN5ZP.d.ts} +127 -1
  4. package/dist/{compactor-CzSvxM1g.d.ts → compactor-DcBpaJsI.d.ts} +1 -1
  5. package/dist/{config-BzFRKkg7.d.ts → config-Bf5mj-ad.d.ts} +20 -2
  6. package/dist/{context-BrLe8pJy.d.ts → context-CLnUMW5g.d.ts} +40 -2
  7. package/dist/coordination/index.d.ts +43 -24
  8. package/dist/coordination/index.js +849 -648
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/defaults/index.d.ts +28 -28
  11. package/dist/defaults/index.js +1636 -845
  12. package/dist/defaults/index.js.map +1 -1
  13. package/dist/execution/index.d.ts +16 -16
  14. package/dist/execution/index.js +218 -49
  15. package/dist/execution/index.js.map +1 -1
  16. package/dist/execution/prompt-enhancer.d.ts +1 -1
  17. package/dist/extension/index.d.ts +7 -7
  18. package/dist/extension/index.js.map +1 -1
  19. package/dist/{global-mailbox-CXkugtNQ.d.ts → global-mailbox-Iqfkgmwu.d.ts} +3 -3
  20. package/dist/{goal-store-DUwdbdoY.d.ts → goal-store-DGb6b5Ed.d.ts} +1 -1
  21. package/dist/hq/index.d.ts +6 -6
  22. package/dist/hq/index.js +178 -75
  23. package/dist/hq/index.js.map +1 -1
  24. package/dist/{index-CtlizLTK.d.ts → index-Cn0NOshr.d.ts} +10 -5
  25. package/dist/{index-neOCEy6q.d.ts → index-L4RZN9jJ.d.ts} +2 -2
  26. package/dist/index.d.ts +56 -48
  27. package/dist/index.js +2789 -1546
  28. package/dist/index.js.map +1 -1
  29. package/dist/infrastructure/index.d.ts +6 -6
  30. package/dist/infrastructure/index.js +26 -7
  31. package/dist/infrastructure/index.js.map +1 -1
  32. package/dist/kernel/index.d.ts +20 -12
  33. package/dist/kernel/index.js +55 -9
  34. package/dist/kernel/index.js.map +1 -1
  35. package/dist/{mailbox-types-_7gaY0Rl.d.ts → mailbox-types-DTl7bRH3.d.ts} +3 -1
  36. package/dist/{mcp-servers-MLL6bMlv.d.ts → mcp-servers-CuZGf9fI.d.ts} +4 -4
  37. package/dist/models/index.d.ts +5 -5
  38. package/dist/models/index.js +223 -139
  39. package/dist/models/index.js.map +1 -1
  40. package/dist/{models-registry-CrkcxQ-g.d.ts → models-registry-8XOdxWQu.d.ts} +16 -1
  41. package/dist/{multi-agent-coordinator-Dc_HuG9p.d.ts → multi-agent-coordinator-CiRtKVTk.d.ts} +8 -1
  42. package/dist/{null-fleet-bus-BMZwMin7.d.ts → null-fleet-bus-d9G-bVy9.d.ts} +26 -22
  43. package/dist/observability/index.d.ts +2 -2
  44. package/dist/{path-resolver-uVK4BatM.d.ts → path-resolver-BhIb6mtd.d.ts} +8 -3
  45. package/dist/{permission-CJR1qfOi.d.ts → permission-BCbQDR2s.d.ts} +1 -1
  46. package/dist/{permission-policy-DLVKKk4w.d.ts → permission-policy-C0ikndX_.d.ts} +2 -18
  47. package/dist/{pipeline-BYR-Vdau.d.ts → pipeline-Dl6XbfE7.d.ts} +10 -6
  48. package/dist/{provider-model-resolve-iREK_1lG.d.ts → provider-model-resolve-B70epO19.d.ts} +3 -3
  49. package/dist/{provider-runner-i7SQXZuC.d.ts → provider-runner-DZ808MSM.d.ts} +3 -3
  50. package/dist/{retry-policy-BmY5ooh3.d.ts → retry-policy-Dt3_z8Aj.d.ts} +1 -1
  51. package/dist/sdd/index.d.ts +19 -10
  52. package/dist/sdd/index.js +411 -240
  53. package/dist/sdd/index.js.map +1 -1
  54. package/dist/{secret-vault-C9leEMzr.d.ts → secret-vault-BUJ2d1gB.d.ts} +1 -1
  55. package/dist/security/index.d.ts +5 -5
  56. package/dist/security/index.js +30 -6
  57. package/dist/security/index.js.map +1 -1
  58. package/dist/{selector-qjpee9BF.d.ts → selector-BCkWgdwy.d.ts} +1 -1
  59. package/dist/{session-event-bridge-m7y--I-H.d.ts → session-event-bridge-CMvIO59_.d.ts} +1 -1
  60. package/dist/{session-reader-BjLH4V9n.d.ts → session-reader-C8aiChUu.d.ts} +1 -1
  61. package/dist/skills/index.js +1 -0
  62. package/dist/skills/index.js.map +1 -1
  63. package/dist/storage/index.d.ts +68 -30
  64. package/dist/storage/index.js +839 -528
  65. package/dist/storage/index.js.map +1 -1
  66. package/dist/{strategy-compactor-C2bmlWYg.d.ts → strategy-compactor-DI1OHVbB.d.ts} +10 -10
  67. package/dist/{todos-checkpoint-oDS9IBNS.d.ts → todos-checkpoint-Ddd2CGr0.d.ts} +56 -9
  68. package/dist/{tool-executor-D4YdaJ-M.d.ts → tool-executor-Bmd5Ygoo.d.ts} +45 -10
  69. package/dist/tools/index.d.ts +2 -2
  70. package/dist/tools/index.js.map +1 -1
  71. package/dist/types/index.d.ts +20 -20
  72. package/dist/types/index.js +331 -98
  73. package/dist/types/index.js.map +1 -1
  74. package/dist/utils/index.d.ts +16 -3
  75. package/dist/utils/index.js +159 -83
  76. package/dist/utils/index.js.map +1 -1
  77. package/dist/{worktree-manager-A1Efnvs0.d.ts → worktree-manager-DBdl_5rs.d.ts} +4 -1
  78. package/instructions/agents/shadow-agent.md +3 -3
  79. package/instructions/coordination/director-preamble.md +3 -3
  80. package/instructions/modes/research-web.md +4 -4
  81. package/package.json +1 -1
  82. package/skills/research-web/SKILL.md +26 -26
  83. package/skills/research-web/SKILL.save.md +1 -1
@@ -1,6 +1,6 @@
1
1
  import { randomBytes, createCipheriv, createDecipheriv, randomUUID, scryptSync } from 'crypto';
2
2
  import * as fs from 'fs/promises';
3
- import * as path6 from 'path';
3
+ import * as path7 from 'path';
4
4
  import * as os from 'os';
5
5
  import * as fs2 from 'fs';
6
6
  import { readFileSync, statSync } from 'fs';
@@ -46,9 +46,9 @@ var ToolErrorCategory = /* @__PURE__ */ ((ToolErrorCategory2) => {
46
46
  // src/types/tool-markers.ts
47
47
  var MALFORMED_ARG_MARKERS = ["__raw", "__raw_arguments", "_raw"];
48
48
  async function atomicWrite(targetPath, content, opts = {}) {
49
- const dir = path6.dirname(targetPath);
49
+ const dir = path7.dirname(targetPath);
50
50
  await fs.mkdir(dir, { recursive: true });
51
- const tmp = path6.join(dir, `.${path6.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
51
+ const tmp = path7.join(dir, `.${path7.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
52
52
  try {
53
53
  if (typeof content === "string") {
54
54
  await fs.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
@@ -107,7 +107,7 @@ async function renameWithRetry(from, to) {
107
107
  if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
108
108
  throw err;
109
109
  }
110
- await new Promise((resolve7) => setTimeout(resolve7, delays[i]));
110
+ await new Promise((resolve8) => setTimeout(resolve8, delays[i]));
111
111
  }
112
112
  }
113
113
  throw lastErr;
@@ -599,6 +599,25 @@ function compileUserRegex(pattern, flags) {
599
599
  };
600
600
  }
601
601
  }
602
+ function sessionScopedPath(dir, sessionId, suffix) {
603
+ if (!sessionId || sessionId.includes("\\") || sessionId.includes("..")) {
604
+ throw invalid(sessionId);
605
+ }
606
+ const resolved = path7.resolve(dir, `${sessionId}${suffix}`);
607
+ const rel = path7.relative(path7.resolve(dir), resolved);
608
+ if (rel.startsWith("..") || path7.isAbsolute(rel)) {
609
+ throw invalid(sessionId);
610
+ }
611
+ return resolved;
612
+ }
613
+ function invalid(sessionId) {
614
+ return new FsError({
615
+ message: `Invalid sessionId: ${sessionId}`,
616
+ code: ERROR_CODES.FS_DELETE_FAILED,
617
+ path: sessionId,
618
+ context: { reason: "path_traversal" }
619
+ });
620
+ }
602
621
 
603
622
  // src/utils/string.ts
604
623
  function truncate(s, max) {
@@ -1506,8 +1525,8 @@ function resolveToolResultRenderMode(modes, toolName) {
1506
1525
  }
1507
1526
  function wstackGlobalRoot() {
1508
1527
  const fromEnv = process.env["WRONGSTACK_HOME"];
1509
- if (fromEnv && fromEnv.trim().length > 0) return path6.resolve(fromEnv);
1510
- return path6.join(os.homedir(), ".wrongstack");
1528
+ if (fromEnv && fromEnv.trim().length > 0) return path7.resolve(fromEnv);
1529
+ return path7.join(os.homedir(), ".wrongstack");
1511
1530
  }
1512
1531
 
1513
1532
  // src/types/errors.ts
@@ -1564,6 +1583,7 @@ var ERROR_CODES = {
1564
1583
  SDD_NOT_READY: "SDD_NOT_READY",
1565
1584
  // General
1566
1585
  VALIDATION_ERROR: "VALIDATION_ERROR",
1586
+ PARSE_FAILED: "PARSE_FAILED",
1567
1587
  UNKNOWN: "UNKNOWN"
1568
1588
  };
1569
1589
  var WrongStackError = class extends Error {
@@ -1738,6 +1758,22 @@ var ToolValidationError = class extends WrongStackError {
1738
1758
  this.name = "ToolValidationError";
1739
1759
  }
1740
1760
  };
1761
+ var ParseError = class extends WrongStackError {
1762
+ source;
1763
+ constructor(opts) {
1764
+ super({
1765
+ message: opts.message,
1766
+ code: ERROR_CODES.PARSE_FAILED,
1767
+ subsystem: "general",
1768
+ severity: "error",
1769
+ recoverable: false,
1770
+ context: { source: opts.source, ...opts.context },
1771
+ cause: opts.cause
1772
+ });
1773
+ this.name = "ParseError";
1774
+ this.source = opts.source;
1775
+ }
1776
+ };
1741
1777
  function isWrongStackError(err) {
1742
1778
  return err instanceof WrongStackError;
1743
1779
  }
@@ -1765,6 +1801,9 @@ function isToolValidationError(err) {
1765
1801
  function isFetchError(err) {
1766
1802
  return err instanceof FetchError;
1767
1803
  }
1804
+ function isParseError(err) {
1805
+ return err instanceof ParseError;
1806
+ }
1768
1807
  function isSddError(err) {
1769
1808
  return err instanceof SddError;
1770
1809
  }
@@ -2112,7 +2151,7 @@ var DefaultSecretVault = class {
2112
2151
  const oldVersion = this._keyVersion;
2113
2152
  const newKey = randomBytes(KEY_BYTES);
2114
2153
  const newVersion = oldVersion + 1;
2115
- fs2.mkdirSync(path6.dirname(this.keyFile), { recursive: true });
2154
+ fs2.mkdirSync(path7.dirname(this.keyFile), { recursive: true });
2116
2155
  const passphrase = getVaultPassphrase();
2117
2156
  if (passphrase) {
2118
2157
  fs2.writeFileSync(this.keyFile, wrapDataKey(newKey, newVersion, passphrase), { mode: 384 });
@@ -2196,7 +2235,7 @@ var DefaultSecretVault = class {
2196
2235
  } catch (err) {
2197
2236
  if (err.code !== "ENOENT") throw err;
2198
2237
  }
2199
- fs2.mkdirSync(path6.dirname(this.keyFile), { recursive: true });
2238
+ fs2.mkdirSync(path7.dirname(this.keyFile), { recursive: true });
2200
2239
  const key = randomBytes(KEY_BYTES);
2201
2240
  const passphrase = getVaultPassphrase();
2202
2241
  const initialBytes = passphrase ? wrapDataKey(key, 1, passphrase) : key;
@@ -2295,7 +2334,7 @@ async function rewriteConfigEncrypted(configPath, vault, patch) {
2295
2334
  }
2296
2335
  const merged = deepMerge(current, patch ?? {});
2297
2336
  const encrypted = encryptConfigSecrets(merged, vault);
2298
- await fs.mkdir(path6.dirname(configPath), { recursive: true });
2337
+ await fs.mkdir(path7.dirname(configPath), { recursive: true });
2299
2338
  await atomicWrite(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
2300
2339
  await restrictFilePermissions(configPath);
2301
2340
  }
@@ -2521,7 +2560,7 @@ var DefaultLogger = class _DefaultLogger {
2521
2560
  this.stderr = opts.stderr !== false;
2522
2561
  this.maxFileBytes = opts.maxFileBytes ?? 10 * 1024 * 1024;
2523
2562
  if (this.file) {
2524
- const dir = path6.dirname(this.file);
2563
+ const dir = path7.dirname(this.file);
2525
2564
  this._tail = this._tail.then(async () => {
2526
2565
  await fs.mkdir(dir, { recursive: true });
2527
2566
  }).catch(() => void 0);
@@ -2668,6 +2707,7 @@ var DefaultTokenCounter = class {
2668
2707
  registry;
2669
2708
  providerId;
2670
2709
  events;
2710
+ sessionId;
2671
2711
  priceCache = /* @__PURE__ */ new Map();
2672
2712
  /** Most recently accounted request's tokens. Used for per-request context pressure. */
2673
2713
  lastInput = 0;
@@ -2676,8 +2716,13 @@ var DefaultTokenCounter = class {
2676
2716
  this.registry = opts.registry;
2677
2717
  this.providerId = opts.providerId;
2678
2718
  this.events = opts.events;
2719
+ this.sessionId = opts.sessionId;
2720
+ }
2721
+ setSessionId(sessionId) {
2722
+ this.sessionId = sessionId;
2679
2723
  }
2680
2724
  account(usage, model) {
2725
+ const eventSessionId = this.currentSessionId();
2681
2726
  this.input += usage.input;
2682
2727
  this.output += usage.output;
2683
2728
  this.cacheRead += usage.cacheRead ?? 0;
@@ -2687,7 +2732,7 @@ var DefaultTokenCounter = class {
2687
2732
  const price = model ? this.priceCache.get(model) : void 0;
2688
2733
  if (price) {
2689
2734
  this.applyPrice(usage, price);
2690
- this.emitAccounted();
2735
+ this.emitAccounted(eventSessionId);
2691
2736
  return;
2692
2737
  }
2693
2738
  if (this.registry && this.providerId && model) {
@@ -2701,18 +2746,22 @@ var DefaultTokenCounter = class {
2701
2746
  this.priceCache.set(model, p);
2702
2747
  this.applyPrice(usage, p);
2703
2748
  }
2704
- this.emitAccounted();
2749
+ this.emitAccounted(eventSessionId);
2705
2750
  }).catch(() => {
2706
- this.events?.emit("token.cost_estimate_unavailable", { model: model ?? "<unknown>" });
2707
- this.emitAccounted();
2751
+ this.events?.emit("token.cost_estimate_unavailable", {
2752
+ ...eventSessionId ? { sessionId: eventSessionId } : {},
2753
+ model: model ?? "<unknown>"
2754
+ });
2755
+ this.emitAccounted(eventSessionId);
2708
2756
  return void 0;
2709
2757
  });
2710
2758
  return;
2711
2759
  }
2712
- this.emitAccounted();
2760
+ this.emitAccounted(eventSessionId);
2713
2761
  }
2714
2762
  /** Synchronous variant for code paths that have already resolved the model. */
2715
2763
  accountWithModel(usage, resolved) {
2764
+ const eventSessionId = this.currentSessionId();
2716
2765
  this.input += usage.input;
2717
2766
  this.output += usage.output;
2718
2767
  this.cacheRead += usage.cacheRead ?? 0;
@@ -2726,7 +2775,7 @@ var DefaultTokenCounter = class {
2726
2775
  }
2727
2776
  this.priceCache.set(resolved.modelId, price);
2728
2777
  this.applyPrice(usage, price);
2729
- this.emitAccounted();
2778
+ this.emitAccounted(eventSessionId);
2730
2779
  }
2731
2780
  total() {
2732
2781
  return {
@@ -2739,6 +2788,10 @@ var DefaultTokenCounter = class {
2739
2788
  currentRequestTokens() {
2740
2789
  return { input: this.lastInput, cacheRead: this.lastCacheRead };
2741
2790
  }
2791
+ setCurrentRequestTokens(input, cacheRead) {
2792
+ this.lastInput = input;
2793
+ this.lastCacheRead = cacheRead ?? 0;
2794
+ }
2742
2795
  estimateCost() {
2743
2796
  return {
2744
2797
  input: round4(this.costInput),
@@ -2759,12 +2812,17 @@ var DefaultTokenCounter = class {
2759
2812
  invalidateCache() {
2760
2813
  this.priceCache.clear();
2761
2814
  }
2762
- emitAccounted() {
2815
+ emitAccounted(sessionId = this.currentSessionId()) {
2763
2816
  this.events?.emit("token.accounted", {
2817
+ ...sessionId ? { sessionId } : {},
2764
2818
  usage: this.total(),
2765
2819
  cost: { input: this.costInput, output: this.costOutput, total: this.costInput + this.costOutput }
2766
2820
  });
2767
2821
  }
2822
+ currentSessionId() {
2823
+ const value = typeof this.sessionId === "function" ? this.sessionId() : this.sessionId;
2824
+ return typeof value === "string" && value.length > 0 ? value : void 0;
2825
+ }
2768
2826
  reset() {
2769
2827
  this.input = 0;
2770
2828
  this.output = 0;
@@ -2818,6 +2876,18 @@ var MEMORY_TYPE_LABELS = {
2818
2876
  };
2819
2877
 
2820
2878
  // src/execution/compaction-core.ts
2879
+ var FAILURE_PATTERN = /(error|fail|exception|timeout|enonet|eacces|eperm|enoent|abort)/i;
2880
+ var CORRECTION_PATTERN = /\b(wrong|no\b|stop\b|don'?t\b|actually|fix that|undo|revert|forget|ignore|skip)\b/i;
2881
+ var ERROR_LANG_PATTERN = /\b(error|exception|fatal|critical|crash|panic|abort|segfault|core dump|undefined is not|null pointer|typeerror|referenceerror|syntaxerror)\b/i;
2882
+ var SECURITY_PATTERN = /\b(security|vulnerability|injection|xss|csrf|secret|apikey|api.key|hardcoded|leak|exploit|cve)\b/i;
2883
+ var ARCHITECTURE_PATTERN = /\b(architecture|design|approach|strategy|pattern|refactor|migrate|restructure|decision|trade.?off)\b/i;
2884
+ var BOILERPLATE_PATTERN = /\b(files_with_matches|count|found \d+ match|directory tree|\.\.\. and \d+ more)\b/i;
2885
+ var PATH_HINT_PATTERN = /(?:(?:[A-Za-z]:)?[./\\]?[\w@.-]+(?:[\\/][\w@(). -]+)+\.[A-Za-z0-9]{1,12})/g;
2886
+ var PATH_BACKSLASH_PATTERN = /\\/g;
2887
+ var PATH_TRIM_PATTERN = /^["'`]+|["'`),;:]+$/g;
2888
+ var ERROR_LINE_PATTERN = /\b(error|exception|failed|failure|fatal|panic|timeout|denied|enoent|eacces|eperm)\b/i;
2889
+ var NEWLINE_SPLIT_PATTERN = /\r?\n/;
2890
+ var WHITESPACE_COLLAPSE_PATTERN = /\s+/g;
2821
2891
  function compactionDebugEnabled() {
2822
2892
  return process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1";
2823
2893
  }
@@ -3053,9 +3123,8 @@ function summarizeToolResultElision(block, tokens) {
3053
3123
  function extractPathHints(content) {
3054
3124
  const text = typeof content === "string" ? content : JSON.stringify(content);
3055
3125
  const out = /* @__PURE__ */ new Set();
3056
- const re = /(?:(?:[A-Za-z]:)?[./\\]?[\w@.-]+(?:[\\/][\w@(). -]+)+\.[A-Za-z0-9]{1,12})/g;
3057
- for (const match of text.matchAll(re)) {
3058
- const clean = match[0]?.replace(/\\/g, "/").replace(/^["'`]+|["'`),;:]+$/g, "");
3126
+ for (const match of text.matchAll(PATH_HINT_PATTERN)) {
3127
+ const clean = match[0]?.replace(PATH_BACKSLASH_PATTERN, "/").replace(PATH_TRIM_PATTERN, "");
3059
3128
  if (clean && clean.length <= 220) out.add(clean);
3060
3129
  if (out.size >= 5) break;
3061
3130
  }
@@ -3063,12 +3132,9 @@ function extractPathHints(content) {
3063
3132
  }
3064
3133
  function firstErrorLine(content) {
3065
3134
  const text = typeof content === "string" ? content : JSON.stringify(content);
3066
- for (const line of text.split(/\r?\n/)) {
3067
- if (!/\b(error|exception|failed|failure|fatal|panic|timeout|denied|enoent|eacces|eperm)\b/i.test(
3068
- line
3069
- ))
3070
- continue;
3071
- const trimmed = line.replace(/\s+/g, " ").trim();
3135
+ for (const line of text.split(NEWLINE_SPLIT_PATTERN)) {
3136
+ if (!ERROR_LINE_PATTERN.test(line)) continue;
3137
+ const trimmed = line.replace(WHITESPACE_COLLAPSE_PATTERN, " ").trim();
3072
3138
  if (trimmed) return trimmed.slice(0, 180);
3073
3139
  }
3074
3140
  return void 0;
@@ -3115,9 +3181,9 @@ function scoreMessage(m, context) {
3115
3181
  if (hasToolUse2(m) || hasResult) return 0;
3116
3182
  }
3117
3183
  if (context?.failureCounts && m.role === "user" && hasToolUse2(m) === false) {
3118
- const isFailure = /error|fail|exception|timeout|enonet|eacces|eperm|enoent|abort/i.test(text);
3119
- if (isFailure) {
3120
- const errKey = /(error|fail|exception|timeout|enonet|eacces|eperm|enoent|abort)/i.exec(text)?.[0]?.toLowerCase() ?? "error";
3184
+ const failureMatch = FAILURE_PATTERN.exec(text);
3185
+ if (failureMatch) {
3186
+ const errKey = failureMatch[0]?.toLowerCase() ?? "error";
3121
3187
  const count = (context.failureCounts.get(errKey) ?? 0) + 1;
3122
3188
  context.failureCounts.set(errKey, count);
3123
3189
  if (count >= 5) return 0;
@@ -3125,29 +3191,21 @@ function scoreMessage(m, context) {
3125
3191
  }
3126
3192
  }
3127
3193
  if (m.role === "user") {
3128
- if (/\b(wrong|no\b|stop\b|don'?t\b|actually|fix that|undo|revert|forget|ignore|skip)\b/i.test(
3129
- text
3130
- )) {
3194
+ if (CORRECTION_PATTERN.test(text)) {
3131
3195
  return 5;
3132
3196
  }
3133
3197
  }
3134
- if (/\b(error|exception|fatal|critical|crash|panic|abort|segfault|core dump|undefined is not|null pointer|typeerror|referenceerror|syntaxerror)\b/i.test(
3135
- text
3136
- )) {
3198
+ if (ERROR_LANG_PATTERN.test(text)) {
3137
3199
  return 5;
3138
3200
  }
3139
- if (/\b(security|vulnerability|injection|xss|csrf|secret|apikey|api.key|hardcoded|leak|exploit|cve)\b/i.test(
3140
- text
3141
- )) {
3201
+ if (SECURITY_PATTERN.test(text)) {
3142
3202
  return 5;
3143
3203
  }
3144
- if (m.role === "assistant" && /\b(architecture|design|approach|strategy|pattern|refactor|migrate|restructure|decision|trade.?off)\b/i.test(
3145
- text
3146
- )) {
3204
+ if (m.role === "assistant" && ARCHITECTURE_PATTERN.test(text)) {
3147
3205
  return 5;
3148
3206
  }
3149
3207
  if (hasLargeToolResult(m)) return 1;
3150
- if (m.role === "user" && !hasToolUse2(m) && /\b(files_with_matches|count|found \d+ match|directory tree|\.\.\. and \d+ more)\b/i.test(text)) {
3208
+ if (m.role === "user" && !hasToolUse2(m) && BOILERPLATE_PATTERN.test(text)) {
3151
3209
  return 1;
3152
3210
  }
3153
3211
  return 3;
@@ -3157,6 +3215,11 @@ function buildSmartDigest(messages) {
3157
3215
  const failureCounts = /* @__PURE__ */ new Map();
3158
3216
  let noiseCount = 0;
3159
3217
  for (const m of messages) {
3218
+ const isPureToolIO = Array.isArray(m.content) && m.content.length > 0 && !hasToolUse2(m) && m.content.every((b) => b.type === "tool_result");
3219
+ if (isPureToolIO) {
3220
+ noiseCount++;
3221
+ continue;
3222
+ }
3160
3223
  const score = scoreMessage(m, { failureCounts });
3161
3224
  const text = extractText(m);
3162
3225
  const toolCount = countToolBlocks(m);
@@ -3369,52 +3432,52 @@ var DefaultPathResolver = class {
3369
3432
  projectRoot;
3370
3433
  cwd;
3371
3434
  constructor(cwd = process.cwd()) {
3372
- this.cwd = path6.resolve(cwd);
3435
+ this.cwd = path7.resolve(cwd);
3373
3436
  this.projectRoot = this.detectProjectRoot(this.cwd);
3374
3437
  }
3375
3438
  detectProjectRoot(start) {
3376
- let dir = path6.resolve(start);
3377
- const root = path6.parse(dir).root;
3378
- const home = path6.resolve(os.homedir());
3379
- const startPath = path6.resolve(start);
3439
+ let dir = path7.resolve(start);
3440
+ const root = path7.parse(dir).root;
3441
+ const home = path7.resolve(os.homedir());
3442
+ const startPath = path7.resolve(start);
3380
3443
  while (dir !== root) {
3381
3444
  if (dir === home && dir !== startPath) {
3382
3445
  break;
3383
3446
  }
3384
3447
  for (const marker of PROJECT_MARKERS) {
3385
3448
  try {
3386
- fs2.accessSync(path6.join(dir, marker));
3449
+ fs2.accessSync(path7.join(dir, marker));
3387
3450
  return dir;
3388
3451
  } catch {
3389
3452
  }
3390
3453
  }
3391
- const parent = path6.dirname(dir);
3454
+ const parent = path7.dirname(dir);
3392
3455
  if (parent === dir) break;
3393
3456
  dir = parent;
3394
3457
  }
3395
3458
  return startPath;
3396
3459
  }
3397
3460
  resolve(input) {
3398
- const abs = path6.isAbsolute(input) ? input : path6.resolve(this.cwd, input);
3461
+ const abs = path7.isAbsolute(input) ? input : path7.resolve(this.cwd, input);
3399
3462
  let real;
3400
3463
  try {
3401
3464
  real = fs2.realpathSync(abs);
3402
3465
  } catch {
3403
- real = path6.normalize(abs);
3466
+ real = path7.normalize(abs);
3404
3467
  }
3405
3468
  return real;
3406
3469
  }
3407
3470
  isInsideRoot(absPath) {
3408
- const normalized = path6.normalize(absPath);
3409
- const root = path6.normalize(this.projectRoot);
3471
+ const normalized = path7.normalize(absPath);
3472
+ const root = path7.normalize(this.projectRoot);
3410
3473
  if (normalized === root) return true;
3411
- const rel = path6.relative(root, normalized);
3412
- return !rel.startsWith("..") && !path6.isAbsolute(rel);
3474
+ const rel = path7.relative(root, normalized);
3475
+ return !rel.startsWith("..") && !path7.isAbsolute(rel);
3413
3476
  }
3414
3477
  ensureInsideRoot(absPath) {
3415
3478
  const resolved = this.resolve(absPath);
3416
3479
  if (!this.isInsideRoot(resolved)) {
3417
- const display = path6.isAbsolute(absPath) ? path6.basename(absPath) : absPath;
3480
+ const display = path7.isAbsolute(absPath) ? path7.basename(absPath) : absPath;
3418
3481
  const err = new Error(`Path "${display}" resolves outside the project root`);
3419
3482
  err.fullPath = absPath;
3420
3483
  err.projectRoot = this.projectRoot;
@@ -3463,7 +3526,7 @@ function buildRecoveryStrategies(opts) {
3463
3526
  label: "downgrade_model",
3464
3527
  async attempt(err, ctx) {
3465
3528
  if (!(err instanceof ProviderError)) return null;
3466
- if (err.status !== 429 && err.status !== 529 && err.status < 500) return null;
3529
+ if (err.status !== 529 && err.status < 500) return null;
3467
3530
  const registry = opts?.modelsRegistry;
3468
3531
  if (!registry) return null;
3469
3532
  try {
@@ -3486,7 +3549,7 @@ function buildRecoveryStrategies(opts) {
3486
3549
  });
3487
3550
  if (candidates.length === 0) return null;
3488
3551
  const fallback = candidates.reduce(
3489
- (prev, curr) => (curr.cost?.input ?? 0) < (prev.cost?.input ?? 0) ? curr : prev
3552
+ (prev, curr) => (curr.cost?.input ?? Number.POSITIVE_INFINITY) < (prev.cost?.input ?? Number.POSITIVE_INFINITY) ? curr : prev
3490
3553
  );
3491
3554
  return {
3492
3555
  action: "retry",
@@ -3769,13 +3832,27 @@ var PATTERNS = [
3769
3832
  // Min 12 chars: some OAuth providers issue shorter-lived tokens (< 20
3770
3833
  // chars). A 12-char base64 string has ~71 bits of entropy — above the
3771
3834
  // threshold where random strings are unlikely to produce false matches.
3772
- regex: /(?:^|[^A-Za-z0-9_.~+/-])Bearer\s+[A-Za-z0-9._~+/-]{12,512}=*(?:$|[^A-Za-z0-9_.~+/-])/g
3835
+ // The trailing boundary is a NON-consuming lookahead: two adjacent bearer
3836
+ // tokens sharing a single delimiter (`Bearer a… Bearer b…`) must both be
3837
+ // redacted. A consuming trailing delimiter would eat the separator the
3838
+ // next match needs for its leading anchor, leaking the second token.
3839
+ regex: /(?:^|[^A-Za-z0-9_.~+/-])Bearer\s+[A-Za-z0-9._~+/-]{12,512}=*(?=$|[^A-Za-z0-9_.~+/-])/g
3773
3840
  },
3774
3841
  {
3775
3842
  type: "high_entropy_env",
3776
3843
  // Anchored with alternation instead of lookbehind to avoid backtracking.
3777
3844
  // Value bounded at 512 chars.
3778
- regex: /(?:^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?:\s|$)/g
3845
+ // The trailing boundary is a NON-consuming lookahead so two secrets
3846
+ // separated by a single delimiter (one space OR one newline, e.g.
3847
+ // `printenv` / `.env` dumps: `API_KEY=… \n SESSION_TOKEN=…`) are BOTH
3848
+ // redacted. A consuming trailing `\s` would swallow the separator the
3849
+ // next match needs for its leading anchor, so every other secret would
3850
+ // leak in plaintext.
3851
+ // The leading delimiter is CAPTURED (group 1) and re-emitted by the
3852
+ // replacement so the separator between adjacent secrets is preserved
3853
+ // rather than collapsed. Capture groups are therefore: 1=leading
3854
+ // delimiter, 2=key name, 3=value.
3855
+ regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g
3779
3856
  }
3780
3857
  ];
3781
3858
  var SIMPLE_PATTERNS = PATTERNS.filter((p) => p.type !== "high_entropy_env");
@@ -3783,6 +3860,7 @@ var COMBINED_REGEX = new RegExp(SIMPLE_PATTERNS.map((p) => `(${p.regex.source})`
3783
3860
  var HIGH_ENTROPY_REGEX = PATTERNS.find((p) => p.type === "high_entropy_env").regex;
3784
3861
  var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
3785
3862
  var SCRUB_CHUNK_BYTES = 64 * 1024;
3863
+ var SCRUB_OVERLAP_BYTES = 1024;
3786
3864
  function hasCredentialAnchors(text) {
3787
3865
  return text.includes("-----BEGIN") || // Private keys (most unique → cheap reject)
3788
3866
  text.includes("sk-") || // Anthropic + OpenAI keys
@@ -3817,8 +3895,16 @@ var DefaultSecretScrubber = class {
3817
3895
  while (i < text.length) {
3818
3896
  let end = Math.min(i + SCRUB_CHUNK_BYTES, text.length);
3819
3897
  if (end < text.length) {
3820
- const nl = text.lastIndexOf("\n", end);
3821
- if (nl > i + SCRUB_CHUNK_BYTES / 2) end = nl + 1;
3898
+ const limit = Math.min(end + SCRUB_OVERLAP_BYTES, text.length);
3899
+ let safe = -1;
3900
+ for (let j = end; j < limit; j++) {
3901
+ const ch = text.charCodeAt(j);
3902
+ if (ch === 32 || ch === 9 || ch === 10 || ch === 13) {
3903
+ safe = j;
3904
+ break;
3905
+ }
3906
+ }
3907
+ end = safe === -1 ? end : safe + 1;
3822
3908
  }
3823
3909
  out.push(this.scrubOne(text.slice(i, end)));
3824
3910
  i = end;
@@ -3836,8 +3922,8 @@ var DefaultSecretScrubber = class {
3836
3922
  return replacement !== void 0 ? replacement : match;
3837
3923
  }
3838
3924
  );
3839
- out = out.replace(HIGH_ENTROPY_REGEX, (_match, group1, _group2) => {
3840
- return `${group1}=[REDACTED:high_entropy_env]`;
3925
+ out = out.replace(HIGH_ENTROPY_REGEX, (_match, lead, key, _value) => {
3926
+ return `${lead}${key}=[REDACTED:high_entropy_env]`;
3841
3927
  });
3842
3928
  return out;
3843
3929
  }
@@ -3898,6 +3984,14 @@ var DefaultModelsRegistry = class {
3898
3984
  payload;
3899
3985
  /** Memoised overlay payload (in-memory / fetched / file). */
3900
3986
  overlayPayload;
3987
+ /**
3988
+ * Extra providers injected at runtime via `mergeOverlay()` — e.g. an
3989
+ * openai-compatible server (omniroute, LiteLLM, …) auto-discovered from its
3990
+ * `/v1/models` endpoint at boot. Applied LAST (on top of base + curated
3991
+ * overlay) and re-applied across `refresh()` so the discovered catalog
3992
+ * survives a models.dev refetch.
3993
+ */
3994
+ extraOverlay;
3901
3995
  fetchedAt;
3902
3996
  cacheFile;
3903
3997
  url;
@@ -3922,7 +4016,7 @@ var DefaultModelsRegistry = class {
3922
4016
  this.overlay = opts.overlay;
3923
4017
  this.overlayUrl = opts.overlayUrl;
3924
4018
  this.overlayFile = opts.overlayFile;
3925
- this.overlayCacheFile = opts.overlayCacheFile ?? (opts.overlayUrl ? path6.join(path6.dirname(opts.cacheFile), "models-overlay-cache.json") : void 0);
4019
+ this.overlayCacheFile = opts.overlayCacheFile ?? (opts.overlayUrl ? path7.join(path7.dirname(opts.cacheFile), "models-overlay-cache.json") : void 0);
3926
4020
  }
3927
4021
  async load(opts = {}) {
3928
4022
  if (this.payload && !opts.force) return this.payload;
@@ -3933,9 +4027,22 @@ var DefaultModelsRegistry = class {
3933
4027
  }
3934
4028
  const overlay = await this.loadOverlay(opts);
3935
4029
  const base = await this.loadBase(opts, Object.keys(overlay).length > 0);
3936
- this.payload = mergeModelsPayload(base, overlay);
4030
+ this.payload = this.withExtraOverlay(mergeModelsPayload(base, overlay));
3937
4031
  return this.payload;
3938
4032
  }
4033
+ /**
4034
+ * Merge an additional provider payload on top of the resolved catalog. Used
4035
+ * for runtime-discovered openai-compatible providers. Remembered so it is
4036
+ * re-applied across `refresh()`. A no-op for an empty payload.
4037
+ */
4038
+ mergeOverlay(payload) {
4039
+ if (!hasEntries(payload)) return;
4040
+ this.extraOverlay = this.extraOverlay ? mergeModelsPayload(this.extraOverlay, payload) : payload;
4041
+ if (this.payload) this.payload = mergeModelsPayload(this.payload, this.extraOverlay);
4042
+ }
4043
+ withExtraOverlay(payload) {
4044
+ return this.extraOverlay ? mergeModelsPayload(payload, this.extraOverlay) : payload;
4045
+ }
3939
4046
  /**
3940
4047
  * Load the models.dev base payload: fresh cache → network → stale cache.
3941
4048
  * On total failure, degrade to `{}` (so a non-empty overlay still drives
@@ -3984,7 +4091,11 @@ var DefaultModelsRegistry = class {
3984
4091
  });
3985
4092
  clearTimeout(timeout);
3986
4093
  if (!res.ok) {
3987
- throw new Error(`ModelsRegistry: HTTP ${res.status} fetching ${this.url}`);
4094
+ throw new FetchError({
4095
+ message: `ModelsRegistry: HTTP ${res.status} fetching ${this.url}`,
4096
+ status: res.status,
4097
+ context: { url: this.url, op: "refreshModels" }
4098
+ });
3988
4099
  }
3989
4100
  const json = await res.json();
3990
4101
  this.fetchedAt = /* @__PURE__ */ new Date();
@@ -3998,7 +4109,11 @@ var DefaultModelsRegistry = class {
3998
4109
  } catch (err) {
3999
4110
  clearTimeout(timeout);
4000
4111
  if (err instanceof Error && err.name === "AbortError") {
4001
- throw new Error(`ModelsRegistry: fetch timed out after ${this.refreshTimeoutMs}ms`);
4112
+ throw new FetchError({
4113
+ message: `ModelsRegistry: fetch timed out after ${this.refreshTimeoutMs}ms`,
4114
+ status: 408,
4115
+ context: { url: this.url, op: "refreshModels", timedOut: true }
4116
+ });
4002
4117
  }
4003
4118
  throw err;
4004
4119
  }
@@ -4034,7 +4149,11 @@ var DefaultModelsRegistry = class {
4034
4149
  method: "GET",
4035
4150
  headers: { accept: "application/json" }
4036
4151
  });
4037
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
4152
+ if (!res.ok) throw new FetchError({
4153
+ message: `HTTP ${res.status}`,
4154
+ status: res.status,
4155
+ context: { url: this.url, op: "refreshModels" }
4156
+ });
4038
4157
  const json = await res.json();
4039
4158
  const envelope = {
4040
4159
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -4068,7 +4187,7 @@ var DefaultModelsRegistry = class {
4068
4187
  async refresh() {
4069
4188
  const base = await this.refreshBase();
4070
4189
  const overlay = await this.loadOverlay({ force: true });
4071
- this.payload = mergeModelsPayload(base, overlay);
4190
+ this.payload = this.withExtraOverlay(mergeModelsPayload(base, overlay));
4072
4191
  return this.payload;
4073
4192
  }
4074
4193
  async listProviders() {
@@ -4146,7 +4265,7 @@ var DefaultModelsRegistry = class {
4146
4265
  }
4147
4266
  /** Used by `wstack models refresh` to expose where the cache lives. */
4148
4267
  cacheLocation() {
4149
- return path6.resolve(this.cacheFile);
4268
+ return path7.resolve(this.cacheFile);
4150
4269
  }
4151
4270
  };
4152
4271
  function formatAge(seconds) {
@@ -4165,18 +4284,18 @@ function hasEntries(payload) {
4165
4284
  function modePrompt(id) {
4166
4285
  for (const dir of modePromptDirCandidates()) {
4167
4286
  try {
4168
- return readFileSync(path6.join(dir, `${id}.md`), "utf8").trimEnd();
4287
+ return readFileSync(path7.join(dir, `${id}.md`), "utf8").trimEnd();
4169
4288
  } catch {
4170
4289
  }
4171
4290
  }
4172
4291
  return "";
4173
4292
  }
4174
4293
  function modePromptDirCandidates() {
4175
- const here = path6.dirname(fileURLToPath(import.meta.url));
4294
+ const here = path7.dirname(fileURLToPath(import.meta.url));
4176
4295
  const candidates = [
4177
- path6.resolve(here, "../../instructions/modes"),
4178
- path6.resolve(here, "../instructions/modes"),
4179
- path6.resolve(here, "instructions/modes")
4296
+ path7.resolve(here, "../../instructions/modes"),
4297
+ path7.resolve(here, "../instructions/modes"),
4298
+ path7.resolve(here, "instructions/modes")
4180
4299
  ];
4181
4300
  return candidates.sort((a, b) => Number(!isDirectory(a)) - Number(!isDirectory(b)));
4182
4301
  }
@@ -4293,7 +4412,7 @@ var DEFAULT_MODES = [
4293
4412
  description: "Current-data research \u2014 search web, verify, inject findings into context",
4294
4413
  prompt: modePrompt("research-web"),
4295
4414
  tags: ["research", "web", "current-data", "up-to-date"],
4296
- toolPreferences: ["web_search", "web_fetch", "search", "fetch", "context_manager"],
4415
+ toolPreferences: ["search/fetch", "search/fetch", "search", "fetch", "context_manager"],
4297
4416
  suggestedSkills: ["research-web", "tech-stack", "node-modern", "security-scanner", "react-modern"]
4298
4417
  }
4299
4418
  ];
@@ -4479,7 +4598,7 @@ var InMemoryAgentBridge = class {
4479
4598
  });
4480
4599
  }
4481
4600
  this.inflightGuards.add(correlationId);
4482
- return new Promise((resolve7, reject) => {
4601
+ return new Promise((resolve8, reject) => {
4483
4602
  const timer = setTimeout(() => {
4484
4603
  this.inflightGuards.delete(correlationId);
4485
4604
  this.pendingRequests.delete(correlationId);
@@ -4498,7 +4617,7 @@ var InMemoryAgentBridge = class {
4498
4617
  return;
4499
4618
  }
4500
4619
  this.pendingRequests.set(correlationId, {
4501
- resolve: resolve7,
4620
+ resolve: resolve8,
4502
4621
  reject,
4503
4622
  timer
4504
4623
  });
@@ -4741,6 +4860,64 @@ var ToolExecutor = class _ToolExecutor {
4741
4860
  const mode = resolveToolResultRenderMode(modes, toolName);
4742
4861
  renderer.setResultRenderMode(toolName, mode);
4743
4862
  }
4863
+ toolLogBase(ctx, use, toolName, durationMs) {
4864
+ return {
4865
+ event: "tool.execution",
4866
+ traceId: ctx.traceId,
4867
+ sessionId: ctx.session.id,
4868
+ agentId: ctx.agentId ?? "<unknown>",
4869
+ toolName,
4870
+ toolUseId: use.id,
4871
+ durationMs
4872
+ };
4873
+ }
4874
+ logToolSuccess(ctx, use, toolName, durationMs, outputChars) {
4875
+ this.opts.events?.emit("tool.completed", {
4876
+ name: toolName,
4877
+ id: use.id,
4878
+ sessionId: ctx.session.id,
4879
+ ...ctx.traceId ? { traceId: ctx.traceId } : {},
4880
+ agentId: ctx.agentId ?? "<unknown>",
4881
+ durationMs,
4882
+ outputChars
4883
+ });
4884
+ this.opts.logger?.info("tool execution completed", {
4885
+ ...this.toolLogBase(ctx, use, toolName, durationMs),
4886
+ outcome: "success",
4887
+ isError: false,
4888
+ outputChars
4889
+ });
4890
+ }
4891
+ logToolFailure(ctx, use, toolName, durationMs, err) {
4892
+ const { category, retryable, detail } = classifyToolError(err);
4893
+ const structured = isWrongStackError(err);
4894
+ const structuredError = structured ? {
4895
+ errorCode: err.code,
4896
+ errorSubsystem: err.subsystem,
4897
+ errorSeverity: err.severity
4898
+ } : {};
4899
+ this.opts.events?.emit("tool.failed", {
4900
+ name: toolName,
4901
+ id: use.id,
4902
+ sessionId: ctx.session.id,
4903
+ ...ctx.traceId ? { traceId: ctx.traceId } : {},
4904
+ agentId: ctx.agentId ?? "<unknown>",
4905
+ durationMs,
4906
+ category,
4907
+ retryable,
4908
+ ...detail ? { detail } : {},
4909
+ ...structuredError
4910
+ });
4911
+ this.opts.logger?.warn("tool execution failed", {
4912
+ ...this.toolLogBase(ctx, use, toolName, durationMs),
4913
+ outcome: "failure",
4914
+ isError: true,
4915
+ errorCategory: category,
4916
+ retryable,
4917
+ errorDetail: detail,
4918
+ ...structuredError
4919
+ });
4920
+ }
4744
4921
  /**
4745
4922
  * Execute a batch of tool uses using the configured strategy.
4746
4923
  * Returns the execution results and the remaining output budget.
@@ -4871,7 +5048,8 @@ ${errorDetails}`,
4871
5048
  "tool.has_dangerous_capabilities": toolCapsForAudit.length > 0
4872
5049
  });
4873
5050
  try {
4874
- let { block: result, bytes } = await this.executeTool(tool, use, ctx, budget);
5051
+ const producedText = await this.produceToolOutput(tool, use, ctx, budget);
5052
+ let { block: result, bytes } = this.settleToolOutput(tool, use, producedText, budget);
4875
5053
  budget -= bytes;
4876
5054
  if (this.opts.hookRunner?.has("PostToolUse")) {
4877
5055
  const post = await this.opts.hookRunner.postToolUse(
@@ -4888,13 +5066,18 @@ ${post.additionalContext}`;
4888
5066
  budget = Math.max(0, budget - Buffer.byteLength(appended, "utf8"));
4889
5067
  }
4890
5068
  }
5069
+ const outputChars = typeof result.content === "string" ? result.content.length : 0;
4891
5070
  span?.setAttribute("tool.is_error", !!result.is_error);
4892
- span?.setAttribute(
4893
- "tool.output_bytes",
4894
- typeof result.content === "string" ? result.content.length : 0
4895
- );
5071
+ span?.setAttribute("tool.output_bytes", outputChars);
5072
+ this.logToolSuccess(ctx, use, tool.name, Date.now() - start, outputChars);
4896
5073
  return { result, tool, durationMs: Date.now() - start };
4897
5074
  } catch (err) {
5075
+ if (isWrongStackError(err)) {
5076
+ if (err instanceof Error) span?.recordError(err);
5077
+ span?.setAttribute("tool.is_error", true);
5078
+ this.logToolFailure(ctx, use, tool.name, Date.now() - start, err);
5079
+ throw err;
5080
+ }
4898
5081
  const msg = toErrorMessage(err);
4899
5082
  const scrubbed = this.opts.secretScrubber.scrub(msg);
4900
5083
  const { category, retryable, detail } = classifyToolError(err);
@@ -4912,6 +5095,7 @@ ${post.additionalContext}`;
4912
5095
  span?.setAttribute("tool.error_category", category);
4913
5096
  span?.setAttribute("tool.error_retryable", retryable);
4914
5097
  if (detail) span?.setAttribute("tool.error_detail", detail);
5098
+ this.logToolFailure(ctx, use, tool.name, Date.now() - start, err);
4915
5099
  return { result, tool, durationMs: Date.now() - start };
4916
5100
  } finally {
4917
5101
  span?.end();
@@ -4921,7 +5105,8 @@ ${post.additionalContext}`;
4921
5105
  try {
4922
5106
  return await runOne(use);
4923
5107
  } catch (err) {
4924
- const msg = toErrorMessage(err);
5108
+ const isStructured = isWrongStackError(err);
5109
+ const msg = isStructured ? err.describe() : toErrorMessage(err);
4925
5110
  const scrubbed = this.opts.secretScrubber.scrub(msg);
4926
5111
  const { category, retryable, detail } = classifyToolError(err);
4927
5112
  const tool = this.registry.get(use.name);
@@ -4931,7 +5116,7 @@ ${post.additionalContext}`;
4931
5116
  const result = {
4932
5117
  type: "tool_result",
4933
5118
  tool_use_id: use.id,
4934
- content: `Tool "${use.name}" execution failed: ${scrubbed}`,
5119
+ content: isStructured ? scrubbed : `Tool "${use.name}" execution failed: ${scrubbed}`,
4935
5120
  is_error: true
4936
5121
  };
4937
5122
  budget = this.budgetForString(result.content, budget);
@@ -4974,7 +5159,23 @@ ${post.additionalContext}`;
4974
5159
  * to call a tool" and "tool.executed".
4975
5160
  */
4976
5161
  async executeTool(tool, use, ctx, budget) {
5162
+ const text = await this.produceToolOutput(tool, use, ctx, budget);
5163
+ return this.settleToolOutput(tool, use, text, budget);
5164
+ }
5165
+ /**
5166
+ * Async "produce" phase: run the tool, serialize, scrub, and spill an
5167
+ * oversized output to a disk artifact. Returns the pre-cap text. This is
5168
+ * the long-running, concurrency-safe part — it touches NO shared budget
5169
+ * state, so multiple invocations can run in parallel without racing.
5170
+ *
5171
+ * The `budgetHint` only gates the disk-spill threshold (a heuristic for
5172
+ * "is this output large enough to persist"); it is NOT the output cap.
5173
+ * The authoritative cap is applied synchronously in settleToolOutput()
5174
+ * against the live budget.
5175
+ */
5176
+ async produceToolOutput(tool, use, ctx, budgetHint) {
4977
5177
  this.opts.events?.emit("tool.started", {
5178
+ sessionId: ctx.session.id,
4978
5179
  name: tool.name,
4979
5180
  id: use.id,
4980
5181
  input: use.input
@@ -4983,8 +5184,20 @@ ${post.additionalContext}`;
4983
5184
  const output = await this.runWithTimeout(tool, use.input, ctx.signal, ctx, use.id);
4984
5185
  const text = this.serializer.serialize(output, { toolName: tool.name, input: use.input, tool });
4985
5186
  const scrubbed = this.opts.secretScrubber.scrub(text);
4986
- const withArtifact = await maybePersistLargeToolOutput(tool.name, scrubbed, budget);
4987
- const { text: capped, newBudget } = this.serializer.enforceCap(withArtifact, budget);
5187
+ return maybePersistLargeToolOutput(tool.name, scrubbed, budgetHint);
5188
+ }
5189
+ /**
5190
+ * Synchronous "settle" phase: enforce the output cap against the CURRENT
5191
+ * budget, render, and build the result block. This MUST stay synchronous
5192
+ * (no awaits) so that, in the parallel/smart strategies, two tools settling
5193
+ * one after another against the shared closure budget can't interleave a
5194
+ * stale read with a write. The first to settle consumes its bytes; the next
5195
+ * settles against the reduced budget; once the budget hits 0, enforceCap
5196
+ * truncates — making the per-iteration cap genuinely CUMULATIVE across
5197
+ * parallel tools instead of degrading into a per-tool cap.
5198
+ */
5199
+ settleToolOutput(tool, use, text, budget) {
5200
+ const { text: capped, newBudget } = this.serializer.enforceCap(text, budget);
4988
5201
  this.hintRenderMode(tool.name);
4989
5202
  this.opts.renderer?.writeToolResult(tool.name, capped, false);
4990
5203
  return {
@@ -5037,7 +5250,12 @@ ${post.additionalContext}`;
5037
5250
  let finalOutput;
5038
5251
  let sawFinal = false;
5039
5252
  if (!tool.executeStream) {
5040
- throw new Error(`Tool "${tool.name}" does not support streaming execution`);
5253
+ throw new ToolError({
5254
+ message: `Tool "${tool.name}" does not support streaming execution`,
5255
+ code: "TOOL_EXECUTION_FAILED",
5256
+ toolName: tool.name,
5257
+ context: { reason: "streaming_not_supported" }
5258
+ });
5041
5259
  }
5042
5260
  const stream = tool.executeStream(input, ctx, { signal });
5043
5261
  const iter = stream[Symbol.asyncIterator]();
@@ -5047,6 +5265,7 @@ ${post.additionalContext}`;
5047
5265
  let lastProgressEmitAt = 0;
5048
5266
  const emitProgress = (ev) => {
5049
5267
  this.opts.events?.emit("tool.progress", {
5268
+ sessionId: ctx.session.id,
5050
5269
  name: tool.name,
5051
5270
  id: toolUseId ?? "<unknown>",
5052
5271
  event: ev
@@ -5102,7 +5321,12 @@ ${progressTail}`;
5102
5321
  await iter.return?.(void 0);
5103
5322
  }
5104
5323
  if (!sawFinal) {
5105
- throw new Error(`tool "${tool.name}" executeStream completed without a 'final' event`);
5324
+ throw new ToolError({
5325
+ message: `tool "${tool.name}" executeStream completed without a 'final' event`,
5326
+ code: "TOOL_EXECUTION_FAILED",
5327
+ toolName: tool.name,
5328
+ context: { reason: "missing_final_event" }
5329
+ });
5106
5330
  }
5107
5331
  return finalOutput;
5108
5332
  }
@@ -5226,6 +5450,15 @@ function classifyToolError(err) {
5226
5450
  if (err instanceof Error && err.message.includes("validation")) {
5227
5451
  return { category: "validation" /* VALIDATION */, retryable: false, detail: "validation" };
5228
5452
  }
5453
+ if (err instanceof WrongStackError) {
5454
+ const wse = err;
5455
+ const category = wse.severity === "warning" ? "transient" /* TRANSIENT */ : "fatal" /* FATAL */;
5456
+ return {
5457
+ category,
5458
+ retryable: wse.recoverable,
5459
+ detail: `${wse.code} [${wse.subsystem}]`
5460
+ };
5461
+ }
5229
5462
  return {
5230
5463
  category: "fatal" /* FATAL */,
5231
5464
  retryable: false,
@@ -5253,11 +5486,11 @@ async function maybePersistLargeToolOutput(toolName, content, budget) {
5253
5486
  return content;
5254
5487
  }
5255
5488
  try {
5256
- const dir = path6.join(wstackGlobalRoot(), "tool-output");
5489
+ const dir = path7.join(wstackGlobalRoot(), "tool-output");
5257
5490
  await fs.mkdir(dir, { recursive: true });
5258
5491
  const safeTool = toolName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 40) || "tool";
5259
5492
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5260
- const filePath = path6.join(dir, `${stamp}-${safeTool}-${randomUUID()}.log`);
5493
+ const filePath = path7.join(dir, `${stamp}-${safeTool}-${randomUUID()}.log`);
5261
5494
  await fs.writeFile(filePath, content, "utf8");
5262
5495
  return content + `
5263
5496
  [full tool output: ${bytes} bytes at ${filePath}; read/grep that file selectively instead of re-running or requesting more output]`;
@@ -5617,11 +5850,11 @@ var Context = class {
5617
5850
  * Returns the resolved absolute path.
5618
5851
  */
5619
5852
  setWorkingDir(dir) {
5620
- const resolved = path6.isAbsolute(dir) ? path6.resolve(dir) : path6.resolve(this.projectRoot, dir);
5853
+ const resolved = path7.isAbsolute(dir) ? path7.resolve(dir) : path7.resolve(this.projectRoot, dir);
5621
5854
  if (!this.allowOutsideProjectRoot) {
5622
- const root = path6.resolve(this.projectRoot);
5623
- const rel = path6.relative(root, resolved);
5624
- if (rel.startsWith("..") || path6.isAbsolute(rel)) {
5855
+ const root = path7.resolve(this.projectRoot);
5856
+ const rel = path7.relative(root, resolved);
5857
+ if (rel.startsWith("..") || path7.isAbsolute(rel)) {
5625
5858
  throw new Error(
5626
5859
  `Working directory "${resolved}" is outside project root "${root}"`
5627
5860
  );
@@ -5668,7 +5901,7 @@ var DefaultSessionReader = class _DefaultSessionReader {
5668
5901
  if (!rootDir) {
5669
5902
  return await this.store.load(sessionId);
5670
5903
  }
5671
- const sessionPath = path6.join(rootDir, `${sessionId}.jsonl`);
5904
+ const sessionPath = sessionScopedPath(rootDir, sessionId, ".jsonl");
5672
5905
  let mtimeMs = null;
5673
5906
  try {
5674
5907
  const stat4 = await fs.stat(sessionPath);
@@ -6029,6 +6262,6 @@ function renderPlainText(meta, events) {
6029
6262
  return lines.join("\n");
6030
6263
  }
6031
6264
 
6032
- export { AgentError, BUILTIN_PROMPT_CATEGORIES, CONTEXT_WINDOW_MODES, ConfigError, Context, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CIRCUIT_BREAKER_CONFIG, DEFAULT_CONTEXT_CONFIG, DEFAULT_CONTEXT_WINDOW_MODE_ID, DEFAULT_MODES, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SESSION_LOGGING_CONFIG, DEFAULT_SESSION_PRUNE_DAYS, DEFAULT_SPEC_TEMPLATE, DEFAULT_TOOLS_CONFIG, DEFAULT_TUI_THINKING_WORD, DESIGN_STACKS, DefaultErrorHandler, DefaultLogger, DefaultModelsRegistry, DefaultPathResolver, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultTokenCounter, ERROR_CODES, FetchError, FsError, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, MALFORMED_ARG_MARKERS, MAX_TUI_THINKING_WORD_LENGTH, MEMORY_TYPE_LABELS, PROMPT_CATEGORY_LABELS, PluginError, ProviderError, SddError, SessionError, StreamHangError, ToolError, ToolErrorCategory, ToolExecutor, ToolValidationError, WrongStackError, asBlocks, asText, buildRecoveryStrategies, classifyFamily, computeTaskProgress, createMessage, decryptConfigSecrets, diffRegistry, encryptConfigSecrets, findCriticalPath, formatContextWindowModeList, getContextWindowMode, isAgentError, isBuiltinCategory, isConfigError, isContextWindowModeId, isDesignStack, isFetchError, isFsError, isImageBlock, isPluginError, isSddError, isSecretField, isSessionError, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isToolValidationError, isWrongStackError, listContextWindowModes, migratePlaintextSecrets, noOpLogger, normalizeTokenSavingTier, normalizeTuiThinkingWord, resolveContextWindowPolicy, rewriteConfigEncrypted, rotateConfigKeys, toWrongStackError, topologicalSort, validateRegistryManifest };
6265
+ export { AgentError, BUILTIN_PROMPT_CATEGORIES, CONTEXT_WINDOW_MODES, ConfigError, Context, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CIRCUIT_BREAKER_CONFIG, DEFAULT_CONTEXT_CONFIG, DEFAULT_CONTEXT_WINDOW_MODE_ID, DEFAULT_MODES, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SESSION_LOGGING_CONFIG, DEFAULT_SESSION_PRUNE_DAYS, DEFAULT_SPEC_TEMPLATE, DEFAULT_TOOLS_CONFIG, DEFAULT_TUI_THINKING_WORD, DESIGN_STACKS, DefaultErrorHandler, DefaultLogger, DefaultModelsRegistry, DefaultPathResolver, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultTokenCounter, ERROR_CODES, FetchError, FsError, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, MALFORMED_ARG_MARKERS, MAX_TUI_THINKING_WORD_LENGTH, MEMORY_TYPE_LABELS, PROMPT_CATEGORY_LABELS, ParseError, PluginError, ProviderError, SddError, SessionError, StreamHangError, ToolError, ToolErrorCategory, ToolExecutor, ToolValidationError, WrongStackError, asBlocks, asText, buildRecoveryStrategies, classifyFamily, classifyToolError, computeTaskProgress, createMessage, decryptConfigSecrets, diffRegistry, encryptConfigSecrets, findCriticalPath, formatContextWindowModeList, getContextWindowMode, isAgentError, isBuiltinCategory, isConfigError, isContextWindowModeId, isDesignStack, isFetchError, isFsError, isImageBlock, isParseError, isPluginError, isSddError, isSecretField, isSessionError, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isToolValidationError, isWrongStackError, listContextWindowModes, migratePlaintextSecrets, noOpLogger, normalizeTokenSavingTier, normalizeTuiThinkingWord, resolveContextWindowPolicy, rewriteConfigEncrypted, rotateConfigKeys, toWrongStackError, topologicalSort, validateRegistryManifest };
6033
6266
  //# sourceMappingURL=index.js.map
6034
6267
  //# sourceMappingURL=index.js.map