@wrongstack/core 0.264.0 → 0.267.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/{agent-bridge-D8sa1vtv.d.ts → agent-bridge-STJ3JwwK.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-c9DLkaas.d.ts → agent-subagent-runner-CzPGP3jA.d.ts} +131 -11
  3. package/dist/{brain-O1IdKPaK.d.ts → brain-Cdg77tVN.d.ts} +103 -2
  4. package/dist/{compactor-BBy0rCtB.d.ts → compactor-iMZ84CXq.d.ts} +19 -1
  5. package/dist/{config-Dz2F3H2K.d.ts → config-Du3pYYln.d.ts} +132 -13
  6. package/dist/{context-BGSpZNSE.d.ts → context-dT5Ueund.d.ts} +90 -12
  7. package/dist/coordination/index.d.ts +78 -22
  8. package/dist/coordination/index.js +695 -273
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/{default-config-CXsDvOmP.d.ts → default-config-B0cj-Hry.d.ts} +11 -1
  11. package/dist/defaults/index.d.ts +28 -28
  12. package/dist/defaults/index.js +2327 -965
  13. package/dist/defaults/index.js.map +1 -1
  14. package/dist/execution/index.d.ts +16 -16
  15. package/dist/execution/index.js +1500 -371
  16. package/dist/execution/index.js.map +1 -1
  17. package/dist/execution/prompt-enhancer.d.ts +2 -2
  18. package/dist/execution/prompt-enhancer.js +1 -1
  19. package/dist/execution/prompt-enhancer.js.map +1 -1
  20. package/dist/extension/index.d.ts +6 -6
  21. package/dist/{goal-preamble-DzjFuN3p.d.ts → goal-preamble-SulMTowG.d.ts} +33 -12
  22. package/dist/{goal-store-CxWmCGbH.d.ts → goal-store-CABDwdFE.d.ts} +1 -1
  23. package/dist/{index-CbLSI66_.d.ts → index-Bms0m4oy.d.ts} +5 -5
  24. package/dist/{index-CYIQrXVF.d.ts → index-DtCVWel4.d.ts} +8 -8
  25. package/dist/index-IEuxQd-E.d.ts +82 -0
  26. package/dist/index.d.ts +261 -57
  27. package/dist/index.js +4799 -2212
  28. package/dist/index.js.map +1 -1
  29. package/dist/infrastructure/index.d.ts +6 -6
  30. package/dist/infrastructure/index.js +84 -9
  31. package/dist/infrastructure/index.js.map +1 -1
  32. package/dist/kernel/index.d.ts +9 -9
  33. package/dist/kernel/index.js +1 -1
  34. package/dist/kernel/index.js.map +1 -1
  35. package/dist/{mcp-servers-DC4QRPUI.d.ts → mcp-servers-C2cBTxUR.d.ts} +3 -3
  36. package/dist/models/index.d.ts +5 -5
  37. package/dist/models/index.js +104 -31
  38. package/dist/models/index.js.map +1 -1
  39. package/dist/{models-registry-B_siPxqN.d.ts → models-registry-BqGZNJQ-.d.ts} +1 -1
  40. package/dist/{multi-agent-coordinator-CK5Jdj9K.d.ts → multi-agent-coordinator-B8R43uPz.d.ts} +1 -1
  41. package/dist/{null-fleet-bus-DgvD4SCO.d.ts → null-fleet-bus-CnXa5oTH.d.ts} +14 -9
  42. package/dist/observability/index.d.ts +2 -2
  43. package/dist/{parallel-eternal-engine-bK0JQBR_.d.ts → parallel-eternal-engine-DdNnw9BQ.d.ts} +11 -9
  44. package/dist/{path-resolver-BPEDlN38.d.ts → path-resolver-COIMLCQL.d.ts} +3 -3
  45. package/dist/{permission-4yvGmMRB.d.ts → permission-B75JAi3-.d.ts} +1 -1
  46. package/dist/{permission-policy-C6XpsBOy.d.ts → permission-policy-DlR9eJAM.d.ts} +2 -2
  47. package/dist/{pipeline-CXCeMz8J.d.ts → pipeline-BfD2k1rT.d.ts} +3 -3
  48. package/dist/{plan-templates-BvzRBkJc.d.ts → plan-templates-DSIKCXZN.d.ts} +32 -8
  49. package/dist/provider-model-resolve-BNRsNuJx.d.ts +107 -0
  50. package/dist/{provider-runner-C5aQpDWE.d.ts → provider-runner-CX7iIvox.d.ts} +3 -3
  51. package/dist/{retry-policy-CFhdtRzz.d.ts → retry-policy-BilV1ujH.d.ts} +1 -1
  52. package/dist/sdd/index.d.ts +8 -8
  53. package/dist/sdd/index.js +286 -105
  54. package/dist/sdd/index.js.map +1 -1
  55. package/dist/secret-vault-BAKpgFw_.d.ts +57 -0
  56. package/dist/{secret-vault-CxiVLbt1.d.ts → secret-vault-gkvEZZfE.d.ts} +43 -4
  57. package/dist/security/index.d.ts +6 -68
  58. package/dist/security/index.js +296 -95
  59. package/dist/security/index.js.map +1 -1
  60. package/dist/{selector-gIuhRTkN.d.ts → selector-Bc7eWtT3.d.ts} +1 -1
  61. package/dist/{session-event-bridge-DkvvrpDt.d.ts → session-event-bridge-D-araDEz.d.ts} +1 -1
  62. package/dist/{session-reader-KdfVwkKP.d.ts → session-reader-D7Dapswh.d.ts} +1 -1
  63. package/dist/storage/index.d.ts +112 -15
  64. package/dist/storage/index.js +491 -156
  65. package/dist/storage/index.js.map +1 -1
  66. package/dist/tools/index.d.ts +4 -2
  67. package/dist/tools/index.js.map +1 -1
  68. package/dist/types/index.d.ts +21 -21
  69. package/dist/types/index.js +1523 -450
  70. package/dist/types/index.js.map +1 -1
  71. package/dist/utils/index.d.ts +455 -407
  72. package/dist/utils/index.js +2191 -1203
  73. package/dist/utils/index.js.map +1 -1
  74. package/dist/{wstack-paths-CJjEwPXn.d.ts → wstack-paths-hOpNLmvf.d.ts} +2 -0
  75. package/package.json +1 -1
  76. package/skills/api-design/SKILL.md +1 -1
  77. package/skills/audit-log/SKILL.md +6 -6
  78. package/skills/bug-hunter/SKILL.md +5 -5
  79. package/skills/chimera/SKILL.md +4 -4
  80. package/skills/docker-deploy/SKILL.md +1 -1
  81. package/skills/git-flow/SKILL.md +3 -3
  82. package/skills/multi-agent/SKILL.md +3 -3
  83. package/skills/node-modern/SKILL.md +1 -0
  84. package/skills/observability/SKILL.md +2 -2
  85. package/skills/output-standards/SKILL.md +51 -28
  86. package/skills/refactor-planner/SKILL.md +3 -3
  87. package/skills/security-scanner/SKILL.md +4 -3
  88. package/skills/tech-stack/SKILL.md +1 -2
  89. package/dist/llm-selector-DzxuZnNz.d.ts +0 -58
  90. package/dist/secret-vault-BJDY28ev.d.ts +0 -25
@@ -1,9 +1,9 @@
1
1
  import * as crypto2 from 'crypto';
2
2
  import { randomBytes, createCipheriv, createDecipheriv, randomUUID, createHash } from 'crypto';
3
3
  import * as fsp2 from 'fs/promises';
4
- import * as path3 from 'path';
4
+ import * as path4 from 'path';
5
5
  import { isAbsolute, resolve } from 'path';
6
- import * as fs from 'fs';
6
+ import * as fs4 from 'fs';
7
7
  import * as os from 'os';
8
8
  import { hostname } from 'os';
9
9
  import { execFile } from 'child_process';
@@ -33,9 +33,9 @@ __export(atomic_write_exports, {
33
33
  withFileLock: () => withFileLock
34
34
  });
35
35
  async function atomicWrite(targetPath, content, opts = {}) {
36
- const dir = path3.dirname(targetPath);
36
+ const dir = path4.dirname(targetPath);
37
37
  await fsp2.mkdir(dir, { recursive: true });
38
- const tmp = path3.join(dir, `.${path3.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
38
+ const tmp = path4.join(dir, `.${path4.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
39
39
  try {
40
40
  if (typeof content === "string") {
41
41
  await fsp2.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
@@ -74,9 +74,9 @@ async function ensureDir(dir) {
74
74
  await fsp2.mkdir(dir, { recursive: true });
75
75
  }
76
76
  async function withFileLock(targetPath, fn, opts = {}) {
77
- const dir = path3.dirname(targetPath);
77
+ const dir = path4.dirname(targetPath);
78
78
  await fsp2.mkdir(dir, { recursive: true });
79
- const lockPath = path3.join(dir, `.${path3.basename(targetPath)}.lock`);
79
+ const lockPath = path4.join(dir, `.${path4.basename(targetPath)}.lock`);
80
80
  const timeoutMs = opts.timeoutMs ?? 5e3;
81
81
  const staleMs = opts.staleMs ?? 3e4;
82
82
  const started = Date.now();
@@ -105,7 +105,7 @@ async function withFileLock(targetPath, fn, opts = {}) {
105
105
  if (Date.now() - started >= timeoutMs) {
106
106
  throw new Error(`Timed out waiting for file lock: ${targetPath}`);
107
107
  }
108
- await new Promise((resolve5) => setTimeout(resolve5, 25));
108
+ await new Promise((resolve6) => setTimeout(resolve6, 25));
109
109
  }
110
110
  }
111
111
  try {
@@ -138,7 +138,7 @@ async function renameWithRetry(from, to) {
138
138
  if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
139
139
  throw err;
140
140
  }
141
- await new Promise((resolve5) => setTimeout(resolve5, delays[i]));
141
+ await new Promise((resolve6) => setTimeout(resolve6, delays[i]));
142
142
  }
143
143
  }
144
144
  throw lastErr;
@@ -234,7 +234,7 @@ var DefaultLogger = class _DefaultLogger {
234
234
  this.maxFileBytes = opts.maxFileBytes ?? 10 * 1024 * 1024;
235
235
  if (this.file) {
236
236
  try {
237
- fs.mkdirSync(path3.dirname(this.file), { recursive: true });
237
+ fs4.mkdirSync(path4.dirname(this.file), { recursive: true });
238
238
  } catch {
239
239
  }
240
240
  }
@@ -275,10 +275,10 @@ var DefaultLogger = class _DefaultLogger {
275
275
  maybeRotate(file) {
276
276
  if (this.writesSinceRotateCheck++ % _DefaultLogger.ROTATE_CHECK_EVERY !== 0) return;
277
277
  try {
278
- const st = fs.statSync(file);
278
+ const st = fs4.statSync(file);
279
279
  if (st.size < this.maxFileBytes) return;
280
- fs.rmSync(`${file}.1`, { force: true });
281
- fs.renameSync(file, `${file}.1`);
280
+ fs4.rmSync(`${file}.1`, { force: true });
281
+ fs4.renameSync(file, `${file}.1`);
282
282
  } catch {
283
283
  }
284
284
  }
@@ -294,7 +294,7 @@ var DefaultLogger = class _DefaultLogger {
294
294
  if (this.file) {
295
295
  try {
296
296
  this.maybeRotate(this.file);
297
- fs.appendFileSync(this.file, `${JSON.stringify(entry)}
297
+ fs4.appendFileSync(this.file, `${JSON.stringify(entry)}
298
298
  `);
299
299
  } catch {
300
300
  }
@@ -436,32 +436,268 @@ function isEmptyMessage(msg) {
436
436
  return msg.content.length === 0;
437
437
  }
438
438
 
439
+ // src/utils/assert-never.ts
440
+ function assertNever(x, message) {
441
+ const err = new Error(
442
+ `Unhandled case: ${JSON.stringify(x)}`
443
+ );
444
+ err.name = "AssertNeverError";
445
+ throw err;
446
+ }
447
+
439
448
  // src/utils/index.ts
440
449
  init_atomic_write();
450
+ var MAX_DIGEST_CHARS = 4e3;
451
+ function createContextEvidenceState() {
452
+ return {
453
+ sessionGoals: [],
454
+ implicitFacts: [],
455
+ activeErrors: [],
456
+ toolCalls: [],
457
+ fileGraph: {},
458
+ repeatedReads: [],
459
+ updatedAt: Date.now()
460
+ };
461
+ }
462
+ function buildContextEvidenceDigest(ctx) {
463
+ const state = ensureEvidence(ctx);
464
+ const lines = [];
465
+ if (state.currentIntent?.text) {
466
+ lines.push(`intent: ${state.currentIntent.text}`);
467
+ }
468
+ const goals = state.sessionGoals.slice(-3);
469
+ if (goals.length > 0) {
470
+ lines.push("session_goals:");
471
+ for (const goal of goals) lines.push(`- ${goal}`);
472
+ }
473
+ const activeErrors = state.activeErrors.slice(-5);
474
+ if (activeErrors.length > 0) {
475
+ lines.push("active_errors:");
476
+ for (const err of activeErrors) lines.push(`- ${err}`);
477
+ }
478
+ const files = Object.values(state.fileGraph).sort((a, b) => b.writes - a.writes || b.reads - a.reads || a.path.localeCompare(b.path)).slice(0, 12);
479
+ if (files.length > 0) {
480
+ lines.push("dependency_graph:");
481
+ for (const file of files) {
482
+ const actions = [
483
+ file.reads > 0 ? `read ${file.reads}x` : "",
484
+ file.writes > 0 ? `write ${file.writes}x` : ""
485
+ ].filter(Boolean).join(", ");
486
+ const refs = file.referenced ? "; referenced by assistant" : "";
487
+ const via = file.lastToolUseId ? `; last via ${file.lastToolUseId}` : "";
488
+ lines.push(`- ${file.path} (${actions || "seen"}${refs}${via})`);
489
+ }
490
+ }
491
+ const referenced = state.toolCalls.filter((tool) => tool.status === "referenced").slice(-10);
492
+ const recentSeen = state.toolCalls.filter((tool) => tool.status === "seen").slice(-5);
493
+ const trail = [...referenced, ...recentSeen];
494
+ if (trail.length > 0) {
495
+ lines.push("tool_trail:");
496
+ for (const tool of trail) {
497
+ const size = tool.outputTokens ? `; ~${tool.outputTokens} tokens` : "";
498
+ const filesText = tool.files.length > 0 ? `; files=${tool.files.slice(0, 4).join(", ")}` : "";
499
+ const symbolsText = tool.symbols.length > 0 ? `; symbols=${tool.symbols.slice(0, 4).join(", ")}` : "";
500
+ lines.push(
501
+ `- ${tool.toolUseId} ${tool.toolName} ${tool.status}: ${tool.summary}${filesText}${symbolsText}${size}`
502
+ );
503
+ }
504
+ }
505
+ const facts = state.implicitFacts.slice(-8);
506
+ if (facts.length > 0) {
507
+ lines.push("implicit_facts:");
508
+ for (const fact of facts) lines.push(`- ${fact}`);
509
+ }
510
+ const digest = lines.join("\n");
511
+ if (digest.length <= MAX_DIGEST_CHARS) return digest;
512
+ return `${digest.slice(0, MAX_DIGEST_CHARS)}... [+${digest.length - MAX_DIGEST_CHARS} chars]`;
513
+ }
514
+ function repeatedReadPressure(ctx) {
515
+ return ensureEvidence(ctx).repeatedReads.reduce((max, item) => Math.max(max, item.count), 0);
516
+ }
517
+ function ensureEvidence(ctx) {
518
+ if (!ctx.contextEvidence) {
519
+ ctx.contextEvidence = createContextEvidenceState();
520
+ }
521
+ return ctx.contextEvidence;
522
+ }
523
+
524
+ // src/utils/deep-merge.ts
525
+ var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set([
526
+ "__proto__",
527
+ "constructor",
528
+ "prototype",
529
+ "__defineGetter__",
530
+ "__defineSetter__",
531
+ "__lookupGetter__",
532
+ "__lookupSetter__"
533
+ ]);
534
+ function isPrimitiveArray(a) {
535
+ return a.every((v) => v === null || typeof v !== "object" && typeof v !== "function");
536
+ }
537
+ function deepMerge(base, patch, options = {}) {
538
+ const {
539
+ conflictResolution = "prefer-patch",
540
+ arrayMode = "replace",
541
+ protectProto = true,
542
+ onNonPrimitiveArrayReplace
543
+ } = options;
544
+ if (typeof base !== "object" || base === null) {
545
+ return conflictResolution === "prefer-patch" ? patch : base;
546
+ }
547
+ if (typeof patch !== "object" || patch === null) {
548
+ return conflictResolution === "prefer-patch" ? patch : base;
549
+ }
550
+ if (Array.isArray(base) && Array.isArray(patch)) {
551
+ if (arrayMode === "concat-primitives" && isPrimitiveArray(base) && isPrimitiveArray(patch)) {
552
+ return [.../* @__PURE__ */ new Set([...base, ...patch])];
553
+ }
554
+ return conflictResolution === "prefer-patch" ? patch : base;
555
+ }
556
+ if (Array.isArray(base) || Array.isArray(patch)) {
557
+ return conflictResolution === "prefer-patch" ? patch : base;
558
+ }
559
+ const baseObj = base;
560
+ const patchObj = patch;
561
+ const out = { ...baseObj };
562
+ for (const [k, v] of Object.entries(patchObj)) {
563
+ if (protectProto && FORBIDDEN_PROTO_KEYS.has(k)) continue;
564
+ const existing = out[k];
565
+ if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
566
+ out[k] = deepMerge(existing, v, options);
567
+ } else if (Array.isArray(v) && Array.isArray(existing)) {
568
+ if (onNonPrimitiveArrayReplace && !isPrimitiveArray(v)) {
569
+ onNonPrimitiveArrayReplace(k, existing.length, v.length);
570
+ }
571
+ out[k] = deepMerge(existing, v, options);
572
+ } else if (v !== void 0) {
573
+ if (onNonPrimitiveArrayReplace && Array.isArray(v) && !isPrimitiveArray(v)) {
574
+ const existingLen = Array.isArray(existing) ? existing.length : 0;
575
+ onNonPrimitiveArrayReplace(k, existingLen, v.length);
576
+ }
577
+ out[k] = v;
578
+ }
579
+ }
580
+ return out;
581
+ }
441
582
 
442
583
  // src/utils/error.ts
443
584
  function toErrorMessage(err) {
444
585
  return err instanceof Error ? err.message : String(err);
445
586
  }
446
-
447
- // src/utils/safe-json.ts
448
- function safeParse(input, maxBytes = 5e6) {
449
- if (input.length > maxBytes) {
450
- return { ok: false, error: `Input exceeds limit (${maxBytes} bytes)` };
587
+ var GLOB_CHARS = /* @__PURE__ */ new Set(["*", "?", "["]);
588
+ var IS_WINDOWS = process.platform === "win32";
589
+ var SEP = IS_WINDOWS ? "\\" : "/";
590
+ function isGlob(p) {
591
+ for (const c of p) {
592
+ if (GLOB_CHARS.has(c)) return true;
451
593
  }
452
- try {
453
- return { ok: true, value: JSON.parse(input) };
454
- } catch (err) {
455
- return {
456
- ok: false,
457
- error: toErrorMessage(err)
458
- };
594
+ return false;
595
+ }
596
+ function globToRegex(pat) {
597
+ let i = 0;
598
+ let re = "^";
599
+ while (i < pat.length) {
600
+ const c = expectDefined(pat[i]);
601
+ if (c === "*") {
602
+ if (pat[i + 1] === "*") {
603
+ re += ".*";
604
+ i += 2;
605
+ if (pat[i] === "/") i++;
606
+ } else {
607
+ re += "[^/\\\\]*";
608
+ i++;
609
+ }
610
+ } else if (c === "?") {
611
+ re += "[^/\\\\]";
612
+ i++;
613
+ } else if (c === "[") {
614
+ let cls = "[";
615
+ i++;
616
+ if (pat[i] === "!" || pat[i] === "^") {
617
+ cls += "^";
618
+ i++;
619
+ }
620
+ while (i < pat.length && pat[i] !== "]") {
621
+ const ch = pat[i] ?? "";
622
+ if (ch === "\\") cls += "\\\\";
623
+ else if (ch === "]" || ch === "^") cls += `\\${ch}`;
624
+ else cls += ch;
625
+ i++;
626
+ }
627
+ cls += "]";
628
+ re += cls;
629
+ i++;
630
+ } else {
631
+ re += c.replace(/[.+^${}()|\\]/g, "\\$&");
632
+ i++;
633
+ }
459
634
  }
635
+ return new RegExp(re + "$");
460
636
  }
461
-
462
- // src/utils/string.ts
463
- function truncate(s, max) {
464
- return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
637
+ function baseDir(pat) {
638
+ let i = pat.length - 1;
639
+ while (i >= 0 && !GLOB_CHARS.has(expectDefined(pat[i])) && pat[i] !== SEP && pat[i] !== "/") i--;
640
+ const cut = i >= 0 ? pat.lastIndexOf(SEP, i) : pat.lastIndexOf("/", i);
641
+ return cut < 0 ? "." : pat.slice(0, cut);
642
+ }
643
+ async function expandGlob(pattern) {
644
+ if (!isGlob(pattern)) return [pattern];
645
+ const results = /* @__PURE__ */ new Set();
646
+ const abs = isAbsolute(pattern);
647
+ const base = abs ? baseDir(pattern) : baseDir(pattern);
648
+ const relPat = base === "." ? pattern : pattern.slice(base.length + 1);
649
+ async function walk3(dir, pat) {
650
+ let entries;
651
+ try {
652
+ entries = await fsp2.readdir(dir);
653
+ } catch {
654
+ return;
655
+ }
656
+ const firstGlob = pat.search(/[*?[[]/);
657
+ if (firstGlob < 0) {
658
+ const re = globToRegex(pat);
659
+ for (const e of entries) {
660
+ if (re.test(e)) {
661
+ const full = `${dir}${SEP}${e}`;
662
+ results.add(abs ? resolve(full) : full);
663
+ }
664
+ }
665
+ return;
666
+ }
667
+ const before = pat.slice(0, firstGlob);
668
+ const rest = pat.slice(firstGlob);
669
+ if (before.endsWith("**")) {
670
+ await walk3(dir, rest);
671
+ for (const e of entries) {
672
+ const full = `${dir}${SEP}${e}`;
673
+ try {
674
+ const stat6 = await fsp2.stat(full);
675
+ if (stat6.isDirectory()) await walk3(full, rest);
676
+ } catch {
677
+ }
678
+ }
679
+ } else if (before === "") {
680
+ const re = globToRegex(rest);
681
+ for (const e of entries) {
682
+ if (re.test(e)) {
683
+ const full = `${dir}${SEP}${e}`;
684
+ results.add(abs ? resolve(full) : full);
685
+ }
686
+ }
687
+ } else {
688
+ const seg = before.replace(/[*?[\]]/g, "").replace(/\/$/, "");
689
+ if (entries.includes(seg)) {
690
+ const full = `${dir}${SEP}${seg}`;
691
+ try {
692
+ const stat6 = await fsp2.stat(full);
693
+ if (stat6.isDirectory()) await walk3(full, rest);
694
+ } catch {
695
+ }
696
+ }
697
+ }
698
+ }
699
+ await walk3(base === "." ? "." : base, relPat);
700
+ return [...results];
465
701
  }
466
702
 
467
703
  // src/utils/glob-match.ts
@@ -539,235 +775,441 @@ function matchGlob(pattern, input) {
539
775
  function matchAny(patterns, input) {
540
776
  return patterns.some((p) => matchGlob(p, input));
541
777
  }
542
- function projectHash(absRoot) {
543
- return createHash("sha256").update(path3.resolve(absRoot)).digest("hex").slice(0, 12);
544
- }
545
- function projectSlug(absRoot) {
546
- const base = slugify(path3.basename(absRoot));
547
- const hash = createHash("sha256").update(path3.resolve(absRoot)).digest("hex").slice(0, 6);
548
- return `${base}-${hash}`;
549
- }
550
- function slugify(name) {
551
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
552
- }
553
- function wstackGlobalRoot() {
554
- const fromEnv = process.env["WRONGSTACK_HOME"];
555
- if (fromEnv && fromEnv.trim().length > 0) return path3.resolve(fromEnv);
556
- return path3.join(os.homedir(), ".wrongstack");
557
- }
558
- function resolveWstackPaths(opts) {
559
- const globalRoot = opts.globalRoot ?? (opts.userHome ? path3.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
560
- const hash = projectHash(opts.projectRoot);
561
- const slug = projectSlug(opts.projectRoot);
562
- const projectDir = path3.join(globalRoot, "projects", slug);
563
- return {
564
- globalRoot,
565
- configDir: globalRoot,
566
- globalConfig: path3.join(globalRoot, "config.json"),
567
- secretsKey: path3.join(globalRoot, ".key"),
568
- globalMemory: path3.join(globalRoot, "memory.md"),
569
- globalSkills: path3.join(globalRoot, "skills"),
570
- globalPrompts: path3.join(globalRoot, "prompts"),
571
- cacheDir: path3.join(globalRoot, "cache"),
572
- modelsCache: path3.join(globalRoot, "cache", "models.dev.json"),
573
- modelsOverlayCache: path3.join(globalRoot, "cache", "models-overlay.json"),
574
- historyFile: path3.join(globalRoot, "history"),
575
- logFile: path3.join(globalRoot, "logs", "wrongstack.log"),
576
- projectDir,
577
- projectCodebaseIndex: path3.join(projectDir, "codebase-index"),
578
- projectMemory: path3.join(projectDir, "memory.md"),
579
- projectSessions: path3.join(projectDir, "sessions"),
580
- projectTrust: path3.join(projectDir, "trust.json"),
581
- projectMeta: path3.join(projectDir, "meta.json"),
582
- projectLocalConfig: path3.join(projectDir, "config.local.json"),
583
- inProjectConfig: path3.join(opts.projectRoot, ".wrongstack", "config.json"),
584
- inProjectAgentsFile: path3.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
585
- inProjectSkills: path3.join(opts.projectRoot, ".wrongstack", "skills"),
586
- inProjectWorktrees: path3.join(opts.projectRoot, ".wrongstack", "worktrees"),
587
- projectHash: hash,
588
- projectSlug: slug,
589
- projectGoal: path3.join(projectDir, "goal.json"),
590
- projectSpecs: path3.join(projectDir, "specs"),
591
- projectTaskGraphs: path3.join(projectDir, "task-graphs"),
592
- projectSddSession: path3.join(projectDir, "sdd-session.json"),
593
- projectPlan: path3.join(projectDir, "plan.json"),
594
- projectAutophase: path3.join(projectDir, "autophase"),
595
- syncConfig: path3.join(globalRoot, "sync.json")
596
- };
597
- }
598
-
599
- // src/utils/sleep.ts
600
- function sleep(ms) {
601
- return new Promise((resolve5) => setTimeout(resolve5, ms));
602
- }
603
778
 
604
- // src/utils/assert-never.ts
605
- function assertNever(x, message) {
606
- const err = new Error(
607
- `Unhandled case: ${JSON.stringify(x)}`
608
- );
609
- err.name = "AssertNeverError";
610
- throw err;
779
+ // src/utils/json-repair.ts
780
+ function completePartialObject(s) {
781
+ if (!s.trim().startsWith("{")) return s;
782
+ if (tryParse(s).ok) return s;
783
+ return repairTruncated(s);
611
784
  }
612
-
613
- // src/utils/deep-merge.ts
614
- var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set([
615
- "__proto__",
616
- "constructor",
617
- "prototype",
618
- "__defineGetter__",
619
- "__defineSetter__",
620
- "__lookupGetter__",
621
- "__lookupSetter__"
622
- ]);
623
- function isPrimitiveArray(a) {
624
- return a.every((v) => v === null || typeof v !== "object" && typeof v !== "function");
625
- }
626
- function deepMerge(base, patch, options = {}) {
627
- const {
628
- conflictResolution = "prefer-patch",
629
- arrayMode = "replace",
630
- protectProto = true,
631
- onNonPrimitiveArrayReplace
632
- } = options;
633
- if (typeof base !== "object" || base === null) {
634
- return conflictResolution === "prefer-patch" ? patch : base;
635
- }
636
- if (typeof patch !== "object" || patch === null) {
637
- return conflictResolution === "prefer-patch" ? patch : base;
785
+ function repairTruncated(s) {
786
+ const stack = [];
787
+ let inString = false;
788
+ let escaped = false;
789
+ let sawKey = false;
790
+ let prevSig = "";
791
+ let contentEnd = 0;
792
+ let stringBraceDepth = 0;
793
+ for (let i = 0; i < s.length; i++) {
794
+ const ch = expectDefined(s[i]);
795
+ if (inString) {
796
+ contentEnd = i + 1;
797
+ if (escaped) {
798
+ escaped = false;
799
+ continue;
800
+ }
801
+ if (ch === "\\") {
802
+ escaped = true;
803
+ continue;
804
+ }
805
+ if (ch === '"') {
806
+ inString = false;
807
+ prevSig = '"';
808
+ stringBraceDepth = 0;
809
+ continue;
810
+ }
811
+ if (ch === "{") stringBraceDepth++;
812
+ else if (ch === "}" && stringBraceDepth > 0) stringBraceDepth--;
813
+ continue;
814
+ }
815
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") continue;
816
+ contentEnd = i + 1;
817
+ if (ch === '"') {
818
+ inString = true;
819
+ sawKey = true;
820
+ stringBraceDepth = 0;
821
+ prevSig = '"';
822
+ } else if (ch === "{" || ch === "[") {
823
+ stack.push(ch);
824
+ prevSig = ch;
825
+ } else if (ch === "}" || ch === "]") {
826
+ stack.pop();
827
+ prevSig = ch;
828
+ } else {
829
+ prevSig = ch;
830
+ }
638
831
  }
639
- if (Array.isArray(base) && Array.isArray(patch)) {
640
- if (arrayMode === "concat-primitives" && isPrimitiveArray(base) && isPrimitiveArray(patch)) {
641
- return [.../* @__PURE__ */ new Set([...base, ...patch])];
832
+ if (!sawKey && !inString) return s;
833
+ let result = s.slice(0, contentEnd);
834
+ if (inString) {
835
+ if (escaped) {
836
+ result = result.slice(0, -1);
837
+ } else if (endsWithInvalidEscape(result)) {
838
+ result = result.slice(0, -2);
642
839
  }
643
- return conflictResolution === "prefer-patch" ? patch : base;
840
+ if (stringBraceDepth > 0) result += "}".repeat(stringBraceDepth);
841
+ result += '"';
842
+ } else if (prevSig === ":") {
843
+ result += "null";
644
844
  }
645
- if (Array.isArray(base) || Array.isArray(patch)) {
646
- return conflictResolution === "prefer-patch" ? patch : base;
845
+ for (let k = stack.length - 1; k >= 0; k--) {
846
+ result += stack[k] === "{" ? "}" : "]";
647
847
  }
648
- const baseObj = base;
649
- const patchObj = patch;
650
- const out = { ...baseObj };
651
- for (const [k, v] of Object.entries(patchObj)) {
652
- if (protectProto && FORBIDDEN_PROTO_KEYS.has(k)) continue;
653
- const existing = out[k];
654
- if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
655
- out[k] = deepMerge(existing, v, options);
656
- } else if (Array.isArray(v) && Array.isArray(existing)) {
657
- if (onNonPrimitiveArrayReplace && !isPrimitiveArray(v)) {
658
- onNonPrimitiveArrayReplace(k, existing.length, v.length);
659
- }
660
- out[k] = deepMerge(existing, v, options);
661
- } else if (v !== void 0) {
662
- if (onNonPrimitiveArrayReplace && Array.isArray(v) && !isPrimitiveArray(v)) {
663
- const existingLen = Array.isArray(existing) ? existing.length : 0;
664
- onNonPrimitiveArrayReplace(k, existingLen, v.length);
665
- }
666
- out[k] = v;
667
- }
848
+ if (!tryParse(result).ok) {
849
+ const patched = result.replace(/:(\s*)([}\]])/g, ":null$2");
850
+ if (tryParse(patched).ok) result = patched;
851
+ }
852
+ return result;
853
+ }
854
+ var VALID_ESCAPE = /* @__PURE__ */ new Set(['"', "\\", "/", "b", "f", "n", "r", "t", "u"]);
855
+ function endsWithInvalidEscape(str) {
856
+ const last = str[str.length - 1];
857
+ if (str[str.length - 2] !== "\\" || last === void 0) return false;
858
+ if (VALID_ESCAPE.has(last)) return false;
859
+ let backslashes = 0;
860
+ for (let k = str.length - 2; k >= 0 && str[k] === "\\"; k--) backslashes++;
861
+ return backslashes % 2 === 1;
862
+ }
863
+ function tryParse(s) {
864
+ try {
865
+ return { ok: true, value: JSON.parse(s) };
866
+ } catch {
867
+ return { ok: false };
668
868
  }
669
- return out;
670
869
  }
671
870
 
672
- // src/utils/tool-output-serializer.ts
673
- function createToolOutputSerializer(opts = {}) {
674
- const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
675
- function serialize(value) {
676
- if (typeof value === "string") return value;
677
- if (value === null || value === void 0) return "";
678
- if (typeof value === "object") {
679
- if (Array.isArray(value)) return value.map(serialize).join("\n");
680
- if ("text" in value) {
681
- const t = value.text;
682
- return typeof t === "string" ? t : JSON.stringify(value, null, 2);
683
- }
684
- try {
685
- return JSON.stringify(value, null, 2);
686
- } catch {
687
- return String(value);
688
- }
871
+ // src/utils/json-schema-validate.ts
872
+ function validateAgainstSchema(value, schema) {
873
+ const errors = [];
874
+ walk(value, schema, "", errors);
875
+ return { ok: errors.length === 0, errors };
876
+ }
877
+ function walk(value, schema, path21, errors) {
878
+ if (schema.enum !== void 0) {
879
+ if (!schema.enum.some((e) => deepEqual(e, value))) {
880
+ errors.push({
881
+ path: path21 || "<root>",
882
+ message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
883
+ });
884
+ return;
689
885
  }
690
- return String(value);
691
886
  }
692
- function enforceCap(text, remainingBudget) {
693
- if (remainingBudget <= 0) {
694
- return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
887
+ if (typeof schema.type === "string") {
888
+ if (!checkType(value, schema.type)) {
889
+ errors.push({
890
+ path: path21 || "<root>",
891
+ message: `expected ${schema.type}, got ${describeType(value)}`
892
+ });
893
+ return;
695
894
  }
696
- const textBytes = Buffer.byteLength(text, "utf8");
697
- if (textBytes <= remainingBudget) {
698
- return { text, newBudget: remainingBudget - textBytes };
895
+ }
896
+ if (schema.type === "object" && isPlainObject(value)) {
897
+ const obj = value;
898
+ for (const req of schema.required ?? []) {
899
+ if (!(req in obj)) {
900
+ errors.push({ path: joinPath(path21, req), message: "required property missing" });
901
+ }
699
902
  }
700
- const marker = `
701
- \u2026[truncated ${textBytes - remainingBudget} bytes]\u2026
702
- `;
703
- const markerBytes = Buffer.byteLength(marker, "utf8");
704
- const available = remainingBudget - markerBytes;
705
- if (available <= 0) {
706
- return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
903
+ if (schema.properties) {
904
+ for (const [key, subSchema] of Object.entries(schema.properties)) {
905
+ if (key in obj) {
906
+ walk(obj[key], subSchema, joinPath(path21, key), errors);
907
+ }
908
+ }
707
909
  }
708
- const half = Math.floor(available / 2);
709
- const first = text.slice(0, half);
710
- const second = text.slice(text.length - half);
711
- return { text: `${first}${marker}${second}`, newBudget: 0 };
712
- }
713
- return { serialize, enforceCap, capBytes };
714
- }
715
-
716
- // src/utils/token-estimate.ts
717
- var RoughTokenEstimate = (text, charsPerToken = 3.5) => Math.max(1, Math.ceil(text.length / charsPerToken));
718
- var CALIBRATION_GLOBAL_KEY = "__global__";
719
- var _cals = /* @__PURE__ */ new Map();
720
- function calState(key) {
721
- let state = _cals.get(key);
722
- if (!state) {
723
- state = { ratio: 1, count: 0, prevEst: 0 };
724
- _cals.set(key, state);
725
910
  }
726
- return state;
727
- }
728
- var MIN_SAMPLES_FOR_CALIBRATION = 3;
729
- var ESTIMATE_CACHE = /* @__PURE__ */ new Map();
730
- var ESTIMATE_CACHE_MAX_SIZE = 1e4;
731
- function getCachedEstimate(key, compute) {
732
- const existing = ESTIMATE_CACHE.get(key);
733
- if (existing !== void 0) return existing;
734
- if (ESTIMATE_CACHE.size >= ESTIMATE_CACHE_MAX_SIZE) {
735
- let evicted = 0;
736
- const maxEvict = Math.floor(ESTIMATE_CACHE_MAX_SIZE / 4);
737
- for (const k of ESTIMATE_CACHE.keys()) {
738
- if (evicted >= maxEvict) break;
739
- ESTIMATE_CACHE.delete(k);
740
- evicted++;
911
+ if (schema.type === "array" && Array.isArray(value) && schema.items) {
912
+ for (let i = 0; i < value.length; i++) {
913
+ walk(value[i], schema.items, `${path21}[${i}]`, errors);
741
914
  }
742
915
  }
743
- const estimate = compute(key);
744
- ESTIMATE_CACHE.set(key, estimate);
745
- return estimate;
746
916
  }
747
- function estimateToolInputTokens(input) {
748
- if (typeof input === "string") return RoughTokenEstimate(input);
749
- if (input === null || typeof input !== "object") {
750
- return RoughTokenEstimate(String(input));
917
+ function checkType(value, type) {
918
+ switch (type) {
919
+ case "string":
920
+ return typeof value === "string";
921
+ case "number":
922
+ return typeof value === "number" && !Number.isNaN(value);
923
+ case "integer":
924
+ return typeof value === "number" && Number.isInteger(value);
925
+ case "boolean":
926
+ return typeof value === "boolean";
927
+ case "null":
928
+ return value === null;
929
+ case "array":
930
+ return Array.isArray(value);
931
+ case "object":
932
+ return isPlainObject(value);
933
+ default:
934
+ return true;
751
935
  }
752
- return getCachedEstimate(JSON.stringify(input), (key) => RoughTokenEstimate(key));
753
936
  }
754
- function estimateToolResultTokens(content) {
755
- if (typeof content === "string") return RoughTokenEstimate(content);
756
- return getCachedEstimate(JSON.stringify(content), (key) => RoughTokenEstimate(key));
937
+ function isPlainObject(v) {
938
+ return typeof v === "object" && v !== null && !Array.isArray(v);
757
939
  }
758
- function estimateTextTokens(text) {
759
- return RoughTokenEstimate(text);
940
+ function describeType(v) {
941
+ if (v === null) return "null";
942
+ if (Array.isArray(v)) return "array";
943
+ return typeof v;
760
944
  }
761
- function computeMessageTokens(msg) {
762
- if (typeof msg.content === "string") return estimateTextTokens(msg.content);
763
- let total = 0;
764
- for (const b of msg.content) {
765
- if (b.type === "text") total += estimateTextTokens(b.text);
766
- else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
767
- else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
768
- else total += RoughTokenEstimate(JSON.stringify(b));
769
- }
770
- return total;
945
+ function joinPath(parent, key) {
946
+ if (!parent) return key;
947
+ return `${parent}.${key}`;
948
+ }
949
+ function deepEqual(a, b) {
950
+ if (a === b) return true;
951
+ if (typeof a !== typeof b) return false;
952
+ if (a === null || b === null) return a === b;
953
+ if (Array.isArray(a) && Array.isArray(b)) {
954
+ return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
955
+ }
956
+ if (typeof a === "object" && typeof b === "object") {
957
+ const ak = Object.keys(a);
958
+ const bk = Object.keys(b);
959
+ if (ak.length !== bk.length) return false;
960
+ return ak.every(
961
+ (k) => deepEqual(a[k], b[k])
962
+ );
963
+ }
964
+ return false;
965
+ }
966
+
967
+ // src/utils/merge-models-payload.ts
968
+ function mergeModelsPayload(base, overlay) {
969
+ const out = {};
970
+ for (const [id, provider] of Object.entries(base)) {
971
+ out[id] = cloneProvider(provider);
972
+ }
973
+ for (const [id, ovProvider] of Object.entries(overlay)) {
974
+ const existing = out[id];
975
+ out[id] = existing ? mergeProvider(existing, ovProvider) : cloneProvider(ovProvider);
976
+ }
977
+ return out;
978
+ }
979
+ function mergeProvider(base, overlay) {
980
+ const models = {};
981
+ for (const [mid, m] of Object.entries(base.models ?? {})) {
982
+ models[mid] = { ...m };
983
+ }
984
+ for (const [mid, ovModel] of Object.entries(overlay.models ?? {})) {
985
+ const existing = models[mid];
986
+ models[mid] = existing ? mergeModel(existing, ovModel) : { ...ovModel };
987
+ }
988
+ return {
989
+ ...base,
990
+ // Overlay scalar fields win when explicitly provided; otherwise keep base.
991
+ ...stripUndefined({
992
+ id: overlay.id,
993
+ name: overlay.name,
994
+ npm: overlay.npm,
995
+ api: overlay.api,
996
+ env: overlay.env,
997
+ doc: overlay.doc
998
+ }),
999
+ models
1000
+ };
1001
+ }
1002
+ function mergeModel(base, overlay) {
1003
+ const merged = { ...base, ...overlay };
1004
+ if (base.limit || overlay.limit) {
1005
+ merged.limit = { ...base.limit, ...overlay.limit };
1006
+ }
1007
+ if (base.cost || overlay.cost) {
1008
+ merged.cost = { ...base.cost, ...overlay.cost };
1009
+ }
1010
+ if (base.modalities || overlay.modalities) {
1011
+ merged.modalities = { ...base.modalities, ...overlay.modalities };
1012
+ }
1013
+ return merged;
1014
+ }
1015
+ function cloneProvider(p) {
1016
+ const models = {};
1017
+ for (const [mid, m] of Object.entries(p.models ?? {})) {
1018
+ models[mid] = { ...m };
1019
+ }
1020
+ return { ...p, models };
1021
+ }
1022
+ function stripUndefined(obj) {
1023
+ const out = {};
1024
+ for (const [k, v] of Object.entries(obj)) {
1025
+ if (v !== void 0) out[k] = v;
1026
+ }
1027
+ return out;
1028
+ }
1029
+
1030
+ // src/utils/regex-guard.ts
1031
+ var MAX_PATTERN_LEN = 512;
1032
+ var DANGEROUS_PATTERNS = [
1033
+ /(\([^)]*[+*][^)]*\))[+*]/,
1034
+ // (a+)+, (.*)+, etc
1035
+ /(\(\?:[^)]*[+*][^)]*\))[+*]/
1036
+ // same, with non-capturing group
1037
+ ];
1038
+ function compileUserRegex(pattern, flags) {
1039
+ if (typeof pattern !== "string") {
1040
+ return { ok: false, reason: "pattern must be a string" };
1041
+ }
1042
+ if (pattern.length === 0) {
1043
+ return { ok: false, reason: "pattern is empty" };
1044
+ }
1045
+ if (pattern.length > MAX_PATTERN_LEN) {
1046
+ return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
1047
+ }
1048
+ for (const rx of DANGEROUS_PATTERNS) {
1049
+ if (rx.test(pattern)) {
1050
+ return {
1051
+ ok: false,
1052
+ reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
1053
+ };
1054
+ }
1055
+ }
1056
+ try {
1057
+ return { ok: true, regex: new RegExp(pattern, flags) };
1058
+ } catch (err) {
1059
+ return {
1060
+ ok: false,
1061
+ reason: err instanceof Error ? err.message : "invalid regex"
1062
+ };
1063
+ }
1064
+ }
1065
+
1066
+ // src/utils/safe-json.ts
1067
+ function safeParse(input, maxBytes = 5e6) {
1068
+ if (input.length > maxBytes) {
1069
+ return { ok: false, error: `Input exceeds limit (${maxBytes} bytes)` };
1070
+ }
1071
+ try {
1072
+ return { ok: true, value: JSON.parse(input) };
1073
+ } catch (err) {
1074
+ return {
1075
+ ok: false,
1076
+ error: toErrorMessage(err)
1077
+ };
1078
+ }
1079
+ }
1080
+
1081
+ // src/utils/sleep.ts
1082
+ function sleep(ms) {
1083
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
1084
+ }
1085
+
1086
+ // src/utils/string.ts
1087
+ function truncate(s, max) {
1088
+ return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
1089
+ }
1090
+
1091
+ // src/utils/tool-wire-compact.ts
1092
+ var TOOL_DESCRIPTION_MAX_CHARS = 640;
1093
+ var SCHEMA_DESCRIPTION_MAX_CHARS = 180;
1094
+ var compactCache = /* @__PURE__ */ new WeakMap();
1095
+ function compactToolDefinitionForWire(tool, opts = {}) {
1096
+ const useDefaultOptions = opts.descriptionMaxChars === void 0 && opts.schemaDescriptionMaxChars === void 0;
1097
+ if (useDefaultOptions && typeof tool === "object" && tool !== null) {
1098
+ const cached = compactCache.get(tool);
1099
+ if (cached) return cached;
1100
+ }
1101
+ const compact = {
1102
+ name: tool.name,
1103
+ description: compactDescription(
1104
+ tool.description ?? "",
1105
+ opts.descriptionMaxChars ?? TOOL_DESCRIPTION_MAX_CHARS
1106
+ ),
1107
+ inputSchema: compactSchemaDescriptions(
1108
+ tool.inputSchema,
1109
+ opts.schemaDescriptionMaxChars ?? SCHEMA_DESCRIPTION_MAX_CHARS
1110
+ )
1111
+ };
1112
+ if (useDefaultOptions && typeof tool === "object" && tool !== null) {
1113
+ compactCache.set(tool, compact);
1114
+ }
1115
+ return compact;
1116
+ }
1117
+ function compactSchemaDescriptions(schema, maxDescriptionChars = SCHEMA_DESCRIPTION_MAX_CHARS) {
1118
+ const compact = compactSchemaNode(schema, maxDescriptionChars);
1119
+ return isRecord(compact) ? compact : { type: "object", properties: {} };
1120
+ }
1121
+ function compactSchemaNode(node, maxDescriptionChars) {
1122
+ if (Array.isArray(node)) {
1123
+ return node.map((item) => compactSchemaNode(item, maxDescriptionChars));
1124
+ }
1125
+ if (!isRecord(node)) return node;
1126
+ const out = {};
1127
+ for (const [key, value] of Object.entries(node)) {
1128
+ if (key === "description" && typeof value === "string") {
1129
+ out[key] = compactDescription(value, maxDescriptionChars);
1130
+ } else {
1131
+ out[key] = compactSchemaNode(value, maxDescriptionChars);
1132
+ }
1133
+ }
1134
+ return out;
1135
+ }
1136
+ function compactDescription(text, maxChars) {
1137
+ const normalized = text.replace(/\s+/g, " ").trim();
1138
+ if (normalized.length <= maxChars) return normalized;
1139
+ if (maxChars <= 20) return normalized.slice(0, maxChars);
1140
+ const hardLimit = maxChars - 12;
1141
+ const boundary = findSemanticBoundary(normalized, hardLimit);
1142
+ const head = normalized.slice(0, boundary > 0 ? boundary : hardLimit).trimEnd();
1143
+ return `${head} ...`;
1144
+ }
1145
+ function findSemanticBoundary(text, limit) {
1146
+ const punctuation = Math.max(
1147
+ text.lastIndexOf(". ", limit),
1148
+ text.lastIndexOf("; ", limit),
1149
+ text.lastIndexOf(": ", limit)
1150
+ );
1151
+ if (punctuation >= Math.floor(limit * 0.45)) return punctuation + 1;
1152
+ const comma = text.lastIndexOf(", ", limit);
1153
+ if (comma >= Math.floor(limit * 0.6)) return comma + 1;
1154
+ const space = text.lastIndexOf(" ", limit);
1155
+ return space >= Math.floor(limit * 0.6) ? space : limit;
1156
+ }
1157
+ function isRecord(value) {
1158
+ return !!value && typeof value === "object" && !Array.isArray(value);
1159
+ }
1160
+
1161
+ // src/utils/token-estimate.ts
1162
+ var RoughTokenEstimate = (text, charsPerToken = 3.5) => Math.max(1, Math.ceil(text.length / charsPerToken));
1163
+ var CALIBRATION_GLOBAL_KEY = "__global__";
1164
+ var _cals = /* @__PURE__ */ new Map();
1165
+ function calState(key) {
1166
+ let state = _cals.get(key);
1167
+ if (!state) {
1168
+ state = { ratio: 1, count: 0, prevEst: 0 };
1169
+ _cals.set(key, state);
1170
+ }
1171
+ return state;
1172
+ }
1173
+ var MIN_SAMPLES_FOR_CALIBRATION = 3;
1174
+ var ESTIMATE_CACHE = /* @__PURE__ */ new Map();
1175
+ var ESTIMATE_CACHE_MAX_SIZE = 1e4;
1176
+ function getCachedEstimate(key, compute) {
1177
+ const existing = ESTIMATE_CACHE.get(key);
1178
+ if (existing !== void 0) return existing;
1179
+ if (ESTIMATE_CACHE.size >= ESTIMATE_CACHE_MAX_SIZE) {
1180
+ for (const k of ESTIMATE_CACHE.keys()) {
1181
+ if (ESTIMATE_CACHE.size <= Math.floor(ESTIMATE_CACHE_MAX_SIZE / 2)) break;
1182
+ ESTIMATE_CACHE.delete(k);
1183
+ }
1184
+ }
1185
+ const estimate = compute(key);
1186
+ ESTIMATE_CACHE.set(key, estimate);
1187
+ return estimate;
1188
+ }
1189
+ function estimateToolInputTokens(input) {
1190
+ if (typeof input === "string") return RoughTokenEstimate(input);
1191
+ if (input === null || typeof input !== "object") {
1192
+ return RoughTokenEstimate(String(input));
1193
+ }
1194
+ return getCachedEstimate(JSON.stringify(input), (key) => RoughTokenEstimate(key));
1195
+ }
1196
+ function estimateToolResultTokens(content) {
1197
+ if (typeof content === "string") return RoughTokenEstimate(content);
1198
+ return getCachedEstimate(JSON.stringify(content), (key) => RoughTokenEstimate(key));
1199
+ }
1200
+ function estimateTextTokens(text) {
1201
+ return RoughTokenEstimate(text);
1202
+ }
1203
+ function computeMessageTokens(msg) {
1204
+ if (typeof msg.content === "string") return estimateTextTokens(msg.content);
1205
+ let total = 0;
1206
+ for (const b of msg.content) {
1207
+ if (b.type === "text") total += estimateTextTokens(b.text);
1208
+ else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
1209
+ else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
1210
+ else total += RoughTokenEstimate(JSON.stringify(b));
1211
+ }
1212
+ return total;
771
1213
  }
772
1214
  function estimateMessageTokens(messages) {
773
1215
  let total = 0;
@@ -783,7 +1225,8 @@ function estimateMessageTokens(messages) {
783
1225
  function estimateToolDefTokens(tool) {
784
1226
  const cached = tool._estDefTokens;
785
1227
  if (typeof cached === "number" && cached > 0) return cached;
786
- return RoughTokenEstimate(tool.name) + RoughTokenEstimate(tool.description ?? "") + RoughTokenEstimate(JSON.stringify(tool.inputSchema));
1228
+ const compact = compactToolDefinitionForWire(tool);
1229
+ return RoughTokenEstimate(tool.name) + RoughTokenEstimate(compact.description) + RoughTokenEstimate(JSON.stringify(compact.inputSchema));
787
1230
  }
788
1231
  function estimateRequestTokens(messages, systemPrompt, tools, calibrationKey = CALIBRATION_GLOBAL_KEY) {
789
1232
  let messagesTokens = 0;
@@ -860,406 +1303,702 @@ function estimateRequestTokensCalibrated(messages, systemPrompt, tools, calibrat
860
1303
  return result;
861
1304
  }
862
1305
 
863
- // src/utils/json-schema-validate.ts
864
- function validateAgainstSchema(value, schema) {
865
- const errors = [];
866
- walk(value, schema, "", errors);
867
- return { ok: errors.length === 0, errors };
868
- }
869
- function walk(value, schema, path19, errors) {
870
- if (schema.enum !== void 0) {
871
- if (!schema.enum.some((e) => deepEqual(e, value))) {
872
- errors.push({
873
- path: path19 || "<root>",
874
- message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
875
- });
876
- return;
877
- }
878
- }
879
- if (typeof schema.type === "string") {
880
- if (!checkType(value, schema.type)) {
881
- errors.push({
882
- path: path19 || "<root>",
883
- message: `expected ${schema.type}, got ${describeType(value)}`
884
- });
885
- return;
886
- }
887
- }
888
- if (schema.type === "object" && isPlainObject(value)) {
889
- const obj = value;
890
- for (const req of schema.required ?? []) {
891
- if (!(req in obj)) {
892
- errors.push({ path: joinPath(path19, req), message: "required property missing" });
893
- }
894
- }
895
- if (schema.properties) {
896
- for (const [key, subSchema] of Object.entries(schema.properties)) {
897
- if (key in obj) {
898
- walk(obj[key], subSchema, joinPath(path19, key), errors);
899
- }
900
- }
901
- }
902
- }
903
- if (schema.type === "array" && Array.isArray(value) && schema.items) {
904
- for (let i = 0; i < value.length; i++) {
905
- walk(value[i], schema.items, `${path19}[${i}]`, errors);
906
- }
907
- }
908
- }
909
- function checkType(value, type) {
910
- switch (type) {
911
- case "string":
912
- return typeof value === "string";
913
- case "number":
914
- return typeof value === "number" && !Number.isNaN(value);
915
- case "integer":
916
- return typeof value === "number" && Number.isInteger(value);
917
- case "boolean":
918
- return typeof value === "boolean";
919
- case "null":
920
- return value === null;
921
- case "array":
922
- return Array.isArray(value);
923
- case "object":
924
- return isPlainObject(value);
925
- default:
926
- return true;
927
- }
928
- }
929
- function isPlainObject(v) {
930
- return typeof v === "object" && v !== null && !Array.isArray(v);
931
- }
932
- function describeType(v) {
933
- if (v === null) return "null";
934
- if (Array.isArray(v)) return "array";
935
- return typeof v;
936
- }
937
- function joinPath(parent, key) {
938
- if (!parent) return key;
939
- return `${parent}.${key}`;
940
- }
941
- function deepEqual(a, b) {
942
- if (a === b) return true;
943
- if (typeof a !== typeof b) return false;
944
- if (a === null || b === null) return a === b;
945
- if (Array.isArray(a) && Array.isArray(b)) {
946
- return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
947
- }
948
- if (typeof a === "object" && typeof b === "object") {
949
- const ak = Object.keys(a);
950
- const bk = Object.keys(b);
951
- if (ak.length !== bk.length) return false;
952
- return ak.every(
953
- (k) => deepEqual(a[k], b[k])
954
- );
955
- }
956
- return false;
957
- }
958
-
959
- // src/utils/regex-guard.ts
960
- var MAX_PATTERN_LEN = 512;
961
- var DANGEROUS_PATTERNS = [
962
- /(\([^)]*[+*][^)]*\))[+*]/,
963
- // (a+)+, (.*)+, etc
964
- /(\(\?:[^)]*[+*][^)]*\))[+*]/
965
- // same, with non-capturing group
966
- ];
967
- function compileUserRegex(pattern, flags) {
968
- if (typeof pattern !== "string") {
969
- return { ok: false, reason: "pattern must be a string" };
970
- }
971
- if (pattern.length === 0) {
972
- return { ok: false, reason: "pattern is empty" };
973
- }
974
- if (pattern.length > MAX_PATTERN_LEN) {
975
- return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
976
- }
977
- for (const rx of DANGEROUS_PATTERNS) {
978
- if (rx.test(pattern)) {
979
- return {
980
- ok: false,
981
- reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
982
- };
983
- }
984
- }
985
- try {
986
- return { ok: true, regex: new RegExp(pattern, flags) };
987
- } catch (err) {
988
- return {
989
- ok: false,
990
- reason: err instanceof Error ? err.message : "invalid regex"
991
- };
992
- }
993
- }
994
- var GLOB_CHARS = /* @__PURE__ */ new Set(["*", "?", "["]);
995
- var IS_WINDOWS = process.platform === "win32";
996
- var SEP = IS_WINDOWS ? "\\" : "/";
997
- function isGlob(p) {
998
- for (const c of p) {
999
- if (GLOB_CHARS.has(c)) return true;
1000
- }
1001
- return false;
1002
- }
1003
- function globToRegex(pat) {
1004
- let i = 0;
1005
- let re = "^";
1006
- while (i < pat.length) {
1007
- const c = expectDefined(pat[i]);
1008
- if (c === "*") {
1009
- if (pat[i + 1] === "*") {
1010
- re += ".*";
1011
- i += 2;
1012
- if (pat[i] === "/") i++;
1013
- } else {
1014
- re += "[^/\\\\]*";
1015
- i++;
1016
- }
1017
- } else if (c === "?") {
1018
- re += "[^/\\\\]";
1019
- i++;
1020
- } else if (c === "[") {
1021
- let cls = "[";
1022
- i++;
1023
- if (pat[i] === "!" || pat[i] === "^") {
1024
- cls += "^";
1025
- i++;
1026
- }
1027
- while (i < pat.length && pat[i] !== "]") {
1028
- const ch = pat[i] ?? "";
1029
- if (ch === "\\") cls += "\\\\";
1030
- else if (ch === "]" || ch === "^") cls += `\\${ch}`;
1031
- else cls += ch;
1032
- i++;
1033
- }
1034
- cls += "]";
1035
- re += cls;
1036
- i++;
1037
- } else {
1038
- re += c.replace(/[.+^${}()|\\]/g, "\\$&");
1039
- i++;
1040
- }
1041
- }
1042
- return new RegExp(re + "$");
1043
- }
1044
- function baseDir(pat) {
1045
- let i = pat.length - 1;
1046
- while (i >= 0 && !GLOB_CHARS.has(expectDefined(pat[i])) && pat[i] !== SEP && pat[i] !== "/") i--;
1047
- const cut = i >= 0 ? pat.lastIndexOf(SEP, i) : pat.lastIndexOf("/", i);
1048
- return cut < 0 ? "." : pat.slice(0, cut);
1049
- }
1050
- async function expandGlob(pattern) {
1051
- if (!isGlob(pattern)) return [pattern];
1052
- const results = /* @__PURE__ */ new Set();
1053
- const abs = isAbsolute(pattern);
1054
- const base = abs ? baseDir(pattern) : baseDir(pattern);
1055
- const relPat = base === "." ? pattern : pattern.slice(base.length + 1);
1056
- async function walk3(dir, pat) {
1057
- let entries;
1058
- try {
1059
- entries = await fsp2.readdir(dir);
1060
- } catch {
1061
- return;
1062
- }
1063
- const firstGlob = pat.search(/[*?[[]/);
1064
- if (firstGlob < 0) {
1065
- const re = globToRegex(pat);
1066
- for (const e of entries) {
1067
- if (re.test(e)) {
1068
- const full = `${dir}${SEP}${e}`;
1069
- results.add(abs ? resolve(full) : full);
1070
- }
1071
- }
1072
- return;
1073
- }
1074
- const before = pat.slice(0, firstGlob);
1075
- const rest = pat.slice(firstGlob);
1076
- if (before.endsWith("**")) {
1077
- await walk3(dir, rest);
1078
- for (const e of entries) {
1079
- const full = `${dir}${SEP}${e}`;
1080
- try {
1081
- const stat6 = await fsp2.stat(full);
1082
- if (stat6.isDirectory()) await walk3(full, rest);
1083
- } catch {
1084
- }
1306
+ // src/utils/tool-output-serializer.ts
1307
+ var DEFAULT_LIST_LIMIT = 500;
1308
+ var LOG_ENTRY_LIMIT = 200;
1309
+ var INLINE_LIMIT = 240;
1310
+ var GREP_FILE_LIMIT = 80;
1311
+ var GREP_MATCHES_PER_FILE = 3;
1312
+ var DIFF_INLINE_LINE_LIMIT = 260;
1313
+ var DIFF_HUNK_LIMIT = 8;
1314
+ var DIFF_HUNK_CONTEXT = 14;
1315
+ function createToolOutputSerializer(opts = {}) {
1316
+ const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
1317
+ function serialize(value, context = {}) {
1318
+ if (typeof value === "string") return value;
1319
+ if (value === null || value === void 0) return "";
1320
+ if (typeof value === "object") {
1321
+ if (Array.isArray(value)) return value.map((item) => serialize(item)).join("\n");
1322
+ if (context.toolName) {
1323
+ const compact = renderToolObject(context.toolName, value, context.input);
1324
+ if (compact !== void 0) return compact;
1325
+ return renderGenericToolObject(context.toolName, value);
1085
1326
  }
1086
- } else if (before === "") {
1087
- const re = globToRegex(rest);
1088
- for (const e of entries) {
1089
- if (re.test(e)) {
1090
- const full = `${dir}${SEP}${e}`;
1091
- results.add(abs ? resolve(full) : full);
1092
- }
1327
+ if ("text" in value) {
1328
+ const t = value.text;
1329
+ return typeof t === "string" ? t : JSON.stringify(value, null, 2);
1093
1330
  }
1094
- } else {
1095
- const seg = before.replace(/[*?[\]]/g, "").replace(/\/$/, "");
1096
- if (entries.includes(seg)) {
1097
- const full = `${dir}${SEP}${seg}`;
1098
- try {
1099
- const stat6 = await fsp2.stat(full);
1100
- if (stat6.isDirectory()) await walk3(full, rest);
1101
- } catch {
1102
- }
1331
+ try {
1332
+ return JSON.stringify(value, null, 2);
1333
+ } catch {
1334
+ return String(value);
1103
1335
  }
1104
1336
  }
1337
+ return String(value);
1105
1338
  }
1106
- await walk3(base === "." ? "." : base, relPat);
1107
- return [...results];
1108
- }
1109
-
1110
- // src/utils/json-repair.ts
1111
- function completePartialObject(s) {
1112
- if (!s.trim().startsWith("{")) return s;
1113
- if (tryParse(s).ok) return s;
1114
- return repairTruncated(s);
1115
- }
1116
- function repairTruncated(s) {
1117
- const stack = [];
1118
- let inString = false;
1119
- let escaped = false;
1120
- let sawKey = false;
1121
- let prevSig = "";
1122
- let contentEnd = 0;
1123
- let stringBraceDepth = 0;
1124
- for (let i = 0; i < s.length; i++) {
1125
- const ch = expectDefined(s[i]);
1126
- if (inString) {
1127
- contentEnd = i + 1;
1128
- if (escaped) {
1129
- escaped = false;
1130
- continue;
1131
- }
1132
- if (ch === "\\") {
1133
- escaped = true;
1134
- continue;
1135
- }
1136
- if (ch === '"') {
1137
- inString = false;
1138
- prevSig = '"';
1139
- stringBraceDepth = 0;
1140
- continue;
1141
- }
1142
- if (ch === "{") stringBraceDepth++;
1143
- else if (ch === "}" && stringBraceDepth > 0) stringBraceDepth--;
1144
- continue;
1339
+ function enforceCap(text, remainingBudget) {
1340
+ if (remainingBudget <= 0) {
1341
+ return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
1145
1342
  }
1146
- if (ch === " " || ch === " " || ch === "\n" || ch === "\r") continue;
1147
- contentEnd = i + 1;
1148
- if (ch === '"') {
1149
- inString = true;
1150
- sawKey = true;
1151
- stringBraceDepth = 0;
1152
- prevSig = '"';
1153
- } else if (ch === "{" || ch === "[") {
1154
- stack.push(ch);
1155
- prevSig = ch;
1156
- } else if (ch === "}" || ch === "]") {
1157
- stack.pop();
1158
- prevSig = ch;
1159
- } else {
1160
- prevSig = ch;
1343
+ const textBytes = Buffer.byteLength(text, "utf8");
1344
+ if (textBytes <= remainingBudget) {
1345
+ return { text, newBudget: remainingBudget - textBytes };
1346
+ }
1347
+ const marker = `
1348
+ \u2026[truncated ${textBytes - remainingBudget} bytes]\u2026
1349
+ `;
1350
+ const markerBytes = Buffer.byteLength(marker, "utf8");
1351
+ const available = remainingBudget - markerBytes;
1352
+ if (available <= 0) {
1353
+ return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
1161
1354
  }
1355
+ const half = Math.floor(available / 2);
1356
+ const first = text.slice(0, half);
1357
+ const second = text.slice(text.length - half);
1358
+ return { text: `${first}${marker}${second}`, newBudget: 0 };
1162
1359
  }
1163
- if (!sawKey && !inString) return s;
1164
- let result = s.slice(0, contentEnd);
1165
- if (inString) {
1166
- if (escaped) {
1167
- result = result.slice(0, -1);
1168
- } else if (endsWithInvalidEscape(result)) {
1169
- result = result.slice(0, -2);
1360
+ return { serialize, enforceCap, capBytes };
1361
+ }
1362
+ function renderToolObject(toolName, obj, input) {
1363
+ if (toolName === "read" && typeof obj["text"] === "string") {
1364
+ return joinSections([
1365
+ renderHeader(
1366
+ `read: ${stringFromInput(input, "path") ?? stringField(obj, "path") ?? "<unknown>"}`,
1367
+ {
1368
+ offset: numberFromInput(input, "offset"),
1369
+ limit: numberFromInput(input, "limit"),
1370
+ total_lines: obj["total_lines"],
1371
+ encoding: obj["encoding"],
1372
+ truncated: obj["truncated"],
1373
+ cached: obj["cached"],
1374
+ note: obj["note"]
1375
+ }
1376
+ ),
1377
+ obj["text"]
1378
+ ]);
1379
+ }
1380
+ if (toolName === "grep" && Array.isArray(obj["matches"])) {
1381
+ const matches = stringArrayField(obj, "matches");
1382
+ return joinSections([
1383
+ renderHeader(`grep: ${stringFromInput(input, "pattern") ?? "<pattern>"}`, {
1384
+ path: stringFromInput(input, "path"),
1385
+ glob: stringFromInput(input, "glob"),
1386
+ mode: stringFromInput(input, "output_mode"),
1387
+ count: obj["count"],
1388
+ shown: matches.length,
1389
+ truncated: obj["truncated"],
1390
+ used: obj["used"]
1391
+ }),
1392
+ renderGrepMatches(matches, stringFromInput(input, "output_mode"))
1393
+ ]);
1394
+ }
1395
+ if (toolName === "patch" && Array.isArray(obj["files"])) {
1396
+ const files = stringArrayField(obj, "files");
1397
+ return joinSections([
1398
+ renderHeader("patch", {
1399
+ applied: obj["applied"],
1400
+ rejected: obj["rejected"],
1401
+ files: files.length,
1402
+ dry_run: obj["dry_run"]
1403
+ }),
1404
+ typeof obj["message"] === "string" ? `message:
1405
+ ${obj["message"]}` : void 0,
1406
+ files.length > 0 ? `files:
1407
+ ${renderStringList(files)}` : void 0
1408
+ ]);
1409
+ }
1410
+ if (toolName === "glob" && Array.isArray(obj["files"])) {
1411
+ const files = stringArrayField(obj, "files");
1412
+ return joinSections([
1413
+ renderHeader(
1414
+ `${toolName}: ${stringFromInput(input, "pattern") ?? stringFromInput(input, "files") ?? stringFromInput(input, "path") ?? ""}`.trim(),
1415
+ {
1416
+ path: stringFromInput(input, "path"),
1417
+ files: files.length,
1418
+ truncated: obj["truncated"]
1419
+ }
1420
+ ),
1421
+ renderStringList(files, "(no files)")
1422
+ ]);
1423
+ }
1424
+ if (toolName === "tree" && typeof obj["tree"] === "string") {
1425
+ return joinSections([
1426
+ renderHeader(
1427
+ `tree: ${stringField(obj, "path") ?? stringFromInput(input, "path") ?? "<cwd>"}`,
1428
+ {
1429
+ total_files: obj["total_files"],
1430
+ total_dirs: obj["total_dirs"],
1431
+ truncated: obj["truncated"]
1432
+ }
1433
+ ),
1434
+ obj["tree"]
1435
+ ]);
1436
+ }
1437
+ if (toolName === "fetch" && typeof obj["content"] === "string") {
1438
+ return joinSections([
1439
+ renderHeader(
1440
+ `fetch: ${stringField(obj, "url") ?? stringFromInput(input, "url") ?? "<url>"}`,
1441
+ {
1442
+ status: obj["status"],
1443
+ content_type: obj["content_type"]
1444
+ }
1445
+ ),
1446
+ obj["content"]
1447
+ ]);
1448
+ }
1449
+ if (toolName === "replace" && Array.isArray(obj["results"])) {
1450
+ const results = obj["results"].filter(isRecord2);
1451
+ const sections = [
1452
+ renderHeader("replace", {
1453
+ files_modified: obj["files_modified"],
1454
+ total_replacements: obj["total_replacements"],
1455
+ dry_run: obj["dry_run"]
1456
+ })
1457
+ ];
1458
+ for (const r of results.slice(0, DEFAULT_LIST_LIMIT)) {
1459
+ sections.push(
1460
+ joinSections([
1461
+ renderHeader(`file: ${stringField(r, "path") ?? "<unknown>"}`, {
1462
+ replacements: r["replacements"]
1463
+ }),
1464
+ typeof r["diff"] === "string" ? r["diff"] : void 0
1465
+ ])
1466
+ );
1170
1467
  }
1171
- if (stringBraceDepth > 0) result += "}".repeat(stringBraceDepth);
1172
- result += '"';
1173
- } else if (prevSig === ":") {
1174
- result += "null";
1468
+ if (results.length > DEFAULT_LIST_LIMIT) {
1469
+ sections.push(`[serializer omitted ${results.length - DEFAULT_LIST_LIMIT} result item(s)]`);
1470
+ }
1471
+ return joinSections(sections);
1472
+ }
1473
+ if (typeof obj["diff"] === "string") {
1474
+ const diff = obj["diff"];
1475
+ return joinSections([
1476
+ renderHeader(toolName, {
1477
+ path: obj["path"],
1478
+ replacements: obj["replacements"],
1479
+ bytes_written: obj["bytes_written"],
1480
+ created: obj["created"],
1481
+ note: obj["note"],
1482
+ files: Array.isArray(obj["files"]) ? obj["files"].length : void 0,
1483
+ truncated: obj["truncated"],
1484
+ mode: obj["mode"]
1485
+ }),
1486
+ compactDiff(diff)
1487
+ ]);
1175
1488
  }
1176
- for (let k = stack.length - 1; k >= 0; k--) {
1177
- result += stack[k] === "{" ? "}" : "]";
1489
+ if (toolName === "test" && typeof obj["output"] === "string") {
1490
+ return renderTestOutput(obj, input);
1178
1491
  }
1179
- if (!tryParse(result).ok) {
1180
- const patched = result.replace(/:(\s*)([}\]])/g, ":null$2");
1181
- if (tryParse(patched).ok) result = patched;
1492
+ if ((toolName === "typecheck" || toolName === "lint" || toolName === "format") && typeof obj["output"] === "string") {
1493
+ return renderVerifierOutput(toolName, obj, input);
1182
1494
  }
1183
- return result;
1184
- }
1185
- var VALID_ESCAPE = /* @__PURE__ */ new Set(['"', "\\", "/", "b", "f", "n", "r", "t", "u"]);
1186
- function endsWithInvalidEscape(str) {
1187
- const last = str[str.length - 1];
1188
- if (str[str.length - 2] !== "\\" || last === void 0) return false;
1189
- if (VALID_ESCAPE.has(last)) return false;
1190
- let backslashes = 0;
1191
- for (let k = str.length - 2; k >= 0 && str[k] === "\\"; k--) backslashes++;
1192
- return backslashes % 2 === 1;
1193
- }
1194
- function tryParse(s) {
1195
- try {
1196
- return { ok: true, value: JSON.parse(s) };
1197
- } catch {
1198
- return { ok: false };
1495
+ if (hasCommandOutputShape(obj)) {
1496
+ return renderCommandOutput(toolName, obj, input);
1199
1497
  }
1200
- }
1201
-
1202
- // src/utils/merge-models-payload.ts
1203
- function mergeModelsPayload(base, overlay) {
1204
- const out = {};
1205
- for (const [id, provider] of Object.entries(base)) {
1206
- out[id] = cloneProvider(provider);
1498
+ if (toolName === "json" && typeof obj["formatted"] === "string") {
1499
+ return joinSections([
1500
+ renderHeader("json", {
1501
+ type: obj["type"],
1502
+ keys: Array.isArray(obj["keys"]) ? obj["keys"].length : void 0,
1503
+ query: stringFromInput(input, "query"),
1504
+ error: obj["error"]
1505
+ }),
1506
+ obj["formatted"]
1507
+ ]);
1207
1508
  }
1208
- for (const [id, ovProvider] of Object.entries(overlay)) {
1209
- const existing = out[id];
1210
- out[id] = existing ? mergeProvider(existing, ovProvider) : cloneProvider(ovProvider);
1509
+ if (toolName === "logs" && Array.isArray(obj["entries"])) {
1510
+ const entries = obj["entries"].filter(isRecord2);
1511
+ const lines = entries.slice(0, LOG_ENTRY_LIMIT).map((entry) => {
1512
+ const ts = stringField(entry, "timestamp") ?? "";
1513
+ const level = stringField(entry, "level") ?? "info";
1514
+ const message = stringField(entry, "message") ?? "";
1515
+ const source = stringField(entry, "source");
1516
+ return [ts, level, source, message].filter(Boolean).join(" ");
1517
+ });
1518
+ if (entries.length > LOG_ENTRY_LIMIT) {
1519
+ lines.push(`[serializer omitted ${entries.length - LOG_ENTRY_LIMIT} log entry item(s)]`);
1520
+ }
1521
+ return joinSections([
1522
+ renderHeader(`logs: ${stringField(obj, "source") ?? "<source>"}`, {
1523
+ total: obj["total"],
1524
+ shown: Math.min(entries.length, LOG_ENTRY_LIMIT),
1525
+ truncated: obj["truncated"],
1526
+ stream_mode: obj["stream_mode"]
1527
+ }),
1528
+ lines.length > 0 ? lines.join("\n") : "(no log entries)"
1529
+ ]);
1211
1530
  }
1212
- return out;
1213
- }
1214
- function mergeProvider(base, overlay) {
1215
- const models = {};
1216
- for (const [mid, m] of Object.entries(base.models ?? {})) {
1217
- models[mid] = { ...m };
1531
+ if (toolName === "audit" && Array.isArray(obj["vulnerabilities"])) {
1532
+ const vulns = obj["vulnerabilities"].filter(isRecord2);
1533
+ const lines = vulns.slice(0, DEFAULT_LIST_LIMIT).map((v) => {
1534
+ const severity = stringField(v, "severity") ?? "unknown";
1535
+ const pkg = stringField(v, "package") ?? "<package>";
1536
+ const title = stringField(v, "title") ?? "";
1537
+ const url = stringField(v, "url");
1538
+ return [severity, pkg, title, url].filter(Boolean).join(" | ");
1539
+ });
1540
+ if (vulns.length > DEFAULT_LIST_LIMIT) {
1541
+ lines.push(`[serializer omitted ${vulns.length - DEFAULT_LIST_LIMIT} vulnerability item(s)]`);
1542
+ }
1543
+ return joinSections([
1544
+ renderHeader("audit", {
1545
+ exit_code: obj["exit_code"],
1546
+ total: obj["total"],
1547
+ summary: obj["summary"],
1548
+ truncated: obj["truncated"]
1549
+ }),
1550
+ lines.length > 0 ? lines.join("\n") : stringField(obj, "output")
1551
+ ]);
1218
1552
  }
1219
- for (const [mid, ovModel] of Object.entries(overlay.models ?? {})) {
1220
- const existing = models[mid];
1221
- models[mid] = existing ? mergeModel(existing, ovModel) : { ...ovModel };
1553
+ if (toolName === "outdated" && Array.isArray(obj["packages"])) {
1554
+ const packages = obj["packages"].filter(isRecord2);
1555
+ const lines = packages.slice(0, DEFAULT_LIST_LIMIT).map(
1556
+ (p) => [
1557
+ stringField(p, "name") ?? "<package>",
1558
+ `current=${stringField(p, "current") ?? "unknown"}`,
1559
+ `wanted=${stringField(p, "wanted") ?? "unknown"}`,
1560
+ `latest=${stringField(p, "latest") ?? "unknown"}`,
1561
+ stringField(p, "type")
1562
+ ].filter(Boolean).join(" | ")
1563
+ );
1564
+ if (packages.length > DEFAULT_LIST_LIMIT) {
1565
+ lines.push(`[serializer omitted ${packages.length - DEFAULT_LIST_LIMIT} package item(s)]`);
1566
+ }
1567
+ return joinSections([
1568
+ renderHeader("outdated", {
1569
+ exit_code: obj["exit_code"],
1570
+ total: obj["total"],
1571
+ truncated: obj["truncated"]
1572
+ }),
1573
+ lines.length > 0 ? lines.join("\n") : stringField(obj, "output")
1574
+ ]);
1222
1575
  }
1223
- return {
1224
- ...base,
1225
- // Overlay scalar fields win when explicitly provided; otherwise keep base.
1226
- ...stripUndefined({
1227
- id: overlay.id,
1228
- name: overlay.name,
1229
- npm: overlay.npm,
1230
- api: overlay.api,
1231
- env: overlay.env,
1232
- doc: overlay.doc
1233
- }),
1234
- models
1235
- };
1576
+ return void 0;
1236
1577
  }
1237
- function mergeModel(base, overlay) {
1238
- const merged = { ...base, ...overlay };
1239
- if (base.limit || overlay.limit) {
1240
- merged.limit = { ...base.limit, ...overlay.limit };
1578
+ function renderTestOutput(obj, input) {
1579
+ const exitCode = numberField(obj, "exit_code") ?? 0;
1580
+ const failed = numberField(obj, "failed") ?? 0;
1581
+ const output = stringField(obj, "output") ?? "";
1582
+ const header = renderHeader(`test: ${stringField(obj, "runner") ?? "runner"}`, {
1583
+ exit_code: obj["exit_code"],
1584
+ tests_run: obj["tests_run"],
1585
+ passed: obj["passed"],
1586
+ failed: obj["failed"],
1587
+ duration_ms: obj["duration_ms"],
1588
+ truncated: obj["truncated"],
1589
+ files: inputListSummary(input, "files"),
1590
+ grep: stringFromInput(input, "grep")
1591
+ });
1592
+ if (exitCode === 0 && failed === 0) {
1593
+ return joinSections([
1594
+ header,
1595
+ joinSections([
1596
+ "report:",
1597
+ `status=passed`,
1598
+ `tests_run=${obj["tests_run"] ?? 0}`,
1599
+ `passed=${obj["passed"] ?? 0}`,
1600
+ `failed=${obj["failed"] ?? 0}`,
1601
+ `duration_ms=${obj["duration_ms"] ?? 0}`,
1602
+ extractSpoolNote(output)
1603
+ ])
1604
+ ]);
1241
1605
  }
1242
- if (base.cost || overlay.cost) {
1243
- merged.cost = { ...base.cost, ...overlay.cost };
1606
+ return joinSections([
1607
+ header,
1608
+ `error_context:
1609
+ ${compactFailureOutput(output || "(no runner output)")}`
1610
+ ]);
1611
+ }
1612
+ function renderVerifierOutput(toolName, obj, input) {
1613
+ const exitCode = numberField(obj, "exit_code") ?? 0;
1614
+ const errors = numberField(obj, "errors") ?? 0;
1615
+ const warnings = numberField(obj, "warnings") ?? 0;
1616
+ const output = stringField(obj, "output") ?? "";
1617
+ const changed = numberField(obj, "files_changed") ?? 0;
1618
+ const header = renderHeader(toolName, {
1619
+ exit_code: obj["exit_code"],
1620
+ errors: obj["errors"],
1621
+ warnings: obj["warnings"],
1622
+ files_checked: obj["files_checked"],
1623
+ files_changed: obj["files_changed"],
1624
+ fix_applied: obj["fix_applied"],
1625
+ fixer: obj["fixer"],
1626
+ linter: obj["linter"],
1627
+ project: obj["project"],
1628
+ truncated: obj["truncated"],
1629
+ files: inputListSummary(input, "files"),
1630
+ cwd: stringFromInput(input, "cwd")
1631
+ });
1632
+ if (exitCode === 0 && errors === 0 && (toolName !== "format" || changed === 0)) {
1633
+ return joinSections([
1634
+ header,
1635
+ joinSections([
1636
+ "report:",
1637
+ "status=passed",
1638
+ `errors=${errors}`,
1639
+ `warnings=${warnings}`,
1640
+ toolName === "format" ? `files_changed=${changed}` : void 0,
1641
+ extractSpoolNote(output)
1642
+ ])
1643
+ ]);
1244
1644
  }
1245
- if (base.modalities || overlay.modalities) {
1246
- merged.modalities = { ...base.modalities, ...overlay.modalities };
1645
+ if (exitCode === 0 && toolName === "format") {
1646
+ return joinSections([
1647
+ header,
1648
+ joinSections([
1649
+ "report:",
1650
+ "status=changed",
1651
+ `files_changed=${changed}`,
1652
+ extractSpoolNote(output)
1653
+ ])
1654
+ ]);
1247
1655
  }
1248
- return merged;
1656
+ return joinSections([
1657
+ header,
1658
+ `error_context:
1659
+ ${compactFailureOutput(output || "(no verifier output)")}`
1660
+ ]);
1249
1661
  }
1250
- function cloneProvider(p) {
1251
- const models = {};
1252
- for (const [mid, m] of Object.entries(p.models ?? {})) {
1253
- models[mid] = { ...m };
1662
+ function renderGrepMatches(matches, mode) {
1663
+ if (matches.length === 0) return "(no matches)";
1664
+ if (mode === "files_with_matches") return renderStringList(matches, "(no files)");
1665
+ if (mode === "count") return renderStringList(matches, "(no counts)");
1666
+ const groups = /* @__PURE__ */ new Map();
1667
+ const passthrough = [];
1668
+ for (const match of matches) {
1669
+ const parsed = parseGrepContentLine(match);
1670
+ if (!parsed) {
1671
+ passthrough.push(match);
1672
+ continue;
1673
+ }
1674
+ const list = groups.get(parsed.file) ?? [];
1675
+ list.push(`${parsed.line}:${parsed.text}`);
1676
+ groups.set(parsed.file, list);
1254
1677
  }
1255
- return { ...p, models };
1678
+ if (groups.size === 0) return renderStringList(matches, "(no matches)");
1679
+ const sections = [];
1680
+ let fileIndex = 0;
1681
+ for (const [file, lines] of groups) {
1682
+ fileIndex++;
1683
+ if (fileIndex > GREP_FILE_LIMIT) break;
1684
+ const shown = lines.slice(0, GREP_MATCHES_PER_FILE);
1685
+ sections.push(
1686
+ `${file} (${lines.length} match(es), showing ${shown.length})
1687
+ ${shown.join("\n")}`
1688
+ );
1689
+ }
1690
+ if (groups.size > GREP_FILE_LIMIT) {
1691
+ sections.push(`[serializer omitted ${groups.size - GREP_FILE_LIMIT} file group(s)]`);
1692
+ }
1693
+ if (passthrough.length > 0) {
1694
+ sections.push(`ungrouped:
1695
+ ${renderStringList(passthrough, "", 50)}`);
1696
+ }
1697
+ return sections.join("\n");
1698
+ }
1699
+ function parseGrepContentLine(line) {
1700
+ const match = /^(.+?):(\d+):(.*)$/.exec(line);
1701
+ if (!match?.[1] || !match[2]) return void 0;
1702
+ return { file: match[1], line: match[2], text: match[3] ?? "" };
1703
+ }
1704
+ function compactDiff(diff) {
1705
+ const lines = diff.split(/\r?\n/);
1706
+ if (lines.length <= DIFF_INLINE_LINE_LIMIT) return diff;
1707
+ const fileCount = Math.max(
1708
+ new Set(
1709
+ lines.map(
1710
+ (line) => /^diff --git\s+a\/(.+?)\s+b\//.exec(line)?.[1] ?? /^---\s+(.+)/.exec(line)?.[1]
1711
+ ).filter(Boolean)
1712
+ ).size,
1713
+ 0
1714
+ );
1715
+ const hunks = lines.filter((line) => line.startsWith("@@")).length;
1716
+ const added = lines.filter((line) => line.startsWith("+") && !line.startsWith("+++")).length;
1717
+ const removed = lines.filter((line) => line.startsWith("-") && !line.startsWith("---")).length;
1718
+ const selected = /* @__PURE__ */ new Set();
1719
+ let hunkCount = 0;
1720
+ for (let i = 0; i < lines.length; i++) {
1721
+ const line = lines[i] ?? "";
1722
+ if (line.startsWith("diff --git") || line.startsWith("--- ") || line.startsWith("+++ ")) {
1723
+ selected.add(i);
1724
+ continue;
1725
+ }
1726
+ if (!line.startsWith("@@")) continue;
1727
+ if (hunkCount >= DIFF_HUNK_LIMIT) continue;
1728
+ hunkCount++;
1729
+ for (let j = i; j <= Math.min(lines.length - 1, i + DIFF_HUNK_CONTEXT); j++) {
1730
+ selected.add(j);
1731
+ }
1732
+ }
1733
+ if (selected.size === 0) {
1734
+ return joinSections([
1735
+ renderHeader("diff_summary", {
1736
+ files: fileCount,
1737
+ hunks,
1738
+ added,
1739
+ removed,
1740
+ lines: lines.length
1741
+ }),
1742
+ lines.slice(0, DIFF_INLINE_LINE_LIMIT).join("\n"),
1743
+ `[serializer omitted ${Math.max(0, lines.length - DIFF_INLINE_LINE_LIMIT)} diff line(s)]`
1744
+ ]);
1745
+ }
1746
+ const excerpt = [];
1747
+ let previous = -1;
1748
+ for (const index of [...selected].sort((a, b) => a - b)) {
1749
+ if (index > previous + 1) {
1750
+ const omitted = previous === -1 ? index : index - previous - 1;
1751
+ excerpt.push(`[serializer omitted ${omitted} diff line(s)]`);
1752
+ }
1753
+ excerpt.push(lines[index] ?? "");
1754
+ previous = index;
1755
+ }
1756
+ const trailing = lines.length - previous - 1;
1757
+ if (trailing > 0) excerpt.push(`[serializer omitted ${trailing} trailing diff line(s)]`);
1758
+ return joinSections([
1759
+ renderHeader("diff_summary", {
1760
+ files: fileCount,
1761
+ hunks,
1762
+ shown_hunks: Math.min(hunks, DIFF_HUNK_LIMIT),
1763
+ added,
1764
+ removed,
1765
+ lines: lines.length
1766
+ }),
1767
+ excerpt.join("\n")
1768
+ ]);
1769
+ }
1770
+ function compactFailureOutput(output) {
1771
+ const lines = output.split(/\r?\n/);
1772
+ if (lines.length <= 260) return output.trimEnd();
1773
+ const selected = /* @__PURE__ */ new Set();
1774
+ const marker = /\b(fail|failed|failure|error|exception|assertionerror|expected|received|actual|timeout|stack)\b/i;
1775
+ let markerHits = 0;
1776
+ for (let i = 0; i < lines.length; i++) {
1777
+ if (!marker.test(lines[i] ?? "")) continue;
1778
+ markerHits++;
1779
+ for (let j = Math.max(0, i - 4); j <= Math.min(lines.length - 1, i + 10); j++) {
1780
+ selected.add(j);
1781
+ }
1782
+ }
1783
+ if (markerHits === 0) {
1784
+ return lines.slice(-220).join("\n").trimEnd();
1785
+ }
1786
+ const ordered = [...selected].sort((a, b) => a - b);
1787
+ const out = [];
1788
+ let previous = -1;
1789
+ for (const index of ordered) {
1790
+ if (index > previous + 1) {
1791
+ const omitted = previous === -1 ? index : index - previous - 1;
1792
+ out.push(`[serializer omitted ${omitted} line(s)]`);
1793
+ }
1794
+ out.push(lines[index] ?? "");
1795
+ previous = index;
1796
+ }
1797
+ return out.join("\n").trimEnd();
1798
+ }
1799
+ function extractSpoolNote(output) {
1800
+ return output.split(/\r?\n/).find((line) => line.startsWith("[output truncated") && line.includes("full"));
1801
+ }
1802
+ function hasCommandOutputShape(obj) {
1803
+ return typeof obj["stdout"] === "string" || typeof obj["stderr"] === "string" || typeof obj["output"] === "string" || typeof obj["exitCode"] === "number" || typeof obj["exit_code"] === "number";
1804
+ }
1805
+ function renderCommandOutput(toolName, obj, input) {
1806
+ const command = stringField(obj, "command") ?? stringFromInput(input, "command");
1807
+ const args = stringArrayField(obj, "args");
1808
+ const commandLine = command ? [command, ...args].join(" ") : void 0;
1809
+ const output = stringField(obj, "output");
1810
+ const stdout = stringField(obj, "stdout");
1811
+ const stderr = stringField(obj, "stderr");
1812
+ return joinSections([
1813
+ renderHeader(commandLine ? `${toolName}: ${commandLine}` : toolName, {
1814
+ exit_code: obj["exit_code"] ?? obj["exitCode"],
1815
+ timed_out: obj["timed_out"],
1816
+ pid: obj["pid"],
1817
+ allowed: obj["allowed"],
1818
+ truncated: obj["truncated"],
1819
+ runner: obj["runner"],
1820
+ linter: obj["linter"],
1821
+ fixer: obj["fixer"],
1822
+ project: obj["project"],
1823
+ tests_run: obj["tests_run"],
1824
+ passed: obj["passed"],
1825
+ failed: obj["failed"],
1826
+ duration_ms: obj["duration_ms"],
1827
+ errors: obj["errors"],
1828
+ warnings: obj["warnings"],
1829
+ files_checked: obj["files_checked"],
1830
+ files_changed: obj["files_changed"],
1831
+ fix_applied: obj["fix_applied"]
1832
+ }),
1833
+ stringField(obj, "error") ? `error:
1834
+ ${stringField(obj, "error")}` : void 0,
1835
+ output ? `output:
1836
+ ${output}` : void 0,
1837
+ stdout ? `stdout:
1838
+ ${stdout}` : void 0,
1839
+ stderr ? `stderr:
1840
+ ${stderr}` : void 0
1841
+ ]);
1842
+ }
1843
+ function renderGenericToolObject(toolName, obj) {
1844
+ const scalars = {};
1845
+ const blocks = [];
1846
+ for (const [key, value] of Object.entries(obj)) {
1847
+ if (value === void 0) continue;
1848
+ if (isScalar(value)) {
1849
+ const inline = String(value);
1850
+ if (inline.length <= INLINE_LIMIT && !inline.includes("\n")) {
1851
+ scalars[key] = value;
1852
+ } else {
1853
+ blocks.push(`${key}:
1854
+ ${inline}`);
1855
+ }
1856
+ continue;
1857
+ }
1858
+ if (Array.isArray(value)) {
1859
+ if (value.every((item) => typeof item === "string")) {
1860
+ blocks.push(`${key}:
1861
+ ${renderStringList(value)}`);
1862
+ } else {
1863
+ blocks.push(`${key}:
1864
+ ${renderUnknownList(value)}`);
1865
+ }
1866
+ continue;
1867
+ }
1868
+ blocks.push(`${key}: ${clipInline(oneLineJson(value))}`);
1869
+ }
1870
+ return joinSections([renderHeader(toolName, scalars), ...blocks]);
1256
1871
  }
1257
- function stripUndefined(obj) {
1258
- const out = {};
1259
- for (const [k, v] of Object.entries(obj)) {
1260
- if (v !== void 0) out[k] = v;
1872
+ function renderHeader(label, fields) {
1873
+ const parts = Object.entries(fields).filter(([, value]) => value !== void 0 && value !== null && value !== "").map(([key, value]) => `${key}=${clipInline(formatInlineValue(value))}`);
1874
+ return parts.length > 0 ? `${label} (${parts.join(" ")})` : label;
1875
+ }
1876
+ function renderStringList(items, empty = "", limit = DEFAULT_LIST_LIMIT) {
1877
+ if (items.length === 0) return empty;
1878
+ const shown = items.slice(0, limit);
1879
+ const omitted = items.length - shown.length;
1880
+ return [
1881
+ ...shown,
1882
+ ...omitted > 0 ? [`[serializer omitted ${omitted} item(s); narrow the request for more]`] : []
1883
+ ].join("\n");
1884
+ }
1885
+ function renderUnknownList(items, limit = DEFAULT_LIST_LIMIT) {
1886
+ const shown = items.slice(0, limit).map((item) => clipInline(oneLineJson(item), 1e3));
1887
+ const omitted = items.length - shown.length;
1888
+ if (omitted > 0)
1889
+ shown.push(`[serializer omitted ${omitted} item(s); narrow the request for more]`);
1890
+ return shown.join("\n");
1891
+ }
1892
+ function joinSections(sections) {
1893
+ return sections.map((section) => typeof section === "string" ? section.trimEnd() : void 0).filter((section) => !!section).join("\n");
1894
+ }
1895
+ function formatInlineValue(value) {
1896
+ if (Array.isArray(value)) return `[${value.map(formatInlineValue).join(",")}]`;
1897
+ if (isScalar(value)) return String(value);
1898
+ return oneLineJson(value);
1899
+ }
1900
+ function clipInline(value, max = INLINE_LIMIT) {
1901
+ const compact = value.replace(/\s+/g, " ").trim();
1902
+ return compact.length <= max ? compact : `${compact.slice(0, max - 15)}...(${compact.length} chars)`;
1903
+ }
1904
+ function oneLineJson(value) {
1905
+ try {
1906
+ return JSON.stringify(value);
1907
+ } catch {
1908
+ return String(value);
1261
1909
  }
1262
- return out;
1910
+ }
1911
+ function stringField(obj, key) {
1912
+ const value = obj[key];
1913
+ return typeof value === "string" ? value : void 0;
1914
+ }
1915
+ function numberField(obj, key) {
1916
+ const value = obj[key];
1917
+ return typeof value === "number" ? value : void 0;
1918
+ }
1919
+ function stringArrayField(obj, key) {
1920
+ const value = obj[key];
1921
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
1922
+ }
1923
+ function stringFromInput(input, key) {
1924
+ if (!isRecord2(input)) return void 0;
1925
+ const value = input[key];
1926
+ return typeof value === "string" ? value : void 0;
1927
+ }
1928
+ function numberFromInput(input, key) {
1929
+ if (!isRecord2(input)) return void 0;
1930
+ const value = input[key];
1931
+ return typeof value === "number" ? value : void 0;
1932
+ }
1933
+ function inputListSummary(input, key) {
1934
+ if (!isRecord2(input)) return void 0;
1935
+ const value = input[key];
1936
+ if (typeof value === "string") return value;
1937
+ if (Array.isArray(value)) return value.filter((item) => typeof item === "string").join(",");
1938
+ return void 0;
1939
+ }
1940
+ function isRecord2(value) {
1941
+ return !!value && typeof value === "object" && !Array.isArray(value);
1942
+ }
1943
+ function isScalar(value) {
1944
+ return value === null || ["string", "number", "boolean"].includes(typeof value);
1945
+ }
1946
+ function projectHash(absRoot) {
1947
+ return createHash("sha256").update(path4.resolve(absRoot)).digest("hex").slice(0, 12);
1948
+ }
1949
+ function projectSlug(absRoot) {
1950
+ const base = slugify(path4.basename(absRoot));
1951
+ const hash = createHash("sha256").update(path4.resolve(absRoot)).digest("hex").slice(0, 6);
1952
+ return `${base}-${hash}`;
1953
+ }
1954
+ function slugify(name) {
1955
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
1956
+ }
1957
+ function wstackGlobalRoot() {
1958
+ const fromEnv = process.env["WRONGSTACK_HOME"];
1959
+ if (fromEnv && fromEnv.trim().length > 0) return path4.resolve(fromEnv);
1960
+ return path4.join(os.homedir(), ".wrongstack");
1961
+ }
1962
+ function resolveWstackPaths(opts) {
1963
+ const globalRoot = opts.globalRoot ?? (opts.userHome ? path4.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
1964
+ const hash = projectHash(opts.projectRoot);
1965
+ const slug = projectSlug(opts.projectRoot);
1966
+ const projectDir = path4.join(globalRoot, "projects", slug);
1967
+ return {
1968
+ globalRoot,
1969
+ configDir: globalRoot,
1970
+ globalConfig: path4.join(globalRoot, "config.json"),
1971
+ secretsKey: path4.join(globalRoot, ".key"),
1972
+ globalMemory: path4.join(globalRoot, "memory.md"),
1973
+ globalSkills: path4.join(globalRoot, "skills"),
1974
+ globalPrompts: path4.join(globalRoot, "prompts"),
1975
+ cacheDir: path4.join(globalRoot, "cache"),
1976
+ modelsCache: path4.join(globalRoot, "cache", "models.dev.json"),
1977
+ modelsOverlayCache: path4.join(globalRoot, "cache", "models-overlay.json"),
1978
+ historyFile: path4.join(globalRoot, "history"),
1979
+ logFile: path4.join(globalRoot, "logs", "wrongstack.log"),
1980
+ projectDir,
1981
+ projectCodebaseIndex: path4.join(projectDir, "codebase-index"),
1982
+ projectMemory: path4.join(projectDir, "memory.md"),
1983
+ projectSessions: path4.join(projectDir, "sessions"),
1984
+ projectTrust: path4.join(projectDir, "trust.json"),
1985
+ projectMeta: path4.join(projectDir, "meta.json"),
1986
+ projectLocalConfig: path4.join(projectDir, "config.local.json"),
1987
+ inProjectConfig: path4.join(opts.projectRoot, ".wrongstack", "config.json"),
1988
+ inProjectAgentsFile: path4.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
1989
+ inProjectSkills: path4.join(opts.projectRoot, ".wrongstack", "skills"),
1990
+ inProjectWorktrees: path4.join(opts.projectRoot, ".wrongstack", "worktrees"),
1991
+ projectHash: hash,
1992
+ projectSlug: slug,
1993
+ projectGoal: path4.join(projectDir, "goal.json"),
1994
+ projectSpecs: path4.join(projectDir, "specs"),
1995
+ projectTaskGraphs: path4.join(projectDir, "task-graphs"),
1996
+ projectSddSession: path4.join(projectDir, "sdd-session.json"),
1997
+ projectPlan: path4.join(projectDir, "plan.json"),
1998
+ projectAutophase: path4.join(projectDir, "autophase"),
1999
+ syncConfig: path4.join(globalRoot, "sync.json"),
2000
+ projectStatus: (projectHash2) => path4.join(globalRoot, "projects", projectHash2, "status.json")
2001
+ };
1263
2002
  }
1264
2003
 
1265
2004
  // src/storage/session-store.ts
@@ -1277,11 +2016,34 @@ var DefaultSessionStore = class _DefaultSessionStore {
1277
2016
  dir;
1278
2017
  events;
1279
2018
  secretScrubber;
2019
+ /**
2020
+ * In-memory cache for load() results, keyed by session ID. The cache is
2021
+ * invalidated when the file's mtimeMs or size changes (indicating the
2022
+ * file was written to). This eliminates redundant full-file reads and
2023
+ * JSON parses when the same session is loaded multiple times within the
2024
+ * store's lifetime (e.g., webui session detail views, list() fallbacks).
2025
+ *
2026
+ * Max size is capped to prevent unbounded memory growth in long-running
2027
+ * processes. When the limit is reached, the oldest entry is evicted.
2028
+ */
2029
+ _loadCache = /* @__PURE__ */ new Map();
2030
+ static LOAD_CACHE_MAX_ENTRIES = 50;
1280
2031
  constructor(opts) {
1281
2032
  this.dir = opts.dir;
1282
2033
  this.events = opts.events;
1283
2034
  this.secretScrubber = opts.secretScrubber;
1284
2035
  }
2036
+ /**
2037
+ * Clear the load() cache. Useful for testing or when the caller knows
2038
+ * the file has changed externally (e.g., another process wrote to it).
2039
+ */
2040
+ clearLoadCache(sessionId) {
2041
+ if (sessionId !== void 0) {
2042
+ this._loadCache.delete(sessionId);
2043
+ } else {
2044
+ this._loadCache.clear();
2045
+ }
2046
+ }
1285
2047
  // ── Storage event helpers ───────────────────────────────────────────────────
1286
2048
  emitRead(sessionId, filePath, operation, outcome, durationMs, error) {
1287
2049
  this.events?.emit("storage.read", {
@@ -1318,11 +2080,11 @@ var DefaultSessionStore = class _DefaultSessionStore {
1318
2080
  }
1319
2081
  /** Absolute path to the session index file. */
1320
2082
  get indexFile() {
1321
- return path3.join(this.dir, "_index.jsonl");
2083
+ return path4.join(this.dir, "_index.jsonl");
1322
2084
  }
1323
2085
  /** Join session ID to its absolute path within the store directory. */
1324
2086
  sessionPath(id, ext) {
1325
- return path3.join(this.dir, `${id}${ext}`);
2087
+ return path4.join(this.dir, `${id}${ext}`);
1326
2088
  }
1327
2089
  /**
1328
2090
  * Ensure the directory implied by the session ID exists. When the ID
@@ -1330,7 +2092,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
1330
2092
  * subdirectory so sessions group naturally by day.
1331
2093
  */
1332
2094
  async ensureShardDir(id) {
1333
- const dirPath = path3.dirname(path3.join(this.dir, id));
2095
+ const dirPath = path4.dirname(path4.join(this.dir, id));
1334
2096
  await ensureDir(dirPath);
1335
2097
  return dirPath;
1336
2098
  }
@@ -1338,7 +2100,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
1338
2100
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1339
2101
  const id = meta.id && meta.id.length > 0 ? meta.id : generateSessionId(startedAt, meta.model ?? meta.provider);
1340
2102
  const shardDir = await this.ensureShardDir(id);
1341
- const file = path3.join(shardDir, `${path3.basename(id)}.jsonl`);
2103
+ const file = path4.join(shardDir, `${path4.basename(id)}.jsonl`);
1342
2104
  const t0 = Date.now();
1343
2105
  let handle;
1344
2106
  try {
@@ -1400,7 +2162,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
1400
2162
  // Shard directory (sessions/<date>/) — must match create() so the
1401
2163
  // .summary.json sidecar lands next to the JSONL instead of the
1402
2164
  // sessions root (where summaryFor() would never find it).
1403
- dir: path3.dirname(file),
2165
+ dir: path4.dirname(file),
1404
2166
  filePath: file,
1405
2167
  secretScrubber: this.secretScrubber,
1406
2168
  onClose: (s) => this.appendToIndex(s)
@@ -1424,7 +2186,20 @@ var DefaultSessionStore = class _DefaultSessionStore {
1424
2186
  const t0 = Date.now();
1425
2187
  let outcome = "success";
1426
2188
  let errorMsg;
2189
+ let cacheHit = false;
1427
2190
  try {
2191
+ let stat6;
2192
+ try {
2193
+ const s = await fsp2.stat(file);
2194
+ stat6 = { mtimeMs: s.mtimeMs, size: s.size };
2195
+ } catch (err) {
2196
+ throw err;
2197
+ }
2198
+ const cached = this._loadCache.get(id);
2199
+ if (cached && cached.mtimeMs === stat6.mtimeMs && cached.size === stat6.size) {
2200
+ cacheHit = true;
2201
+ return cached.data;
2202
+ }
1428
2203
  const raw = await fsp2.readFile(file, "utf8");
1429
2204
  const lines = raw.split("\n").filter((l) => l.trim());
1430
2205
  const events = [];
@@ -1440,13 +2215,30 @@ var DefaultSessionStore = class _DefaultSessionStore {
1440
2215
  const meta = this.metaFromEvents(id, events);
1441
2216
  const { messages, usage } = this.replay(events, id);
1442
2217
  const toolCallEnds = extractToolCallEnds(events);
1443
- return { metadata: meta, events, messages, usage, toolCallEnds };
2218
+ const data = { metadata: meta, events, messages, usage, toolCallEnds };
2219
+ if (this._loadCache.size >= _DefaultSessionStore.LOAD_CACHE_MAX_ENTRIES) {
2220
+ const oldest = this._loadCache.keys().next().value;
2221
+ if (oldest !== void 0) {
2222
+ this._loadCache.delete(oldest);
2223
+ }
2224
+ }
2225
+ this._loadCache.set(id, { mtimeMs: stat6.mtimeMs, size: stat6.size, data });
2226
+ return data;
1444
2227
  } catch (err) {
1445
2228
  outcome = "failure";
1446
2229
  errorMsg = toErrorMessage(err);
1447
2230
  throw err;
1448
2231
  } finally {
1449
2232
  this.emitRead(id, file, "load", outcome, Date.now() - t0, errorMsg);
2233
+ if (cacheHit) {
2234
+ this.events?.emit("storage.cache_hit", {
2235
+ sessionId: id,
2236
+ store: "session",
2237
+ filePath: file,
2238
+ operation: "load",
2239
+ durationMs: Date.now() - t0
2240
+ });
2241
+ }
1450
2242
  }
1451
2243
  }
1452
2244
  async list(limit = 20) {
@@ -1594,7 +2386,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
1594
2386
  continue;
1595
2387
  if (entry.isDirectory()) {
1596
2388
  const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
1597
- ids.push(...await this.collectSessionIds(path3.join(dir, entry.name), childPrefix, depth + 1));
2389
+ ids.push(...await this.collectSessionIds(path4.join(dir, entry.name), childPrefix, depth + 1));
1598
2390
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
1599
2391
  if (entry.name === "_index.jsonl") continue;
1600
2392
  const base = entry.name.replace(/\.jsonl$/, "");
@@ -1645,14 +2437,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
1645
2437
  async deleteSession(id) {
1646
2438
  const jsonlPath = this.sessionPath(id, ".jsonl");
1647
2439
  const summaryPath = this.sessionPath(id, ".summary.json");
1648
- const shardDir = path3.dirname(path3.join(this.dir, id));
1649
- const base = path3.basename(id);
1650
- const sessDir = path3.join(shardDir, base);
2440
+ const shardDir = path4.dirname(path4.join(this.dir, id));
2441
+ const base = path4.basename(id);
2442
+ const sessDir = path4.join(shardDir, base);
1651
2443
  const deletions = [
1652
2444
  fsp2.unlink(jsonlPath),
1653
2445
  fsp2.unlink(summaryPath),
1654
- fsp2.unlink(path3.join(shardDir, `${base}.plan.json`)),
1655
- fsp2.unlink(path3.join(shardDir, `${base}.todos.json`))
2446
+ fsp2.unlink(path4.join(shardDir, `${base}.plan.json`)),
2447
+ fsp2.unlink(path4.join(shardDir, `${base}.todos.json`))
1656
2448
  ];
1657
2449
  const results = await Promise.allSettled(deletions);
1658
2450
  for (const r of results) {
@@ -1688,14 +2480,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
1688
2480
  let deleted = 0;
1689
2481
  let activeSessionId = null;
1690
2482
  try {
1691
- const raw = await fsp2.readFile(path3.join(this.dir, "active.json"), "utf8");
2483
+ const raw = await fsp2.readFile(path4.join(this.dir, "active.json"), "utf8");
1692
2484
  const active = JSON.parse(raw);
1693
2485
  activeSessionId = active.sessionId ?? null;
1694
2486
  } catch {
1695
2487
  }
1696
2488
  const isPrunableJsonl = (name) => name.endsWith(".jsonl") && name !== "_index.jsonl" && name !== "_mailbox.jsonl" && !name.endsWith(".replay.jsonl") && !name.endsWith(".audit.jsonl");
1697
2489
  const pruneFile = async (dir, name, prefix) => {
1698
- const jsonlPath = path3.join(dir, name);
2490
+ const jsonlPath = path4.join(dir, name);
1699
2491
  try {
1700
2492
  const stat6 = await fsp2.stat(jsonlPath);
1701
2493
  if (stat6.mtimeMs >= cutoff) return;
@@ -1715,7 +2507,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
1715
2507
  continue;
1716
2508
  }
1717
2509
  if (!entry.isDirectory()) continue;
1718
- const dateDir = path3.join(this.dir, entry.name);
2510
+ const dateDir = path4.join(this.dir, entry.name);
1719
2511
  const files = await fsp2.readdir(dateDir, { withFileTypes: true }).catch(() => []);
1720
2512
  for (const file of files) {
1721
2513
  if (!file.isFile() || !isPrunableJsonl(file.name)) continue;
@@ -1727,7 +2519,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
1727
2519
  }
1728
2520
  for (const entry of entries) {
1729
2521
  if (!entry.isDirectory()) continue;
1730
- const dateDir = path3.join(this.dir, entry.name);
2522
+ const dateDir = path4.join(this.dir, entry.name);
1731
2523
  try {
1732
2524
  const remaining = await fsp2.readdir(dateDir);
1733
2525
  if (remaining.length === 0) {
@@ -1902,7 +2694,7 @@ var FileSessionWriter = class _FileSessionWriter {
1902
2694
  this.meta = meta;
1903
2695
  this.events = events;
1904
2696
  this.resumed = opts.resumed ?? false;
1905
- this.manifestFile = opts.dir ? path3.join(opts.dir, `${path3.basename(id)}.summary.json`) : "";
2697
+ this.manifestFile = opts.dir ? path4.join(opts.dir, `${path4.basename(id)}.summary.json`) : "";
1906
2698
  this.filePath = opts.filePath ?? "";
1907
2699
  this.secretScrubber = opts.secretScrubber;
1908
2700
  this.onCloseCb = opts.onClose;
@@ -2406,7 +3198,7 @@ var QueueStore = class {
2406
3198
  events;
2407
3199
  traceId;
2408
3200
  constructor(opts) {
2409
- this.file = path3.join(opts.dir, "queue.json");
3201
+ this.file = path4.join(opts.dir, "queue.json");
2410
3202
  this.events = opts.events;
2411
3203
  this.traceId = opts.traceId;
2412
3204
  }
@@ -2593,7 +3385,7 @@ var DefaultAttachmentStore = class {
2593
3385
  let data = input.data;
2594
3386
  if (this.spoolDir && bytes >= this.spoolThreshold) {
2595
3387
  await fsp2.mkdir(this.spoolDir, { recursive: true });
2596
- spooledPath = path3.join(this.spoolDir, `${id}.bin`);
3388
+ spooledPath = path4.join(this.spoolDir, `${id}.bin`);
2597
3389
  await atomicWrite(spooledPath, input.data, {
2598
3390
  encoding: input.kind === "image" ? "base64" : "utf8"
2599
3391
  });
@@ -2805,7 +3597,7 @@ var FileMemoryBackend = class {
2805
3597
  }
2806
3598
  async remember(scope, entry, filePath) {
2807
3599
  const file = this.resolveFile(filePath, scope);
2808
- await ensureDir(path3.dirname(file));
3600
+ await ensureDir(path4.dirname(file));
2809
3601
  let existing = "";
2810
3602
  try {
2811
3603
  existing = await fsp2.readFile(file, "utf8");
@@ -3421,9 +4213,9 @@ ${body.trim()}`);
3421
4213
  if (!this.persistBackup || scope === "project-agents") return;
3422
4214
  try {
3423
4215
  const content = await this.backend.readAll(scope, this.files[scope]);
3424
- const { writeFile: writeFile4, mkdir: mkdir6 } = await import('fs/promises');
3425
- await mkdir6(this.backupDir, { recursive: true });
3426
- await writeFile4(`${this.backupDir}/${scope}.md`, content, "utf8");
4216
+ const { writeFile: writeFile5, mkdir: mkdir7 } = await import('fs/promises');
4217
+ await mkdir7(this.backupDir, { recursive: true });
4218
+ await writeFile5(`${this.backupDir}/${scope}.md`, content, "utf8");
3427
4219
  } catch {
3428
4220
  }
3429
4221
  }
@@ -3663,7 +4455,10 @@ function deepFreeze(obj) {
3663
4455
  }
3664
4456
 
3665
4457
  // src/types/secret-vault.ts
3666
- var ENCRYPTED_PREFIX = "enc:v1:";
4458
+ var ENCRYPTED_PREFIX_PATTERN = /^enc:v(\d+):/;
4459
+ function encryptedPrefixForVersion(version) {
4460
+ return `enc:v${version}:`;
4461
+ }
3667
4462
 
3668
4463
  // src/security/secret-vault.ts
3669
4464
  init_atomic_write();
@@ -3672,10 +4467,12 @@ var IV_BYTES = 12;
3672
4467
  var TAG_BYTES = 16;
3673
4468
  var ALGO = "aes-256-gcm";
3674
4469
  var KEY_FILE_MODE = 384;
4470
+ var KEY_FILE_MAGIC = Buffer.from("WSKV", "ascii");
4471
+ var VERSIONED_KEY_FILE_SIZE = KEY_FILE_MAGIC.length + 1 + KEY_BYTES;
3675
4472
  function checkKeyFilePermissions(keyFile) {
3676
4473
  if (process.platform === "win32") return;
3677
4474
  try {
3678
- const stat6 = fs.statSync(keyFile);
4475
+ const stat6 = fs4.statSync(keyFile);
3679
4476
  const actualMode = stat6.mode & 511;
3680
4477
  if (actualMode !== KEY_FILE_MODE) {
3681
4478
  console.warn(JSON.stringify({
@@ -3694,11 +4491,17 @@ function checkKeyFilePermissions(keyFile) {
3694
4491
  var DefaultSecretVault = class {
3695
4492
  keyFile;
3696
4493
  key;
4494
+ _keyVersion = 1;
3697
4495
  constructor(opts) {
3698
4496
  this.keyFile = opts.keyFile;
3699
4497
  }
4498
+ /** Current key version. Starts at 1; incremented by rotateKey(). */
4499
+ get keyVersion() {
4500
+ if (!this.key) this.loadOrCreateKey();
4501
+ return this._keyVersion;
4502
+ }
3700
4503
  isEncrypted(value) {
3701
- return typeof value === "string" && value.startsWith(ENCRYPTED_PREFIX);
4504
+ return typeof value === "string" && ENCRYPTED_PREFIX_PATTERN.test(value);
3702
4505
  }
3703
4506
  encrypt(plaintext) {
3704
4507
  if (this.isEncrypted(plaintext)) return plaintext;
@@ -3707,11 +4510,20 @@ var DefaultSecretVault = class {
3707
4510
  const cipher = createCipheriv(ALGO, key, iv);
3708
4511
  const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
3709
4512
  const tag = cipher.getAuthTag();
3710
- return `${ENCRYPTED_PREFIX}${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
4513
+ const prefix = encryptedPrefixForVersion(this._keyVersion);
4514
+ return `${prefix}${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
3711
4515
  }
3712
4516
  decrypt(value) {
3713
4517
  if (!this.isEncrypted(value)) return value;
3714
- const rest = value.slice(ENCRYPTED_PREFIX.length);
4518
+ const prefixMatch = value.match(ENCRYPTED_PREFIX_PATTERN);
4519
+ if (!prefixMatch) {
4520
+ throw new ConfigError({
4521
+ message: "SecretVault: malformed encrypted value",
4522
+ code: ERROR_CODES.CONFIG_PARSE_FAILED,
4523
+ context: { field: "encrypted_value" }
4524
+ });
4525
+ }
4526
+ const rest = value.slice(prefixMatch[0].length);
3715
4527
  const parts = rest.split(":");
3716
4528
  if (parts.length !== 3) {
3717
4529
  throw new ConfigError({
@@ -3740,42 +4552,104 @@ var DefaultSecretVault = class {
3740
4552
  const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
3741
4553
  return pt.toString("utf8");
3742
4554
  }
4555
+ /**
4556
+ * Generate a new encryption key, write it to disk, and increment the key version.
4557
+ * After rotation, encrypt() emits the new version prefix (e.g. enc:v2:).
4558
+ * The caller must re-encrypt existing config values (see rotateConfigKeys()).
4559
+ */
4560
+ rotateKey() {
4561
+ const oldVersion = this._keyVersion;
4562
+ const newKey = randomBytes(KEY_BYTES);
4563
+ const newVersion = oldVersion + 1;
4564
+ const keyFileBuf = Buffer.alloc(VERSIONED_KEY_FILE_SIZE);
4565
+ KEY_FILE_MAGIC.copy(keyFileBuf, 0);
4566
+ keyFileBuf[KEY_FILE_MAGIC.length] = newVersion;
4567
+ newKey.copy(keyFileBuf, KEY_FILE_MAGIC.length + 1);
4568
+ fs4.mkdirSync(path4.dirname(this.keyFile), { recursive: true });
4569
+ fs4.writeFileSync(this.keyFile, keyFileBuf, { mode: 384 });
4570
+ checkKeyFilePermissions(this.keyFile);
4571
+ this.key = newKey;
4572
+ this._keyVersion = newVersion;
4573
+ return { oldVersion, newVersion };
4574
+ }
3743
4575
  loadOrCreateKey() {
3744
4576
  if (this.key) return this.key;
3745
4577
  try {
3746
- const buf = fs.readFileSync(this.keyFile);
3747
- if (buf.length !== KEY_BYTES) {
3748
- throw new ConfigError({
3749
- message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`,
3750
- code: ERROR_CODES.CONFIG_INVALID,
3751
- context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
3752
- });
4578
+ const buf = fs4.readFileSync(this.keyFile);
4579
+ if (buf.length === KEY_BYTES) {
4580
+ this.key = buf;
4581
+ this._keyVersion = 1;
4582
+ checkKeyFilePermissions(this.keyFile);
4583
+ return this.key;
4584
+ }
4585
+ if (buf.length === VERSIONED_KEY_FILE_SIZE) {
4586
+ const magic = buf.subarray(0, KEY_FILE_MAGIC.length);
4587
+ if (!magic.equals(KEY_FILE_MAGIC)) {
4588
+ throw new ConfigError({
4589
+ message: `SecretVault: key file ${this.keyFile} has invalid magic header`,
4590
+ code: ERROR_CODES.CONFIG_INVALID,
4591
+ context: { keyFile: this.keyFile }
4592
+ });
4593
+ }
4594
+ const version = buf[KEY_FILE_MAGIC.length];
4595
+ const key2 = buf.subarray(KEY_FILE_MAGIC.length + 1);
4596
+ if (key2.length !== KEY_BYTES) {
4597
+ throw new ConfigError({
4598
+ message: `SecretVault: key file ${this.keyFile} has wrong key size (${key2.length} bytes, expected ${KEY_BYTES})`,
4599
+ code: ERROR_CODES.CONFIG_INVALID,
4600
+ context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: key2.length }
4601
+ });
4602
+ }
4603
+ this.key = Buffer.from(key2);
4604
+ this._keyVersion = version;
4605
+ checkKeyFilePermissions(this.keyFile);
4606
+ return this.key;
3753
4607
  }
3754
- this.key = buf;
3755
- checkKeyFilePermissions(this.keyFile);
3756
- return this.key;
4608
+ throw new ConfigError({
4609
+ message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES} for v1 or ${VERSIONED_KEY_FILE_SIZE} for v2+). Remove it manually to generate a new key.`,
4610
+ code: ERROR_CODES.CONFIG_INVALID,
4611
+ context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
4612
+ });
3757
4613
  } catch (err) {
3758
4614
  if (err.code !== "ENOENT") throw err;
3759
4615
  }
3760
- fs.mkdirSync(path3.dirname(this.keyFile), { recursive: true });
4616
+ fs4.mkdirSync(path4.dirname(this.keyFile), { recursive: true });
3761
4617
  const key = randomBytes(KEY_BYTES);
3762
4618
  try {
3763
- fs.writeFileSync(this.keyFile, key, { mode: 384, flag: "wx" });
4619
+ fs4.writeFileSync(this.keyFile, key, { mode: 384, flag: "wx" });
3764
4620
  } catch (err) {
3765
4621
  if (err.code !== "EEXIST") throw err;
3766
- const buf = fs.readFileSync(this.keyFile);
3767
- if (buf.length !== KEY_BYTES) {
3768
- throw new ConfigError({
3769
- message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`,
3770
- code: ERROR_CODES.CONFIG_INVALID,
3771
- context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
3772
- });
4622
+ const buf = fs4.readFileSync(this.keyFile);
4623
+ if (buf.length === KEY_BYTES) {
4624
+ this.key = buf;
4625
+ this._keyVersion = 1;
4626
+ checkKeyFilePermissions(this.keyFile);
4627
+ return this.key;
4628
+ }
4629
+ if (buf.length === VERSIONED_KEY_FILE_SIZE) {
4630
+ const magic = buf.subarray(0, KEY_FILE_MAGIC.length);
4631
+ if (!magic.equals(KEY_FILE_MAGIC)) {
4632
+ throw new ConfigError({
4633
+ message: `SecretVault: key file ${this.keyFile} has invalid magic header`,
4634
+ code: ERROR_CODES.CONFIG_INVALID,
4635
+ context: { keyFile: this.keyFile }
4636
+ });
4637
+ }
4638
+ const version = buf[KEY_FILE_MAGIC.length];
4639
+ const winnerKey = buf.subarray(KEY_FILE_MAGIC.length + 1);
4640
+ this.key = Buffer.from(winnerKey);
4641
+ this._keyVersion = version;
4642
+ checkKeyFilePermissions(this.keyFile);
4643
+ return this.key;
3773
4644
  }
3774
- this.key = buf;
3775
- checkKeyFilePermissions(this.keyFile);
3776
- return this.key;
4645
+ throw new ConfigError({
4646
+ message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES} for v1 or ${VERSIONED_KEY_FILE_SIZE} for v2+). Remove it manually to generate a new key.`,
4647
+ code: ERROR_CODES.CONFIG_INVALID,
4648
+ context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
4649
+ });
3777
4650
  }
3778
4651
  this.key = key;
4652
+ this._keyVersion = 1;
3779
4653
  return key;
3780
4654
  }
3781
4655
  };
@@ -3829,7 +4703,7 @@ async function rewriteConfigEncrypted(configPath, vault, patch) {
3829
4703
  }
3830
4704
  const merged = deepMerge(current, patch ?? {});
3831
4705
  const encrypted = encryptConfigSecrets(merged, vault);
3832
- await fsp2.mkdir(path3.dirname(configPath), { recursive: true });
4706
+ await fsp2.mkdir(path4.dirname(configPath), { recursive: true });
3833
4707
  await atomicWrite(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
3834
4708
  await restrictFilePermissions(configPath);
3835
4709
  }
@@ -3971,7 +4845,8 @@ var DEFAULT_TOOLS_CONFIG = Object.freeze({
3971
4845
  iterationTimeoutMs: 3e5,
3972
4846
  sessionTimeoutMs: 18e5,
3973
4847
  perIterationOutputCapBytes: 1e5,
3974
- autoExtendLimit: true
4848
+ autoExtendLimit: true,
4849
+ restrictToProjectRoot: false
3975
4850
  });
3976
4851
  var DEFAULT_CONTEXT_CONFIG = Object.freeze({
3977
4852
  preserveK: 10,
@@ -4014,7 +4889,8 @@ var BEHAVIOR_DEFAULTS = {
4014
4889
  iterationTimeoutMs: DEFAULT_TOOLS_CONFIG.iterationTimeoutMs,
4015
4890
  sessionTimeoutMs: DEFAULT_TOOLS_CONFIG.sessionTimeoutMs,
4016
4891
  perIterationOutputCapBytes: DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
4017
- autoExtendLimit: DEFAULT_TOOLS_CONFIG.autoExtendLimit
4892
+ autoExtendLimit: DEFAULT_TOOLS_CONFIG.autoExtendLimit,
4893
+ restrictToProjectRoot: DEFAULT_TOOLS_CONFIG.restrictToProjectRoot
4018
4894
  },
4019
4895
  log: { level: "info" },
4020
4896
  features: {
@@ -4457,7 +5333,7 @@ var RecoveryLock = class {
4457
5333
  sessionStore;
4458
5334
  probe;
4459
5335
  constructor(opts) {
4460
- this.file = path3.join(opts.dir, LOCK_FILE);
5336
+ this.file = path4.join(opts.dir, LOCK_FILE);
4461
5337
  this.pid = opts.pid ?? process.pid;
4462
5338
  this.hostname = opts.hostname ?? os.hostname();
4463
5339
  this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
@@ -4518,7 +5394,7 @@ var RecoveryLock = class {
4518
5394
  * null return before calling this.
4519
5395
  */
4520
5396
  async write(sessionId) {
4521
- await ensureDir(path3.dirname(this.file));
5397
+ await ensureDir(path4.dirname(this.file));
4522
5398
  const lock = {
4523
5399
  v: 1,
4524
5400
  sessionId,
@@ -5283,6 +6159,7 @@ async function savePlan(filePath, plan, events) {
5283
6159
  outcome: "success",
5284
6160
  durationMs: Date.now() - t0
5285
6161
  });
6162
+ return true;
5286
6163
  } catch (err) {
5287
6164
  events?.emit("storage.error", {
5288
6165
  sessionId: "~boot~",
@@ -5296,6 +6173,7 @@ async function savePlan(filePath, plan, events) {
5296
6173
  "[plan-store] save failed:",
5297
6174
  toErrorMessage(err)
5298
6175
  );
6176
+ return false;
5299
6177
  }
5300
6178
  }
5301
6179
  function emptyPlan(sessionId, title) {
@@ -5732,6 +6610,27 @@ var PATTERNS = [
5732
6610
  { type: "postgres_uri", regex: /postgres(?:ql)?:\/\/[^\s"'`]+/g },
5733
6611
  { type: "mysql_uri", regex: /mysql:\/\/[^\s"'`]+/g },
5734
6612
  { type: "redis_uri", regex: /redis:\/\/[^\s"'`]+/g },
6613
+ // AI/ML provider keys — modern LLM services with well-known prefixes
6614
+ {
6615
+ type: "huggingface_token",
6616
+ // HuggingFace tokens: hf_ followed by 34 alphanumeric chars
6617
+ regex: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{34}(?![A-Za-z0-9])/g
6618
+ },
6619
+ {
6620
+ type: "replicate_token",
6621
+ // Replicate tokens: r8_ followed by 40+ alphanumeric chars
6622
+ regex: /(?<![A-Za-z0-9])r8_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g
6623
+ },
6624
+ {
6625
+ type: "perplexity_key",
6626
+ // Perplexity API keys: pplx- followed by 40+ alphanumeric chars
6627
+ regex: /(?<![A-Za-z0-9])pplx-[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g
6628
+ },
6629
+ {
6630
+ type: "groq_key",
6631
+ // Groq API keys: gsk_ followed by 40+ alphanumeric chars
6632
+ regex: /(?<![A-Za-z0-9])gsk_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g
6633
+ },
5735
6634
  {
5736
6635
  type: "bearer_token",
5737
6636
  // Anchored with alternation instead of negative lookahead — avoids V8
@@ -5765,6 +6664,10 @@ function hasCredentialAnchors(text) {
5765
6664
  text.includes("xox") || // Slack token (xoxa/xoxb/xoxp/xoxo/xoxs)
5766
6665
  text.includes("Bearer ") || // Bearer token (space suffix reduces false positives)
5767
6666
  text.includes("/bot") || // Telegram bot token (URL path pattern)
6667
+ text.includes("hf_") || // HuggingFace token
6668
+ text.includes("r8_") || // Replicate token
6669
+ text.includes("pplx-") || // Perplexity API key
6670
+ text.includes("gsk_") || // Groq API key
5768
6671
  text.includes("_KEY=") || // High-entropy env vars: API_KEY=, SECRET_KEY=, ...
5769
6672
  text.includes("_TOKEN=") || // ACCESS_TOKEN=, AUTH_TOKEN=, ...
5770
6673
  text.includes("_SECRET=") || // API_SECRET=, CLIENT_SECRET=, ...
@@ -5911,9 +6814,9 @@ function getInputString(input, key) {
5911
6814
  function pathLooksInsideProject(rawPath, projectRoot) {
5912
6815
  if (!projectRoot) return false;
5913
6816
  if (rawPath === "~" || rawPath.startsWith("~/") || rawPath.startsWith("~\\")) return false;
5914
- const resolved = path3.resolve(projectRoot, rawPath);
5915
- const relative2 = path3.relative(projectRoot, resolved);
5916
- return !!relative2 && !relative2.startsWith("..") && !path3.isAbsolute(relative2);
6817
+ const resolved = path4.resolve(projectRoot, rawPath);
6818
+ const relative3 = path4.relative(projectRoot, resolved);
6819
+ return !!relative3 && !relative3.startsWith("..") && !path4.isAbsolute(relative3);
5917
6820
  }
5918
6821
  function tokenizeShell(command) {
5919
6822
  return command.match(/"[^"]*"|'[^']*'|\S+/g)?.map((token) => token.replace(/^['"]|['"]$/g, "")) ?? [];
@@ -5923,7 +6826,7 @@ function pathTokenIsOutsideProject(token, projectRoot) {
5923
6826
  if (token === "/" || token === "~" || token === "." || token === "..") return token !== ".";
5924
6827
  if (token.includes("*")) return true;
5925
6828
  if (token.startsWith("..") || token.includes("../") || token.includes("..\\")) return true;
5926
- if (path3.isAbsolute(token) || token.startsWith("~/")) return !pathLooksInsideProject(token, projectRoot);
6829
+ if (path4.isAbsolute(token) || token.startsWith("~/")) return !pathLooksInsideProject(token, projectRoot);
5927
6830
  return false;
5928
6831
  }
5929
6832
  function hasDangerousDeleteTarget(tokens, start, projectRoot) {
@@ -6318,9 +7221,12 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
6318
7221
  const caps = tool.capabilities ?? [];
6319
7222
  const hasAllowedCap = caps.some((c) => this.allowedCapabilities.includes(c));
6320
7223
  const isMcp = _AutoApprovePermissionPolicy.isMcpTool(tool.name);
6321
- const blocked = tool.permission === "deny" || isMcp || !hasAllowedCap;
7224
+ const dangerousNotAllowed = getDangerousCapabilities(tool).filter(
7225
+ (c) => !this.allowedCapabilities.includes(c)
7226
+ );
7227
+ const blocked = tool.permission === "deny" || isMcp || !hasAllowedCap || dangerousNotAllowed.length > 0;
6322
7228
  if (blocked) {
6323
- const reason = isMcp ? `MCP tool ${tool.name} is not auto-approved for subagents \u2014 ask the leader to allow it explicitly` : tool.permission === "deny" ? "tool default deny" : `tool lacks allowed capability (has: ${caps.join(", ") || "none"}, allowed: ${this.allowedCapabilities.join(", ")})`;
7229
+ const reason = isMcp ? `MCP tool ${tool.name} is not auto-approved for subagents \u2014 ask the leader to allow it explicitly` : tool.permission === "deny" ? "tool default deny" : dangerousNotAllowed.length > 0 ? `tool requires un-granted dangerous capability (needs: ${dangerousNotAllowed.join(", ")}, allowed: ${this.allowedCapabilities.join(", ")})` : `tool lacks allowed capability (has: ${caps.join(", ") || "none"}, allowed: ${this.allowedCapabilities.join(", ")})`;
6324
7230
  return {
6325
7231
  permission: "deny",
6326
7232
  source: "subagent_guard",
@@ -6580,7 +7486,7 @@ var DefaultSkillLoader = class {
6580
7486
  const entries = await fsp2.readdir(dir, { withFileTypes: true });
6581
7487
  for (const e of entries) {
6582
7488
  if (!e.isDirectory()) continue;
6583
- const skillFile = path3.join(dir, e.name, "SKILL.md");
7489
+ const skillFile = path4.join(dir, e.name, "SKILL.md");
6584
7490
  try {
6585
7491
  const raw = await fsp2.readFile(skillFile, "utf8");
6586
7492
  const meta = parseFrontmatter(raw);
@@ -6605,12 +7511,12 @@ var DefaultSkillLoader = class {
6605
7511
  }
6606
7512
  async find(name) {
6607
7513
  const all = await this.list();
6608
- return all.find((s) => s.name === name);
7514
+ const lower = name.toLowerCase();
7515
+ return all.find((s) => s.name.toLowerCase() === lower);
6609
7516
  }
6610
7517
  async manifestText() {
6611
- const skills = await this.list();
6612
- if (skills.length === 0) return "";
6613
7518
  const entries = await this.listEntries();
7519
+ if (entries.length === 0) return "";
6614
7520
  const lines = ["## Available skills"];
6615
7521
  for (const e of entries) {
6616
7522
  const scopeTag = e.scope.length > 0 ? ` \u2014 ${e.scope.slice(0, 3).join(", ")}` : "";
@@ -6624,12 +7530,8 @@ var DefaultSkillLoader = class {
6624
7530
  const skills = await this.list();
6625
7531
  const entries = [];
6626
7532
  for (const s of skills) {
6627
- try {
6628
- const raw = await fsp2.readFile(s.path, "utf8");
6629
- const { trigger, scope } = parseDescription(raw);
6630
- entries.push({ name: s.name, trigger, scope, source: s.source, path: s.path });
6631
- } catch {
6632
- }
7533
+ const { trigger, scope } = parseDescriptionFromText(s.description ?? "");
7534
+ entries.push({ name: s.name, trigger, scope, source: s.source, path: s.path });
6633
7535
  }
6634
7536
  this.entriesCache = entries;
6635
7537
  return entries;
@@ -6640,21 +7542,22 @@ var DefaultSkillLoader = class {
6640
7542
  this.bodyCache.clear();
6641
7543
  }
6642
7544
  async readBody(name) {
6643
- const cached = this.bodyCache.get(name);
7545
+ const key = name.toLowerCase();
7546
+ const cached = this.bodyCache.get(key);
6644
7547
  if (cached !== void 0) return cached;
6645
7548
  const m = await this.find(name);
6646
7549
  if (!m) throw new Error(`Skill "${name}" not found`);
6647
7550
  const body = await fsp2.readFile(m.path, "utf8");
6648
- this.bodyCache.set(name, body);
7551
+ this.bodyCache.set(key, body);
6649
7552
  return body;
6650
7553
  }
6651
7554
  async readSaveBody(name) {
6652
- const key = `save:${name}`;
7555
+ const key = `save:${name.toLowerCase()}`;
6653
7556
  const cached = this.bodyCache.get(key);
6654
7557
  if (cached !== void 0) return cached;
6655
7558
  const m = await this.find(name);
6656
7559
  if (!m) throw new Error(`Skill "${name}" not found`);
6657
- const savePath = path3.join(path3.dirname(m.path), "SKILL.save.md");
7560
+ const savePath = path4.join(path4.dirname(m.path), "SKILL.save.md");
6658
7561
  let result;
6659
7562
  try {
6660
7563
  result = await fsp2.readFile(savePath, "utf8");
@@ -6710,9 +7613,7 @@ function parseFrontmatter(raw) {
6710
7613
  flush();
6711
7614
  return out;
6712
7615
  }
6713
- function parseDescription(raw) {
6714
- const fm = parseFrontmatter(raw);
6715
- const desc = fm.description ?? "";
7616
+ function parseDescriptionFromText(desc) {
6716
7617
  const firstSentenceEnd = desc.indexOf(". ");
6717
7618
  const trigger = firstSentenceEnd !== -1 ? desc.slice(0, firstSentenceEnd + 1).trim() : desc.trim().split("\n")[0] ?? "";
6718
7619
  const scope = [];
@@ -6970,8 +7871,8 @@ async function streamProviderToResponse(provider, req, signal, ctx, events, logg
6970
7871
  });
6971
7872
  await Promise.race([
6972
7873
  drainPromise,
6973
- new Promise((resolve5) => {
6974
- drainTimer = setTimeout(resolve5, STREAM_DRAIN_TIMEOUT_MS);
7874
+ new Promise((resolve6) => {
7875
+ drainTimer = setTimeout(resolve6, STREAM_DRAIN_TIMEOUT_MS);
6975
7876
  })
6976
7877
  ]);
6977
7878
  } finally {
@@ -7077,26 +7978,29 @@ async function runProviderWithRetry(opts) {
7077
7978
  description
7078
7979
  });
7079
7980
  }
7080
- await new Promise((resolve5, reject) => {
7981
+ await new Promise((resolve6, reject) => {
7081
7982
  let settled = false;
7983
+ const cleanup = () => {
7984
+ clearTimeout(t);
7985
+ signal.removeEventListener("abort", onAbort);
7986
+ };
7082
7987
  const onAbort = () => {
7083
7988
  if (settled) return;
7084
7989
  settled = true;
7085
- clearTimeout(t);
7990
+ cleanup();
7086
7991
  reject(new Error("aborted"));
7087
7992
  };
7088
7993
  const t = setTimeout(() => {
7089
7994
  if (settled) return;
7090
7995
  settled = true;
7091
- clearTimeout(t);
7092
- signal.removeEventListener("abort", onAbort);
7093
- resolve5();
7996
+ cleanup();
7997
+ resolve6();
7094
7998
  }, delay);
7095
7999
  if (signal.aborted) {
7096
8000
  onAbort();
7097
8001
  return;
7098
8002
  }
7099
- signal.addEventListener("abort", onAbort, { once: true });
8003
+ signal.addEventListener("abort", onAbort);
7100
8004
  });
7101
8005
  attempt++;
7102
8006
  }
@@ -7116,7 +8020,11 @@ function isTextBlock(b) {
7116
8020
  }
7117
8021
 
7118
8022
  // src/execution/compaction-core.ts
8023
+ function compactionDebugEnabled() {
8024
+ return process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1";
8025
+ }
7119
8026
  function emitCompactionMetrics(event, metrics) {
8027
+ if (!compactionDebugEnabled()) return;
7120
8028
  console.log(
7121
8029
  JSON.stringify({
7122
8030
  level: "debug",
@@ -7151,38 +8059,41 @@ function findPreserveStart(messages, preserveK) {
7151
8059
  preserveStart = i;
7152
8060
  }
7153
8061
  }
7154
- let forwardWalkIterations = 0;
7155
- let forwardWalkInnerIterations = 0;
7156
- for (let i = preserveStart; i < messages.length; i++) {
7157
- forwardWalkIterations++;
7158
- const m = messages[i];
7159
- if (!m || typeof m.content === "string" || !Array.isArray(m.content)) continue;
7160
- const hasToolUse3 = m.content.some((b) => {
7161
- forwardWalkInnerIterations++;
7162
- return b.type === "tool_use";
8062
+ let pairRepairIterations = 0;
8063
+ let pairRepairInnerIterations = 0;
8064
+ while (preserveStart > 0) {
8065
+ pairRepairIterations++;
8066
+ const first = messages[preserveStart];
8067
+ const prev = messages[preserveStart - 1];
8068
+ if (!first || !prev || first.role !== "user" || prev.role !== "assistant") break;
8069
+ if (typeof first.content === "string" || typeof prev.content === "string") break;
8070
+ const resultIds = /* @__PURE__ */ new Set();
8071
+ for (const block of first.content) {
8072
+ pairRepairInnerIterations++;
8073
+ if (block.type === "tool_result") resultIds.add(block.tool_use_id);
8074
+ }
8075
+ if (resultIds.size === 0) break;
8076
+ const hasMatchingUse = prev.content.some((block) => {
8077
+ pairRepairInnerIterations++;
8078
+ return block.type === "tool_use" && resultIds.has(block.id);
7163
8079
  });
7164
- if (hasToolUse3 && i + 1 < messages.length) {
7165
- const next = messages[i + 1];
7166
- if (next && next.role === "user" && typeof next.content !== "string" && Array.isArray(next.content) && next.content.some((b) => {
7167
- forwardWalkInnerIterations++;
7168
- return b.type === "tool_result";
7169
- })) {
7170
- preserveStart = i + 1;
7171
- }
7172
- }
8080
+ if (!hasMatchingUse) break;
8081
+ preserveStart--;
8082
+ }
8083
+ if (compactionDebugEnabled()) {
8084
+ console.log(
8085
+ JSON.stringify({
8086
+ level: "debug",
8087
+ event: "compaction.find_preserve_start.ended",
8088
+ messageCount: messages.length,
8089
+ preserveK,
8090
+ preserveStart,
8091
+ pairRepairIterations,
8092
+ pairRepairInnerIterations,
8093
+ pairRepairInnerPerOuter: pairRepairIterations > 0 ? pairRepairInnerIterations / pairRepairIterations : 0
8094
+ })
8095
+ );
7173
8096
  }
7174
- console.log(
7175
- JSON.stringify({
7176
- level: "debug",
7177
- event: "compaction.find_preserve_start.ended",
7178
- messageCount: messages.length,
7179
- preserveK,
7180
- preserveStart,
7181
- forwardWalkIterations,
7182
- forwardWalkInnerIterations,
7183
- forwardWalkInnerPerOuter: forwardWalkIterations > 0 ? forwardWalkInnerIterations / forwardWalkIterations : 0
7184
- })
7185
- );
7186
8097
  return preserveStart;
7187
8098
  }
7188
8099
  function eliseOldToolResults(messages, opts) {
@@ -7196,7 +8107,8 @@ function eliseOldToolResults(messages, opts) {
7196
8107
  if (!msg || !Array.isArray(msg.content)) continue;
7197
8108
  for (const b of msg.content) {
7198
8109
  fastPathInnerIterations++;
7199
- if (b.type === "tool_result" && estimateToolResultTokens(b.content) >= opts.eliseThreshold) {
8110
+ const oversized = b.type === "tool_result" && estimateToolResultTokens(b.content) >= opts.eliseThreshold || b.type === "tool_use" && estimateToolInputTokens(b.input) >= opts.eliseThreshold;
8111
+ if (oversized) {
7200
8112
  hasOversized = true;
7201
8113
  break;
7202
8114
  }
@@ -7230,6 +8142,13 @@ function eliseOldToolResults(messages, opts) {
7230
8142
  }
7231
8143
  const original = msg.content;
7232
8144
  const newContent = original.map((b) => {
8145
+ if (b.type === "tool_use") {
8146
+ const tokens2 = estimateToolInputTokens(b.input);
8147
+ if (tokens2 < opts.eliseThreshold) return b;
8148
+ const elidedInput = summarizeToolUseInputElision(b, tokens2);
8149
+ saved += Math.max(0, tokens2 - estimateToolInputTokens(elidedInput));
8150
+ return { ...b, input: elidedInput };
8151
+ }
7233
8152
  if (b.type !== "tool_result") return b;
7234
8153
  const tokens = estimateToolResultTokens(b.content);
7235
8154
  if (tokens < opts.eliseThreshold) return b;
@@ -7237,7 +8156,7 @@ function eliseOldToolResults(messages, opts) {
7237
8156
  const elided = {
7238
8157
  type: "tool_result",
7239
8158
  tool_use_id: b.tool_use_id,
7240
- content: `[elided: ~${tokens} tokens]`,
8159
+ content: summarizeToolResultElision(b, tokens),
7241
8160
  is_error: b.is_error
7242
8161
  };
7243
8162
  return elided;
@@ -7249,7 +8168,7 @@ function eliseOldToolResults(messages, opts) {
7249
8168
  changed = true;
7250
8169
  }
7251
8170
  fullPassInnerIterations += original.length;
7252
- if (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1") {
8171
+ if (compactionDebugEnabled()) {
7253
8172
  const ratio = fullPassInnerIterations / fullPassIterations;
7254
8173
  if (ratio > 10) {
7255
8174
  console.error(
@@ -7277,6 +8196,65 @@ function eliseOldToolResults(messages, opts) {
7277
8196
  });
7278
8197
  return { messages: changed ? next : messages, saved, changed };
7279
8198
  }
8199
+ function summarizeToolUseInputElision(block, tokens) {
8200
+ const fields = {};
8201
+ for (const [key, value] of Object.entries(block.input ?? {})) {
8202
+ fields[key] = summarizeToolUseInputValue(value);
8203
+ }
8204
+ return {
8205
+ __elided_tool_input: `~${tokens} tokens; original arguments are in the session log`,
8206
+ tool: block.name,
8207
+ fields
8208
+ };
8209
+ }
8210
+ function summarizeToolUseInputValue(value) {
8211
+ if (value === null || value === void 0) return value;
8212
+ if (typeof value === "number" || typeof value === "boolean") return value;
8213
+ if (typeof value === "string") {
8214
+ const oneLine = value.replace(/\s+/g, " ").trim();
8215
+ return oneLine.length <= 160 ? oneLine : `${oneLine.slice(0, 120)}...(${oneLine.length} chars)`;
8216
+ }
8217
+ if (Array.isArray(value)) {
8218
+ return `[array:${value.length}]`;
8219
+ }
8220
+ if (typeof value === "object") {
8221
+ const keys = Object.keys(value);
8222
+ return `[object:${keys.slice(0, 8).join(",")}${keys.length > 8 ? ",..." : ""}]`;
8223
+ }
8224
+ return String(value);
8225
+ }
8226
+ function summarizeToolResultElision(block, tokens) {
8227
+ const parts = [`elided: ~${tokens} tokens`];
8228
+ if (block.name) parts.push(`tool=${block.name}`);
8229
+ const files = extractPathHints(block.content).slice(0, 5);
8230
+ if (files.length > 0) parts.push(`files=${files.join(", ")}`);
8231
+ const error = firstErrorLine(block.content);
8232
+ if (error) parts.push(`error=${error}`);
8233
+ return `[${parts.join("; ")}]`;
8234
+ }
8235
+ function extractPathHints(content) {
8236
+ const text = typeof content === "string" ? content : JSON.stringify(content);
8237
+ const out = /* @__PURE__ */ new Set();
8238
+ const re = /(?:(?:[A-Za-z]:)?[./\\]?[\w@.-]+(?:[\\/][\w@(). -]+)+\.[A-Za-z0-9]{1,12})/g;
8239
+ for (const match of text.matchAll(re)) {
8240
+ const clean = match[0]?.replace(/\\/g, "/").replace(/^["'`]+|["'`),;:]+$/g, "");
8241
+ if (clean && clean.length <= 220) out.add(clean);
8242
+ if (out.size >= 5) break;
8243
+ }
8244
+ return [...out];
8245
+ }
8246
+ function firstErrorLine(content) {
8247
+ const text = typeof content === "string" ? content : JSON.stringify(content);
8248
+ for (const line of text.split(/\r?\n/)) {
8249
+ if (!/\b(error|exception|failed|failure|fatal|panic|timeout|denied|enoent|eacces|eperm)\b/i.test(
8250
+ line
8251
+ ))
8252
+ continue;
8253
+ const trimmed = line.replace(/\s+/g, " ").trim();
8254
+ if (trimmed) return trimmed.slice(0, 180);
8255
+ }
8256
+ return void 0;
8257
+ }
7280
8258
  function buildLosslessDigest(messages) {
7281
8259
  const lines = [];
7282
8260
  for (const m of messages) {
@@ -7387,15 +8365,15 @@ function buildSmartDigest(messages) {
7387
8365
  lines.push(`[${m.role}]: ${display}${marker}`);
7388
8366
  }
7389
8367
  if (noiseCount > 0) {
7390
- lines.push(`[system]: ${noiseCount} low-importance turn(s) collapsed (repeated failures / pure tool I/O)`);
8368
+ lines.push(
8369
+ `[system]: ${noiseCount} low-importance turn(s) collapsed (repeated failures / pure tool I/O)`
8370
+ );
7391
8371
  }
7392
8372
  return lines.join("\n");
7393
8373
  }
7394
8374
  function countToolBlocks(m) {
7395
8375
  if (typeof m.content === "string") return 0;
7396
- return m.content.filter(
7397
- (b) => b.type === "tool_use" || b.type === "tool_result"
7398
- ).length;
8376
+ return m.content.filter((b) => b.type === "tool_use" || b.type === "tool_result").length;
7399
8377
  }
7400
8378
  function firstSentence(text) {
7401
8379
  const trimmed = text.trim();
@@ -7464,10 +8442,12 @@ var HybridCompactor = class {
7464
8442
  if (elide.changed) ctx.state.replaceMessages(elide.messages);
7465
8443
  if (elide.saved > 0) reductions.push({ phase: "elision", saved: elide.saved });
7466
8444
  let collapsedDigest;
8445
+ let evidenceDigest;
7467
8446
  if (opts.aggressive) {
7468
8447
  const phase2 = this.collapseAncientTurns(ctx, preserveK);
7469
8448
  if (phase2.saved > 0) reductions.push({ phase: "summary", saved: phase2.saved });
7470
8449
  collapsedDigest = phase2.digest;
8450
+ evidenceDigest = phase2.evidenceDigest;
7471
8451
  }
7472
8452
  const repaired = repairToolUseAdjacency(ctx.messages);
7473
8453
  if (repaired.report.changed) {
@@ -7475,6 +8455,11 @@ var HybridCompactor = class {
7475
8455
  }
7476
8456
  const afterTokens = estimateMessages(ctx.messages);
7477
8457
  const afterFull = this.estimateFullRequest(ctx);
8458
+ const quality = checkCompactionQuality(ctx, {
8459
+ collapsedDigest,
8460
+ evidenceDigest,
8461
+ reduced: beforeTokens > afterTokens || beforeFull > afterFull
8462
+ });
7478
8463
  return {
7479
8464
  before: beforeTokens,
7480
8465
  after: afterTokens,
@@ -7482,6 +8467,8 @@ var HybridCompactor = class {
7482
8467
  fullRequestTokensAfter: afterFull,
7483
8468
  reductions,
7484
8469
  collapsedDigest,
8470
+ evidenceDigest,
8471
+ quality,
7485
8472
  repaired: repaired.report.changed ? {
7486
8473
  removedToolUses: repaired.report.removedToolUses,
7487
8474
  removedToolResults: repaired.report.removedToolResults,
@@ -7523,7 +8510,13 @@ var HybridCompactor = class {
7523
8510
  if (boundary <= 0) return { saved: 0 };
7524
8511
  const removed = messages.slice(0, boundary);
7525
8512
  const removedTokens = estimateMessages(removed);
7526
- const digest = this.smart ? buildSmartDigest(removed) || `${removed.length} earlier turns (no textual content; tool I/O omitted \u2014 see session log)` : buildLosslessDigest(removed) || `${removed.length} earlier turns (no textual content; tool I/O omitted \u2014 see session log)`;
8513
+ const historyDigest = this.smart ? buildSmartDigest(removed) || `${removed.length} earlier turns (no textual content; tool I/O omitted; see session log)` : buildLosslessDigest(removed) || `${removed.length} earlier turns (no textual content; tool I/O omitted; see session log)`;
8514
+ const evidenceDigest = buildContextEvidenceDigest(ctx);
8515
+ const digest = evidenceDigest ? `[context_state]
8516
+ ${evidenceDigest}
8517
+
8518
+ [prior_history]
8519
+ ${historyDigest}` : historyDigest;
7527
8520
  const summaryMsg = {
7528
8521
  role: "system",
7529
8522
  content: `[prior_turns_digest: ${digest}]`
@@ -7532,10 +8525,29 @@ var HybridCompactor = class {
7532
8525
  ctx.state.replaceMessages([summaryMsg, ...tail]);
7533
8526
  return {
7534
8527
  saved: Math.max(0, removedTokens - estimateMessages([summaryMsg])),
7535
- digest
8528
+ digest,
8529
+ evidenceDigest: evidenceDigest || void 0
7536
8530
  };
7537
8531
  }
7538
8532
  };
8533
+ function checkCompactionQuality(ctx, opts) {
8534
+ const evidence = ctx.contextEvidence;
8535
+ const digest = `${opts.collapsedDigest ?? ""}
8536
+ ${opts.evidenceDigest ?? ""}`;
8537
+ const hasIntent = Boolean(evidence?.currentIntent?.text || /\b(intent|goal|session_goals)\b/i.test(digest));
8538
+ const hasPathTrail = Boolean(
8539
+ Object.keys(evidence?.fileGraph ?? {}).length > 0 || (evidence?.toolCalls.length ?? 0) > 0 || /\b(dependency_graph|tool_trail|files=)\b/i.test(digest)
8540
+ );
8541
+ const issues = [];
8542
+ if (opts.reduced && !hasIntent) issues.push("missing intent anchor");
8543
+ if (opts.reduced && !hasPathTrail) issues.push("missing tool/path trail");
8544
+ return {
8545
+ ok: issues.length === 0,
8546
+ hasIntent,
8547
+ hasPathTrail,
8548
+ issues
8549
+ };
8550
+ }
7539
8551
  function readContextWindowPolicy(ctx) {
7540
8552
  const policy = ctx.meta?.["contextWindowPolicy"];
7541
8553
  if (!policy || typeof policy !== "object") return null;
@@ -7656,7 +8668,12 @@ var IntelligentCompactor = class {
7656
8668
  };
7657
8669
  const ac = ctx.signal ? void 0 : new AbortController();
7658
8670
  const signal = ctx.signal ?? ac?.signal;
7659
- const res = await this.provider.complete(req, { signal });
8671
+ let res;
8672
+ try {
8673
+ res = await this.provider.complete(req, { signal });
8674
+ } finally {
8675
+ ac?.abort();
8676
+ }
7660
8677
  const textBlocks = res.content.filter(isTextBlock);
7661
8678
  return textBlocks.map((b) => b.text).join("\n").trim() || "(empty summary)";
7662
8679
  }
@@ -7700,9 +8717,9 @@ Rules:
7700
8717
  - If unsure, keep rather than collapse (errors are more costly than waste)
7701
8718
 
7702
8719
  Return ONLY the JSON object, no markdown, no explanation outside the JSON.`;
7703
- function formatMessages(messages, maxChars = 8e3) {
8720
+ function formatMessages(messages, maxTokens = 2048) {
7704
8721
  const lines = [];
7705
- let used = 0;
8722
+ let usedTokens = 0;
7706
8723
  for (let i = 0; i < messages.length; i++) {
7707
8724
  const m = expectDefined(messages[i]);
7708
8725
  const role = m.role.padEnd(10, " ");
@@ -7714,13 +8731,14 @@ function formatMessages(messages, maxChars = 8e3) {
7714
8731
  text = content.filter(isTextBlock).map((b) => b.text).join(" ");
7715
8732
  const toolUses = content.filter((b) => b.type === "tool_use");
7716
8733
  if (toolUses.length > 0) {
7717
- text += ` [tools: ${toolUses.map((b) => b.name).join(", ")}]`;
8734
+ text += ` [tools: ${toolUses.map((b) => b.name).filter(Boolean).join(", ")}]`;
7718
8735
  }
7719
8736
  }
7720
8737
  const line = `[${i}][${role}]: ${text}`;
7721
- if (used + line.length > maxChars) break;
8738
+ const lineTokens = estimateTextTokens(line);
8739
+ if (usedTokens + lineTokens > maxTokens) break;
7722
8740
  lines.push(line);
7723
- used += line.length;
8741
+ usedTokens += lineTokens;
7724
8742
  }
7725
8743
  return lines.join("\n");
7726
8744
  }
@@ -7729,20 +8747,29 @@ var LLMSelector = class {
7729
8747
  model;
7730
8748
  maxContextTokens;
7731
8749
  systemPrompt;
8750
+ maxOutputTokens;
7732
8751
  constructor(opts) {
7733
8752
  this.provider = opts.provider;
7734
8753
  this.model = opts.model ?? "unknown";
8754
+ if (this.model === "unknown" && (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1")) {
8755
+ console.warn(
8756
+ "[LLMSelector] model not set \u2014 selector will use the provider default. Set `model` explicitly in LLMSelectorOptions to silence this warning."
8757
+ );
8758
+ }
7735
8759
  this.maxContextTokens = opts.maxContextTokens ?? 4e4;
7736
8760
  this.systemPrompt = opts.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
8761
+ this.maxOutputTokens = opts.maxOutputTokens ?? 1024;
7737
8762
  }
7738
8763
  async select(messages, maxToKeep) {
7739
8764
  const effectiveBudget = Math.min(maxToKeep, this.maxContextTokens);
7740
- const historyText = formatMessages(messages);
7741
8765
  const totalTokens = estimateMessageTokens(messages);
7742
8766
  const systemText = `${this.systemPrompt}
7743
8767
 
7744
8768
  Conversation (${messages.length} messages, ~${totalTokens} tokens, budget: ${effectiveBudget}):
7745
8769
  `;
8770
+ const systemTokens = estimateTextTokens(systemText);
8771
+ const historyBudget = Math.max(512, effectiveBudget - systemTokens - this.maxOutputTokens);
8772
+ const historyText = formatMessages(messages, historyBudget);
7746
8773
  const budgetInstruction = totalTokens > effectiveBudget ? `
7747
8774
 
7748
8775
  IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiveBudget}). You MUST collapse enough to fit. Prefer collapsing older/lower-importance ranges.` : "";
@@ -7750,18 +8777,26 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
7750
8777
  model: this.model,
7751
8778
  system: [{ type: "text", text: systemText + budgetInstruction }],
7752
8779
  messages: [{ role: "user", content: historyText }],
7753
- maxTokens: 1024
8780
+ maxTokens: this.maxOutputTokens
7754
8781
  };
7755
8782
  let raw;
8783
+ const ac = new AbortController();
7756
8784
  try {
7757
- const ac = new AbortController();
7758
- const res = await this.provider.complete(req, { signal: ac.signal });
8785
+ const timeoutSignal = AbortSignal.timeout(3e4);
8786
+ const res = await this.provider.complete(req, {
8787
+ signal: AbortSignal.any([ac.signal, timeoutSignal])
8788
+ });
7759
8789
  const textBlocks = res.content.filter(isTextBlock);
7760
8790
  raw = textBlocks.map((b) => b.text).join("\n").trim();
7761
- } catch (_err) {
8791
+ } catch (err) {
8792
+ if (err instanceof Error) {
8793
+ console.warn("[LLMSelector] selector call failed, using recency fallback:", err.message);
8794
+ }
7762
8795
  return this.fallbackSelect(messages, effectiveBudget);
8796
+ } finally {
8797
+ ac.abort();
7763
8798
  }
7764
- return this.parseSelectorOutput(raw, messages.length);
8799
+ return this.parseSelectorOutput(raw, messages);
7765
8800
  }
7766
8801
  fallbackSelect(messages, budget) {
7767
8802
  const toKeep = [];
@@ -7788,34 +8823,63 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
7788
8823
  reasoning: `Fallback: kept last ${messages.length - startIdx} messages within ${budget} token budget`
7789
8824
  };
7790
8825
  }
7791
- parseSelectorOutput(raw, messageCount) {
8826
+ /**
8827
+ * Parse and validate the raw LLM output into a SelectorResult.
8828
+ * Falls back to recency-based selection if the LLM output is malformed,
8829
+ * out-of-bounds, or internally inconsistent.
8830
+ */
8831
+ parseSelectorOutput(raw, messages) {
8832
+ const messageCount = messages.length;
8833
+ if (messageCount === 0) {
8834
+ return { kept: [], collapsed: [], reasoning: "empty session" };
8835
+ }
7792
8836
  const jsonStart = raw.indexOf("{");
7793
8837
  const jsonEnd = raw.lastIndexOf("}");
7794
8838
  if (jsonStart === -1 || jsonEnd === -1) {
7795
- return this.fallbackSelect(
7796
- Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
7797
- this.maxContextTokens
7798
- );
8839
+ return this.fallbackSelect(messages, this.maxContextTokens);
7799
8840
  }
7800
8841
  let parsed;
7801
8842
  try {
7802
8843
  parsed = JSON.parse(raw.slice(jsonStart, jsonEnd + 1));
7803
8844
  } catch {
7804
- return this.fallbackSelect(
7805
- Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
7806
- this.maxContextTokens
7807
- );
8845
+ return this.fallbackSelect(messages, this.maxContextTokens);
7808
8846
  }
7809
8847
  const obj = parsed;
7810
- const kept = obj.kept ?? [];
7811
- const collapsed = obj.collapsed ?? [];
7812
- return {
7813
- kept: kept.map((k) => ({
8848
+ const keptRaw = obj.kept ?? [];
8849
+ const collapsedRaw = obj.collapsed ?? [];
8850
+ const kept = [];
8851
+ for (const k of keptRaw) {
8852
+ if (typeof k.from !== "number" || typeof k.to !== "number" || k.from < 0 || k.to >= messageCount || k.from > k.to) {
8853
+ return this.fallbackSelect(messages, this.maxContextTokens);
8854
+ }
8855
+ kept.push({
7814
8856
  from: k.from,
7815
8857
  to: k.to,
7816
8858
  importance: k.importance ?? "medium"
7817
- })),
7818
- collapsed: collapsed.map((c) => ({ from: c.from, to: c.to, summary: c.summary })),
8859
+ });
8860
+ }
8861
+ const collapsed = [];
8862
+ for (const c of collapsedRaw) {
8863
+ if (typeof c.from !== "number" || typeof c.to !== "number" || c.from < 0 || c.to >= messageCount || c.from > c.to) {
8864
+ return this.fallbackSelect(messages, this.maxContextTokens);
8865
+ }
8866
+ collapsed.push({ from: c.from, to: c.to, summary: c.summary });
8867
+ }
8868
+ const allRanges = [...kept, ...collapsed];
8869
+ for (let i = 0; i < allRanges.length; i++) {
8870
+ const a = allRanges[i];
8871
+ if (!a) continue;
8872
+ for (let j = i + 1; j < allRanges.length; j++) {
8873
+ const b = allRanges[j];
8874
+ if (!b) continue;
8875
+ if (a.from <= b.to && a.to >= b.from) {
8876
+ return this.fallbackSelect(messages, this.maxContextTokens);
8877
+ }
8878
+ }
8879
+ }
8880
+ return {
8881
+ kept,
8882
+ collapsed,
7819
8883
  reasoning: typeof obj.reasoning === "string" ? obj.reasoning : ""
7820
8884
  };
7821
8885
  }
@@ -7835,14 +8899,19 @@ var SelectiveCompactor = class {
7835
8899
  summarizerPrompt;
7836
8900
  constructor(opts) {
7837
8901
  this.provider = opts.provider;
7838
- this.selector = opts.selector ?? new LLMSelector({ provider: opts.provider, model: opts.selectorModel });
8902
+ this.selector = opts.selector ?? new LLMSelector({ provider: opts.provider, model: opts.selectorModel, maxOutputTokens: opts.selectorMaxOutputTokens });
7839
8903
  this.warnThreshold = opts.warnThreshold ?? 0.6;
7840
8904
  this.softThreshold = opts.softThreshold ?? 0.75;
7841
8905
  this.hardThreshold = opts.hardThreshold ?? 0.9;
7842
8906
  this.maxContext = opts.maxContext ?? 128e3;
7843
8907
  this.preserveK = opts.preserveK ?? 4;
7844
8908
  this.eliseThreshold = opts.eliseThreshold ?? 500;
7845
- this.summarizerModel = opts.summarizerModel ?? opts.selectorModel ?? "unknown";
8909
+ this.summarizerModel = opts.summarizerModel ?? opts.selectorModel;
8910
+ if (this.summarizerModel === void 0 && (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1")) {
8911
+ console.warn(
8912
+ "[SelectiveCompactor] summarizerModel not set \u2014 will fall back to ctx.model at summarize time. Set `summarizerModel` explicitly to silence this warning."
8913
+ );
8914
+ }
7846
8915
  this.summarizerPrompt = opts.summarizerPrompt ?? "You are a context summarizer. Given a list of messages, produce a concise summary that preserves all factual information, decisions, file changes, and state changes. Do not add commentary or opinions.";
7847
8916
  }
7848
8917
  async compact(ctx, opts = {}) {
@@ -7948,14 +9017,15 @@ var SelectiveCompactor = class {
7948
9017
  Summarize the following message range:`;
7949
9018
  const body = messages.map((m, i) => `[${i}] ${m.role}: ${this.messagePreview(m)}`).join("\n");
7950
9019
  const req = {
7951
- model: this.summarizerModel,
9020
+ model: this.summarizerModel ?? ctx.model,
7952
9021
  system: [{ type: "text", text: systemText }],
7953
9022
  messages: [{ role: "user", content: body }],
7954
9023
  maxTokens: 512
7955
9024
  };
7956
9025
  try {
9026
+ const timeoutSignal = AbortSignal.timeout(3e4);
7957
9027
  const res = await this.provider.complete(req, {
7958
- signal: ctx.signal ?? new AbortController().signal
9028
+ signal: AbortSignal.any([ctx.signal, timeoutSignal])
7959
9029
  });
7960
9030
  return res.content.filter(isTextBlock).map((b) => b.text).join("\n").trim() || "(empty)";
7961
9031
  } catch {
@@ -8014,27 +9084,16 @@ Summarize the following message range:`;
8014
9084
  if (typeof m.content === "string") return m.content.trim().length > 0;
8015
9085
  return m.content.some((b) => b.type === "text" && b.text.trim().length > 0);
8016
9086
  }
9087
+ /**
9088
+ * Estimate message-array tokens via the shared `estimateMessages` primitive
9089
+ * so SelectiveCompactor's before/after/load figures agree with the
9090
+ * middleware threshold math and the other compactors. Previously this used a
9091
+ * private `ceil(len/3.5)` walk that diverged from the calibrated shared
9092
+ * estimator, causing the selective `load`/`targetBudget` comparison to mix
9093
+ * two incompatible token scales.
9094
+ */
8017
9095
  estimateTokens(messages) {
8018
- let total = 0;
8019
- for (const m of messages) {
8020
- if (typeof m.content === "string") {
8021
- total += this.roughTokenEstimate(m.content);
8022
- } else {
8023
- for (const b of m.content) {
8024
- if (b.type === "text") total += this.roughTokenEstimate(b.text);
8025
- else if (b.type === "tool_use") total += this.roughTokenEstimate(JSON.stringify(b.input));
8026
- else if (b.type === "tool_result") {
8027
- total += this.roughTokenEstimate(
8028
- typeof b.content === "string" ? b.content : JSON.stringify(b.content)
8029
- );
8030
- }
8031
- }
8032
- }
8033
- }
8034
- return total;
8035
- }
8036
- roughTokenEstimate(text) {
8037
- return Math.max(1, Math.ceil(text.length / 3.5));
9096
+ return estimateMessages(messages);
8038
9097
  }
8039
9098
  };
8040
9099
 
@@ -8089,6 +9148,7 @@ var ProviderBackedCompactor = class {
8089
9148
  return new SelectiveCompactor({
8090
9149
  ...common,
8091
9150
  selectorModel: this.opts.summarizerModel,
9151
+ selectorMaxOutputTokens: this.opts.selectorMaxOutputTokens,
8092
9152
  summarizerModel: this.opts.summarizerModel
8093
9153
  });
8094
9154
  }
@@ -8224,15 +9284,20 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
8224
9284
  this._cachedMsgCount = msgCount;
8225
9285
  this._cachedToolCount = toolCount;
8226
9286
  }
8227
- const load = tokens / this._maxContext;
9287
+ const budget = computeContextWindowBudget(ctx, tokens, this._maxContext);
9288
+ const load = budget.load;
8228
9289
  const policy = this.policyProvider?.(ctx);
8229
9290
  const thresholds = policy?.thresholds ?? {
8230
9291
  warn: this.warnThreshold,
8231
9292
  soft: this.softThreshold,
8232
9293
  hard: this.hardThreshold
8233
9294
  };
9295
+ const repetition = repeatedReadPressure(ctx);
9296
+ const adaptiveThresholds = adaptThresholdsForSignals(thresholds, {
9297
+ repeatedReadCount: repetition
9298
+ });
8234
9299
  const aggressiveOn = policy?.aggressiveOn ?? this.aggressiveOn;
8235
- const level = load >= thresholds.hard ? "hard" : load >= thresholds.soft ? "soft" : load >= thresholds.warn ? "warn" : null;
9300
+ const level = load >= adaptiveThresholds.hard ? "hard" : load >= adaptiveThresholds.soft ? "soft" : load >= adaptiveThresholds.warn ? "warn" : null;
8236
9301
  if (!level) {
8237
9302
  this.lastNoopAttempt = null;
8238
9303
  return next(ctx);
@@ -8241,7 +9306,13 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
8241
9306
  return next(ctx);
8242
9307
  }
8243
9308
  const aggressive = level === "hard" ? true : level === "soft" ? aggressiveOn !== "hard" : aggressiveOn === "warn";
8244
- await this.compact(ctx, aggressive, { level, tokens, load });
9309
+ await this.compact(ctx, aggressive, {
9310
+ level,
9311
+ tokens,
9312
+ load,
9313
+ budget,
9314
+ signals: { repeatedReadCount: repetition }
9315
+ });
8245
9316
  return next(ctx);
8246
9317
  };
8247
9318
  }
@@ -8294,6 +9365,8 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
8294
9365
  tokens: pressure.tokens,
8295
9366
  load: pressure.load,
8296
9367
  maxContext: this._maxContext,
9368
+ budget: pressure.budget,
9369
+ signals: pressure.signals,
8297
9370
  report,
8298
9371
  aggressive
8299
9372
  });
@@ -8305,6 +9378,8 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
8305
9378
  level: pressure.level,
8306
9379
  aggressive,
8307
9380
  reductions: report.reductions?.map((r) => ({ phase: r.phase, saved: r.saved })),
9381
+ budget: pressure.budget,
9382
+ signals: pressure.signals,
8308
9383
  // Record what was collapsed so the audit trail shows the preserved
8309
9384
  // content, not just token counts. Bounded to keep the log line small;
8310
9385
  // the full original turns are already in the session JSONL.
@@ -8320,6 +9395,8 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
8320
9395
  level: pressure.level,
8321
9396
  tokens: pressure.tokens,
8322
9397
  maxContext: this._maxContext,
9398
+ budget: pressure.budget,
9399
+ signals: pressure.signals,
8323
9400
  load: pressure.load,
8324
9401
  fatal
8325
9402
  });
@@ -8339,8 +9416,37 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
8339
9416
  }
8340
9417
  }
8341
9418
  };
8342
-
8343
- // src/execution/tool-executor.ts
9419
+ function computeContextWindowBudget(ctx, inputTokens, maxContext) {
9420
+ const reservedOutputTokens = readPositiveMetaNumber(ctx, "contextOutputReserveTokens") ?? Math.floor(Math.min(8192, maxContext * 0.08));
9421
+ const reservedSafetyTokens = readPositiveMetaNumber(ctx, "contextSafetyBufferTokens") ?? Math.floor(Math.min(4096, maxContext * 0.02));
9422
+ const availableInputTokens = Math.max(
9423
+ 1,
9424
+ maxContext - reservedOutputTokens - reservedSafetyTokens
9425
+ );
9426
+ const remainingInputTokens = availableInputTokens - inputTokens;
9427
+ return {
9428
+ maxContext,
9429
+ inputTokens,
9430
+ availableInputTokens,
9431
+ remainingInputTokens,
9432
+ reservedOutputTokens,
9433
+ reservedSafetyTokens,
9434
+ load: inputTokens / availableInputTokens,
9435
+ overflowTokens: Math.max(0, -remainingInputTokens)
9436
+ };
9437
+ }
9438
+ function readPositiveMetaNumber(ctx, key) {
9439
+ const value = ctx.meta?.[key];
9440
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : void 0;
9441
+ }
9442
+ function adaptThresholdsForSignals(thresholds, signals) {
9443
+ if (signals.repeatedReadCount < 3) return thresholds;
9444
+ return {
9445
+ warn: Math.max(0.25, thresholds.warn - 0.08),
9446
+ soft: Math.max(0.35, thresholds.soft - 0.04),
9447
+ hard: thresholds.hard
9448
+ };
9449
+ }
8344
9450
  var ToolExecutor = class _ToolExecutor {
8345
9451
  constructor(registry, opts) {
8346
9452
  this.registry = registry;
@@ -8437,7 +9543,8 @@ ${errorDetails}`,
8437
9543
  let effectivePermission = decision.permission;
8438
9544
  const policy = this.opts.permissionPolicy;
8439
9545
  const yolo = policy.getYolo?.() === true || policy.getYoloDestructive?.() === true;
8440
- if (toolDangerousCaps.length > 0 && effectivePermission === "auto" && !yolo) {
9546
+ const authoritativeAuto = decision.source === "yolo";
9547
+ if (toolDangerousCaps.length > 0 && effectivePermission === "auto" && !yolo && !authoritativeAuto) {
8441
9548
  effectivePermission = "confirm";
8442
9549
  }
8443
9550
  if (effectivePermission === "deny") {
@@ -8579,9 +9686,10 @@ ${post.additionalContext}`;
8579
9686
  });
8580
9687
  this.opts.renderer?.writeToolCall(tool.name, use.input);
8581
9688
  const output = await this.runWithTimeout(tool, use.input, ctx.signal, ctx, use.id);
8582
- const text = this.serializer.serialize(output);
9689
+ const text = this.serializer.serialize(output, { toolName: tool.name, input: use.input });
8583
9690
  const scrubbed = this.opts.secretScrubber.scrub(text);
8584
- const { text: capped, newBudget } = this.serializer.enforceCap(scrubbed, budget);
9691
+ const withArtifact = await maybePersistLargeToolOutput(tool.name, scrubbed, budget);
9692
+ const { text: capped, newBudget } = this.serializer.enforceCap(withArtifact, budget);
8585
9693
  this.opts.renderer?.writeToolResult(tool.name, capped, false);
8586
9694
  return {
8587
9695
  block: {
@@ -8606,38 +9714,27 @@ ${post.additionalContext}`;
8606
9714
  tool.timeoutMs ?? this.iterationTimeoutMs,
8607
9715
  this.maxToolTimeoutMs
8608
9716
  );
8609
- const ctrl = new AbortController();
8610
- const timer = setTimeout(() => ctrl.abort(new Error("tool timeout")), timeoutMs);
8611
- const combined = AbortSignal.any([parentSignal, ctrl.signal]);
8612
- let cleanupCalled = false;
8613
- let caught = false;
9717
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
9718
+ const combined = AbortSignal.any([parentSignal, timeoutSignal]);
9719
+ let output;
8614
9720
  try {
8615
- if (typeof tool.executeStream === "function") {
8616
- return await this.runStreamedTool(tool, input, ctx, combined, toolUseId);
8617
- }
8618
- return await tool.execute(input, ctx, { signal: combined });
9721
+ output = typeof tool.executeStream === "function" ? await this.runStreamedTool(tool, input, ctx, combined, toolUseId) : await tool.execute(input, ctx, { signal: combined });
8619
9722
  } catch (err) {
8620
- caught = true;
8621
- if (combined.aborted && typeof tool.cleanup === "function") {
8622
- cleanupCalled = true;
8623
- try {
8624
- await tool.cleanup(input, ctx);
8625
- } catch {
8626
- }
8627
- }
9723
+ if (combined.aborted) await this.runToolCleanup(tool, input, ctx);
8628
9724
  throw err;
8629
- } finally {
8630
- clearTimeout(timer);
8631
- if (combined.aborted && !caught) {
8632
- if (!cleanupCalled && typeof tool.cleanup === "function") {
8633
- try {
8634
- await tool.cleanup(input, ctx);
8635
- } catch {
8636
- }
8637
- }
8638
- const reason = combined.reason instanceof Error ? combined.reason : new Error(typeof combined.reason === "string" ? combined.reason : "aborted");
8639
- throw reason;
8640
- }
9725
+ }
9726
+ if (combined.aborted) {
9727
+ await this.runToolCleanup(tool, input, ctx);
9728
+ throw combined.reason instanceof Error ? combined.reason : new Error(typeof combined.reason === "string" ? combined.reason : "tool timeout");
9729
+ }
9730
+ return output;
9731
+ }
9732
+ /** Best-effort tool cleanup; never let it mask the original error. */
9733
+ async runToolCleanup(tool, input, ctx) {
9734
+ if (typeof tool.cleanup !== "function") return;
9735
+ try {
9736
+ await tool.cleanup(input, ctx);
9737
+ } catch {
8641
9738
  }
8642
9739
  }
8643
9740
  async runStreamedTool(tool, input, ctx, signal, toolUseId) {
@@ -8806,6 +9903,25 @@ function extractMalformedRaw(input) {
8806
9903
  return String(value);
8807
9904
  }
8808
9905
  }
9906
+ var TOOL_OUTPUT_ARTIFACT_THRESHOLD_BYTES = 64 * 1024;
9907
+ async function maybePersistLargeToolOutput(toolName, content, budget) {
9908
+ const bytes = Buffer.byteLength(content, "utf8");
9909
+ if (bytes <= Math.min(TOOL_OUTPUT_ARTIFACT_THRESHOLD_BYTES, Math.max(0, budget))) {
9910
+ return content;
9911
+ }
9912
+ try {
9913
+ const dir = path4.join(wstackGlobalRoot(), "tool-output");
9914
+ await fsp2.mkdir(dir, { recursive: true });
9915
+ const safeTool = toolName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 40) || "tool";
9916
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
9917
+ const filePath = path4.join(dir, `${stamp}-${safeTool}-${randomUUID()}.log`);
9918
+ await fsp2.writeFile(filePath, content, "utf8");
9919
+ return content + `
9920
+ [full tool output: ${bytes} bytes at ${filePath}; read/grep that file selectively instead of re-running or requesting more output]`;
9921
+ } catch {
9922
+ return content;
9923
+ }
9924
+ }
8809
9925
 
8810
9926
  // src/execution/autonomous-runner.ts
8811
9927
  var DoneConditionChecker = class {
@@ -9818,6 +10934,7 @@ ${recentJournal}` : ""
9818
10934
 
9819
10935
  // src/coordination/subagent-budget.ts
9820
10936
  var TIMEOUT_PREEMPT_FRACTION = 0.85;
10937
+ var DECISION_TIMEOUT_MS = 6e4;
9821
10938
  var BudgetExceededError = class extends Error {
9822
10939
  kind;
9823
10940
  limit;
@@ -9847,6 +10964,31 @@ var BudgetThresholdSignal = class extends Error {
9847
10964
  };
9848
10965
  var SubagentBudget = class _SubagentBudget {
9849
10966
  limits;
10967
+ /** Patch one or more budget limits in-place after construction.
10968
+ * Used by the coordinator watchdog when granting an extension.
10969
+ * All fields are optional — only provided fields are updated.
10970
+ * This is the single write path for limit mutations so that future
10971
+ * validation or side-effects live in one place (M1). */
10972
+ patchLimits(ext) {
10973
+ if (ext.maxIterations !== void 0) {
10974
+ this.limits.maxIterations = ext.maxIterations;
10975
+ }
10976
+ if (ext.maxToolCalls !== void 0) {
10977
+ this.limits.maxToolCalls = ext.maxToolCalls;
10978
+ }
10979
+ if (ext.maxTokens !== void 0) {
10980
+ this.limits.maxTokens = ext.maxTokens;
10981
+ }
10982
+ if (ext.maxCostUsd !== void 0) {
10983
+ this.limits.maxCostUsd = ext.maxCostUsd;
10984
+ }
10985
+ if (ext.timeoutMs !== void 0) {
10986
+ this.limits.timeoutMs = ext.timeoutMs;
10987
+ }
10988
+ if (ext.idleTimeoutMs !== void 0) {
10989
+ this.limits.idleTimeoutMs = ext.idleTimeoutMs;
10990
+ }
10991
+ }
9850
10992
  iterations = 0;
9851
10993
  toolCalls = 0;
9852
10994
  tokenInput = 0;
@@ -9867,12 +11009,44 @@ var SubagentBudget = class _SubagentBudget {
9867
11009
  * or hung listener (Director not built / event filter detached mid-run)
9868
11010
  * leaves the budget over-limit and never enforces anything.
9869
11011
  */
9870
- static DECISION_TIMEOUT_MS = 6e4;
11012
+ static DECISION_TIMEOUT_MS = DECISION_TIMEOUT_MS;
9871
11013
  /**
9872
11014
  * Injected by the runner when wiring the budget to its EventBus.
9873
11015
  * Used to emit `budget.threshold_reached` events in `'auto'` mode.
9874
11016
  */
9875
11017
  _events;
11018
+ /**
11019
+ * Guard against dual-path races between the coordinator watchdog
11020
+ * (`executeWithTimeout`) and the budget's own `checkTimeout()`.
11021
+ * Both paths detect `elapsed >= timeoutMs` and can emit
11022
+ * `budget.threshold_reached` for kind `'timeout'` simultaneously.
11023
+ * Set to the current `timeoutMs` ceiling by the coordinator BEFORE
11024
+ * calling `onThreshold`, and cleared after the negotiation resolves.
11025
+ * `checkTimeout()` skips its wall-clock check while this is set so
11026
+ * the coordinator's watchdog is the sole source of wall-clock timeout
11027
+ * events — `checkTimeout()` focuses exclusively on `idle_timeout`.
11028
+ */
11029
+ _watchdogActive;
11030
+ /** Returns the timeout ceiling currently being negotiated by the watchdog,
11031
+ * or `undefined` when no wall-clock negotiation is in flight.
11032
+ * Used by `executeWithTimeout` to detect a stale lock (M3). */
11033
+ get watchdogActive() {
11034
+ return this._watchdogActive;
11035
+ }
11036
+ /** Called by the coordinator watchdog BEFORE calling `onThreshold` so that
11037
+ * `checkTimeout()` skips its wall-clock check for this ceiling. Prevents
11038
+ * the budget's own `checkTimeout()` from emitting a second
11039
+ * `budget.threshold_reached` event while the watchdog is already
11040
+ * negotiating the same wall-clock deadline (C1). */
11041
+ setWatchdogNegotiation(timeoutMs) {
11042
+ this._watchdogActive = timeoutMs;
11043
+ }
11044
+ /** Clears the watchdog guard after negotiation resolves. Called in the
11045
+ * `finally` block of both the pre-empt and deadline branches so it fires
11046
+ * on every exit path: grant, deny, throw, or error. */
11047
+ clearWatchdogNegotiation() {
11048
+ this._watchdogActive = void 0;
11049
+ }
9876
11050
  /**
9877
11051
  * Negotiation mode — controls whether a threshold hit tries to emit
9878
11052
  * `budget.threshold_reached` and wait for a coordinator decision, or
@@ -9973,7 +11147,8 @@ var SubagentBudget = class _SubagentBudget {
9973
11147
  if (this.limits.idleTimeoutMs !== void 0 && idle > this.limits.idleTimeoutMs) {
9974
11148
  exceeded.push({ kind: "idle_timeout", used: idle, limit: this.limits.idleTimeoutMs });
9975
11149
  }
9976
- if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs) {
11150
+ const wallOwnedByWatchdog = this._onThreshold !== void 0 && this._watchdogActive === this.limits.timeoutMs;
11151
+ if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs && !wallOwnedByWatchdog) {
9977
11152
  exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
9978
11153
  }
9979
11154
  }
@@ -9987,19 +11162,99 @@ var SubagentBudget = class _SubagentBudget {
9987
11162
  throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
9988
11163
  }
9989
11164
  const bus = this._events;
9990
- if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
11165
+ if (!bus) {
9991
11166
  const first2 = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
9992
11167
  throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
9993
11168
  }
11169
+ const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
11170
+ if (bus.hasListenerFor("budget.threshold_reached")) {
11171
+ for (const entry of exceeded) {
11172
+ if (this._pendingNegotiations.has(entry.kind)) continue;
11173
+ this._pendingNegotiations.set(entry.kind, this._negotiateExtension(entry));
11174
+ }
11175
+ const decision = this._pendingNegotiations.get(first.kind);
11176
+ if (!decision) throw new Error(`No pending negotiation for ${first.kind}`);
11177
+ throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
11178
+ }
11179
+ let hardStop = null;
9994
11180
  for (const entry of exceeded) {
9995
11181
  if (this._pendingNegotiations.has(entry.kind)) continue;
9996
- const decision2 = this._negotiateExtension(entry.kind, exceeded);
9997
- this._pendingNegotiations.set(entry.kind, decision2);
11182
+ const marker = Promise.resolve("stop");
11183
+ this._pendingNegotiations.set(entry.kind, marker);
11184
+ void marker.finally(() => this._pendingNegotiations.delete(entry.kind));
11185
+ const sync = this._invokeHandlerSync(entry);
11186
+ if (!sync) hardStop ??= new BudgetExceededError(entry.kind, entry.limit, entry.used);
9998
11187
  }
9999
- const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
10000
- const decision = this._pendingNegotiations.get(first.kind);
10001
- if (!decision) throw new Error(`No pending negotiation for ${first.kind}`);
10002
- throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
11188
+ if (hardStop) throw hardStop;
11189
+ return exceeded;
11190
+ }
11191
+ /**
11192
+ * Invoke `onThreshold` once for `entry` on the NO-LISTENER path and report
11193
+ * whether it decided synchronously. Returns `true` when the handler returned
11194
+ * a synchronous decision (already honored — an `extend` patched the limits),
11195
+ * or `false` when it returned a Promise (async; the caller hard-stops, since
11196
+ * there is no listener to resolve the negotiation). The handler is given the
11197
+ * full info shape (`requestDecision` plus direct `extend`/`deny`) so both
11198
+ * recording handlers and policy handlers work without a wired listener.
11199
+ */
11200
+ _invokeHandlerSync(entry) {
11201
+ const handler = this._onThreshold;
11202
+ if (!handler) return false;
11203
+ let extendArg;
11204
+ const result = handler({
11205
+ kind: entry.kind,
11206
+ used: entry.used,
11207
+ limit: entry.limit,
11208
+ requestDecision: () => this._busRequestDecision(entry),
11209
+ // Direct hooks for synchronous policy/recording handlers.
11210
+ extend: (extra) => {
11211
+ extendArg = extra;
11212
+ },
11213
+ deny: () => {
11214
+ }
11215
+ });
11216
+ if (result && typeof result.then === "function") return false;
11217
+ if (result === "throw") return false;
11218
+ if (result && typeof result === "object" && "extend" in result) {
11219
+ extendArg = result.extend;
11220
+ }
11221
+ if (extendArg) this.patchLimits(extendArg);
11222
+ return true;
11223
+ }
11224
+ /**
11225
+ * Emit `budget.threshold_reached` and resolve to the listener's verdict.
11226
+ * Resolves to `'stop'` immediately when there is no listener (or no bus) so
11227
+ * no negotiation can hang and no fallback timer leaks. Mirrors the
11228
+ * coordinator watchdog's own request path so both agree on the no-listener
11229
+ * default.
11230
+ */
11231
+ _busRequestDecision(entry) {
11232
+ const bus = this._events;
11233
+ if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
11234
+ return Promise.resolve("stop");
11235
+ }
11236
+ return new Promise((resolve6) => {
11237
+ let resolved = false;
11238
+ const respond = (d) => {
11239
+ if (resolved) return;
11240
+ resolved = true;
11241
+ clearTimeout(fallback);
11242
+ resolve6(d);
11243
+ };
11244
+ const fallback = setTimeout(() => respond("stop"), _SubagentBudget.DECISION_TIMEOUT_MS);
11245
+ bus.emit("budget.threshold_reached", {
11246
+ kind: entry.kind,
11247
+ used: entry.used,
11248
+ limit: entry.limit,
11249
+ timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
11250
+ // deny() wins over a same-dispatch extend(): a listener that both grants
11251
+ // and denies (or two listeners disagreeing) is resolved as a stop. The
11252
+ // grant is deferred a microtask so a synchronous deny in the same emit
11253
+ // pre-empts it; async grants still resolve normally.
11254
+ extend: (extra) => queueMicrotask(() => respond({ extend: extra })),
11255
+ deny: () => respond("stop")
11256
+ });
11257
+ });
10003
11258
  }
10004
11259
  /**
10005
11260
  * Per-kind in-flight negotiation Promises. Each budget kind can have its
@@ -10019,77 +11274,33 @@ var SubagentBudget = class _SubagentBudget {
10019
11274
  * `{ extend: {} }` — keep going without patching; next overrun fires
10020
11275
  * a fresh signal.
10021
11276
  */
10022
- async _negotiateExtension(kind, exceeded) {
11277
+ async _negotiateExtension(entry) {
10023
11278
  if (!this._onThreshold) {
10024
11279
  return "stop";
10025
11280
  }
10026
11281
  try {
10027
- const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
10028
11282
  const result = this._onThreshold({
10029
- kind: first.kind,
10030
- used: first.used,
10031
- limit: first.limit,
10032
- requestDecision: () => {
10033
- const bus = this._events;
10034
- if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
10035
- return Promise.resolve("stop");
10036
- }
10037
- return new Promise((resolve5) => {
10038
- let resolved = false;
10039
- const respond = (d) => {
10040
- if (resolved) return;
10041
- resolved = true;
10042
- resolve5(d);
10043
- };
10044
- const fallback = setTimeout(
10045
- () => respond("stop"),
10046
- _SubagentBudget.DECISION_TIMEOUT_MS
10047
- );
10048
- for (const { kind: kind2, used, limit } of exceeded) {
10049
- bus.emit("budget.threshold_reached", {
10050
- kind: kind2,
10051
- used,
10052
- limit,
10053
- timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
10054
- extend: (extra) => {
10055
- clearTimeout(fallback);
10056
- respond({ extend: extra });
10057
- },
10058
- deny: () => {
10059
- clearTimeout(fallback);
10060
- respond("stop");
10061
- }
10062
- });
10063
- }
10064
- });
11283
+ kind: entry.kind,
11284
+ used: entry.used,
11285
+ limit: entry.limit,
11286
+ // One event for THIS kind only — each exceeded kind has its own
11287
+ // negotiation (and its own resolve), so there is no cross-kind
11288
+ // first-wins drop and no O(N^2) re-emission.
11289
+ requestDecision: () => this._busRequestDecision(entry),
11290
+ extend: (extra) => {
11291
+ this.patchLimits(extra);
11292
+ },
11293
+ deny: () => {
10065
11294
  }
10066
11295
  });
10067
11296
  if (result === "throw") return "stop";
10068
11297
  if (result === "continue") return { extend: {} };
10069
11298
  const decision = await result;
10070
11299
  if (decision === "stop") return "stop";
10071
- const ext = decision.extend;
10072
- if (ext.maxIterations !== void 0) {
10073
- this.limits.maxIterations = ext.maxIterations;
10074
- }
10075
- if (ext.maxToolCalls !== void 0) {
10076
- this.limits.maxToolCalls = ext.maxToolCalls;
10077
- }
10078
- if (ext.maxTokens !== void 0) {
10079
- this.limits.maxTokens = ext.maxTokens;
10080
- }
10081
- if (ext.maxCostUsd !== void 0) {
10082
- this.limits.maxCostUsd = ext.maxCostUsd;
10083
- }
10084
- if (ext.timeoutMs !== void 0) {
10085
- this.limits.timeoutMs = ext.timeoutMs;
10086
- }
10087
- if (ext.idleTimeoutMs !== void 0) {
10088
- this.limits.idleTimeoutMs = ext.idleTimeoutMs;
10089
- }
11300
+ this.patchLimits(decision.extend);
10090
11301
  return decision;
10091
11302
  } finally {
10092
- this._pendingNegotiations.delete(kind);
11303
+ this._pendingNegotiations.delete(entry.kind);
10093
11304
  }
10094
11305
  }
10095
11306
  recordIteration() {
@@ -10132,7 +11343,8 @@ var SubagentBudget = class _SubagentBudget {
10132
11343
  const { timeoutMs, idleTimeoutMs } = this.limits;
10133
11344
  if (timeoutMs === void 0 && idleTimeoutMs === void 0) return;
10134
11345
  const elapsed = Date.now() - this.startTime;
10135
- const wallTripped = timeoutMs !== void 0 && elapsed > timeoutMs;
11346
+ const wallSkipped = this._onThreshold !== void 0 && this._watchdogActive !== void 0 && timeoutMs !== void 0 && this._watchdogActive === timeoutMs;
11347
+ const wallTripped = wallSkipped ? false : timeoutMs !== void 0 && elapsed > timeoutMs;
10136
11348
  const idleTripped = idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs;
10137
11349
  if (!wallTripped && !idleTripped) return;
10138
11350
  void this.checkLimits(elapsed);
@@ -13640,6 +14852,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
13640
14852
  terminating = /* @__PURE__ */ new Set();
13641
14853
  constructor(config, options = {}) {
13642
14854
  super();
14855
+ this.setMaxListeners(0);
13643
14856
  this.coordinatorId = config.coordinatorId;
13644
14857
  this.config = config;
13645
14858
  this.runner = options.runner;
@@ -13837,7 +15050,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
13837
15050
  taskIds.map((id) => {
13838
15051
  const cached = this.completedResults.find((r) => r.taskId === id);
13839
15052
  if (cached) return cached;
13840
- return new Promise((resolve5, reject) => {
15053
+ return new Promise((resolve6, reject) => {
13841
15054
  const timeout = setTimeout(() => {
13842
15055
  this.off("task.completed", handler);
13843
15056
  reject(new Error(`awaitTasks timed out waiting for task "${id}"`));
@@ -13846,7 +15059,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
13846
15059
  if (result.taskId === id) {
13847
15060
  clearTimeout(timeout);
13848
15061
  this.off("task.completed", handler);
13849
- resolve5(result);
15062
+ resolve6(result);
13850
15063
  }
13851
15064
  };
13852
15065
  this.on("task.completed", handler);
@@ -14034,7 +15247,13 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
14034
15247
  let result;
14035
15248
  budget.start();
14036
15249
  try {
14037
- const outcome = await this.executeWithTimeout(this.runner, task, runCtx, budget);
15250
+ const outcome = await this.executeWithTimeout(
15251
+ this.runner,
15252
+ task,
15253
+ runCtx,
15254
+ budget,
15255
+ subagent.config.preemptFraction
15256
+ );
14038
15257
  result = {
14039
15258
  subagentId,
14040
15259
  taskId: task.id,
@@ -14061,7 +15280,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
14061
15280
  }
14062
15281
  this.recordCompletion(result);
14063
15282
  }
14064
- async executeWithTimeout(runner, task, ctx, budget) {
15283
+ async executeWithTimeout(runner, task, ctx, budget, preemptFraction = TIMEOUT_PREEMPT_FRACTION) {
14065
15284
  const initialTimeoutMs = budget.limits.timeoutMs;
14066
15285
  const idleLimitMs = budget.limits.idleTimeoutMs;
14067
15286
  if (initialTimeoutMs === void 0 && idleLimitMs === void 0) {
@@ -14069,8 +15288,21 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
14069
15288
  }
14070
15289
  const start = Date.now();
14071
15290
  let timer = null;
14072
- let preemptedForLimit = null;
15291
+ let PreemptState;
15292
+ ((PreemptState2) => {
15293
+ PreemptState2["ACTIVE"] = "active";
15294
+ PreemptState2["LOCKED"] = "locked";
15295
+ })(PreemptState || (PreemptState = {}));
15296
+ let preemptedCeiling = null;
15297
+ let preemptState = "active" /* ACTIVE */;
15298
+ let lastGrantActivityTs = -1;
14073
15299
  const timeoutPromise = new Promise((_, reject) => {
15300
+ const terminate = (kind, limit, used) => {
15301
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
15302
+ reject(
15303
+ budget._events?.hasListenerFor("budget.threshold_reached") ? new Error(`subagent stopped: budget ${kind} (limit=${limit}, used=${used})`) : new BudgetExceededError(kind, limit, used)
15304
+ );
15305
+ };
14074
15306
  const armFor = (ms) => {
14075
15307
  if (timer) clearTimeout(timer);
14076
15308
  timer = setTimeout(onTick, Math.max(0, ms));
@@ -14079,7 +15311,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
14079
15311
  const wallLimit = budget.limits.timeoutMs ?? initialTimeoutMs;
14080
15312
  const wallRemaining = initialTimeoutMs === void 0 ? Number.POSITIVE_INFINITY : wallLimit - (Date.now() - start);
14081
15313
  const idleRemaining = idleLimitMs === void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
14082
- const preemptRemaining = initialTimeoutMs === void 0 || preemptedForLimit === wallLimit ? Number.POSITIVE_INFINITY : wallLimit * TIMEOUT_PREEMPT_FRACTION - (Date.now() - start);
15314
+ const preemptRemaining = initialTimeoutMs === void 0 || preemptedCeiling === wallLimit ? Number.POSITIVE_INFINITY : wallLimit * preemptFraction - (Date.now() - start);
14083
15315
  armFor(Math.max(25, Math.min(wallRemaining, idleRemaining, preemptRemaining)));
14084
15316
  };
14085
15317
  const negotiateTimeout = async (used, limit) => {
@@ -14089,16 +15321,42 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
14089
15321
  kind: "timeout",
14090
15322
  used,
14091
15323
  limit,
14092
- requestDecision: () => new Promise((resolveDecision) => {
14093
- budget._events?.emit("budget.threshold_reached", {
14094
- kind: "timeout",
14095
- used,
14096
- limit,
14097
- timeoutMs: 6e4,
14098
- extend: (extra) => resolveDecision({ extend: extra }),
14099
- deny: () => resolveDecision("stop")
15324
+ requestDecision: () => {
15325
+ if (!budget._events?.hasListenerFor("budget.threshold_reached")) {
15326
+ return Promise.resolve("stop");
15327
+ }
15328
+ return new Promise((resolveDecision) => {
15329
+ let settled = false;
15330
+ const resolve6 = (d) => {
15331
+ if (settled) return;
15332
+ settled = true;
15333
+ resolveDecision(d);
15334
+ };
15335
+ const fallback = setTimeout(() => resolve6("stop"), DECISION_TIMEOUT_MS);
15336
+ budget._events?.emit("budget.threshold_reached", {
15337
+ kind: "timeout",
15338
+ used,
15339
+ limit,
15340
+ // Informational: the budget's own decision deadline. Listeners may use
15341
+ // this to display a countdown. The coordinator does NOT enforce it —
15342
+ // it is the budget's own `setTimeout(fallback)` that races against
15343
+ // the listener's `extend()`/`deny()` call to guarantee progress.
15344
+ timeoutMs: DECISION_TIMEOUT_MS,
15345
+ // deny() wins over a same-dispatch extend(): defer the grant a
15346
+ // microtask so a synchronous deny in the same emit pre-empts it
15347
+ // (a listener that both grants and denies, or two listeners
15348
+ // disagreeing, resolves as a stop). Async grants still resolve.
15349
+ extend: (extra) => {
15350
+ clearTimeout(fallback);
15351
+ queueMicrotask(() => resolve6({ extend: extra }));
15352
+ },
15353
+ deny: () => {
15354
+ clearTimeout(fallback);
15355
+ resolve6("stop");
15356
+ }
15357
+ });
14100
15358
  });
14101
- })
15359
+ }
14102
15360
  });
14103
15361
  return typeof result === "string" ? result : await result;
14104
15362
  };
@@ -14109,21 +15367,45 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
14109
15367
  const wallExceeded = wallLimit !== void 0 && elapsed >= wallLimit;
14110
15368
  const idleExceeded = idleLimit !== void 0 && budget.idleMs() >= idleLimit;
14111
15369
  if (idleExceeded && !wallExceeded) {
15370
+ budget._events?.emit("budget.threshold_reached", {
15371
+ kind: "idle_timeout",
15372
+ used: budget.idleMs(),
15373
+ limit: idleLimit ?? 0,
15374
+ timeoutMs: DECISION_TIMEOUT_MS,
15375
+ extend: () => {
15376
+ },
15377
+ deny: () => {
15378
+ }
15379
+ });
14112
15380
  this.subagents.get(ctx.subagentId)?.abortController.abort();
14113
- reject(new BudgetExceededError("timeout", idleLimit ?? 0, budget.idleMs()));
15381
+ reject(new BudgetExceededError("idle_timeout", idleLimit ?? 0, budget.idleMs()));
14114
15382
  return;
14115
15383
  }
14116
- if (wallLimit !== void 0 && !wallExceeded && budget.onThreshold && preemptedForLimit !== wallLimit && elapsed >= wallLimit * TIMEOUT_PREEMPT_FRACTION) {
15384
+ if (wallLimit !== void 0 && !wallExceeded && budget.onThreshold && preemptState === "active" /* ACTIVE */ && elapsed >= wallLimit * preemptFraction) {
15385
+ const activityTs = Date.now() - budget.idleMs();
15386
+ if (activityTs <= lastGrantActivityTs) {
15387
+ preemptState = "locked" /* LOCKED */;
15388
+ preemptedCeiling = wallLimit;
15389
+ scheduleNext();
15390
+ return;
15391
+ }
15392
+ budget.setWatchdogNegotiation(wallLimit);
14117
15393
  try {
14118
15394
  const decision = await negotiateTimeout(elapsed, wallLimit);
14119
15395
  if (typeof decision !== "string" && decision.extend.timeoutMs !== void 0) {
14120
- budget.limits.timeoutMs = decision.extend.timeoutMs;
14121
- preemptedForLimit = null;
15396
+ budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
15397
+ lastGrantActivityTs = Date.now() - budget.idleMs();
15398
+ preemptState = "active" /* ACTIVE */;
15399
+ preemptedCeiling = null;
14122
15400
  } else {
14123
- preemptedForLimit = wallLimit;
15401
+ preemptState = "locked" /* LOCKED */;
15402
+ preemptedCeiling = wallLimit;
14124
15403
  }
14125
15404
  } catch {
14126
- preemptedForLimit = wallLimit;
15405
+ preemptState = "locked" /* LOCKED */;
15406
+ preemptedCeiling = wallLimit;
15407
+ } finally {
15408
+ budget.clearWatchdogNegotiation();
14127
15409
  }
14128
15410
  scheduleNext();
14129
15411
  return;
@@ -14138,26 +15420,41 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
14138
15420
  reject(new BudgetExceededError("timeout", limit, elapsed));
14139
15421
  return;
14140
15422
  }
15423
+ budget.setWatchdogNegotiation(limit);
14141
15424
  try {
14142
15425
  const decision = await negotiateTimeout(elapsed, limit);
14143
- if (decision === "continue" || decision === "throw" || decision === "stop") {
14144
- preemptedForLimit = null;
15426
+ if (decision === "throw") {
15427
+ terminate("timeout", limit, elapsed);
15428
+ return;
15429
+ }
15430
+ if (decision === "continue") {
15431
+ preemptState = "locked" /* LOCKED */;
15432
+ preemptedCeiling = wallLimit;
14145
15433
  armFor(Math.max(1e3, limit));
14146
15434
  return;
14147
15435
  }
15436
+ if (decision === "stop") {
15437
+ terminate("timeout", limit, elapsed);
15438
+ return;
15439
+ }
14148
15440
  if (decision.extend.timeoutMs !== void 0) {
14149
- budget.limits.timeoutMs = decision.extend.timeoutMs;
14150
- preemptedForLimit = null;
15441
+ budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
15442
+ lastGrantActivityTs = Date.now() - budget.idleMs();
15443
+ preemptState = "active" /* ACTIVE */;
15444
+ preemptedCeiling = null;
14151
15445
  scheduleNext();
14152
15446
  return;
14153
15447
  }
14154
- this.subagents.get(ctx.subagentId)?.abortController.abort();
14155
- reject(new BudgetExceededError("timeout", limit, elapsed));
15448
+ terminate("timeout", limit, elapsed);
15449
+ return;
14156
15450
  } catch (err) {
14157
15451
  this.subagents.get(ctx.subagentId)?.abortController.abort();
14158
15452
  reject(
14159
15453
  err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
14160
15454
  );
15455
+ return;
15456
+ } finally {
15457
+ budget.clearWatchdogNegotiation();
14161
15458
  }
14162
15459
  };
14163
15460
  scheduleNext();
@@ -14943,7 +16240,7 @@ var InMemoryAgentBridge = class {
14943
16240
  });
14944
16241
  }
14945
16242
  this.inflightGuards.add(correlationId);
14946
- return new Promise((resolve5, reject) => {
16243
+ return new Promise((resolve6, reject) => {
14947
16244
  const timer = setTimeout(() => {
14948
16245
  this.inflightGuards.delete(correlationId);
14949
16246
  this.pendingRequests.delete(correlationId);
@@ -14962,7 +16259,7 @@ var InMemoryAgentBridge = class {
14962
16259
  return;
14963
16260
  }
14964
16261
  this.pendingRequests.set(correlationId, {
14965
- resolve: resolve5,
16262
+ resolve: resolve6,
14966
16263
  reject,
14967
16264
  timer
14968
16265
  });
@@ -15634,6 +16931,24 @@ Working rules:
15634
16931
  var DEFAULT_SUBAGENT_BASELINE = `You are a subagent operating under a Director. You were spawned to handle
15635
16932
  a specific slice of a larger plan \u2014 do that slice well and report back.
15636
16933
 
16934
+ Capabilities & operating rules:
16935
+ - You have full developer tools for your task: read, write/edit, search,
16936
+ shell + build (lint, format, typecheck, test), and dependency install.
16937
+ Use them directly to finish the task end-to-end. You run non-interactively
16938
+ \u2014 there is no human to approve individual tool calls, so routine work is
16939
+ pre-authorized; do not stop to ask for permission to read, edit, or build.
16940
+ - Stay inside the project root. Do not write files outside the repository,
16941
+ and do not touch machine config, credentials, or global state \u2014 those
16942
+ require an explicit grant you do not have.
16943
+ - Prefer the least-destructive path. Do not run irreversible or destructive
16944
+ commands (e.g. \`rm -rf\`, \`git push --force\`, history rewrites, dropping
16945
+ databases, mass deletes) unless the task explicitly requires it and names
16946
+ the target.
16947
+ - When you change code, verify it: run the relevant build / typecheck / tests
16948
+ and fix what you broke before reporting done.
16949
+ - Make only the changes the task calls for \u2014 don't refactor or reformat
16950
+ unrelated code.
16951
+
15637
16952
  Bridge contract:
15638
16953
  - You have a parent (the Director). You may call \`request\` on the
15639
16954
  parent bridge to ask a clarifying question. Use this sparingly; the
@@ -16591,6 +17906,8 @@ var Director = class _Director {
16591
17906
  sessionsRoot;
16592
17907
  /** Director run id for JSONL path resolution. */
16593
17908
  directorRunId;
17909
+ /** Optional logger for structured logging. Falls back to noop when omitted. */
17910
+ logger;
16594
17911
  /** Resolves task descriptions back from `assign()` so completion events
16595
17912
  * can also carry a human-readable title. */
16596
17913
  taskDescriptions = /* @__PURE__ */ new Map();
@@ -16679,6 +17996,7 @@ var Director = class _Director {
16679
17996
  opts.checkpointDebounceMs ?? 250
16680
17997
  ) : null;
16681
17998
  this.fleetManager = opts.fleetManager;
17999
+ this.logger = opts.logger;
16682
18000
  if (this.sharedScratchpadPath) {
16683
18001
  void fsp2.mkdir(this.sharedScratchpadPath, { recursive: true }).catch((err) => this.logShutdownError("shared_scratchpad_mkdir", err));
16684
18002
  }
@@ -16978,7 +18296,10 @@ var Director = class _Director {
16978
18296
  entry.session.cancel(reason);
16979
18297
  for (const [_role, subagentId] of entry.session.getSubagentIds()) {
16980
18298
  this.coordinator.stop(subagentId).catch((err) => {
16981
- console.debug(`[director] stop subagent ${subagentId} failed (may have already completed): ${err}`);
18299
+ this.logger?.debug(`stop subagent ${subagentId} failed (may have already completed)`, {
18300
+ subagentId,
18301
+ err: err instanceof Error ? err.message : String(err)
18302
+ });
16982
18303
  });
16983
18304
  }
16984
18305
  }
@@ -17038,7 +18359,7 @@ var Director = class _Director {
17038
18359
  * Caller-supplied `priceLookup` is optional but recommended — without
17039
18360
  * it the `cost` column in `usage.snapshot()` stays at 0.
17040
18361
  */
17041
- async spawn(config, priceLookup) {
18362
+ async spawn(callerConfig, priceLookup) {
17042
18363
  if (this.workCompleteFlag) {
17043
18364
  throw new FleetSpawnBudgetError(
17044
18365
  "max_spawns",
@@ -17047,6 +18368,7 @@ var Director = class _Director {
17047
18368
  "workComplete() has been called \u2014 director closed further spawning"
17048
18369
  );
17049
18370
  }
18371
+ const config = { ...callerConfig };
17050
18372
  if (!config.model && this.modelMatrix) {
17051
18373
  const matrix = typeof this.modelMatrix === "function" ? this.modelMatrix() : this.modelMatrix;
17052
18374
  const entry = resolveModelMatrix(matrix, config.role);
@@ -17272,7 +18594,7 @@ var Director = class _Director {
17272
18594
  })),
17273
18595
  usage: this.usage.snapshot()
17274
18596
  };
17275
- await fsp2.mkdir(path3.dirname(this.manifestPath), { recursive: true });
18597
+ await fsp2.mkdir(path4.dirname(this.manifestPath), { recursive: true });
17276
18598
  await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
17277
18599
  return this.manifestPath;
17278
18600
  }
@@ -17399,11 +18721,11 @@ var Director = class _Director {
17399
18721
  if (cached) return cached;
17400
18722
  const existing = this.taskWaiters.get(id);
17401
18723
  if (existing) return existing.promise;
17402
- let resolve5;
18724
+ let resolve6;
17403
18725
  const promise = new Promise((res) => {
17404
- resolve5 = res;
18726
+ resolve6 = res;
17405
18727
  });
17406
- this.taskWaiters.set(id, { promise, resolve: resolve5 });
18728
+ this.taskWaiters.set(id, { promise, resolve: resolve6 });
17407
18729
  return promise;
17408
18730
  })
17409
18731
  );
@@ -17486,7 +18808,7 @@ var Director = class _Director {
17486
18808
  */
17487
18809
  async readSession(subagentId, tail) {
17488
18810
  if (!this.sessionsRoot) return null;
17489
- const filePath = path3.join(this.sessionsRoot, this.directorRunId, `${subagentId}.jsonl`);
18811
+ const filePath = path4.join(this.sessionsRoot, this.directorRunId, `${subagentId}.jsonl`);
17490
18812
  let raw;
17491
18813
  try {
17492
18814
  raw = await fsp2.readFile(filePath, "utf8");
@@ -17799,7 +19121,7 @@ function createDelegateTool(opts) {
17799
19121
  subagentId
17800
19122
  });
17801
19123
  const dir = director;
17802
- const result = await new Promise((resolve5) => {
19124
+ const result = await new Promise((resolve6) => {
17803
19125
  let settled = false;
17804
19126
  let timer;
17805
19127
  const finish = (value) => {
@@ -17809,7 +19131,7 @@ function createDelegateTool(opts) {
17809
19131
  offTool();
17810
19132
  offIter();
17811
19133
  offProgress();
17812
- resolve5(value);
19134
+ resolve6(value);
17813
19135
  };
17814
19136
  const arm = () => {
17815
19137
  if (timer) clearTimeout(timer);
@@ -18016,13 +19338,13 @@ async function readSubagentPartial(opts, subagentId) {
18016
19338
  if (!opts.sessionsRoot) return void 0;
18017
19339
  const candidates = [];
18018
19340
  if (opts.directorRunId) {
18019
- candidates.push(path3.join(opts.sessionsRoot, opts.directorRunId, `${subagentId}.jsonl`));
19341
+ candidates.push(path4.join(opts.sessionsRoot, opts.directorRunId, `${subagentId}.jsonl`));
18020
19342
  } else {
18021
19343
  try {
18022
19344
  const entries = await fsp2.readdir(opts.sessionsRoot, { withFileTypes: true });
18023
19345
  for (const entry of entries) {
18024
19346
  if (entry.isDirectory()) {
18025
- candidates.push(path3.join(opts.sessionsRoot, entry.name, `${subagentId}.jsonl`));
19347
+ candidates.push(path4.join(opts.sessionsRoot, entry.name, `${subagentId}.jsonl`));
18026
19348
  }
18027
19349
  }
18028
19350
  } catch {
@@ -18072,9 +19394,9 @@ function makeDirectorSessionFactory(opts) {
18072
19394
  let dir;
18073
19395
  if (opts.store) {
18074
19396
  store = opts.store;
18075
- dir = opts.sessionsRoot ? path3.join(opts.sessionsRoot, runId) : "(caller-managed)";
19397
+ dir = opts.sessionsRoot ? path4.join(opts.sessionsRoot, runId) : "(caller-managed)";
18076
19398
  } else if (opts.sessionsRoot) {
18077
- dir = path3.join(opts.sessionsRoot, runId);
19399
+ dir = path4.join(opts.sessionsRoot, runId);
18078
19400
  store = new DefaultSessionStore({ dir });
18079
19401
  } else {
18080
19402
  throw new Error("makeDirectorSessionFactory requires either `store` or `sessionsRoot`");
@@ -18120,6 +19442,7 @@ function attachAutoExtend(events, policy = {}) {
18120
19442
  const extendCounts = /* @__PURE__ */ new Map();
18121
19443
  let progress = 0;
18122
19444
  let lastTimeoutProgress = -1;
19445
+ let lastSeenKey = null;
18123
19446
  const unsubs = [
18124
19447
  events.on("tool.executed", () => {
18125
19448
  progress++;
@@ -18129,6 +19452,9 @@ function attachAutoExtend(events, policy = {}) {
18129
19452
  }),
18130
19453
  events.on("budget.threshold_reached", (e) => {
18131
19454
  const { kind, limit, extend, deny } = e;
19455
+ const key = `${kind}:${limit}`;
19456
+ if (key === lastSeenKey) return;
19457
+ lastSeenKey = key;
18132
19458
  if (kind === "timeout" || kind === "idle_timeout") {
18133
19459
  if (progress > lastTimeoutProgress) {
18134
19460
  lastTimeoutProgress = progress;
@@ -18220,7 +19546,7 @@ var DefaultModelsRegistry = class {
18220
19546
  this.overlay = opts.overlay;
18221
19547
  this.overlayUrl = opts.overlayUrl;
18222
19548
  this.overlayFile = opts.overlayFile;
18223
- this.overlayCacheFile = opts.overlayCacheFile ?? (opts.overlayUrl ? path3.join(path3.dirname(opts.cacheFile), "models-overlay-cache.json") : void 0);
19549
+ this.overlayCacheFile = opts.overlayCacheFile ?? (opts.overlayUrl ? path4.join(path4.dirname(opts.cacheFile), "models-overlay-cache.json") : void 0);
18224
19550
  }
18225
19551
  async load(opts = {}) {
18226
19552
  if (this.payload && !opts.force) return this.payload;
@@ -18433,7 +19759,7 @@ var DefaultModelsRegistry = class {
18433
19759
  }
18434
19760
  /** Used by `wstack models refresh` to expose where the cache lives. */
18435
19761
  cacheLocation() {
18436
- return path3.resolve(this.cacheFile);
19762
+ return path4.resolve(this.cacheFile);
18437
19763
  }
18438
19764
  };
18439
19765
  function hasEntries(payload) {
@@ -18800,7 +20126,7 @@ var DefaultModeStore = class {
18800
20126
  }
18801
20127
  async loadActiveMode() {
18802
20128
  try {
18803
- const configPath = path3.join(this.configDir, "mode.json");
20129
+ const configPath = path4.join(this.configDir, "mode.json");
18804
20130
  const content = await fsp2.readFile(configPath, "utf8");
18805
20131
  const data = JSON.parse(content);
18806
20132
  this.activeModeId = data.activeMode ?? null;
@@ -18811,7 +20137,7 @@ var DefaultModeStore = class {
18811
20137
  async saveActiveMode() {
18812
20138
  try {
18813
20139
  await fsp2.mkdir(this.configDir, { recursive: true });
18814
- const configPath = path3.join(this.configDir, "mode.json");
20140
+ const configPath = path4.join(this.configDir, "mode.json");
18815
20141
  await atomicWrite(
18816
20142
  configPath,
18817
20143
  JSON.stringify({ activeMode: this.activeModeId }, null, 2)
@@ -18826,11 +20152,11 @@ async function loadProjectModes(modesDir) {
18826
20152
  const entries = await fsp2.readdir(modesDir);
18827
20153
  for (const entry of entries) {
18828
20154
  if (!entry.endsWith(".md") && !entry.endsWith(".txt")) continue;
18829
- const filePath = path3.join(modesDir, entry);
20155
+ const filePath = path4.join(modesDir, entry);
18830
20156
  const stat6 = await fsp2.stat(filePath);
18831
20157
  if (!stat6.isFile()) continue;
18832
20158
  const content = await fsp2.readFile(filePath, "utf8");
18833
- const id = path3.basename(entry, path3.extname(entry));
20159
+ const id = path4.basename(entry, path4.extname(entry));
18834
20160
  modes.push({
18835
20161
  id,
18836
20162
  name: id.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
@@ -18846,7 +20172,7 @@ async function loadProjectModes(modesDir) {
18846
20172
  async function loadUserModes(modesDir) {
18847
20173
  const modes = [];
18848
20174
  try {
18849
- const manifestPath = path3.join(modesDir, "modes.json");
20175
+ const manifestPath = path4.join(modesDir, "modes.json");
18850
20176
  const content = await fsp2.readFile(manifestPath, "utf8");
18851
20177
  const manifest = JSON.parse(content);
18852
20178
  for (const mode of manifest.modes) {
@@ -18857,6 +20183,35 @@ async function loadUserModes(modesDir) {
18857
20183
  return modes;
18858
20184
  }
18859
20185
 
20186
+ // src/models/provider-model-resolve.ts
20187
+ function describeCatalogModel(m) {
20188
+ return {
20189
+ id: m.id,
20190
+ name: m.name,
20191
+ releaseDate: m.release_date,
20192
+ contextWindow: m.limit?.context,
20193
+ inputCost: m.cost?.input,
20194
+ outputCost: m.cost?.output,
20195
+ capabilities: [
20196
+ ...m.tool_call ? ["tools"] : [],
20197
+ ...m.reasoning ? ["reasoning"] : [],
20198
+ ...m.modalities?.input?.includes("image") ? ["vision"] : [],
20199
+ ...m.open_weights ? ["open_weights"] : []
20200
+ ]
20201
+ };
20202
+ }
20203
+ function resolveProviderModelList(savedModels, catalog) {
20204
+ if (savedModels && savedModels.length > 0) {
20205
+ const byId = new Map((catalog?.models ?? []).map((m) => [m.id, m]));
20206
+ return savedModels.map((id) => {
20207
+ const hit = byId.get(id);
20208
+ return hit ? describeCatalogModel(hit) : { id, name: id, capabilities: [] };
20209
+ });
20210
+ }
20211
+ if (catalog) return catalog.models.map(describeCatalogModel);
20212
+ return [];
20213
+ }
20214
+
18860
20215
  // src/sdd/spec-parser.ts
18861
20216
  var SpecParser = class {
18862
20217
  parse(content) {
@@ -19798,7 +21153,7 @@ var SpecStore = class {
19798
21153
  indexPath;
19799
21154
  constructor(opts) {
19800
21155
  this.baseDir = opts.baseDir;
19801
- this.indexPath = path3.join(this.baseDir, "_index.json");
21156
+ this.indexPath = path4.join(this.baseDir, "_index.json");
19802
21157
  }
19803
21158
  async save(spec) {
19804
21159
  await ensureDir(this.baseDir);
@@ -19867,7 +21222,7 @@ var SpecStore = class {
19867
21222
  return updated;
19868
21223
  }
19869
21224
  filePath(id) {
19870
- return path3.join(this.baseDir, `${id}.json`);
21225
+ return path4.join(this.baseDir, `${id}.json`);
19871
21226
  }
19872
21227
  async readIndex() {
19873
21228
  try {
@@ -19924,7 +21279,7 @@ var TaskGraphStore = class {
19924
21279
  indexPath;
19925
21280
  constructor(opts) {
19926
21281
  this.baseDir = opts.baseDir;
19927
- this.indexPath = path3.join(this.baseDir, "_index.json");
21282
+ this.indexPath = path4.join(this.baseDir, "_index.json");
19928
21283
  }
19929
21284
  async save(graph) {
19930
21285
  await ensureDir(this.baseDir);
@@ -19962,7 +21317,7 @@ var TaskGraphStore = class {
19962
21317
  }
19963
21318
  }
19964
21319
  filePath(id) {
19965
- return path3.join(this.baseDir, `${id}.json`);
21320
+ return path4.join(this.baseDir, `${id}.json`);
19966
21321
  }
19967
21322
  async readIndex() {
19968
21323
  try {
@@ -20207,9 +21562,9 @@ var AISpecBuilder = class {
20207
21562
  if (!this.sessionPath) return;
20208
21563
  try {
20209
21564
  const fsp16 = await import('fs/promises');
20210
- const path19 = await import('path');
21565
+ const path21 = await import('path');
20211
21566
  const { atomicWrite: atomicWrite2 } = await Promise.resolve().then(() => (init_atomic_write(), atomic_write_exports));
20212
- await fsp16.mkdir(path19.dirname(this.sessionPath), { recursive: true });
21567
+ await fsp16.mkdir(path21.dirname(this.sessionPath), { recursive: true });
20213
21568
  await atomicWrite2(this.sessionPath, JSON.stringify(this.session, null, 2));
20214
21569
  } catch {
20215
21570
  }
@@ -20936,15 +22291,15 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
20936
22291
  maxId = id;
20937
22292
  }
20938
22293
  }
20939
- const path19 = [];
22294
+ const path21 = [];
20940
22295
  let current = maxId;
20941
22296
  const visited = /* @__PURE__ */ new Set();
20942
22297
  while (current && !visited.has(current)) {
20943
22298
  visited.add(current);
20944
- path19.unshift(current);
22299
+ path21.unshift(current);
20945
22300
  current = prev.get(current) ?? null;
20946
22301
  }
20947
- return path19;
22302
+ return path21;
20948
22303
  }
20949
22304
  function computeParallelGroups(graph, blockedByMap) {
20950
22305
  const groups = [];
@@ -21767,9 +23122,9 @@ var DefaultHealthRegistry = class {
21767
23122
  }
21768
23123
  async runOne(check) {
21769
23124
  let timer = null;
21770
- const timeout = new Promise((resolve5) => {
23125
+ const timeout = new Promise((resolve6) => {
21771
23126
  timer = setTimeout(
21772
- () => resolve5({ status: "unhealthy", detail: `timeout after ${this.timeoutMs}ms` }),
23127
+ () => resolve6({ status: "unhealthy", detail: `timeout after ${this.timeoutMs}ms` }),
21773
23128
  this.timeoutMs
21774
23129
  );
21775
23130
  });
@@ -21952,7 +23307,7 @@ async function startMetricsServer(opts) {
21952
23307
  const tls = opts.tls;
21953
23308
  const useHttps = !!(tls?.cert && tls?.key);
21954
23309
  const host = opts.host ?? "127.0.0.1";
21955
- const path19 = opts.path ?? "/metrics";
23310
+ const path21 = opts.path ?? "/metrics";
21956
23311
  const healthPath = opts.healthPath ?? "/healthz";
21957
23312
  const healthRegistry = opts.healthRegistry;
21958
23313
  const listener = (req, res) => {
@@ -21962,7 +23317,7 @@ async function startMetricsServer(opts) {
21962
23317
  return;
21963
23318
  }
21964
23319
  const url = req.url.split("?")[0];
21965
- if (url === path19) {
23320
+ if (url === path21) {
21966
23321
  let body;
21967
23322
  try {
21968
23323
  body = renderPrometheus(opts.sink.snapshot());
@@ -22008,14 +23363,14 @@ async function startMetricsServer(opts) {
22008
23363
  const { createServer } = await import('http');
22009
23364
  server = createServer(listener);
22010
23365
  }
22011
- await new Promise((resolve5, reject) => {
23366
+ await new Promise((resolve6, reject) => {
22012
23367
  const onError = (err) => {
22013
23368
  server.off("listening", onListening);
22014
23369
  reject(err);
22015
23370
  };
22016
23371
  const onListening = () => {
22017
23372
  server.off("error", onError);
22018
- resolve5();
23373
+ resolve6();
22019
23374
  };
22020
23375
  server.once("error", onError);
22021
23376
  server.once("listening", onListening);
@@ -22026,9 +23381,9 @@ async function startMetricsServer(opts) {
22026
23381
  const protocol = useHttps ? "https" : "http";
22027
23382
  return {
22028
23383
  port: boundPort,
22029
- url: `${protocol}://${host}:${boundPort}${path19}`,
22030
- close: () => new Promise((resolve5, reject) => {
22031
- server.close((err) => err ? reject(err) : resolve5());
23384
+ url: `${protocol}://${host}:${boundPort}${path21}`,
23385
+ close: () => new Promise((resolve6, reject) => {
23386
+ server.close((err) => err ? reject(err) : resolve6());
22032
23387
  })
22033
23388
  };
22034
23389
  }
@@ -22343,11 +23698,12 @@ function createContextManagerTool(opts = {}) {
22343
23698
  const applyMessages = (next) => {
22344
23699
  const repaired = repairToolUseAdjacency(next);
22345
23700
  const finalMessages = repaired.messages;
23701
+ if (finalMessages === messages) return repaired.report;
22346
23702
  if (ctx.state) {
22347
23703
  ctx.state.replaceMessages(finalMessages);
22348
23704
  } else {
22349
23705
  messages.length = 0;
22350
- messages.splice(0, 0, ...finalMessages);
23706
+ messages.push(...finalMessages);
22351
23707
  }
22352
23708
  return repaired.report;
22353
23709
  };
@@ -22422,9 +23778,15 @@ function createContextManagerTool(opts = {}) {
22422
23778
  }
22423
23779
  const report = await opts.compactor.compact(ctx);
22424
23780
  ctx.clearFileTracking();
22425
- const repair = applyMessages([...ctx.messages]);
22426
- const afterTokens = repair.changed ? roughEstimate(ctx.messages) : report.after;
22427
- const repaired = report.repaired ?? (repair.changed ? repair : void 0);
23781
+ let repaired = report.repaired;
23782
+ let afterTokens;
23783
+ if (!ctx.state) {
23784
+ const repair = applyMessages([...ctx.messages]);
23785
+ repaired = report.repaired ?? (repair.changed ? repair : void 0);
23786
+ afterTokens = repair.changed ? roughEstimate(ctx.messages) : report.after;
23787
+ } else {
23788
+ afterTokens = report.after;
23789
+ }
22428
23790
  const reduced = report.fullRequestTokensBefore > report.fullRequestTokensAfter;
22429
23791
  const repairedSomething = !!report.repaired;
22430
23792
  if (reduced || repairedSomething) {
@@ -22679,6 +24041,6 @@ var allServers = () => ({
22679
24041
  playwright: { ...playwrightServer(), enabled: false }
22680
24042
  });
22681
24043
 
22682
- export { AGENTS_BY_PHASE, AGENT_CATALOG, AISpecBuilder, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, ConfigMigrationError, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_CONFIG, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_SUBAGENT_BASELINE, DEFAULT_TOOLS_CONFIG, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPermissionPolicy, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultTaskStore, Director, DirectorStateCheckpoint, DoneConditionChecker, EternalAutonomyEngine, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FleetBus, FleetSpawnBudgetError, FleetUsageAggregator, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, IntelligentCompactor, LLMSelector, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, ParallelEternalEngine, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, SddParallelRun, SddTaskDecomposer, SelectiveCompactor, SessionAnalyzer, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, ToolExecutor, addPlanItem, allServers, analyzeCriticalPath, applyRosterBudget, attachAutoExtend, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildGoalPreamble, buildOtlpMetricsRequest, buildOtlpTracesRequest, classifyFamily, clearPlan, composeDirectorPrompt, composeSubagentPrompt, context7Server, contextManagerTool, createAutoExecutor, createContextManagerTool, createDelegateTool, createMessage, createSessionEventBridge, createStrategyCompactor, decryptConfigSecrets, deriveTodosFromPlanItem, dispatchAgent, emptyPlan, encryptConfigSecrets, everArtServer, filesystemServer, formatPlan, formatPlanTemplates, getAgentDefinition, getPlanTemplate, getTemplate, githubServer, googleMapsServer, listPlanTemplates, listTemplates, loadDirectorState, loadPlan, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeAskTool, makeAssignTool, makeAutonomyPromptContributor, makeAwaitTasksTool, makeCollabDebugTool, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeRollUpTool, makeSpawnTool, makeTerminateTool, migratePlaintextSecrets, miniMaxVisionServer, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, resolveAuditLevel, resolveSessionLoggingConfig, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, savePlan, saveTodosCheckpoint, scoreAgents, sentinelServer, setPlanItemStatus, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, templateToMarkdown, wireMetricsToEvents, zaiVisionServer };
24044
+ export { AGENTS_BY_PHASE, AGENT_CATALOG, AISpecBuilder, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, ConfigMigrationError, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_CONFIG, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_SUBAGENT_BASELINE, DEFAULT_TOOLS_CONFIG, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPermissionPolicy, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultTaskStore, Director, DirectorStateCheckpoint, DoneConditionChecker, EternalAutonomyEngine, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FleetBus, FleetSpawnBudgetError, FleetUsageAggregator, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, IntelligentCompactor, LLMSelector, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, ParallelEternalEngine, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, SddParallelRun, SddTaskDecomposer, SelectiveCompactor, SessionAnalyzer, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, ToolExecutor, addPlanItem, allServers, analyzeCriticalPath, applyRosterBudget, attachAutoExtend, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildGoalPreamble, buildOtlpMetricsRequest, buildOtlpTracesRequest, classifyFamily, clearPlan, composeDirectorPrompt, composeSubagentPrompt, context7Server, contextManagerTool, createAutoExecutor, createContextManagerTool, createDelegateTool, createMessage, createSessionEventBridge, createStrategyCompactor, decryptConfigSecrets, deriveTodosFromPlanItem, describeCatalogModel, dispatchAgent, emptyPlan, encryptConfigSecrets, everArtServer, filesystemServer, formatPlan, formatPlanTemplates, getAgentDefinition, getPlanTemplate, getTemplate, githubServer, googleMapsServer, listPlanTemplates, listTemplates, loadDirectorState, loadPlan, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeAskTool, makeAssignTool, makeAutonomyPromptContributor, makeAwaitTasksTool, makeCollabDebugTool, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeRollUpTool, makeSpawnTool, makeTerminateTool, migratePlaintextSecrets, miniMaxVisionServer, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, resolveAuditLevel, resolveProviderModelList, resolveSessionLoggingConfig, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, savePlan, saveTodosCheckpoint, scoreAgents, sentinelServer, setPlanItemStatus, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, templateToMarkdown, wireMetricsToEvents, zaiVisionServer };
22683
24045
  //# sourceMappingURL=index.js.map
22684
24046
  //# sourceMappingURL=index.js.map