@wrongstack/core 0.260.0 → 0.265.1

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 (99) hide show
  1. package/dist/{agent-bridge-BbskZ7HH.d.ts → agent-bridge-DrkBxszZ.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-BNIGZx18.d.ts → agent-subagent-runner-DM2pP-B6.d.ts} +116 -12
  3. package/dist/{brain-C2yDd7Lw.d.ts → brain-BXd_61kQ.d.ts} +32 -3
  4. package/dist/{compactor-t0R_AIt_.d.ts → compactor-B8pOf45Y.d.ts} +1 -1
  5. package/dist/{config-FG6As4H5.d.ts → config-BMCj_XDs.d.ts} +86 -12
  6. package/dist/{context-JFOVvu6z.d.ts → context-MRk5PhNv.d.ts} +26 -1
  7. package/dist/coordination/index.d.ts +1737 -15
  8. package/dist/coordination/index.js +3152 -494
  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 +1804 -1363
  13. package/dist/defaults/index.js.map +1 -1
  14. package/dist/dispatcher-types.d-BBeXBQgS.d.ts +66 -0
  15. package/dist/execution/index.d.ts +16 -16
  16. package/dist/execution/index.js +933 -672
  17. package/dist/execution/index.js.map +1 -1
  18. package/dist/execution/prompt-enhancer.d.ts +1 -1
  19. package/dist/execution/prompt-enhancer.js +7 -1
  20. package/dist/execution/prompt-enhancer.js.map +1 -1
  21. package/dist/extension/index.d.ts +6 -6
  22. package/dist/extension/index.js.map +1 -1
  23. package/dist/{goal-preamble-B1IXJtLX.d.ts → goal-preamble-DvHDSKSe.d.ts} +26 -10
  24. package/dist/{goal-store-CPXz6Mml.d.ts → goal-store-DtLMySNb.d.ts} +1 -1
  25. package/dist/{index-CebbJB94.d.ts → index-B-ch8K9C.d.ts} +8 -8
  26. package/dist/{index-BPcg4N3M.d.ts → index-CEDeNodM.d.ts} +5 -5
  27. package/dist/index.d.ts +189 -104
  28. package/dist/index.js +24693 -21162
  29. package/dist/index.js.map +1 -1
  30. package/dist/infrastructure/index.d.ts +6 -6
  31. package/dist/infrastructure/index.js +12 -8
  32. package/dist/infrastructure/index.js.map +1 -1
  33. package/dist/kernel/index.d.ts +9 -9
  34. package/dist/kernel/index.js +7 -2
  35. package/dist/kernel/index.js.map +1 -1
  36. package/dist/{llm-selector-DXxI2tlu.d.ts → llm-selector-C0tfTCUe.d.ts} +14 -2
  37. package/dist/{mcp-servers-OwNHo43-.d.ts → mcp-servers-2x4w6Jn9.d.ts} +3 -3
  38. package/dist/models/index.d.ts +5 -5
  39. package/dist/models/index.js +80 -31
  40. package/dist/models/index.js.map +1 -1
  41. package/dist/{models-registry-Djlmq4uB.d.ts → models-registry-DmJlKuNp.d.ts} +1 -1
  42. package/dist/{multi-agent-coordinator-CEmrSCMJ.d.ts → multi-agent-coordinator-DyCkCZnU.d.ts} +2 -2
  43. package/dist/{null-fleet-bus-DT92xqgJ.d.ts → null-fleet-bus-CG9QY2aP.d.ts} +6 -6
  44. package/dist/observability/index.d.ts +2 -2
  45. package/dist/observability/index.js +8 -3
  46. package/dist/observability/index.js.map +1 -1
  47. package/dist/{parallel-eternal-engine-0SItuq5r.d.ts → parallel-eternal-engine-Jw9uhEoT.d.ts} +9 -9
  48. package/dist/{path-resolver-DKBh6Jlo.d.ts → path-resolver-Dy2ej-gE.d.ts} +3 -3
  49. package/dist/{permission-BJ7eO9Vl.d.ts → permission-B9SB45lp.d.ts} +1 -1
  50. package/dist/{permission-policy-DEXOfnpm.d.ts → permission-policy-CkjSXabK.d.ts} +2 -2
  51. package/dist/{pipeline-zflkI2dp.d.ts → pipeline-DPDxH_7m.d.ts} +59 -4
  52. package/dist/{plan-templates-BFXyRkEK.d.ts → plan-templates-CzD9GnAU.d.ts} +32 -8
  53. package/dist/{provider-runner-BC-uywtT.d.ts → provider-runner-DMa70ODu.d.ts} +3 -3
  54. package/dist/{retry-policy-Cavrzmtk.d.ts → retry-policy-CN0khdlj.d.ts} +1 -1
  55. package/dist/sdd/index.d.ts +8 -8
  56. package/dist/sdd/index.js +313 -122
  57. package/dist/sdd/index.js.map +1 -1
  58. package/dist/{secret-vault-CDvDYXWX.d.ts → secret-vault-B2yw84VT.d.ts} +43 -4
  59. package/dist/secret-vault-BAKpgFw_.d.ts +57 -0
  60. package/dist/security/index.d.ts +5 -5
  61. package/dist/security/index.js +411 -225
  62. package/dist/security/index.js.map +1 -1
  63. package/dist/{selector-B7AivHsu.d.ts → selector-CzHh_igB.d.ts} +1 -1
  64. package/dist/{session-event-bridge-BmIDxdJd.d.ts → session-event-bridge-BUI6Jf-4.d.ts} +8 -2
  65. package/dist/{session-reader-DtofsB-2.d.ts → session-reader-CMgdMSRP.d.ts} +1 -1
  66. package/dist/skills/index.js +67 -64
  67. package/dist/skills/index.js.map +1 -1
  68. package/dist/storage/index.d.ts +132 -16
  69. package/dist/storage/index.js +851 -432
  70. package/dist/storage/index.js.map +1 -1
  71. package/dist/tools/index.d.ts +57 -0
  72. package/dist/tools/index.js +411 -0
  73. package/dist/tools/index.js.map +1 -0
  74. package/dist/types/index.d.ts +21 -21
  75. package/dist/types/index.js +928 -711
  76. package/dist/types/index.js.map +1 -1
  77. package/dist/utils/error.d.ts +7 -0
  78. package/dist/utils/error.js +8 -0
  79. package/dist/utils/error.js.map +1 -0
  80. package/dist/utils/index.d.ts +8 -68
  81. package/dist/utils/index.js +20 -10
  82. package/dist/utils/index.js.map +1 -1
  83. package/dist/{wstack-paths-CJjEwPXn.d.ts → wstack-paths-hOpNLmvf.d.ts} +2 -0
  84. package/package.json +5 -1
  85. package/skills/api-design/SKILL.md +1 -1
  86. package/skills/audit-log/SKILL.md +6 -6
  87. package/skills/bug-hunter/SKILL.md +5 -5
  88. package/skills/chimera/SKILL.md +4 -4
  89. package/skills/docker-deploy/SKILL.md +1 -1
  90. package/skills/git-flow/SKILL.md +3 -3
  91. package/skills/multi-agent/SKILL.md +3 -3
  92. package/skills/node-modern/SKILL.md +1 -0
  93. package/skills/observability/SKILL.md +2 -2
  94. package/skills/output-standards/SKILL.md +51 -28
  95. package/skills/refactor-planner/SKILL.md +3 -3
  96. package/skills/security-scanner/SKILL.md +4 -3
  97. package/skills/tech-stack/SKILL.md +1 -2
  98. package/dist/package-outdated-watcher-C70ag2G9.d.ts +0 -581
  99. 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
- import * as fsp from 'fs/promises';
4
- import * as path11 from 'path';
3
+ import * as fsp2 from 'fs/promises';
4
+ import * as path3 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,17 +33,17 @@ __export(atomic_write_exports, {
33
33
  withFileLock: () => withFileLock
34
34
  });
35
35
  async function atomicWrite(targetPath, content, opts = {}) {
36
- const dir = path11.dirname(targetPath);
37
- await fsp.mkdir(dir, { recursive: true });
38
- const tmp = path11.join(dir, `.${path11.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
36
+ const dir = path3.dirname(targetPath);
37
+ await fsp2.mkdir(dir, { recursive: true });
38
+ const tmp = path3.join(dir, `.${path3.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
39
39
  try {
40
40
  if (typeof content === "string") {
41
- await fsp.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
41
+ await fsp2.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
42
42
  } else {
43
- await fsp.writeFile(tmp, content, { flag: "wx" });
43
+ await fsp2.writeFile(tmp, content, { flag: "wx" });
44
44
  }
45
45
  try {
46
- const fh = await fsp.open(tmp, "r+");
46
+ const fh = await fsp2.open(tmp, "r+");
47
47
  try {
48
48
  await fh.sync();
49
49
  } finally {
@@ -53,45 +53,50 @@ async function atomicWrite(targetPath, content, opts = {}) {
53
53
  }
54
54
  let mode;
55
55
  try {
56
- const stat6 = await fsp.stat(targetPath);
56
+ const stat6 = await fsp2.stat(targetPath);
57
57
  mode = stat6.mode & 511;
58
58
  } catch {
59
59
  mode = opts.mode;
60
60
  }
61
61
  if (mode !== void 0) {
62
- await fsp.chmod(tmp, mode);
62
+ await fsp2.chmod(tmp, mode);
63
63
  }
64
64
  await renameWithRetry(tmp, targetPath);
65
65
  } catch (err) {
66
66
  try {
67
- await fsp.unlink(tmp);
67
+ await fsp2.unlink(tmp);
68
68
  } catch {
69
69
  }
70
70
  throw err;
71
71
  }
72
72
  }
73
73
  async function ensureDir(dir) {
74
- await fsp.mkdir(dir, { recursive: true });
74
+ await fsp2.mkdir(dir, { recursive: true });
75
75
  }
76
76
  async function withFileLock(targetPath, fn, opts = {}) {
77
- const dir = path11.dirname(targetPath);
78
- await fsp.mkdir(dir, { recursive: true });
79
- const lockPath = path11.join(dir, `.${path11.basename(targetPath)}.lock`);
77
+ const dir = path3.dirname(targetPath);
78
+ await fsp2.mkdir(dir, { recursive: true });
79
+ const lockPath = path3.join(dir, `.${path3.basename(targetPath)}.lock`);
80
80
  const timeoutMs = opts.timeoutMs ?? 5e3;
81
81
  const staleMs = opts.staleMs ?? 3e4;
82
82
  const started = Date.now();
83
83
  let handle;
84
84
  for (; ; ) {
85
85
  try {
86
- handle = await fsp.open(lockPath, "wx");
86
+ handle = await fsp2.open(lockPath, "wx");
87
87
  await handle.writeFile(`${process.pid}:${Date.now()}`);
88
88
  break;
89
89
  } catch (err) {
90
- if (err.code !== "EEXIST") throw err;
90
+ const code = err.code;
91
+ if (code === "ENOENT") {
92
+ await fsp2.mkdir(dir, { recursive: true });
93
+ continue;
94
+ }
95
+ if (code !== "EEXIST") throw err;
91
96
  try {
92
- const stat6 = await fsp.stat(lockPath);
97
+ const stat6 = await fsp2.stat(lockPath);
93
98
  if (Date.now() - stat6.mtimeMs > staleMs) {
94
- await fsp.unlink(lockPath);
99
+ await fsp2.unlink(lockPath);
95
100
  continue;
96
101
  }
97
102
  } catch {
@@ -111,21 +116,21 @@ async function withFileLock(targetPath, fn, opts = {}) {
111
116
  } catch {
112
117
  }
113
118
  try {
114
- await fsp.unlink(lockPath);
119
+ await fsp2.unlink(lockPath);
115
120
  } catch {
116
121
  }
117
122
  }
118
123
  }
119
124
  async function renameWithRetry(from, to) {
120
125
  if (process.platform !== "win32") {
121
- await fsp.rename(from, to);
126
+ await fsp2.rename(from, to);
122
127
  return;
123
128
  }
124
129
  const delays = [10, 25, 60, 120, 250];
125
130
  let lastErr;
126
131
  for (let i = 0; i <= delays.length; i++) {
127
132
  try {
128
- await fsp.rename(from, to);
133
+ await fsp2.rename(from, to);
129
134
  return;
130
135
  } catch (err) {
131
136
  lastErr = err;
@@ -229,7 +234,7 @@ var DefaultLogger = class _DefaultLogger {
229
234
  this.maxFileBytes = opts.maxFileBytes ?? 10 * 1024 * 1024;
230
235
  if (this.file) {
231
236
  try {
232
- fs.mkdirSync(path11.dirname(this.file), { recursive: true });
237
+ fs4.mkdirSync(path3.dirname(this.file), { recursive: true });
233
238
  } catch {
234
239
  }
235
240
  }
@@ -270,10 +275,10 @@ var DefaultLogger = class _DefaultLogger {
270
275
  maybeRotate(file) {
271
276
  if (this.writesSinceRotateCheck++ % _DefaultLogger.ROTATE_CHECK_EVERY !== 0) return;
272
277
  try {
273
- const st = fs.statSync(file);
278
+ const st = fs4.statSync(file);
274
279
  if (st.size < this.maxFileBytes) return;
275
- fs.rmSync(`${file}.1`, { force: true });
276
- fs.renameSync(file, `${file}.1`);
280
+ fs4.rmSync(`${file}.1`, { force: true });
281
+ fs4.renameSync(file, `${file}.1`);
277
282
  } catch {
278
283
  }
279
284
  }
@@ -289,7 +294,7 @@ var DefaultLogger = class _DefaultLogger {
289
294
  if (this.file) {
290
295
  try {
291
296
  this.maybeRotate(this.file);
292
- fs.appendFileSync(this.file, `${JSON.stringify(entry)}
297
+ fs4.appendFileSync(this.file, `${JSON.stringify(entry)}
293
298
  `);
294
299
  } catch {
295
300
  }
@@ -386,49 +391,873 @@ function repairToolUseAdjacency(messages) {
386
391
  changed = true;
387
392
  continue;
388
393
  }
389
- out.push(msg);
394
+ out.push(msg);
395
+ }
396
+ return {
397
+ messages: changed ? out : messages,
398
+ report: { changed, removedToolUses, removedToolResults, removedMessages }
399
+ };
400
+ }
401
+ function hasToolUse(msg) {
402
+ return contentBlocks(msg).some((b) => b.type === "tool_use");
403
+ }
404
+ function hasToolResult(msg) {
405
+ return contentBlocks(msg).some((b) => b.type === "tool_result");
406
+ }
407
+ function toolUseIds(msg) {
408
+ const ids = /* @__PURE__ */ new Set();
409
+ if (!msg || msg.role !== "assistant") return ids;
410
+ for (const block of contentBlocks(msg)) {
411
+ if (block.type === "tool_use") ids.add(block.id);
412
+ }
413
+ return ids;
414
+ }
415
+ function toolResultIds(msg) {
416
+ const ids = /* @__PURE__ */ new Set();
417
+ if (!msg || msg.role !== "user") return ids;
418
+ for (const block of contentBlocks(msg)) {
419
+ if (block.type === "tool_result") ids.add(block.tool_use_id);
420
+ }
421
+ return ids;
422
+ }
423
+ function contentBlocks(msg) {
424
+ return msg && Array.isArray(msg.content) ? msg.content : [];
425
+ }
426
+ function mapContent(msg, fn) {
427
+ if (!Array.isArray(msg.content)) return msg;
428
+ const next = fn(msg.content);
429
+ if (next.length === msg.content.length && next.every((b, idx) => b === msg.content[idx])) {
430
+ return msg;
431
+ }
432
+ return { ...msg, content: next };
433
+ }
434
+ function isEmptyMessage(msg) {
435
+ if (typeof msg.content === "string") return msg.content.trim().length === 0;
436
+ return msg.content.length === 0;
437
+ }
438
+
439
+ // src/utils/index.ts
440
+ init_atomic_write();
441
+
442
+ // src/utils/error.ts
443
+ function toErrorMessage(err) {
444
+ return err instanceof Error ? err.message : String(err);
445
+ }
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)` };
451
+ }
452
+ try {
453
+ return { ok: true, value: JSON.parse(input) };
454
+ } catch (err) {
455
+ return {
456
+ ok: false,
457
+ error: toErrorMessage(err)
458
+ };
459
+ }
460
+ }
461
+
462
+ // src/utils/string.ts
463
+ function truncate(s, max) {
464
+ return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
465
+ }
466
+
467
+ // src/utils/glob-match.ts
468
+ function escapeRegex(s) {
469
+ return s.replace(/[.+^${}()|\\]/g, "\\$&");
470
+ }
471
+ var COMPILED_GLOB_CACHE = /* @__PURE__ */ new Map();
472
+ var CACHE_MAX_SIZE = 2e3;
473
+ function getCachedGlob(pattern) {
474
+ const cached = COMPILED_GLOB_CACHE.get(pattern);
475
+ if (cached) return cached;
476
+ if (COMPILED_GLOB_CACHE.size >= CACHE_MAX_SIZE) {
477
+ const keys = [...COMPILED_GLOB_CACHE.keys()];
478
+ for (let i = 0; i < Math.floor(CACHE_MAX_SIZE / 4); i++) {
479
+ COMPILED_GLOB_CACHE.delete(expectDefined(keys[i]));
480
+ }
481
+ }
482
+ const re = compileGlob(pattern);
483
+ COMPILED_GLOB_CACHE.set(pattern, re);
484
+ return re;
485
+ }
486
+ var MAX_GLOB_PATTERN_LEN = 1024;
487
+ function compileGlob(pattern) {
488
+ if (pattern.length > MAX_GLOB_PATTERN_LEN) {
489
+ throw new Error(`Glob pattern exceeds ${MAX_GLOB_PATTERN_LEN} characters`);
490
+ }
491
+ let i = 0;
492
+ let re = "^";
493
+ while (i < pattern.length) {
494
+ const c = pattern[i];
495
+ if (c === "*") {
496
+ if (pattern[i + 1] === "*") {
497
+ re += ".*";
498
+ i += 2;
499
+ if (pattern[i] === "/") i++;
500
+ } else {
501
+ re += "[^/]*";
502
+ i++;
503
+ }
504
+ } else if (c === "?") {
505
+ re += "[^/]";
506
+ i++;
507
+ } else if (c === "[") {
508
+ let cls = "[";
509
+ i++;
510
+ if (pattern[i] === "!" || pattern[i] === "^") {
511
+ cls += "^";
512
+ i++;
513
+ }
514
+ while (i < pattern.length && pattern[i] !== "]") {
515
+ const ch = pattern[i] ?? "";
516
+ if (ch === "\\") {
517
+ cls += "\\\\";
518
+ } else if (ch === "]" || ch === "^") {
519
+ cls += `\\${ch}`;
520
+ } else {
521
+ cls += ch;
522
+ }
523
+ i++;
524
+ }
525
+ cls += "]";
526
+ re += cls;
527
+ i++;
528
+ } else {
529
+ re += escapeRegex(c ?? "");
530
+ i++;
531
+ }
532
+ }
533
+ re += "$";
534
+ return new RegExp(re);
535
+ }
536
+ function matchGlob(pattern, input) {
537
+ return getCachedGlob(pattern).test(input);
538
+ }
539
+ function matchAny(patterns, input) {
540
+ return patterns.some((p) => matchGlob(p, input));
541
+ }
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
+ projectStatus: (projectHash2) => path3.join(globalRoot, "projects", projectHash2, "status.json")
597
+ };
598
+ }
599
+
600
+ // src/utils/sleep.ts
601
+ function sleep(ms) {
602
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
603
+ }
604
+
605
+ // src/utils/assert-never.ts
606
+ function assertNever(x, message) {
607
+ const err = new Error(
608
+ `Unhandled case: ${JSON.stringify(x)}`
609
+ );
610
+ err.name = "AssertNeverError";
611
+ throw err;
612
+ }
613
+
614
+ // src/utils/deep-merge.ts
615
+ var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set([
616
+ "__proto__",
617
+ "constructor",
618
+ "prototype",
619
+ "__defineGetter__",
620
+ "__defineSetter__",
621
+ "__lookupGetter__",
622
+ "__lookupSetter__"
623
+ ]);
624
+ function isPrimitiveArray(a) {
625
+ return a.every((v) => v === null || typeof v !== "object" && typeof v !== "function");
626
+ }
627
+ function deepMerge(base, patch, options = {}) {
628
+ const {
629
+ conflictResolution = "prefer-patch",
630
+ arrayMode = "replace",
631
+ protectProto = true,
632
+ onNonPrimitiveArrayReplace
633
+ } = options;
634
+ if (typeof base !== "object" || base === null) {
635
+ return conflictResolution === "prefer-patch" ? patch : base;
636
+ }
637
+ if (typeof patch !== "object" || patch === null) {
638
+ return conflictResolution === "prefer-patch" ? patch : base;
639
+ }
640
+ if (Array.isArray(base) && Array.isArray(patch)) {
641
+ if (arrayMode === "concat-primitives" && isPrimitiveArray(base) && isPrimitiveArray(patch)) {
642
+ return [.../* @__PURE__ */ new Set([...base, ...patch])];
643
+ }
644
+ return conflictResolution === "prefer-patch" ? patch : base;
645
+ }
646
+ if (Array.isArray(base) || Array.isArray(patch)) {
647
+ return conflictResolution === "prefer-patch" ? patch : base;
648
+ }
649
+ const baseObj = base;
650
+ const patchObj = patch;
651
+ const out = { ...baseObj };
652
+ for (const [k, v] of Object.entries(patchObj)) {
653
+ if (protectProto && FORBIDDEN_PROTO_KEYS.has(k)) continue;
654
+ const existing = out[k];
655
+ if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
656
+ out[k] = deepMerge(existing, v, options);
657
+ } else if (Array.isArray(v) && Array.isArray(existing)) {
658
+ if (onNonPrimitiveArrayReplace && !isPrimitiveArray(v)) {
659
+ onNonPrimitiveArrayReplace(k, existing.length, v.length);
660
+ }
661
+ out[k] = deepMerge(existing, v, options);
662
+ } else if (v !== void 0) {
663
+ if (onNonPrimitiveArrayReplace && Array.isArray(v) && !isPrimitiveArray(v)) {
664
+ const existingLen = Array.isArray(existing) ? existing.length : 0;
665
+ onNonPrimitiveArrayReplace(k, existingLen, v.length);
666
+ }
667
+ out[k] = v;
668
+ }
669
+ }
670
+ return out;
671
+ }
672
+
673
+ // src/utils/tool-output-serializer.ts
674
+ function createToolOutputSerializer(opts = {}) {
675
+ const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
676
+ function serialize(value) {
677
+ if (typeof value === "string") return value;
678
+ if (value === null || value === void 0) return "";
679
+ if (typeof value === "object") {
680
+ if (Array.isArray(value)) return value.map(serialize).join("\n");
681
+ if ("text" in value) {
682
+ const t = value.text;
683
+ return typeof t === "string" ? t : JSON.stringify(value, null, 2);
684
+ }
685
+ try {
686
+ return JSON.stringify(value, null, 2);
687
+ } catch {
688
+ return String(value);
689
+ }
690
+ }
691
+ return String(value);
692
+ }
693
+ function enforceCap(text, remainingBudget) {
694
+ if (remainingBudget <= 0) {
695
+ return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
696
+ }
697
+ const textBytes = Buffer.byteLength(text, "utf8");
698
+ if (textBytes <= remainingBudget) {
699
+ return { text, newBudget: remainingBudget - textBytes };
700
+ }
701
+ const marker = `
702
+ \u2026[truncated ${textBytes - remainingBudget} bytes]\u2026
703
+ `;
704
+ const markerBytes = Buffer.byteLength(marker, "utf8");
705
+ const available = remainingBudget - markerBytes;
706
+ if (available <= 0) {
707
+ return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
708
+ }
709
+ const half = Math.floor(available / 2);
710
+ const first = text.slice(0, half);
711
+ const second = text.slice(text.length - half);
712
+ return { text: `${first}${marker}${second}`, newBudget: 0 };
713
+ }
714
+ return { serialize, enforceCap, capBytes };
715
+ }
716
+
717
+ // src/utils/token-estimate.ts
718
+ var RoughTokenEstimate = (text, charsPerToken = 3.5) => Math.max(1, Math.ceil(text.length / charsPerToken));
719
+ var CALIBRATION_GLOBAL_KEY = "__global__";
720
+ var _cals = /* @__PURE__ */ new Map();
721
+ function calState(key) {
722
+ let state = _cals.get(key);
723
+ if (!state) {
724
+ state = { ratio: 1, count: 0, prevEst: 0 };
725
+ _cals.set(key, state);
726
+ }
727
+ return state;
728
+ }
729
+ var MIN_SAMPLES_FOR_CALIBRATION = 3;
730
+ var ESTIMATE_CACHE = /* @__PURE__ */ new Map();
731
+ var ESTIMATE_CACHE_MAX_SIZE = 1e4;
732
+ function getCachedEstimate(key, compute) {
733
+ const existing = ESTIMATE_CACHE.get(key);
734
+ if (existing !== void 0) return existing;
735
+ if (ESTIMATE_CACHE.size >= ESTIMATE_CACHE_MAX_SIZE) {
736
+ for (const k of ESTIMATE_CACHE.keys()) {
737
+ if (ESTIMATE_CACHE.size <= Math.floor(ESTIMATE_CACHE_MAX_SIZE / 2)) break;
738
+ ESTIMATE_CACHE.delete(k);
739
+ }
740
+ }
741
+ const estimate = compute(key);
742
+ ESTIMATE_CACHE.set(key, estimate);
743
+ return estimate;
744
+ }
745
+ function estimateToolInputTokens(input) {
746
+ if (typeof input === "string") return RoughTokenEstimate(input);
747
+ if (input === null || typeof input !== "object") {
748
+ return RoughTokenEstimate(String(input));
749
+ }
750
+ return getCachedEstimate(JSON.stringify(input), (key) => RoughTokenEstimate(key));
751
+ }
752
+ function estimateToolResultTokens(content) {
753
+ if (typeof content === "string") return RoughTokenEstimate(content);
754
+ return getCachedEstimate(JSON.stringify(content), (key) => RoughTokenEstimate(key));
755
+ }
756
+ function estimateTextTokens(text) {
757
+ return RoughTokenEstimate(text);
758
+ }
759
+ function computeMessageTokens(msg) {
760
+ if (typeof msg.content === "string") return estimateTextTokens(msg.content);
761
+ let total = 0;
762
+ for (const b of msg.content) {
763
+ if (b.type === "text") total += estimateTextTokens(b.text);
764
+ else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
765
+ else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
766
+ else total += RoughTokenEstimate(JSON.stringify(b));
767
+ }
768
+ return total;
769
+ }
770
+ function estimateMessageTokens(messages) {
771
+ let total = 0;
772
+ for (const m of messages) {
773
+ if (typeof m._estTokens === "number" && m._estTokens > 0) {
774
+ total += m._estTokens;
775
+ continue;
776
+ }
777
+ total += computeMessageTokens(m);
778
+ }
779
+ return total;
780
+ }
781
+ function estimateToolDefTokens(tool) {
782
+ const cached = tool._estDefTokens;
783
+ if (typeof cached === "number" && cached > 0) return cached;
784
+ return RoughTokenEstimate(tool.name) + RoughTokenEstimate(tool.description ?? "") + RoughTokenEstimate(JSON.stringify(tool.inputSchema));
785
+ }
786
+ function estimateRequestTokens(messages, systemPrompt, tools, calibrationKey = CALIBRATION_GLOBAL_KEY) {
787
+ let messagesTokens = 0;
788
+ if (typeof messages === "string") {
789
+ messagesTokens = RoughTokenEstimate(messages);
790
+ } else if (Array.isArray(messages)) {
791
+ for (const m of messages) {
792
+ if (typeof m === "object" && m !== null && "content" in m) {
793
+ const cached = m._estTokens;
794
+ if (typeof cached === "number" && cached > 0) {
795
+ messagesTokens += cached;
796
+ continue;
797
+ }
798
+ const content = m.content;
799
+ if (typeof content === "string") {
800
+ messagesTokens += RoughTokenEstimate(content);
801
+ } else if (Array.isArray(content)) {
802
+ for (const b of content) {
803
+ if (typeof b === "object" && b !== null) {
804
+ if (b.type === "text") {
805
+ messagesTokens += RoughTokenEstimate(b.text);
806
+ } else {
807
+ messagesTokens += RoughTokenEstimate(JSON.stringify(b));
808
+ }
809
+ }
810
+ }
811
+ }
812
+ }
813
+ }
814
+ }
815
+ let systemTokens = 0;
816
+ if (typeof systemPrompt === "string") {
817
+ systemTokens = RoughTokenEstimate(systemPrompt);
818
+ } else if (Array.isArray(systemPrompt)) {
819
+ for (const b of systemPrompt) {
820
+ if (typeof b === "object" && b !== null && b.type === "text") {
821
+ systemTokens += RoughTokenEstimate(b.text);
822
+ }
823
+ }
824
+ }
825
+ let toolsTokens = 0;
826
+ for (const t of tools) {
827
+ toolsTokens += estimateToolDefTokens(t);
828
+ }
829
+ const total = messagesTokens + systemTokens + toolsTokens;
830
+ calState(calibrationKey).prevEst = total;
831
+ return {
832
+ messages: messagesTokens,
833
+ systemPrompt: systemTokens,
834
+ tools: toolsTokens,
835
+ total
836
+ };
837
+ }
838
+ function getCalibrationState(calibrationKey = CALIBRATION_GLOBAL_KEY) {
839
+ const cal = calState(calibrationKey);
840
+ return {
841
+ ratio: cal.ratio,
842
+ count: cal.count,
843
+ calibrated: cal.count >= MIN_SAMPLES_FOR_CALIBRATION
844
+ };
845
+ }
846
+ function estimateRequestTokensCalibrated(messages, systemPrompt, tools, calibrationKey = CALIBRATION_GLOBAL_KEY) {
847
+ const result = estimateRequestTokens(messages, systemPrompt, tools, calibrationKey);
848
+ const cal = calState(calibrationKey);
849
+ if (cal.count >= MIN_SAMPLES_FOR_CALIBRATION) {
850
+ const safeRatio = Math.min(1.5, Math.max(0.5, cal.ratio));
851
+ return {
852
+ messages: Math.round(result.messages * safeRatio),
853
+ systemPrompt: Math.round(result.systemPrompt * safeRatio),
854
+ tools: Math.round(result.tools * safeRatio),
855
+ total: Math.round(result.total * safeRatio)
856
+ };
857
+ }
858
+ return result;
859
+ }
860
+
861
+ // src/utils/json-schema-validate.ts
862
+ function validateAgainstSchema(value, schema) {
863
+ const errors = [];
864
+ walk(value, schema, "", errors);
865
+ return { ok: errors.length === 0, errors };
866
+ }
867
+ function walk(value, schema, path19, errors) {
868
+ if (schema.enum !== void 0) {
869
+ if (!schema.enum.some((e) => deepEqual(e, value))) {
870
+ errors.push({
871
+ path: path19 || "<root>",
872
+ message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
873
+ });
874
+ return;
875
+ }
876
+ }
877
+ if (typeof schema.type === "string") {
878
+ if (!checkType(value, schema.type)) {
879
+ errors.push({
880
+ path: path19 || "<root>",
881
+ message: `expected ${schema.type}, got ${describeType(value)}`
882
+ });
883
+ return;
884
+ }
885
+ }
886
+ if (schema.type === "object" && isPlainObject(value)) {
887
+ const obj = value;
888
+ for (const req of schema.required ?? []) {
889
+ if (!(req in obj)) {
890
+ errors.push({ path: joinPath(path19, req), message: "required property missing" });
891
+ }
892
+ }
893
+ if (schema.properties) {
894
+ for (const [key, subSchema] of Object.entries(schema.properties)) {
895
+ if (key in obj) {
896
+ walk(obj[key], subSchema, joinPath(path19, key), errors);
897
+ }
898
+ }
899
+ }
900
+ }
901
+ if (schema.type === "array" && Array.isArray(value) && schema.items) {
902
+ for (let i = 0; i < value.length; i++) {
903
+ walk(value[i], schema.items, `${path19}[${i}]`, errors);
904
+ }
905
+ }
906
+ }
907
+ function checkType(value, type) {
908
+ switch (type) {
909
+ case "string":
910
+ return typeof value === "string";
911
+ case "number":
912
+ return typeof value === "number" && !Number.isNaN(value);
913
+ case "integer":
914
+ return typeof value === "number" && Number.isInteger(value);
915
+ case "boolean":
916
+ return typeof value === "boolean";
917
+ case "null":
918
+ return value === null;
919
+ case "array":
920
+ return Array.isArray(value);
921
+ case "object":
922
+ return isPlainObject(value);
923
+ default:
924
+ return true;
925
+ }
926
+ }
927
+ function isPlainObject(v) {
928
+ return typeof v === "object" && v !== null && !Array.isArray(v);
929
+ }
930
+ function describeType(v) {
931
+ if (v === null) return "null";
932
+ if (Array.isArray(v)) return "array";
933
+ return typeof v;
934
+ }
935
+ function joinPath(parent, key) {
936
+ if (!parent) return key;
937
+ return `${parent}.${key}`;
938
+ }
939
+ function deepEqual(a, b) {
940
+ if (a === b) return true;
941
+ if (typeof a !== typeof b) return false;
942
+ if (a === null || b === null) return a === b;
943
+ if (Array.isArray(a) && Array.isArray(b)) {
944
+ return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
945
+ }
946
+ if (typeof a === "object" && typeof b === "object") {
947
+ const ak = Object.keys(a);
948
+ const bk = Object.keys(b);
949
+ if (ak.length !== bk.length) return false;
950
+ return ak.every(
951
+ (k) => deepEqual(a[k], b[k])
952
+ );
953
+ }
954
+ return false;
955
+ }
956
+
957
+ // src/utils/regex-guard.ts
958
+ var MAX_PATTERN_LEN = 512;
959
+ var DANGEROUS_PATTERNS = [
960
+ /(\([^)]*[+*][^)]*\))[+*]/,
961
+ // (a+)+, (.*)+, etc
962
+ /(\(\?:[^)]*[+*][^)]*\))[+*]/
963
+ // same, with non-capturing group
964
+ ];
965
+ function compileUserRegex(pattern, flags) {
966
+ if (typeof pattern !== "string") {
967
+ return { ok: false, reason: "pattern must be a string" };
968
+ }
969
+ if (pattern.length === 0) {
970
+ return { ok: false, reason: "pattern is empty" };
971
+ }
972
+ if (pattern.length > MAX_PATTERN_LEN) {
973
+ return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
974
+ }
975
+ for (const rx of DANGEROUS_PATTERNS) {
976
+ if (rx.test(pattern)) {
977
+ return {
978
+ ok: false,
979
+ reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
980
+ };
981
+ }
982
+ }
983
+ try {
984
+ return { ok: true, regex: new RegExp(pattern, flags) };
985
+ } catch (err) {
986
+ return {
987
+ ok: false,
988
+ reason: err instanceof Error ? err.message : "invalid regex"
989
+ };
990
+ }
991
+ }
992
+ var GLOB_CHARS = /* @__PURE__ */ new Set(["*", "?", "["]);
993
+ var IS_WINDOWS = process.platform === "win32";
994
+ var SEP = IS_WINDOWS ? "\\" : "/";
995
+ function isGlob(p) {
996
+ for (const c of p) {
997
+ if (GLOB_CHARS.has(c)) return true;
998
+ }
999
+ return false;
1000
+ }
1001
+ function globToRegex(pat) {
1002
+ let i = 0;
1003
+ let re = "^";
1004
+ while (i < pat.length) {
1005
+ const c = expectDefined(pat[i]);
1006
+ if (c === "*") {
1007
+ if (pat[i + 1] === "*") {
1008
+ re += ".*";
1009
+ i += 2;
1010
+ if (pat[i] === "/") i++;
1011
+ } else {
1012
+ re += "[^/\\\\]*";
1013
+ i++;
1014
+ }
1015
+ } else if (c === "?") {
1016
+ re += "[^/\\\\]";
1017
+ i++;
1018
+ } else if (c === "[") {
1019
+ let cls = "[";
1020
+ i++;
1021
+ if (pat[i] === "!" || pat[i] === "^") {
1022
+ cls += "^";
1023
+ i++;
1024
+ }
1025
+ while (i < pat.length && pat[i] !== "]") {
1026
+ const ch = pat[i] ?? "";
1027
+ if (ch === "\\") cls += "\\\\";
1028
+ else if (ch === "]" || ch === "^") cls += `\\${ch}`;
1029
+ else cls += ch;
1030
+ i++;
1031
+ }
1032
+ cls += "]";
1033
+ re += cls;
1034
+ i++;
1035
+ } else {
1036
+ re += c.replace(/[.+^${}()|\\]/g, "\\$&");
1037
+ i++;
1038
+ }
1039
+ }
1040
+ return new RegExp(re + "$");
1041
+ }
1042
+ function baseDir(pat) {
1043
+ let i = pat.length - 1;
1044
+ while (i >= 0 && !GLOB_CHARS.has(expectDefined(pat[i])) && pat[i] !== SEP && pat[i] !== "/") i--;
1045
+ const cut = i >= 0 ? pat.lastIndexOf(SEP, i) : pat.lastIndexOf("/", i);
1046
+ return cut < 0 ? "." : pat.slice(0, cut);
1047
+ }
1048
+ async function expandGlob(pattern) {
1049
+ if (!isGlob(pattern)) return [pattern];
1050
+ const results = /* @__PURE__ */ new Set();
1051
+ const abs = isAbsolute(pattern);
1052
+ const base = abs ? baseDir(pattern) : baseDir(pattern);
1053
+ const relPat = base === "." ? pattern : pattern.slice(base.length + 1);
1054
+ async function walk3(dir, pat) {
1055
+ let entries;
1056
+ try {
1057
+ entries = await fsp2.readdir(dir);
1058
+ } catch {
1059
+ return;
1060
+ }
1061
+ const firstGlob = pat.search(/[*?[[]/);
1062
+ if (firstGlob < 0) {
1063
+ const re = globToRegex(pat);
1064
+ for (const e of entries) {
1065
+ if (re.test(e)) {
1066
+ const full = `${dir}${SEP}${e}`;
1067
+ results.add(abs ? resolve(full) : full);
1068
+ }
1069
+ }
1070
+ return;
1071
+ }
1072
+ const before = pat.slice(0, firstGlob);
1073
+ const rest = pat.slice(firstGlob);
1074
+ if (before.endsWith("**")) {
1075
+ await walk3(dir, rest);
1076
+ for (const e of entries) {
1077
+ const full = `${dir}${SEP}${e}`;
1078
+ try {
1079
+ const stat6 = await fsp2.stat(full);
1080
+ if (stat6.isDirectory()) await walk3(full, rest);
1081
+ } catch {
1082
+ }
1083
+ }
1084
+ } else if (before === "") {
1085
+ const re = globToRegex(rest);
1086
+ for (const e of entries) {
1087
+ if (re.test(e)) {
1088
+ const full = `${dir}${SEP}${e}`;
1089
+ results.add(abs ? resolve(full) : full);
1090
+ }
1091
+ }
1092
+ } else {
1093
+ const seg = before.replace(/[*?[\]]/g, "").replace(/\/$/, "");
1094
+ if (entries.includes(seg)) {
1095
+ const full = `${dir}${SEP}${seg}`;
1096
+ try {
1097
+ const stat6 = await fsp2.stat(full);
1098
+ if (stat6.isDirectory()) await walk3(full, rest);
1099
+ } catch {
1100
+ }
1101
+ }
1102
+ }
1103
+ }
1104
+ await walk3(base === "." ? "." : base, relPat);
1105
+ return [...results];
1106
+ }
1107
+
1108
+ // src/utils/json-repair.ts
1109
+ function completePartialObject(s) {
1110
+ if (!s.trim().startsWith("{")) return s;
1111
+ if (tryParse(s).ok) return s;
1112
+ return repairTruncated(s);
1113
+ }
1114
+ function repairTruncated(s) {
1115
+ const stack = [];
1116
+ let inString = false;
1117
+ let escaped = false;
1118
+ let sawKey = false;
1119
+ let prevSig = "";
1120
+ let contentEnd = 0;
1121
+ let stringBraceDepth = 0;
1122
+ for (let i = 0; i < s.length; i++) {
1123
+ const ch = expectDefined(s[i]);
1124
+ if (inString) {
1125
+ contentEnd = i + 1;
1126
+ if (escaped) {
1127
+ escaped = false;
1128
+ continue;
1129
+ }
1130
+ if (ch === "\\") {
1131
+ escaped = true;
1132
+ continue;
1133
+ }
1134
+ if (ch === '"') {
1135
+ inString = false;
1136
+ prevSig = '"';
1137
+ stringBraceDepth = 0;
1138
+ continue;
1139
+ }
1140
+ if (ch === "{") stringBraceDepth++;
1141
+ else if (ch === "}" && stringBraceDepth > 0) stringBraceDepth--;
1142
+ continue;
1143
+ }
1144
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") continue;
1145
+ contentEnd = i + 1;
1146
+ if (ch === '"') {
1147
+ inString = true;
1148
+ sawKey = true;
1149
+ stringBraceDepth = 0;
1150
+ prevSig = '"';
1151
+ } else if (ch === "{" || ch === "[") {
1152
+ stack.push(ch);
1153
+ prevSig = ch;
1154
+ } else if (ch === "}" || ch === "]") {
1155
+ stack.pop();
1156
+ prevSig = ch;
1157
+ } else {
1158
+ prevSig = ch;
1159
+ }
1160
+ }
1161
+ if (!sawKey && !inString) return s;
1162
+ let result = s.slice(0, contentEnd);
1163
+ if (inString) {
1164
+ if (escaped) {
1165
+ result = result.slice(0, -1);
1166
+ } else if (endsWithInvalidEscape(result)) {
1167
+ result = result.slice(0, -2);
1168
+ }
1169
+ if (stringBraceDepth > 0) result += "}".repeat(stringBraceDepth);
1170
+ result += '"';
1171
+ } else if (prevSig === ":") {
1172
+ result += "null";
390
1173
  }
391
- return {
392
- messages: changed ? out : messages,
393
- report: { changed, removedToolUses, removedToolResults, removedMessages }
394
- };
1174
+ for (let k = stack.length - 1; k >= 0; k--) {
1175
+ result += stack[k] === "{" ? "}" : "]";
1176
+ }
1177
+ if (!tryParse(result).ok) {
1178
+ const patched = result.replace(/:(\s*)([}\]])/g, ":null$2");
1179
+ if (tryParse(patched).ok) result = patched;
1180
+ }
1181
+ return result;
395
1182
  }
396
- function hasToolUse(msg) {
397
- return contentBlocks(msg).some((b) => b.type === "tool_use");
1183
+ var VALID_ESCAPE = /* @__PURE__ */ new Set(['"', "\\", "/", "b", "f", "n", "r", "t", "u"]);
1184
+ function endsWithInvalidEscape(str) {
1185
+ const last = str[str.length - 1];
1186
+ if (str[str.length - 2] !== "\\" || last === void 0) return false;
1187
+ if (VALID_ESCAPE.has(last)) return false;
1188
+ let backslashes = 0;
1189
+ for (let k = str.length - 2; k >= 0 && str[k] === "\\"; k--) backslashes++;
1190
+ return backslashes % 2 === 1;
398
1191
  }
399
- function hasToolResult(msg) {
400
- return contentBlocks(msg).some((b) => b.type === "tool_result");
1192
+ function tryParse(s) {
1193
+ try {
1194
+ return { ok: true, value: JSON.parse(s) };
1195
+ } catch {
1196
+ return { ok: false };
1197
+ }
401
1198
  }
402
- function toolUseIds(msg) {
403
- const ids = /* @__PURE__ */ new Set();
404
- if (!msg || msg.role !== "assistant") return ids;
405
- for (const block of contentBlocks(msg)) {
406
- if (block.type === "tool_use") ids.add(block.id);
1199
+
1200
+ // src/utils/merge-models-payload.ts
1201
+ function mergeModelsPayload(base, overlay) {
1202
+ const out = {};
1203
+ for (const [id, provider] of Object.entries(base)) {
1204
+ out[id] = cloneProvider(provider);
407
1205
  }
408
- return ids;
1206
+ for (const [id, ovProvider] of Object.entries(overlay)) {
1207
+ const existing = out[id];
1208
+ out[id] = existing ? mergeProvider(existing, ovProvider) : cloneProvider(ovProvider);
1209
+ }
1210
+ return out;
409
1211
  }
410
- function toolResultIds(msg) {
411
- const ids = /* @__PURE__ */ new Set();
412
- if (!msg || msg.role !== "user") return ids;
413
- for (const block of contentBlocks(msg)) {
414
- if (block.type === "tool_result") ids.add(block.tool_use_id);
1212
+ function mergeProvider(base, overlay) {
1213
+ const models = {};
1214
+ for (const [mid, m] of Object.entries(base.models ?? {})) {
1215
+ models[mid] = { ...m };
415
1216
  }
416
- return ids;
1217
+ for (const [mid, ovModel] of Object.entries(overlay.models ?? {})) {
1218
+ const existing = models[mid];
1219
+ models[mid] = existing ? mergeModel(existing, ovModel) : { ...ovModel };
1220
+ }
1221
+ return {
1222
+ ...base,
1223
+ // Overlay scalar fields win when explicitly provided; otherwise keep base.
1224
+ ...stripUndefined({
1225
+ id: overlay.id,
1226
+ name: overlay.name,
1227
+ npm: overlay.npm,
1228
+ api: overlay.api,
1229
+ env: overlay.env,
1230
+ doc: overlay.doc
1231
+ }),
1232
+ models
1233
+ };
417
1234
  }
418
- function contentBlocks(msg) {
419
- return msg && Array.isArray(msg.content) ? msg.content : [];
1235
+ function mergeModel(base, overlay) {
1236
+ const merged = { ...base, ...overlay };
1237
+ if (base.limit || overlay.limit) {
1238
+ merged.limit = { ...base.limit, ...overlay.limit };
1239
+ }
1240
+ if (base.cost || overlay.cost) {
1241
+ merged.cost = { ...base.cost, ...overlay.cost };
1242
+ }
1243
+ if (base.modalities || overlay.modalities) {
1244
+ merged.modalities = { ...base.modalities, ...overlay.modalities };
1245
+ }
1246
+ return merged;
420
1247
  }
421
- function mapContent(msg, fn) {
422
- if (!Array.isArray(msg.content)) return msg;
423
- const next = fn(msg.content);
424
- if (next.length === msg.content.length && next.every((b, idx) => b === msg.content[idx])) {
425
- return msg;
1248
+ function cloneProvider(p) {
1249
+ const models = {};
1250
+ for (const [mid, m] of Object.entries(p.models ?? {})) {
1251
+ models[mid] = { ...m };
426
1252
  }
427
- return { ...msg, content: next };
1253
+ return { ...p, models };
428
1254
  }
429
- function isEmptyMessage(msg) {
430
- if (typeof msg.content === "string") return msg.content.trim().length === 0;
431
- return msg.content.length === 0;
1255
+ function stripUndefined(obj) {
1256
+ const out = {};
1257
+ for (const [k, v] of Object.entries(obj)) {
1258
+ if (v !== void 0) out[k] = v;
1259
+ }
1260
+ return out;
432
1261
  }
433
1262
 
434
1263
  // src/storage/session-store.ts
@@ -446,11 +1275,34 @@ var DefaultSessionStore = class _DefaultSessionStore {
446
1275
  dir;
447
1276
  events;
448
1277
  secretScrubber;
1278
+ /**
1279
+ * In-memory cache for load() results, keyed by session ID. The cache is
1280
+ * invalidated when the file's mtimeMs or size changes (indicating the
1281
+ * file was written to). This eliminates redundant full-file reads and
1282
+ * JSON parses when the same session is loaded multiple times within the
1283
+ * store's lifetime (e.g., webui session detail views, list() fallbacks).
1284
+ *
1285
+ * Max size is capped to prevent unbounded memory growth in long-running
1286
+ * processes. When the limit is reached, the oldest entry is evicted.
1287
+ */
1288
+ _loadCache = /* @__PURE__ */ new Map();
1289
+ static LOAD_CACHE_MAX_ENTRIES = 50;
449
1290
  constructor(opts) {
450
1291
  this.dir = opts.dir;
451
1292
  this.events = opts.events;
452
1293
  this.secretScrubber = opts.secretScrubber;
453
1294
  }
1295
+ /**
1296
+ * Clear the load() cache. Useful for testing or when the caller knows
1297
+ * the file has changed externally (e.g., another process wrote to it).
1298
+ */
1299
+ clearLoadCache(sessionId) {
1300
+ if (sessionId !== void 0) {
1301
+ this._loadCache.delete(sessionId);
1302
+ } else {
1303
+ this._loadCache.clear();
1304
+ }
1305
+ }
454
1306
  // ── Storage event helpers ───────────────────────────────────────────────────
455
1307
  emitRead(sessionId, filePath, operation, outcome, durationMs, error) {
456
1308
  this.events?.emit("storage.read", {
@@ -487,11 +1339,11 @@ var DefaultSessionStore = class _DefaultSessionStore {
487
1339
  }
488
1340
  /** Absolute path to the session index file. */
489
1341
  get indexFile() {
490
- return path11.join(this.dir, "_index.jsonl");
1342
+ return path3.join(this.dir, "_index.jsonl");
491
1343
  }
492
1344
  /** Join session ID to its absolute path within the store directory. */
493
1345
  sessionPath(id, ext) {
494
- return path11.join(this.dir, `${id}${ext}`);
1346
+ return path3.join(this.dir, `${id}${ext}`);
495
1347
  }
496
1348
  /**
497
1349
  * Ensure the directory implied by the session ID exists. When the ID
@@ -499,7 +1351,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
499
1351
  * subdirectory so sessions group naturally by day.
500
1352
  */
501
1353
  async ensureShardDir(id) {
502
- const dirPath = path11.dirname(path11.join(this.dir, id));
1354
+ const dirPath = path3.dirname(path3.join(this.dir, id));
503
1355
  await ensureDir(dirPath);
504
1356
  return dirPath;
505
1357
  }
@@ -507,15 +1359,15 @@ var DefaultSessionStore = class _DefaultSessionStore {
507
1359
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
508
1360
  const id = meta.id && meta.id.length > 0 ? meta.id : generateSessionId(startedAt, meta.model ?? meta.provider);
509
1361
  const shardDir = await this.ensureShardDir(id);
510
- const file = path11.join(shardDir, `${path11.basename(id)}.jsonl`);
1362
+ const file = path3.join(shardDir, `${path3.basename(id)}.jsonl`);
511
1363
  const t0 = Date.now();
512
1364
  let handle;
513
1365
  try {
514
- handle = await fsp.open(file, "a", 384);
1366
+ handle = await fsp2.open(file, "a", 384);
515
1367
  } catch (err) {
516
- this.emitError(id, file, "create", err instanceof Error ? err.message : String(err), false);
1368
+ this.emitError(id, file, "create", toErrorMessage(err), false);
517
1369
  throw new Error(
518
- `Failed to open session file: ${err instanceof Error ? err.message : String(err)}`,
1370
+ `Failed to open session file: ${toErrorMessage(err)}`,
519
1371
  { cause: err }
520
1372
  );
521
1373
  }
@@ -535,7 +1387,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
535
1387
  message: e instanceof Error ? e.message : String(e),
536
1388
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
537
1389
  })));
538
- this.emitError(id, file, "create", err instanceof Error ? err.message : String(err), true);
1390
+ this.emitError(id, file, "create", toErrorMessage(err), true);
539
1391
  throw err;
540
1392
  }
541
1393
  }
@@ -545,11 +1397,11 @@ var DefaultSessionStore = class _DefaultSessionStore {
545
1397
  const data = await this.load(id);
546
1398
  let handle;
547
1399
  try {
548
- handle = await fsp.open(file, "a", 384);
1400
+ handle = await fsp2.open(file, "a", 384);
549
1401
  } catch (err) {
550
- this.emitError(id, file, "resume", err instanceof Error ? err.message : String(err), false);
1402
+ this.emitError(id, file, "resume", toErrorMessage(err), false);
551
1403
  throw new Error(
552
- `Failed to open session "${id}" for append: ${err instanceof Error ? err.message : String(err)}`,
1404
+ `Failed to open session "${id}" for append: ${toErrorMessage(err)}`,
553
1405
  { cause: err }
554
1406
  );
555
1407
  }
@@ -569,7 +1421,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
569
1421
  // Shard directory (sessions/<date>/) — must match create() so the
570
1422
  // .summary.json sidecar lands next to the JSONL instead of the
571
1423
  // sessions root (where summaryFor() would never find it).
572
- dir: path11.dirname(file),
1424
+ dir: path3.dirname(file),
573
1425
  filePath: file,
574
1426
  secretScrubber: this.secretScrubber,
575
1427
  onClose: (s) => this.appendToIndex(s)
@@ -584,7 +1436,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
584
1436
  message: e instanceof Error ? e.message : String(e),
585
1437
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
586
1438
  })));
587
- this.emitError(id, file, "resume", err instanceof Error ? err.message : String(err), true);
1439
+ this.emitError(id, file, "resume", toErrorMessage(err), true);
588
1440
  throw err;
589
1441
  }
590
1442
  }
@@ -593,8 +1445,21 @@ var DefaultSessionStore = class _DefaultSessionStore {
593
1445
  const t0 = Date.now();
594
1446
  let outcome = "success";
595
1447
  let errorMsg;
1448
+ let cacheHit = false;
596
1449
  try {
597
- const raw = await fsp.readFile(file, "utf8");
1450
+ let stat6;
1451
+ try {
1452
+ const s = await fsp2.stat(file);
1453
+ stat6 = { mtimeMs: s.mtimeMs, size: s.size };
1454
+ } catch (err) {
1455
+ throw err;
1456
+ }
1457
+ const cached = this._loadCache.get(id);
1458
+ if (cached && cached.mtimeMs === stat6.mtimeMs && cached.size === stat6.size) {
1459
+ cacheHit = true;
1460
+ return cached.data;
1461
+ }
1462
+ const raw = await fsp2.readFile(file, "utf8");
598
1463
  const lines = raw.split("\n").filter((l) => l.trim());
599
1464
  const events = [];
600
1465
  for (const line of lines) {
@@ -609,13 +1474,30 @@ var DefaultSessionStore = class _DefaultSessionStore {
609
1474
  const meta = this.metaFromEvents(id, events);
610
1475
  const { messages, usage } = this.replay(events, id);
611
1476
  const toolCallEnds = extractToolCallEnds(events);
612
- return { metadata: meta, events, messages, usage, toolCallEnds };
1477
+ const data = { metadata: meta, events, messages, usage, toolCallEnds };
1478
+ if (this._loadCache.size >= _DefaultSessionStore.LOAD_CACHE_MAX_ENTRIES) {
1479
+ const oldest = this._loadCache.keys().next().value;
1480
+ if (oldest !== void 0) {
1481
+ this._loadCache.delete(oldest);
1482
+ }
1483
+ }
1484
+ this._loadCache.set(id, { mtimeMs: stat6.mtimeMs, size: stat6.size, data });
1485
+ return data;
613
1486
  } catch (err) {
614
1487
  outcome = "failure";
615
- errorMsg = err instanceof Error ? err.message : String(err);
1488
+ errorMsg = toErrorMessage(err);
616
1489
  throw err;
617
1490
  } finally {
618
1491
  this.emitRead(id, file, "load", outcome, Date.now() - t0, errorMsg);
1492
+ if (cacheHit) {
1493
+ this.events?.emit("storage.cache_hit", {
1494
+ sessionId: id,
1495
+ store: "session",
1496
+ filePath: file,
1497
+ operation: "load",
1498
+ durationMs: Date.now() - t0
1499
+ });
1500
+ }
619
1501
  }
620
1502
  }
621
1503
  async list(limit = 20) {
@@ -659,7 +1541,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
659
1541
  try {
660
1542
  await ensureDir(this.dir);
661
1543
  const line = JSON.stringify(summary) + "\n";
662
- await fsp.appendFile(this.indexFile, line, "utf8");
1544
+ await fsp2.appendFile(this.indexFile, line, "utf8");
663
1545
  this.indexAppendCount++;
664
1546
  if (this.indexAppendCount >= _DefaultSessionStore.COMPACT_EVERY) {
665
1547
  await this.compactIndex();
@@ -673,7 +1555,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
673
1555
  try {
674
1556
  await ensureDir(this.dir);
675
1557
  const line = JSON.stringify({ action: "delete", id }) + "\n";
676
- await fsp.appendFile(this.indexFile, line, "utf8");
1558
+ await fsp2.appendFile(this.indexFile, line, "utf8");
677
1559
  this.indexAppendCount++;
678
1560
  } catch {
679
1561
  }
@@ -691,11 +1573,11 @@ var DefaultSessionStore = class _DefaultSessionStore {
691
1573
  if (entries.length === 0) return;
692
1574
  const tmp = `${this.indexFile}.compact.tmp`;
693
1575
  const lines = entries.map((s) => JSON.stringify(s)).join("\n") + "\n";
694
- await fsp.writeFile(tmp, lines, "utf8");
695
- await fsp.rename(tmp, this.indexFile);
1576
+ await fsp2.writeFile(tmp, lines, "utf8");
1577
+ await fsp2.rename(tmp, this.indexFile);
696
1578
  } catch (err) {
697
1579
  outcome = "failure";
698
- errorMsg = err instanceof Error ? err.message : String(err);
1580
+ errorMsg = toErrorMessage(err);
699
1581
  } finally {
700
1582
  this.emitWrite("~compact~", this.indexFile, "compact", outcome, Date.now() - t0, void 0, errorMsg);
701
1583
  }
@@ -708,7 +1590,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
708
1590
  async readIndex() {
709
1591
  let raw;
710
1592
  try {
711
- raw = await fsp.readFile(this.indexFile, "utf8");
1593
+ raw = await fsp2.readFile(this.indexFile, "utf8");
712
1594
  } catch {
713
1595
  return [];
714
1596
  }
@@ -741,8 +1623,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
741
1623
  const valid = summaries.filter((s) => s !== null);
742
1624
  const tmp = `${this.indexFile}.tmp`;
743
1625
  const lines = valid.map((s) => JSON.stringify(s)).join("\n") + "\n";
744
- await fsp.writeFile(tmp, lines, "utf8");
745
- await fsp.rename(tmp, this.indexFile);
1626
+ await fsp2.writeFile(tmp, lines, "utf8");
1627
+ await fsp2.rename(tmp, this.indexFile);
746
1628
  return valid.length;
747
1629
  }
748
1630
  /** Recursively collect session IDs from date-shard subdirectories.
@@ -753,7 +1635,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
753
1635
  const ids = [];
754
1636
  let entries;
755
1637
  try {
756
- entries = await fsp.readdir(dir, { withFileTypes: true });
1638
+ entries = await fsp2.readdir(dir, { withFileTypes: true });
757
1639
  } catch {
758
1640
  return ids;
759
1641
  }
@@ -763,7 +1645,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
763
1645
  continue;
764
1646
  if (entry.isDirectory()) {
765
1647
  const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
766
- ids.push(...await this.collectSessionIds(path11.join(dir, entry.name), childPrefix, depth + 1));
1648
+ ids.push(...await this.collectSessionIds(path3.join(dir, entry.name), childPrefix, depth + 1));
767
1649
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
768
1650
  if (entry.name === "_index.jsonl") continue;
769
1651
  const base = entry.name.replace(/\.jsonl$/, "");
@@ -778,15 +1660,15 @@ var DefaultSessionStore = class _DefaultSessionStore {
778
1660
  let outcome = "success";
779
1661
  let errorMsg;
780
1662
  try {
781
- const raw = await fsp.readFile(manifest, "utf8");
1663
+ const raw = await fsp2.readFile(manifest, "utf8");
782
1664
  this.emitRead(id, manifest, "summary", "success", Date.now() - t0);
783
1665
  return JSON.parse(raw);
784
1666
  } catch {
785
1667
  const full = this.sessionPath(id, ".jsonl");
786
- const stat6 = await fsp.stat(full);
1668
+ const stat6 = await fsp2.stat(full);
787
1669
  const summary = await this.summarize(id, stat6.mtime.toISOString());
788
1670
  await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
789
- const msg = err instanceof Error ? err.message : String(err);
1671
+ const msg = toErrorMessage(err);
790
1672
  this.emitError(id, manifest, "summary_fallback", msg, true);
791
1673
  console.warn(JSON.stringify({
792
1674
  level: "warn",
@@ -814,14 +1696,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
814
1696
  async deleteSession(id) {
815
1697
  const jsonlPath = this.sessionPath(id, ".jsonl");
816
1698
  const summaryPath = this.sessionPath(id, ".summary.json");
817
- const shardDir = path11.dirname(path11.join(this.dir, id));
818
- const base = path11.basename(id);
819
- const sessDir = path11.join(shardDir, base);
1699
+ const shardDir = path3.dirname(path3.join(this.dir, id));
1700
+ const base = path3.basename(id);
1701
+ const sessDir = path3.join(shardDir, base);
820
1702
  const deletions = [
821
- fsp.unlink(jsonlPath),
822
- fsp.unlink(summaryPath),
823
- fsp.unlink(path11.join(shardDir, `${base}.plan.json`)),
824
- fsp.unlink(path11.join(shardDir, `${base}.todos.json`))
1703
+ fsp2.unlink(jsonlPath),
1704
+ fsp2.unlink(summaryPath),
1705
+ fsp2.unlink(path3.join(shardDir, `${base}.plan.json`)),
1706
+ fsp2.unlink(path3.join(shardDir, `${base}.todos.json`))
825
1707
  ];
826
1708
  const results = await Promise.allSettled(deletions);
827
1709
  for (const r of results) {
@@ -838,12 +1720,12 @@ var DefaultSessionStore = class _DefaultSessionStore {
838
1720
  }
839
1721
  }
840
1722
  }
841
- await fsp.rm(sessDir, { recursive: true, force: true }).catch((err) => {
1723
+ await fsp2.rm(sessDir, { recursive: true, force: true }).catch((err) => {
842
1724
  console.warn(JSON.stringify({
843
1725
  level: "warn",
844
1726
  event: "session_store.rmdir_failed",
845
1727
  sessionId: id,
846
- message: err instanceof Error ? err.message : String(err),
1728
+ message: toErrorMessage(err),
847
1729
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
848
1730
  }));
849
1731
  });
@@ -857,16 +1739,16 @@ var DefaultSessionStore = class _DefaultSessionStore {
857
1739
  let deleted = 0;
858
1740
  let activeSessionId = null;
859
1741
  try {
860
- const raw = await fsp.readFile(path11.join(this.dir, "active.json"), "utf8");
1742
+ const raw = await fsp2.readFile(path3.join(this.dir, "active.json"), "utf8");
861
1743
  const active = JSON.parse(raw);
862
1744
  activeSessionId = active.sessionId ?? null;
863
1745
  } catch {
864
1746
  }
865
1747
  const isPrunableJsonl = (name) => name.endsWith(".jsonl") && name !== "_index.jsonl" && name !== "_mailbox.jsonl" && !name.endsWith(".replay.jsonl") && !name.endsWith(".audit.jsonl");
866
1748
  const pruneFile = async (dir, name, prefix) => {
867
- const jsonlPath = path11.join(dir, name);
1749
+ const jsonlPath = path3.join(dir, name);
868
1750
  try {
869
- const stat6 = await fsp.stat(jsonlPath);
1751
+ const stat6 = await fsp2.stat(jsonlPath);
870
1752
  if (stat6.mtimeMs >= cutoff) return;
871
1753
  } catch {
872
1754
  return;
@@ -877,15 +1759,15 @@ var DefaultSessionStore = class _DefaultSessionStore {
877
1759
  await this.deleteSession(id);
878
1760
  deleted++;
879
1761
  };
880
- const entries = await fsp.readdir(this.dir, { withFileTypes: true }).catch(() => []);
1762
+ const entries = await fsp2.readdir(this.dir, { withFileTypes: true }).catch(() => []);
881
1763
  for (const entry of entries) {
882
1764
  if (entry.isFile()) {
883
1765
  if (isPrunableJsonl(entry.name)) await pruneFile(this.dir, entry.name, "");
884
1766
  continue;
885
1767
  }
886
1768
  if (!entry.isDirectory()) continue;
887
- const dateDir = path11.join(this.dir, entry.name);
888
- const files = await fsp.readdir(dateDir, { withFileTypes: true }).catch(() => []);
1769
+ const dateDir = path3.join(this.dir, entry.name);
1770
+ const files = await fsp2.readdir(dateDir, { withFileTypes: true }).catch(() => []);
889
1771
  for (const file of files) {
890
1772
  if (!file.isFile() || !isPrunableJsonl(file.name)) continue;
891
1773
  await pruneFile(dateDir, file.name, entry.name);
@@ -896,11 +1778,11 @@ var DefaultSessionStore = class _DefaultSessionStore {
896
1778
  }
897
1779
  for (const entry of entries) {
898
1780
  if (!entry.isDirectory()) continue;
899
- const dateDir = path11.join(this.dir, entry.name);
1781
+ const dateDir = path3.join(this.dir, entry.name);
900
1782
  try {
901
- const remaining = await fsp.readdir(dateDir);
1783
+ const remaining = await fsp2.readdir(dateDir);
902
1784
  if (remaining.length === 0) {
903
- await fsp.rmdir(dateDir).catch(() => void 0);
1785
+ await fsp2.rmdir(dateDir).catch(() => void 0);
904
1786
  }
905
1787
  } catch {
906
1788
  }
@@ -919,8 +1801,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
919
1801
  provider: "unknown"
920
1802
  })}
921
1803
  `;
922
- await fsp.writeFile(file, record, "utf8");
923
- await fsp.unlink(meta).catch(() => void 0);
1804
+ await fsp2.writeFile(file, record, "utf8");
1805
+ await fsp2.unlink(meta).catch(() => void 0);
924
1806
  }
925
1807
  async summarize(id, mtime) {
926
1808
  try {
@@ -1071,7 +1953,7 @@ var FileSessionWriter = class _FileSessionWriter {
1071
1953
  this.meta = meta;
1072
1954
  this.events = events;
1073
1955
  this.resumed = opts.resumed ?? false;
1074
- this.manifestFile = opts.dir ? path11.join(opts.dir, `${path11.basename(id)}.summary.json`) : "";
1956
+ this.manifestFile = opts.dir ? path3.join(opts.dir, `${path3.basename(id)}.summary.json`) : "";
1075
1957
  this.filePath = opts.filePath ?? "";
1076
1958
  this.secretScrubber = opts.secretScrubber;
1077
1959
  this.onCloseCb = opts.onClose;
@@ -1278,7 +2160,7 @@ var FileSessionWriter = class _FileSessionWriter {
1278
2160
  await this.enqueueWrite(batch);
1279
2161
  } catch (err) {
1280
2162
  outcome = "failure";
1281
- errorMsg = err instanceof Error ? err.message : String(err);
2163
+ errorMsg = toErrorMessage(err);
1282
2164
  this.appendFailCount += eventCount;
1283
2165
  const now = Date.now();
1284
2166
  if (now - this.lastAppendWarnAt > 5e3) {
@@ -1286,7 +2168,7 @@ var FileSessionWriter = class _FileSessionWriter {
1286
2168
  const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
1287
2169
  console.warn(
1288
2170
  "[session] flush failed:",
1289
- err instanceof Error ? err.message : String(err),
2171
+ toErrorMessage(err),
1290
2172
  tail
1291
2173
  );
1292
2174
  this.lastAppendWarnAt = now;
@@ -1376,7 +2258,7 @@ var FileSessionWriter = class _FileSessionWriter {
1376
2258
  await atomicWrite(this.manifestFile, JSON.stringify(this.summary), { mode: 384 });
1377
2259
  } catch (err) {
1378
2260
  outcome = "failure";
1379
- errorMsg = err instanceof Error ? err.message : String(err);
2261
+ errorMsg = toErrorMessage(err);
1380
2262
  } finally {
1381
2263
  this.events?.emit("storage.write", {
1382
2264
  sessionId: this.id,
@@ -1397,7 +2279,7 @@ var FileSessionWriter = class _FileSessionWriter {
1397
2279
  await this.onCloseCb?.(this.summary);
1398
2280
  } catch (err) {
1399
2281
  idxOutcome = "failure";
1400
- idxError = err instanceof Error ? err.message : String(err);
2282
+ idxError = toErrorMessage(err);
1401
2283
  } finally {
1402
2284
  this.events?.emit("storage.write", {
1403
2285
  sessionId: this.summary.id,
@@ -1450,7 +2332,7 @@ var FileSessionWriter = class _FileSessionWriter {
1450
2332
  }
1451
2333
  await this.flushBuffer();
1452
2334
  await this.writeChain;
1453
- const raw = await fsp.readFile(this.filePath, "utf8");
2335
+ const raw = await fsp2.readFile(this.filePath, "utf8");
1454
2336
  const lines = raw.split("\n");
1455
2337
  const kept = [];
1456
2338
  let removedCount = 0;
@@ -1488,13 +2370,13 @@ var FileSessionWriter = class _FileSessionWriter {
1488
2370
  }
1489
2371
  const truncated = kept.join("\n");
1490
2372
  const tmpPath = `${this.filePath}.rewind.tmp`;
1491
- await fsp.writeFile(tmpPath, truncated + "\n", "utf8");
2373
+ await fsp2.writeFile(tmpPath, truncated + "\n", "utf8");
1492
2374
  try {
1493
2375
  await this.handle.close();
1494
- await fsp.rename(tmpPath, this.filePath);
1495
- this.handle = await fsp.open(this.filePath, "a", 384);
2376
+ await fsp2.rename(tmpPath, this.filePath);
2377
+ this.handle = await fsp2.open(this.filePath, "a", 384);
1496
2378
  } catch (err) {
1497
- await fsp.unlink(tmpPath).catch(() => void 0);
2379
+ await fsp2.unlink(tmpPath).catch(() => void 0);
1498
2380
  throw err;
1499
2381
  }
1500
2382
  await this.append({
@@ -1526,7 +2408,7 @@ var FileSessionWriter = class _FileSessionWriter {
1526
2408
  provider: this.meta.provider ?? "unknown"
1527
2409
  })}
1528
2410
  `;
1529
- await fsp.writeFile(this.filePath, record, "utf8");
2411
+ await fsp2.writeFile(this.filePath, record, "utf8");
1530
2412
  }
1531
2413
  /**
1532
2414
  * Idea #1 — write an in-flight marker. The agent loop should call
@@ -1575,7 +2457,7 @@ var QueueStore = class {
1575
2457
  events;
1576
2458
  traceId;
1577
2459
  constructor(opts) {
1578
- this.file = path11.join(opts.dir, "queue.json");
2460
+ this.file = path3.join(opts.dir, "queue.json");
1579
2461
  this.events = opts.events;
1580
2462
  this.traceId = opts.traceId;
1581
2463
  }
@@ -1603,7 +2485,7 @@ var QueueStore = class {
1603
2485
  filePath: this.file,
1604
2486
  operation: "write",
1605
2487
  outcome: "failure",
1606
- error: err instanceof Error ? err.message : String(err),
2488
+ error: toErrorMessage(err),
1607
2489
  recoverable: false,
1608
2490
  ...this.traceId !== void 0 && { traceId: this.traceId }
1609
2491
  });
@@ -1611,7 +2493,7 @@ var QueueStore = class {
1611
2493
  level: "warn",
1612
2494
  event: "queue_store.write_failed",
1613
2495
  path: this.file,
1614
- message: err instanceof Error ? err.message : String(err),
2496
+ message: toErrorMessage(err),
1615
2497
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1616
2498
  }));
1617
2499
  }
@@ -1620,7 +2502,7 @@ var QueueStore = class {
1620
2502
  const t0 = Date.now();
1621
2503
  let raw;
1622
2504
  try {
1623
- raw = await fsp.readFile(this.file, "utf8");
2505
+ raw = await fsp2.readFile(this.file, "utf8");
1624
2506
  } catch (err) {
1625
2507
  const code = err.code;
1626
2508
  if (code === "ENOENT") {
@@ -1641,7 +2523,7 @@ var QueueStore = class {
1641
2523
  filePath: this.file,
1642
2524
  operation: "read",
1643
2525
  outcome: "failure",
1644
- error: err instanceof Error ? err.message : String(err),
2526
+ error: toErrorMessage(err),
1645
2527
  recoverable: true,
1646
2528
  ...this.traceId !== void 0 && { traceId: this.traceId }
1647
2529
  });
@@ -1649,7 +2531,7 @@ var QueueStore = class {
1649
2531
  level: "warn",
1650
2532
  event: "queue_store.read_failed",
1651
2533
  path: this.file,
1652
- message: err instanceof Error ? err.message : String(err),
2534
+ message: toErrorMessage(err),
1653
2535
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1654
2536
  }));
1655
2537
  return [];
@@ -1701,7 +2583,7 @@ var QueueStore = class {
1701
2583
  async clear() {
1702
2584
  const t0 = Date.now();
1703
2585
  try {
1704
- await fsp.unlink(this.file);
2586
+ await fsp2.unlink(this.file);
1705
2587
  this.events?.emit("storage.write", {
1706
2588
  sessionId: this.traceId ?? "~boot~",
1707
2589
  store: "queue",
@@ -1720,7 +2602,7 @@ var QueueStore = class {
1720
2602
  filePath: this.file,
1721
2603
  operation: "clear",
1722
2604
  outcome: "failure",
1723
- error: err instanceof Error ? err.message : String(err),
2605
+ error: toErrorMessage(err),
1724
2606
  recoverable: true,
1725
2607
  ...this.traceId !== void 0 && { traceId: this.traceId }
1726
2608
  });
@@ -1761,8 +2643,8 @@ var DefaultAttachmentStore = class {
1761
2643
  let spooledPath;
1762
2644
  let data = input.data;
1763
2645
  if (this.spoolDir && bytes >= this.spoolThreshold) {
1764
- await fsp.mkdir(this.spoolDir, { recursive: true });
1765
- spooledPath = path11.join(this.spoolDir, `${id}.bin`);
2646
+ await fsp2.mkdir(this.spoolDir, { recursive: true });
2647
+ spooledPath = path3.join(this.spoolDir, `${id}.bin`);
1766
2648
  await atomicWrite(spooledPath, input.data, {
1767
2649
  encoding: input.kind === "image" ? "base64" : "utf8"
1768
2650
  });
@@ -1824,7 +2706,7 @@ var DefaultAttachmentStore = class {
1824
2706
  for (const att of this.items.values()) {
1825
2707
  if (att.path) toDelete.push(att.path);
1826
2708
  }
1827
- await Promise.all(toDelete.map((p) => fsp.unlink(p).catch(() => void 0)));
2709
+ await Promise.all(toDelete.map((p) => fsp2.unlink(p).catch(() => void 0)));
1828
2710
  }
1829
2711
  this.items.clear();
1830
2712
  this.refs.length = 0;
@@ -1832,7 +2714,7 @@ var DefaultAttachmentStore = class {
1832
2714
  }
1833
2715
  async toBlock(att) {
1834
2716
  if (att.kind === "image") {
1835
- const data = att.data ?? (att.path ? await fsp.readFile(att.path, { encoding: "base64" }) : "");
2717
+ const data = att.data ?? (att.path ? await fsp2.readFile(att.path, { encoding: "base64" }) : "");
1836
2718
  return {
1837
2719
  type: "image",
1838
2720
  source: {
@@ -1842,7 +2724,7 @@ var DefaultAttachmentStore = class {
1842
2724
  }
1843
2725
  };
1844
2726
  }
1845
- const raw = att.data ?? (att.path ? await fsp.readFile(att.path, "utf8") : "");
2727
+ const raw = att.data ?? (att.path ? await fsp2.readFile(att.path, "utf8") : "");
1846
2728
  const label = att.meta.filename ? `<file path="${att.meta.filename}">` : "<pasted>";
1847
2729
  const close = att.meta.filename ? "</file>" : "</pasted>";
1848
2730
  return { type: "text", text: `${label}
@@ -1974,10 +2856,10 @@ var FileMemoryBackend = class {
1974
2856
  }
1975
2857
  async remember(scope, entry, filePath) {
1976
2858
  const file = this.resolveFile(filePath, scope);
1977
- await ensureDir(path11.dirname(file));
2859
+ await ensureDir(path3.dirname(file));
1978
2860
  let existing = "";
1979
2861
  try {
1980
- existing = await fsp.readFile(file, "utf8");
2862
+ existing = await fsp2.readFile(file, "utf8");
1981
2863
  } catch {
1982
2864
  }
1983
2865
  const id = `mem_${Date.now()}_${randomUUID().slice(0, 8)}`;
@@ -1994,7 +2876,7 @@ ${line}`;
1994
2876
  return withFileLock(file, async () => {
1995
2877
  let existing;
1996
2878
  try {
1997
- existing = await fsp.readFile(file, "utf8");
2879
+ existing = await fsp2.readFile(file, "utf8");
1998
2880
  } catch {
1999
2881
  return 0;
2000
2882
  }
@@ -2030,7 +2912,7 @@ ${line}`;
2030
2912
  async readAll(scope, filePath) {
2031
2913
  const file = this.resolveFile(filePath, scope);
2032
2914
  try {
2033
- return await fsp.readFile(file, "utf8");
2915
+ return await fsp2.readFile(file, "utf8");
2034
2916
  } catch {
2035
2917
  return "";
2036
2918
  }
@@ -2065,7 +2947,7 @@ ${line}`;
2065
2947
  const file = this.resolveFile(filePath, scope);
2066
2948
  let existing;
2067
2949
  try {
2068
- existing = await fsp.readFile(file, "utf8");
2950
+ existing = await fsp2.readFile(file, "utf8");
2069
2951
  } catch {
2070
2952
  return 0;
2071
2953
  }
@@ -2085,7 +2967,7 @@ ${line}`;
2085
2967
  const next = lines.join("\n");
2086
2968
  const backup = `${file}.bak.${Date.now()}`;
2087
2969
  try {
2088
- await fsp.copyFile(file, backup);
2970
+ await fsp2.copyFile(file, backup);
2089
2971
  } catch {
2090
2972
  }
2091
2973
  try {
@@ -2202,7 +3084,7 @@ ${body.trim()}`);
2202
3084
  operation: "readAll",
2203
3085
  outcome: "failure",
2204
3086
  durationMs: dur,
2205
- error: err instanceof Error ? err.message : String(err),
3087
+ error: toErrorMessage(err),
2206
3088
  ...this.traceId !== void 0 && { traceId: this.traceId }
2207
3089
  });
2208
3090
  throw err;
@@ -2235,7 +3117,7 @@ ${body.trim()}`);
2235
3117
  operation: "read",
2236
3118
  outcome: "failure",
2237
3119
  durationMs: dur,
2238
- error: err instanceof Error ? err.message : String(err),
3120
+ error: toErrorMessage(err),
2239
3121
  ...this.traceId !== void 0 && { traceId: this.traceId }
2240
3122
  });
2241
3123
  throw err;
@@ -2288,7 +3170,7 @@ ${body.trim()}`);
2288
3170
  operation: "remember",
2289
3171
  outcome: "failure",
2290
3172
  durationMs: dur,
2291
- error: err instanceof Error ? err.message : String(err),
3173
+ error: toErrorMessage(err),
2292
3174
  ...this.traceId !== void 0 && { traceId: this.traceId }
2293
3175
  });
2294
3176
  throw err;
@@ -2444,7 +3326,7 @@ ${body.trim()}`);
2444
3326
  operation: "forget",
2445
3327
  outcome: "failure",
2446
3328
  durationMs: dur,
2447
- error: err instanceof Error ? err.message : String(err),
3329
+ error: toErrorMessage(err),
2448
3330
  ...this.traceId !== void 0 && { traceId: this.traceId }
2449
3331
  });
2450
3332
  throw err;
@@ -2486,7 +3368,7 @@ ${body.trim()}`);
2486
3368
  operation: "consolidate",
2487
3369
  outcome: "failure",
2488
3370
  durationMs: dur,
2489
- error: err instanceof Error ? err.message : String(err),
3371
+ error: toErrorMessage(err),
2490
3372
  ...this.traceId !== void 0 && { traceId: this.traceId }
2491
3373
  });
2492
3374
  throw err;
@@ -2526,7 +3408,7 @@ ${body.trim()}`);
2526
3408
  operation: "clear",
2527
3409
  outcome: "failure",
2528
3410
  durationMs: dur,
2529
- error: err instanceof Error ? err.message : String(err),
3411
+ error: toErrorMessage(err),
2530
3412
  ...this.traceId !== void 0 && { traceId: this.traceId }
2531
3413
  });
2532
3414
  throw err;
@@ -2562,7 +3444,7 @@ ${body.trim()}`);
2562
3444
  operation: "clear",
2563
3445
  outcome: "failure",
2564
3446
  durationMs: dur,
2565
- error: err instanceof Error ? err.message : String(err),
3447
+ error: toErrorMessage(err),
2566
3448
  ...this.traceId !== void 0 && { traceId: this.traceId }
2567
3449
  });
2568
3450
  throw err;
@@ -2722,7 +3604,7 @@ var AgentError = class extends WrongStackError {
2722
3604
  };
2723
3605
  function toWrongStackError(err, code = ERROR_CODES.AGENT_RUN_FAILED) {
2724
3606
  if (err instanceof WrongStackError) return err;
2725
- const message = err instanceof Error ? err.message : String(err);
3607
+ const message = toErrorMessage(err);
2726
3608
  return new AgentError({
2727
3609
  message,
2728
3610
  code: code === "UNKNOWN" ? ERROR_CODES.AGENT_RUN_FAILED : code,
@@ -2806,7 +3688,7 @@ var DefaultConfigStore = class {
2806
3688
  console.error(JSON.stringify({
2807
3689
  level: "error",
2808
3690
  event: "config_store.watcher_threw",
2809
- message: err instanceof Error ? err.message : String(err),
3691
+ message: toErrorMessage(err),
2810
3692
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
2811
3693
  }));
2812
3694
  }
@@ -2832,80 +3714,24 @@ function deepFreeze(obj) {
2832
3714
  }
2833
3715
 
2834
3716
  // src/types/secret-vault.ts
2835
- var ENCRYPTED_PREFIX = "enc:v1:";
2836
-
2837
- // src/security/secret-vault.ts
2838
- init_atomic_write();
2839
-
2840
- // src/utils/deep-merge.ts
2841
- var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set([
2842
- "__proto__",
2843
- "constructor",
2844
- "prototype",
2845
- "__defineGetter__",
2846
- "__defineSetter__",
2847
- "__lookupGetter__",
2848
- "__lookupSetter__"
2849
- ]);
2850
- function isPrimitiveArray(a) {
2851
- return a.every((v) => v === null || typeof v !== "object" && typeof v !== "function");
2852
- }
2853
- function deepMerge(base, patch, options = {}) {
2854
- const {
2855
- conflictResolution = "prefer-patch",
2856
- arrayMode = "replace",
2857
- protectProto = true,
2858
- onNonPrimitiveArrayReplace
2859
- } = options;
2860
- if (typeof base !== "object" || base === null) {
2861
- return conflictResolution === "prefer-patch" ? patch : base;
2862
- }
2863
- if (typeof patch !== "object" || patch === null) {
2864
- return conflictResolution === "prefer-patch" ? patch : base;
2865
- }
2866
- if (Array.isArray(base) && Array.isArray(patch)) {
2867
- if (arrayMode === "concat-primitives" && isPrimitiveArray(base) && isPrimitiveArray(patch)) {
2868
- return [.../* @__PURE__ */ new Set([...base, ...patch])];
2869
- }
2870
- return conflictResolution === "prefer-patch" ? patch : base;
2871
- }
2872
- if (Array.isArray(base) || Array.isArray(patch)) {
2873
- return conflictResolution === "prefer-patch" ? patch : base;
2874
- }
2875
- const baseObj = base;
2876
- const patchObj = patch;
2877
- const out = { ...baseObj };
2878
- for (const [k, v] of Object.entries(patchObj)) {
2879
- if (protectProto && FORBIDDEN_PROTO_KEYS.has(k)) continue;
2880
- const existing = out[k];
2881
- if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
2882
- out[k] = deepMerge(existing, v, options);
2883
- } else if (Array.isArray(v) && Array.isArray(existing)) {
2884
- if (onNonPrimitiveArrayReplace && !isPrimitiveArray(v)) {
2885
- onNonPrimitiveArrayReplace(k, existing.length, v.length);
2886
- }
2887
- out[k] = deepMerge(existing, v, options);
2888
- } else if (v !== void 0) {
2889
- if (onNonPrimitiveArrayReplace && Array.isArray(v) && !isPrimitiveArray(v)) {
2890
- const existingLen = Array.isArray(existing) ? existing.length : 0;
2891
- onNonPrimitiveArrayReplace(k, existingLen, v.length);
2892
- }
2893
- out[k] = v;
2894
- }
2895
- }
2896
- return out;
3717
+ var ENCRYPTED_PREFIX_PATTERN = /^enc:v(\d+):/;
3718
+ function encryptedPrefixForVersion(version) {
3719
+ return `enc:v${version}:`;
2897
3720
  }
2898
3721
 
2899
3722
  // src/security/secret-vault.ts
3723
+ init_atomic_write();
2900
3724
  var KEY_BYTES = 32;
2901
3725
  var IV_BYTES = 12;
2902
3726
  var TAG_BYTES = 16;
2903
3727
  var ALGO = "aes-256-gcm";
2904
3728
  var KEY_FILE_MODE = 384;
3729
+ var KEY_FILE_MAGIC = Buffer.from("WSKV", "ascii");
3730
+ var VERSIONED_KEY_FILE_SIZE = KEY_FILE_MAGIC.length + 1 + KEY_BYTES;
2905
3731
  function checkKeyFilePermissions(keyFile) {
2906
3732
  if (process.platform === "win32") return;
2907
3733
  try {
2908
- const stat6 = fs.statSync(keyFile);
3734
+ const stat6 = fs4.statSync(keyFile);
2909
3735
  const actualMode = stat6.mode & 511;
2910
3736
  if (actualMode !== KEY_FILE_MODE) {
2911
3737
  console.warn(JSON.stringify({
@@ -2924,11 +3750,17 @@ function checkKeyFilePermissions(keyFile) {
2924
3750
  var DefaultSecretVault = class {
2925
3751
  keyFile;
2926
3752
  key;
3753
+ _keyVersion = 1;
2927
3754
  constructor(opts) {
2928
3755
  this.keyFile = opts.keyFile;
2929
3756
  }
3757
+ /** Current key version. Starts at 1; incremented by rotateKey(). */
3758
+ get keyVersion() {
3759
+ if (!this.key) this.loadOrCreateKey();
3760
+ return this._keyVersion;
3761
+ }
2930
3762
  isEncrypted(value) {
2931
- return typeof value === "string" && value.startsWith(ENCRYPTED_PREFIX);
3763
+ return typeof value === "string" && ENCRYPTED_PREFIX_PATTERN.test(value);
2932
3764
  }
2933
3765
  encrypt(plaintext) {
2934
3766
  if (this.isEncrypted(plaintext)) return plaintext;
@@ -2937,11 +3769,20 @@ var DefaultSecretVault = class {
2937
3769
  const cipher = createCipheriv(ALGO, key, iv);
2938
3770
  const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
2939
3771
  const tag = cipher.getAuthTag();
2940
- return `${ENCRYPTED_PREFIX}${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
3772
+ const prefix = encryptedPrefixForVersion(this._keyVersion);
3773
+ return `${prefix}${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
2941
3774
  }
2942
3775
  decrypt(value) {
2943
3776
  if (!this.isEncrypted(value)) return value;
2944
- const rest = value.slice(ENCRYPTED_PREFIX.length);
3777
+ const prefixMatch = value.match(ENCRYPTED_PREFIX_PATTERN);
3778
+ if (!prefixMatch) {
3779
+ throw new ConfigError({
3780
+ message: "SecretVault: malformed encrypted value",
3781
+ code: ERROR_CODES.CONFIG_PARSE_FAILED,
3782
+ context: { field: "encrypted_value" }
3783
+ });
3784
+ }
3785
+ const rest = value.slice(prefixMatch[0].length);
2945
3786
  const parts = rest.split(":");
2946
3787
  if (parts.length !== 3) {
2947
3788
  throw new ConfigError({
@@ -2970,48 +3811,110 @@ var DefaultSecretVault = class {
2970
3811
  const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
2971
3812
  return pt.toString("utf8");
2972
3813
  }
3814
+ /**
3815
+ * Generate a new encryption key, write it to disk, and increment the key version.
3816
+ * After rotation, encrypt() emits the new version prefix (e.g. enc:v2:).
3817
+ * The caller must re-encrypt existing config values (see rotateConfigKeys()).
3818
+ */
3819
+ rotateKey() {
3820
+ const oldVersion = this._keyVersion;
3821
+ const newKey = randomBytes(KEY_BYTES);
3822
+ const newVersion = oldVersion + 1;
3823
+ const keyFileBuf = Buffer.alloc(VERSIONED_KEY_FILE_SIZE);
3824
+ KEY_FILE_MAGIC.copy(keyFileBuf, 0);
3825
+ keyFileBuf[KEY_FILE_MAGIC.length] = newVersion;
3826
+ newKey.copy(keyFileBuf, KEY_FILE_MAGIC.length + 1);
3827
+ fs4.mkdirSync(path3.dirname(this.keyFile), { recursive: true });
3828
+ fs4.writeFileSync(this.keyFile, keyFileBuf, { mode: 384 });
3829
+ checkKeyFilePermissions(this.keyFile);
3830
+ this.key = newKey;
3831
+ this._keyVersion = newVersion;
3832
+ return { oldVersion, newVersion };
3833
+ }
2973
3834
  loadOrCreateKey() {
2974
3835
  if (this.key) return this.key;
2975
3836
  try {
2976
- const buf = fs.readFileSync(this.keyFile);
2977
- if (buf.length !== KEY_BYTES) {
2978
- throw new ConfigError({
2979
- message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`,
2980
- code: ERROR_CODES.CONFIG_INVALID,
2981
- context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
2982
- });
3837
+ const buf = fs4.readFileSync(this.keyFile);
3838
+ if (buf.length === KEY_BYTES) {
3839
+ this.key = buf;
3840
+ this._keyVersion = 1;
3841
+ checkKeyFilePermissions(this.keyFile);
3842
+ return this.key;
3843
+ }
3844
+ if (buf.length === VERSIONED_KEY_FILE_SIZE) {
3845
+ const magic = buf.subarray(0, KEY_FILE_MAGIC.length);
3846
+ if (!magic.equals(KEY_FILE_MAGIC)) {
3847
+ throw new ConfigError({
3848
+ message: `SecretVault: key file ${this.keyFile} has invalid magic header`,
3849
+ code: ERROR_CODES.CONFIG_INVALID,
3850
+ context: { keyFile: this.keyFile }
3851
+ });
3852
+ }
3853
+ const version = buf[KEY_FILE_MAGIC.length];
3854
+ const key2 = buf.subarray(KEY_FILE_MAGIC.length + 1);
3855
+ if (key2.length !== KEY_BYTES) {
3856
+ throw new ConfigError({
3857
+ message: `SecretVault: key file ${this.keyFile} has wrong key size (${key2.length} bytes, expected ${KEY_BYTES})`,
3858
+ code: ERROR_CODES.CONFIG_INVALID,
3859
+ context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: key2.length }
3860
+ });
3861
+ }
3862
+ this.key = Buffer.from(key2);
3863
+ this._keyVersion = version;
3864
+ checkKeyFilePermissions(this.keyFile);
3865
+ return this.key;
2983
3866
  }
2984
- this.key = buf;
2985
- checkKeyFilePermissions(this.keyFile);
2986
- return this.key;
3867
+ throw new ConfigError({
3868
+ 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.`,
3869
+ code: ERROR_CODES.CONFIG_INVALID,
3870
+ context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
3871
+ });
2987
3872
  } catch (err) {
2988
3873
  if (err.code !== "ENOENT") throw err;
2989
3874
  }
2990
- fs.mkdirSync(path11.dirname(this.keyFile), { recursive: true });
3875
+ fs4.mkdirSync(path3.dirname(this.keyFile), { recursive: true });
2991
3876
  const key = randomBytes(KEY_BYTES);
2992
3877
  try {
2993
- fs.writeFileSync(this.keyFile, key, { mode: 384, flag: "wx" });
3878
+ fs4.writeFileSync(this.keyFile, key, { mode: 384, flag: "wx" });
2994
3879
  } catch (err) {
2995
3880
  if (err.code !== "EEXIST") throw err;
2996
- const buf = fs.readFileSync(this.keyFile);
2997
- if (buf.length !== KEY_BYTES) {
2998
- throw new ConfigError({
2999
- message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`,
3000
- code: ERROR_CODES.CONFIG_INVALID,
3001
- context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
3002
- });
3881
+ const buf = fs4.readFileSync(this.keyFile);
3882
+ if (buf.length === KEY_BYTES) {
3883
+ this.key = buf;
3884
+ this._keyVersion = 1;
3885
+ checkKeyFilePermissions(this.keyFile);
3886
+ return this.key;
3887
+ }
3888
+ if (buf.length === VERSIONED_KEY_FILE_SIZE) {
3889
+ const magic = buf.subarray(0, KEY_FILE_MAGIC.length);
3890
+ if (!magic.equals(KEY_FILE_MAGIC)) {
3891
+ throw new ConfigError({
3892
+ message: `SecretVault: key file ${this.keyFile} has invalid magic header`,
3893
+ code: ERROR_CODES.CONFIG_INVALID,
3894
+ context: { keyFile: this.keyFile }
3895
+ });
3896
+ }
3897
+ const version = buf[KEY_FILE_MAGIC.length];
3898
+ const winnerKey = buf.subarray(KEY_FILE_MAGIC.length + 1);
3899
+ this.key = Buffer.from(winnerKey);
3900
+ this._keyVersion = version;
3901
+ checkKeyFilePermissions(this.keyFile);
3902
+ return this.key;
3003
3903
  }
3004
- this.key = buf;
3005
- checkKeyFilePermissions(this.keyFile);
3006
- return this.key;
3904
+ throw new ConfigError({
3905
+ 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.`,
3906
+ code: ERROR_CODES.CONFIG_INVALID,
3907
+ context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
3908
+ });
3007
3909
  }
3008
3910
  this.key = key;
3911
+ this._keyVersion = 1;
3009
3912
  return key;
3010
3913
  }
3011
3914
  };
3012
3915
  function decryptConfigSecrets(cfg, vault, opts) {
3013
3916
  const warn = opts?.warn ?? ((msg) => console.warn(msg));
3014
- return walk(cfg, vault, (v, key) => {
3917
+ return walk2(cfg, vault, (v, key) => {
3015
3918
  try {
3016
3919
  return vault.decrypt(v);
3017
3920
  } catch (err) {
@@ -3023,20 +3926,20 @@ function decryptConfigSecrets(cfg, vault, opts) {
3023
3926
  });
3024
3927
  }
3025
3928
  function encryptConfigSecrets(cfg, vault, _opts) {
3026
- return walk(cfg, vault, (v) => vault.encrypt(v));
3929
+ return walk2(cfg, vault, (v) => vault.encrypt(v));
3027
3930
  }
3028
- function walk(node, vault, transform) {
3931
+ function walk2(node, vault, transform) {
3029
3932
  if (node === null || node === void 0) return node;
3030
3933
  if (typeof node !== "object") return node;
3031
3934
  if (Array.isArray(node)) {
3032
- return node.map((item) => walk(item, vault, transform));
3935
+ return node.map((item) => walk2(item, vault, transform));
3033
3936
  }
3034
3937
  const out = /* @__PURE__ */ Object.create(null);
3035
3938
  for (const [k, v] of Object.entries(node)) {
3036
3939
  if (typeof v === "string" && isSecretField(k)) {
3037
3940
  out[k] = transform(v, k);
3038
3941
  } else if (typeof v === "object" && v !== null) {
3039
- out[k] = walk(v, vault, transform);
3942
+ out[k] = walk2(v, vault, transform);
3040
3943
  } else {
3041
3944
  out[k] = v;
3042
3945
  }
@@ -3053,20 +3956,20 @@ function isSecretField(name) {
3053
3956
  async function rewriteConfigEncrypted(configPath, vault, patch) {
3054
3957
  let current = {};
3055
3958
  try {
3056
- const raw = await fsp.readFile(configPath, "utf8");
3959
+ const raw = await fsp2.readFile(configPath, "utf8");
3057
3960
  current = JSON.parse(raw);
3058
3961
  } catch {
3059
3962
  }
3060
3963
  const merged = deepMerge(current, patch ?? {});
3061
3964
  const encrypted = encryptConfigSecrets(merged, vault);
3062
- await fsp.mkdir(path11.dirname(configPath), { recursive: true });
3965
+ await fsp2.mkdir(path3.dirname(configPath), { recursive: true });
3063
3966
  await atomicWrite(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
3064
3967
  await restrictFilePermissions(configPath);
3065
3968
  }
3066
3969
  async function migratePlaintextSecrets(configPath, vault, logger) {
3067
3970
  let raw;
3068
3971
  try {
3069
- raw = await fsp.readFile(configPath, "utf8");
3972
+ raw = await fsp2.readFile(configPath, "utf8");
3070
3973
  } catch {
3071
3974
  return { migrated: 0, file: configPath };
3072
3975
  }
@@ -3108,7 +4011,7 @@ async function restrictFilePermissions(filePath, opts) {
3108
4011
  }
3109
4012
  } else {
3110
4013
  try {
3111
- await fsp.chmod(filePath, 384);
4014
+ await fsp2.chmod(filePath, 384);
3112
4015
  } catch {
3113
4016
  }
3114
4017
  }
@@ -3180,33 +4083,18 @@ var CONTEXT_WINDOW_MODES = Object.freeze([
3180
4083
  id: "archival",
3181
4084
  name: "Archival",
3182
4085
  description: "Decision-preserving mode: compacts steadily while keeping summaries prominent.",
3183
- thresholds: { warn: 0.55, soft: 0.7, hard: 0.84 },
3184
- aggressiveOn: "soft",
3185
- preserveK: 8,
3186
- eliseThreshold: 1200,
3187
- targetLoad: 0.58
3188
- }
3189
- ]);
3190
- function listContextWindowModes() {
3191
- return CONTEXT_WINDOW_MODES.map((m) => ({ ...m, thresholds: { ...m.thresholds } }));
3192
- }
3193
- function isContextWindowModeId(id) {
3194
- return CONTEXT_WINDOW_MODES.some((m) => m.id === id);
3195
- }
3196
-
3197
- // src/utils/safe-json.ts
3198
- function safeParse(input, maxBytes = 5e6) {
3199
- if (input.length > maxBytes) {
3200
- return { ok: false, error: `Input exceeds limit (${maxBytes} bytes)` };
3201
- }
3202
- try {
3203
- return { ok: true, value: JSON.parse(input) };
3204
- } catch (err) {
3205
- return {
3206
- ok: false,
3207
- error: err instanceof Error ? err.message : String(err)
3208
- };
4086
+ thresholds: { warn: 0.55, soft: 0.7, hard: 0.84 },
4087
+ aggressiveOn: "soft",
4088
+ preserveK: 8,
4089
+ eliseThreshold: 1200,
4090
+ targetLoad: 0.58
3209
4091
  }
4092
+ ]);
4093
+ function listContextWindowModes() {
4094
+ return CONTEXT_WINDOW_MODES.map((m) => ({ ...m, thresholds: { ...m.thresholds } }));
4095
+ }
4096
+ function isContextWindowModeId(id) {
4097
+ return CONTEXT_WINDOW_MODES.some((m) => m.id === id);
3210
4098
  }
3211
4099
 
3212
4100
  // src/types/default-config.ts
@@ -3216,7 +4104,8 @@ var DEFAULT_TOOLS_CONFIG = Object.freeze({
3216
4104
  iterationTimeoutMs: 3e5,
3217
4105
  sessionTimeoutMs: 18e5,
3218
4106
  perIterationOutputCapBytes: 1e5,
3219
- autoExtendLimit: true
4107
+ autoExtendLimit: true,
4108
+ restrictToProjectRoot: false
3220
4109
  });
3221
4110
  var DEFAULT_CONTEXT_CONFIG = Object.freeze({
3222
4111
  preserveK: 10,
@@ -3259,7 +4148,8 @@ var BEHAVIOR_DEFAULTS = {
3259
4148
  iterationTimeoutMs: DEFAULT_TOOLS_CONFIG.iterationTimeoutMs,
3260
4149
  sessionTimeoutMs: DEFAULT_TOOLS_CONFIG.sessionTimeoutMs,
3261
4150
  perIterationOutputCapBytes: DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
3262
- autoExtendLimit: DEFAULT_TOOLS_CONFIG.autoExtendLimit
4151
+ autoExtendLimit: DEFAULT_TOOLS_CONFIG.autoExtendLimit,
4152
+ restrictToProjectRoot: DEFAULT_TOOLS_CONFIG.restrictToProjectRoot
3263
4153
  },
3264
4154
  log: { level: "info" },
3265
4155
  features: {
@@ -3384,7 +4274,7 @@ var DefaultConfigLoader = class {
3384
4274
  level: "warn",
3385
4275
  event: "config.source_load_failed",
3386
4276
  source: src.name,
3387
- message: err instanceof Error ? err.message : String(err),
4277
+ message: toErrorMessage(err),
3388
4278
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
3389
4279
  }));
3390
4280
  }
@@ -3467,7 +4357,7 @@ var DefaultConfigLoader = class {
3467
4357
  const fp = this.paths.syncConfig;
3468
4358
  const t0 = Date.now();
3469
4359
  try {
3470
- const raw = await fsp.readFile(fp, "utf8");
4360
+ const raw = await fsp2.readFile(fp, "utf8");
3471
4361
  const parsed = safeParse(raw);
3472
4362
  if (!parsed.ok || !parsed.value) {
3473
4363
  this.events?.emit("storage.read", {
@@ -3521,7 +4411,7 @@ var DefaultConfigLoader = class {
3521
4411
  console.warn(JSON.stringify({
3522
4412
  level: "warn",
3523
4413
  event: "config.sync_load_failed",
3524
- message: err instanceof Error ? err.message : String(err),
4414
+ message: toErrorMessage(err),
3525
4415
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
3526
4416
  }));
3527
4417
  return null;
@@ -3531,7 +4421,7 @@ var DefaultConfigLoader = class {
3531
4421
  let raw;
3532
4422
  const t0 = Date.now();
3533
4423
  try {
3534
- raw = await fsp.readFile(file, "utf8");
4424
+ raw = await fsp2.readFile(file, "utf8");
3535
4425
  } catch (err) {
3536
4426
  if (err.code !== "ENOENT") {
3537
4427
  this.events?.emit("storage.read", {
@@ -3548,7 +4438,7 @@ var DefaultConfigLoader = class {
3548
4438
  level: "warn",
3549
4439
  event: "config.read_failed",
3550
4440
  path: file,
3551
- message: err instanceof Error ? err.message : String(err),
4441
+ message: toErrorMessage(err),
3552
4442
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
3553
4443
  }));
3554
4444
  }
@@ -3702,7 +4592,7 @@ var RecoveryLock = class {
3702
4592
  sessionStore;
3703
4593
  probe;
3704
4594
  constructor(opts) {
3705
- this.file = path11.join(opts.dir, LOCK_FILE);
4595
+ this.file = path3.join(opts.dir, LOCK_FILE);
3706
4596
  this.pid = opts.pid ?? process.pid;
3707
4597
  this.hostname = opts.hostname ?? os.hostname();
3708
4598
  this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
@@ -3763,7 +4653,7 @@ var RecoveryLock = class {
3763
4653
  * null return before calling this.
3764
4654
  */
3765
4655
  async write(sessionId) {
3766
- await ensureDir(path11.dirname(this.file));
4656
+ await ensureDir(path3.dirname(this.file));
3767
4657
  const lock = {
3768
4658
  v: 1,
3769
4659
  sessionId,
@@ -3772,7 +4662,7 @@ var RecoveryLock = class {
3772
4662
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
3773
4663
  };
3774
4664
  try {
3775
- await fsp.writeFile(this.file, JSON.stringify(lock), { flag: "wx", mode: 384 });
4665
+ await fsp2.writeFile(this.file, JSON.stringify(lock), { flag: "wx", mode: 384 });
3776
4666
  } catch (err) {
3777
4667
  const code = err.code;
3778
4668
  if (code === "EEXIST") {
@@ -3788,7 +4678,7 @@ var RecoveryLock = class {
3788
4678
  */
3789
4679
  async clear() {
3790
4680
  try {
3791
- await fsp.unlink(this.file);
4681
+ await fsp2.unlink(this.file);
3792
4682
  } catch (err) {
3793
4683
  const code = err.code;
3794
4684
  if (code === "ENOENT") return;
@@ -3798,7 +4688,7 @@ var RecoveryLock = class {
3798
4688
  async readLock() {
3799
4689
  let raw;
3800
4690
  try {
3801
- raw = await fsp.readFile(this.file, "utf8");
4691
+ raw = await fsp2.readFile(this.file, "utf8");
3802
4692
  } catch (err) {
3803
4693
  const code = err.code;
3804
4694
  if (code === "ENOENT") return null;
@@ -3830,42 +4720,6 @@ function defaultIsPidAlive(pid) {
3830
4720
  }
3831
4721
  }
3832
4722
 
3833
- // src/utils/regex-guard.ts
3834
- var MAX_PATTERN_LEN = 512;
3835
- var DANGEROUS_PATTERNS = [
3836
- /(\([^)]*[+*][^)]*\))[+*]/,
3837
- // (a+)+, (.*)+, etc
3838
- /(\(\?:[^)]*[+*][^)]*\))[+*]/
3839
- // same, with non-capturing group
3840
- ];
3841
- function compileUserRegex(pattern, flags) {
3842
- if (typeof pattern !== "string") {
3843
- return { ok: false, reason: "pattern must be a string" };
3844
- }
3845
- if (pattern.length === 0) {
3846
- return { ok: false, reason: "pattern is empty" };
3847
- }
3848
- if (pattern.length > MAX_PATTERN_LEN) {
3849
- return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
3850
- }
3851
- for (const rx of DANGEROUS_PATTERNS) {
3852
- if (rx.test(pattern)) {
3853
- return {
3854
- ok: false,
3855
- reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
3856
- };
3857
- }
3858
- }
3859
- try {
3860
- return { ok: true, regex: new RegExp(pattern, flags) };
3861
- } catch (err) {
3862
- return {
3863
- ok: false,
3864
- reason: err instanceof Error ? err.message : "invalid regex"
3865
- };
3866
- }
3867
- }
3868
-
3869
4723
  // src/storage/session-reader.ts
3870
4724
  var DefaultSessionReader = class {
3871
4725
  store;
@@ -4281,7 +5135,7 @@ function isAllowed(type, level) {
4281
5135
  return true;
4282
5136
  }
4283
5137
  function createSessionEventBridge(writer, level = "standard", options = {}) {
4284
- const normalizedLevel = level ?? "standard";
5138
+ let currentLevel = level ?? "standard";
4285
5139
  const resolveWriter = typeof writer === "function" ? writer : () => writer;
4286
5140
  const progressCounters = /* @__PURE__ */ new Map();
4287
5141
  const toolProgressConfig = options.sampling?.toolProgress ?? {};
@@ -4302,14 +5156,19 @@ function createSessionEventBridge(writer, level = "standard", options = {}) {
4302
5156
  return true;
4303
5157
  }
4304
5158
  return {
4305
- level: normalizedLevel,
5159
+ get level() {
5160
+ return currentLevel;
5161
+ },
5162
+ setAuditLevel(next) {
5163
+ currentLevel = next ?? "standard";
5164
+ },
4306
5165
  allows(type) {
4307
- return isAllowed(type, normalizedLevel);
5166
+ return isAllowed(type, currentLevel);
4308
5167
  },
4309
5168
  async append(event) {
4310
5169
  const target = resolveWriter();
4311
5170
  if (!target) return;
4312
- if (!isAllowed(event.type, normalizedLevel)) return;
5171
+ if (!isAllowed(event.type, currentLevel)) return;
4313
5172
  if (!shouldSample(event)) return;
4314
5173
  try {
4315
5174
  await target.append(event);
@@ -4320,7 +5179,7 @@ function createSessionEventBridge(writer, level = "standard", options = {}) {
4320
5179
  const target = resolveWriter();
4321
5180
  if (!target || events.length === 0) return;
4322
5181
  const allowed = events.filter(
4323
- (e) => isAllowed(e.type, normalizedLevel) && shouldSample(e)
5182
+ (e) => isAllowed(e.type, currentLevel) && shouldSample(e)
4324
5183
  );
4325
5184
  if (allowed.length === 0) return;
4326
5185
  try {
@@ -4357,7 +5216,7 @@ async function loadTodosCheckpoint(filePath, events, traceId) {
4357
5216
  const t0 = Date.now();
4358
5217
  let raw;
4359
5218
  try {
4360
- raw = await fsp.readFile(filePath, "utf8");
5219
+ raw = await fsp2.readFile(filePath, "utf8");
4361
5220
  } catch (err) {
4362
5221
  events?.emit("storage.error", {
4363
5222
  sessionId: traceId ?? "~boot~",
@@ -4365,7 +5224,7 @@ async function loadTodosCheckpoint(filePath, events, traceId) {
4365
5224
  filePath,
4366
5225
  operation: "load",
4367
5226
  outcome: "failure",
4368
- error: err instanceof Error ? err.message : String(err),
5227
+ error: toErrorMessage(err),
4369
5228
  recoverable: true
4370
5229
  });
4371
5230
  return null;
@@ -4437,13 +5296,13 @@ async function saveTodosCheckpoint(filePath, sessionId, todos, events, traceId)
4437
5296
  filePath,
4438
5297
  operation: "save",
4439
5298
  outcome: "failure",
4440
- error: err instanceof Error ? err.message : String(err),
5299
+ error: toErrorMessage(err),
4441
5300
  recoverable: false
4442
5301
  });
4443
5302
  console.warn(JSON.stringify({
4444
5303
  level: "warn",
4445
5304
  event: "todos_checkpoint.save_failed",
4446
- message: err instanceof Error ? err.message : String(err),
5305
+ message: toErrorMessage(err),
4447
5306
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
4448
5307
  }));
4449
5308
  }
@@ -4454,7 +5313,7 @@ function attachTodosCheckpoint(state, filePath, sessionId, events, traceId) {
4454
5313
  let writeChain = Promise.resolve();
4455
5314
  const enqueueWrite = (todos) => {
4456
5315
  writeChain = writeChain.then(() => saveTodosCheckpoint(filePath, sessionId, todos, events, traceId)).catch((err) => {
4457
- const msg = err instanceof Error ? err.message : String(err);
5316
+ const msg = toErrorMessage(err);
4458
5317
  console.error(JSON.stringify({
4459
5318
  level: "error",
4460
5319
  event: "todos_checkpoint.write_chain_failed",
@@ -4499,14 +5358,14 @@ async function loadPlan(filePath, events) {
4499
5358
  const t0 = Date.now();
4500
5359
  let raw;
4501
5360
  try {
4502
- raw = await fsp.readFile(filePath, "utf8");
5361
+ raw = await fsp2.readFile(filePath, "utf8");
4503
5362
  } catch (err) {
4504
5363
  events?.emit("storage.error", {
4505
5364
  sessionId: "~boot~",
4506
5365
  store: "plan",
4507
5366
  filePath,
4508
5367
  operation: "load",
4509
- error: err instanceof Error ? err.message : String(err),
5368
+ error: toErrorMessage(err),
4510
5369
  recoverable: true
4511
5370
  });
4512
5371
  return null;
@@ -4559,19 +5418,21 @@ async function savePlan(filePath, plan, events) {
4559
5418
  outcome: "success",
4560
5419
  durationMs: Date.now() - t0
4561
5420
  });
5421
+ return true;
4562
5422
  } catch (err) {
4563
5423
  events?.emit("storage.error", {
4564
5424
  sessionId: "~boot~",
4565
5425
  store: "plan",
4566
5426
  filePath,
4567
5427
  operation: "save",
4568
- error: err instanceof Error ? err.message : String(err),
5428
+ error: toErrorMessage(err),
4569
5429
  recoverable: false
4570
5430
  });
4571
5431
  console.warn(
4572
5432
  "[plan-store] save failed:",
4573
- err instanceof Error ? err.message : String(err)
5433
+ toErrorMessage(err)
4574
5434
  );
5435
+ return false;
4575
5436
  }
4576
5437
  }
4577
5438
  function emptyPlan(sessionId, title) {
@@ -4791,7 +5652,7 @@ init_atomic_write();
4791
5652
  async function loadDirectorState(filePath) {
4792
5653
  let raw;
4793
5654
  try {
4794
- raw = await fsp.readFile(filePath, "utf8");
5655
+ raw = await fsp2.readFile(filePath, "utf8");
4795
5656
  } catch {
4796
5657
  return null;
4797
5658
  }
@@ -4806,7 +5667,7 @@ async function loadDirectorState(filePath) {
4806
5667
  async function acquireDirectorStateLock(lockPath, processId = process.pid) {
4807
5668
  let existing;
4808
5669
  try {
4809
- existing = await fsp.readFile(lockPath, "utf8");
5670
+ existing = await fsp2.readFile(lockPath, "utf8");
4810
5671
  } catch {
4811
5672
  }
4812
5673
  if (existing) {
@@ -4830,7 +5691,7 @@ async function acquireDirectorStateLock(lockPath, processId = process.pid) {
4830
5691
  }
4831
5692
  async function releaseDirectorStateLock(lockPath) {
4832
5693
  try {
4833
- await fsp.unlink(lockPath);
5694
+ await fsp2.unlink(lockPath);
4834
5695
  } catch {
4835
5696
  }
4836
5697
  }
@@ -4955,7 +5816,7 @@ var DirectorStateCheckpoint = class {
4955
5816
  } catch (err) {
4956
5817
  console.warn(
4957
5818
  "[director-state] checkpoint write failed:",
4958
- err instanceof Error ? err.message : String(err)
5819
+ toErrorMessage(err)
4959
5820
  );
4960
5821
  } finally {
4961
5822
  this.writing = false;
@@ -5008,6 +5869,27 @@ var PATTERNS = [
5008
5869
  { type: "postgres_uri", regex: /postgres(?:ql)?:\/\/[^\s"'`]+/g },
5009
5870
  { type: "mysql_uri", regex: /mysql:\/\/[^\s"'`]+/g },
5010
5871
  { type: "redis_uri", regex: /redis:\/\/[^\s"'`]+/g },
5872
+ // AI/ML provider keys — modern LLM services with well-known prefixes
5873
+ {
5874
+ type: "huggingface_token",
5875
+ // HuggingFace tokens: hf_ followed by 34 alphanumeric chars
5876
+ regex: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{34}(?![A-Za-z0-9])/g
5877
+ },
5878
+ {
5879
+ type: "replicate_token",
5880
+ // Replicate tokens: r8_ followed by 40+ alphanumeric chars
5881
+ regex: /(?<![A-Za-z0-9])r8_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g
5882
+ },
5883
+ {
5884
+ type: "perplexity_key",
5885
+ // Perplexity API keys: pplx- followed by 40+ alphanumeric chars
5886
+ regex: /(?<![A-Za-z0-9])pplx-[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g
5887
+ },
5888
+ {
5889
+ type: "groq_key",
5890
+ // Groq API keys: gsk_ followed by 40+ alphanumeric chars
5891
+ regex: /(?<![A-Za-z0-9])gsk_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g
5892
+ },
5011
5893
  {
5012
5894
  type: "bearer_token",
5013
5895
  // Anchored with alternation instead of negative lookahead — avoids V8
@@ -5041,6 +5923,10 @@ function hasCredentialAnchors(text) {
5041
5923
  text.includes("xox") || // Slack token (xoxa/xoxb/xoxp/xoxo/xoxs)
5042
5924
  text.includes("Bearer ") || // Bearer token (space suffix reduces false positives)
5043
5925
  text.includes("/bot") || // Telegram bot token (URL path pattern)
5926
+ text.includes("hf_") || // HuggingFace token
5927
+ text.includes("r8_") || // Replicate token
5928
+ text.includes("pplx-") || // Perplexity API key
5929
+ text.includes("gsk_") || // Groq API key
5044
5930
  text.includes("_KEY=") || // High-entropy env vars: API_KEY=, SECRET_KEY=, ...
5045
5931
  text.includes("_TOKEN=") || // ACCESS_TOKEN=, AUTH_TOKEN=, ...
5046
5932
  text.includes("_SECRET=") || // API_SECRET=, CLIENT_SECRET=, ...
@@ -5165,82 +6051,6 @@ function getDangerousCapabilities(toolOrCaps) {
5165
6051
 
5166
6052
  // src/security/permission-policy.ts
5167
6053
  init_atomic_write();
5168
-
5169
- // src/utils/glob-match.ts
5170
- function escapeRegex(s) {
5171
- return s.replace(/[.+^${}()|\\]/g, "\\$&");
5172
- }
5173
- var COMPILED_GLOB_CACHE = /* @__PURE__ */ new Map();
5174
- var CACHE_MAX_SIZE = 2e3;
5175
- function getCachedGlob(pattern) {
5176
- const cached = COMPILED_GLOB_CACHE.get(pattern);
5177
- if (cached) return cached;
5178
- if (COMPILED_GLOB_CACHE.size >= CACHE_MAX_SIZE) {
5179
- const keys = [...COMPILED_GLOB_CACHE.keys()];
5180
- for (let i = 0; i < Math.floor(CACHE_MAX_SIZE / 4); i++) {
5181
- COMPILED_GLOB_CACHE.delete(expectDefined(keys[i]));
5182
- }
5183
- }
5184
- const re = compileGlob(pattern);
5185
- COMPILED_GLOB_CACHE.set(pattern, re);
5186
- return re;
5187
- }
5188
- var MAX_GLOB_PATTERN_LEN = 1024;
5189
- function compileGlob(pattern) {
5190
- if (pattern.length > MAX_GLOB_PATTERN_LEN) {
5191
- throw new Error(`Glob pattern exceeds ${MAX_GLOB_PATTERN_LEN} characters`);
5192
- }
5193
- let i = 0;
5194
- let re = "^";
5195
- while (i < pattern.length) {
5196
- const c = pattern[i];
5197
- if (c === "*") {
5198
- if (pattern[i + 1] === "*") {
5199
- re += ".*";
5200
- i += 2;
5201
- if (pattern[i] === "/") i++;
5202
- } else {
5203
- re += "[^/]*";
5204
- i++;
5205
- }
5206
- } else if (c === "?") {
5207
- re += "[^/]";
5208
- i++;
5209
- } else if (c === "[") {
5210
- let cls = "[";
5211
- i++;
5212
- if (pattern[i] === "!" || pattern[i] === "^") {
5213
- cls += "^";
5214
- i++;
5215
- }
5216
- while (i < pattern.length && pattern[i] !== "]") {
5217
- const ch = pattern[i] ?? "";
5218
- if (ch === "\\") {
5219
- cls += "\\\\";
5220
- } else if (ch === "]" || ch === "^") {
5221
- cls += `\\${ch}`;
5222
- } else {
5223
- cls += ch;
5224
- }
5225
- i++;
5226
- }
5227
- cls += "]";
5228
- re += cls;
5229
- i++;
5230
- } else {
5231
- re += escapeRegex(c ?? "");
5232
- i++;
5233
- }
5234
- }
5235
- re += "$";
5236
- return new RegExp(re);
5237
- }
5238
- function matchGlob(pattern, input) {
5239
- return getCachedGlob(pattern).test(input);
5240
- }
5241
- function matchAny(patterns, input) {
5242
- return patterns.some((p) => matchGlob(p, input));
5243
- }
5244
6054
  var DESTRUCTIVE_BASH_PATTERNS = [
5245
6055
  /\bgit\s+(?:clean\s+-[^\s]*[xdf]|reset\s+--hard)\b/i,
5246
6056
  /\b(?:drop|truncate)\s+(?:table|database|schema)\b/i,
@@ -5263,9 +6073,9 @@ function getInputString(input, key) {
5263
6073
  function pathLooksInsideProject(rawPath, projectRoot) {
5264
6074
  if (!projectRoot) return false;
5265
6075
  if (rawPath === "~" || rawPath.startsWith("~/") || rawPath.startsWith("~\\")) return false;
5266
- const resolved = path11.resolve(projectRoot, rawPath);
5267
- const relative2 = path11.relative(projectRoot, resolved);
5268
- return !!relative2 && !relative2.startsWith("..") && !path11.isAbsolute(relative2);
6076
+ const resolved = path3.resolve(projectRoot, rawPath);
6077
+ const relative2 = path3.relative(projectRoot, resolved);
6078
+ return !!relative2 && !relative2.startsWith("..") && !path3.isAbsolute(relative2);
5269
6079
  }
5270
6080
  function tokenizeShell(command) {
5271
6081
  return command.match(/"[^"]*"|'[^']*'|\S+/g)?.map((token) => token.replace(/^['"]|['"]$/g, "")) ?? [];
@@ -5275,7 +6085,7 @@ function pathTokenIsOutsideProject(token, projectRoot) {
5275
6085
  if (token === "/" || token === "~" || token === "." || token === "..") return token !== ".";
5276
6086
  if (token.includes("*")) return true;
5277
6087
  if (token.startsWith("..") || token.includes("../") || token.includes("..\\")) return true;
5278
- if (path11.isAbsolute(token) || token.startsWith("~/")) return !pathLooksInsideProject(token, projectRoot);
6088
+ if (path3.isAbsolute(token) || token.startsWith("~/")) return !pathLooksInsideProject(token, projectRoot);
5279
6089
  return false;
5280
6090
  }
5281
6091
  function hasDangerousDeleteTarget(tokens, start, projectRoot) {
@@ -5421,7 +6231,7 @@ var DefaultPermissionPolicy = class {
5421
6231
  }
5422
6232
  async reload() {
5423
6233
  try {
5424
- const raw = await fsp.readFile(this.trustFile, "utf8");
6234
+ const raw = await fsp2.readFile(this.trustFile, "utf8");
5425
6235
  const parsed = safeParse(raw);
5426
6236
  if (parsed.ok && parsed.value) this.policy = parsed.value;
5427
6237
  } catch {
@@ -5693,11 +6503,6 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
5693
6503
  }
5694
6504
  };
5695
6505
 
5696
- // src/utils/string.ts
5697
- function truncate(s, max) {
5698
- return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
5699
- }
5700
-
5701
6506
  // src/types/provider.ts
5702
6507
  var ProviderError = class extends WrongStackError {
5703
6508
  status;
@@ -5934,12 +6739,12 @@ var DefaultSkillLoader = class {
5934
6739
  const seen = /* @__PURE__ */ new Set();
5935
6740
  for (const { dir, source } of this.dirs) {
5936
6741
  try {
5937
- const entries = await fsp.readdir(dir, { withFileTypes: true });
6742
+ const entries = await fsp2.readdir(dir, { withFileTypes: true });
5938
6743
  for (const e of entries) {
5939
6744
  if (!e.isDirectory()) continue;
5940
- const skillFile = path11.join(dir, e.name, "SKILL.md");
6745
+ const skillFile = path3.join(dir, e.name, "SKILL.md");
5941
6746
  try {
5942
- const raw = await fsp.readFile(skillFile, "utf8");
6747
+ const raw = await fsp2.readFile(skillFile, "utf8");
5943
6748
  const meta = parseFrontmatter(raw);
5944
6749
  if (!meta.name || !meta.description) continue;
5945
6750
  if (seen.has(meta.name)) continue;
@@ -5962,12 +6767,12 @@ var DefaultSkillLoader = class {
5962
6767
  }
5963
6768
  async find(name) {
5964
6769
  const all = await this.list();
5965
- return all.find((s) => s.name === name);
6770
+ const lower = name.toLowerCase();
6771
+ return all.find((s) => s.name.toLowerCase() === lower);
5966
6772
  }
5967
6773
  async manifestText() {
5968
- const skills = await this.list();
5969
- if (skills.length === 0) return "";
5970
6774
  const entries = await this.listEntries();
6775
+ if (entries.length === 0) return "";
5971
6776
  const lines = ["## Available skills"];
5972
6777
  for (const e of entries) {
5973
6778
  const scopeTag = e.scope.length > 0 ? ` \u2014 ${e.scope.slice(0, 3).join(", ")}` : "";
@@ -5981,12 +6786,8 @@ var DefaultSkillLoader = class {
5981
6786
  const skills = await this.list();
5982
6787
  const entries = [];
5983
6788
  for (const s of skills) {
5984
- try {
5985
- const raw = await fsp.readFile(s.path, "utf8");
5986
- const { trigger, scope } = parseDescription(raw);
5987
- entries.push({ name: s.name, trigger, scope, source: s.source, path: s.path });
5988
- } catch {
5989
- }
6789
+ const { trigger, scope } = parseDescriptionFromText(s.description ?? "");
6790
+ entries.push({ name: s.name, trigger, scope, source: s.source, path: s.path });
5990
6791
  }
5991
6792
  this.entriesCache = entries;
5992
6793
  return entries;
@@ -5997,183 +6798,88 @@ var DefaultSkillLoader = class {
5997
6798
  this.bodyCache.clear();
5998
6799
  }
5999
6800
  async readBody(name) {
6000
- const cached = this.bodyCache.get(name);
6801
+ const key = name.toLowerCase();
6802
+ const cached = this.bodyCache.get(key);
6001
6803
  if (cached !== void 0) return cached;
6002
6804
  const m = await this.find(name);
6003
6805
  if (!m) throw new Error(`Skill "${name}" not found`);
6004
- const body = await fsp.readFile(m.path, "utf8");
6005
- this.bodyCache.set(name, body);
6806
+ const body = await fsp2.readFile(m.path, "utf8");
6807
+ this.bodyCache.set(key, body);
6006
6808
  return body;
6007
6809
  }
6008
6810
  async readSaveBody(name) {
6009
- const key = `save:${name}`;
6811
+ const key = `save:${name.toLowerCase()}`;
6010
6812
  const cached = this.bodyCache.get(key);
6011
6813
  if (cached !== void 0) return cached;
6012
6814
  const m = await this.find(name);
6013
6815
  if (!m) throw new Error(`Skill "${name}" not found`);
6014
- const savePath = path11.join(path11.dirname(m.path), "SKILL.save.md");
6816
+ const savePath = path3.join(path3.dirname(m.path), "SKILL.save.md");
6015
6817
  let result;
6016
6818
  try {
6017
- result = await fsp.readFile(savePath, "utf8");
6819
+ result = await fsp2.readFile(savePath, "utf8");
6018
6820
  } catch {
6019
- const full = await fsp.readFile(m.path, "utf8");
6020
- const body = stripFrontmatter(full);
6021
- const compact = compactSkillBody(body);
6022
- if (compact) {
6023
- result = `## Overview
6024
-
6025
- ${compact}`;
6026
- } else {
6027
- result = body.trim().slice(0, 300);
6028
- }
6029
- }
6030
- this.bodyCache.set(key, result);
6031
- return result;
6032
- }
6033
- };
6034
- function parseFrontmatter(raw) {
6035
- if (!raw.startsWith("---")) return {};
6036
- const end = raw.indexOf("\n---", 4);
6037
- if (end === -1) return {};
6038
- const block = raw.slice(4, end);
6039
- const out = {};
6040
- let key = null;
6041
- let value = [];
6042
- const flush = () => {
6043
- if (key) {
6044
- out[key] = value.join("\n").trim();
6045
- }
6046
- key = null;
6047
- value = [];
6048
- };
6049
- for (const line of block.split("\n")) {
6050
- const m = /^([a-zA-Z_]+):\s*(\|?)\s*(.*)$/.exec(line);
6051
- if (m) {
6052
- flush();
6053
- key = m[1] ?? "";
6054
- const pipe = m[2];
6055
- const rest = m[3] ?? "";
6056
- if (pipe === "|") {
6057
- value = [];
6058
- } else if (rest) {
6059
- value = [rest];
6060
- } else {
6061
- value = [];
6062
- }
6063
- } else if (key) {
6064
- value.push(line.replace(/^\s+/, ""));
6065
- }
6066
- }
6067
- flush();
6068
- return out;
6069
- }
6070
- function parseDescription(raw) {
6071
- const fm = parseFrontmatter(raw);
6072
- const desc = fm.description ?? "";
6073
- const firstSentenceEnd = desc.indexOf(". ");
6074
- const trigger = firstSentenceEnd !== -1 ? desc.slice(0, firstSentenceEnd + 1).trim() : desc.trim().split("\n")[0] ?? "";
6075
- const scope = [];
6076
- const coversMatch = /(?:covers|for|including)\s+([^.]+)/i.exec(desc);
6077
- if (coversMatch) {
6078
- const items = coversMatch[1] ?? "".replace(/[·•]/g, ",").split(",").map((s) => s.trim()).filter(Boolean);
6079
- scope.push(...items);
6080
- }
6081
- return { trigger, scope };
6082
- }
6083
-
6084
- // src/utils/json-repair.ts
6085
- function completePartialObject(s) {
6086
- if (!s.trim().startsWith("{")) return s;
6087
- if (tryParse(s).ok) return s;
6088
- return repairTruncated(s);
6089
- }
6090
- function repairTruncated(s) {
6091
- const stack = [];
6092
- let inString = false;
6093
- let escaped = false;
6094
- let sawKey = false;
6095
- let prevSig = "";
6096
- let contentEnd = 0;
6097
- let stringBraceDepth = 0;
6098
- for (let i = 0; i < s.length; i++) {
6099
- const ch = expectDefined(s[i]);
6100
- if (inString) {
6101
- contentEnd = i + 1;
6102
- if (escaped) {
6103
- escaped = false;
6104
- continue;
6105
- }
6106
- if (ch === "\\") {
6107
- escaped = true;
6108
- continue;
6109
- }
6110
- if (ch === '"') {
6111
- inString = false;
6112
- prevSig = '"';
6113
- stringBraceDepth = 0;
6114
- continue;
6115
- }
6116
- if (ch === "{") stringBraceDepth++;
6117
- else if (ch === "}" && stringBraceDepth > 0) stringBraceDepth--;
6118
- continue;
6119
- }
6120
- if (ch === " " || ch === " " || ch === "\n" || ch === "\r") continue;
6121
- contentEnd = i + 1;
6122
- if (ch === '"') {
6123
- inString = true;
6124
- sawKey = true;
6125
- stringBraceDepth = 0;
6126
- prevSig = '"';
6127
- } else if (ch === "{" || ch === "[") {
6128
- stack.push(ch);
6129
- prevSig = ch;
6130
- } else if (ch === "}" || ch === "]") {
6131
- stack.pop();
6132
- prevSig = ch;
6133
- } else {
6134
- prevSig = ch;
6821
+ const full = await fsp2.readFile(m.path, "utf8");
6822
+ const body = stripFrontmatter(full);
6823
+ const compact = compactSkillBody(body);
6824
+ if (compact) {
6825
+ result = `## Overview
6826
+
6827
+ ${compact}`;
6828
+ } else {
6829
+ result = body.trim().slice(0, 300);
6830
+ }
6135
6831
  }
6832
+ this.bodyCache.set(key, result);
6833
+ return result;
6136
6834
  }
6137
- if (!sawKey && !inString) return s;
6138
- let result = s.slice(0, contentEnd);
6139
- if (inString) {
6140
- if (escaped) {
6141
- result = result.slice(0, -1);
6142
- } else if (endsWithInvalidEscape(result)) {
6143
- result = result.slice(0, -2);
6835
+ };
6836
+ function parseFrontmatter(raw) {
6837
+ if (!raw.startsWith("---")) return {};
6838
+ const end = raw.indexOf("\n---", 4);
6839
+ if (end === -1) return {};
6840
+ const block = raw.slice(4, end);
6841
+ const out = {};
6842
+ let key = null;
6843
+ let value = [];
6844
+ const flush = () => {
6845
+ if (key) {
6846
+ out[key] = value.join("\n").trim();
6847
+ }
6848
+ key = null;
6849
+ value = [];
6850
+ };
6851
+ for (const line of block.split("\n")) {
6852
+ const m = /^([a-zA-Z_]+):\s*(\|?)\s*(.*)$/.exec(line);
6853
+ if (m) {
6854
+ flush();
6855
+ key = m[1] ?? "";
6856
+ const pipe = m[2];
6857
+ const rest = m[3] ?? "";
6858
+ if (pipe === "|") {
6859
+ value = [];
6860
+ } else if (rest) {
6861
+ value = [rest];
6862
+ } else {
6863
+ value = [];
6864
+ }
6865
+ } else if (key) {
6866
+ value.push(line.replace(/^\s+/, ""));
6144
6867
  }
6145
- if (stringBraceDepth > 0) result += "}".repeat(stringBraceDepth);
6146
- result += '"';
6147
- } else if (prevSig === ":") {
6148
- result += "null";
6149
- }
6150
- for (let k = stack.length - 1; k >= 0; k--) {
6151
- result += stack[k] === "{" ? "}" : "]";
6152
- }
6153
- if (!tryParse(result).ok) {
6154
- const patched = result.replace(/:(\s*)([}\]])/g, ":null$2");
6155
- if (tryParse(patched).ok) result = patched;
6156
6868
  }
6157
- return result;
6158
- }
6159
- var VALID_ESCAPE = /* @__PURE__ */ new Set(['"', "\\", "/", "b", "f", "n", "r", "t", "u"]);
6160
- function endsWithInvalidEscape(str) {
6161
- const last = str[str.length - 1];
6162
- if (str[str.length - 2] !== "\\" || last === void 0) return false;
6163
- if (VALID_ESCAPE.has(last)) return false;
6164
- let backslashes = 0;
6165
- for (let k = str.length - 2; k >= 0 && str[k] === "\\"; k--) backslashes++;
6166
- return backslashes % 2 === 1;
6869
+ flush();
6870
+ return out;
6167
6871
  }
6168
- function tryParse(s) {
6169
- try {
6170
- return { ok: true, value: JSON.parse(s) };
6171
- } catch {
6172
- return { ok: false };
6872
+ function parseDescriptionFromText(desc) {
6873
+ const firstSentenceEnd = desc.indexOf(". ");
6874
+ const trigger = firstSentenceEnd !== -1 ? desc.slice(0, firstSentenceEnd + 1).trim() : desc.trim().split("\n")[0] ?? "";
6875
+ const scope = [];
6876
+ const coversMatch = /(?:covers|for|including)\s+([^.]+)/i.exec(desc);
6877
+ if (coversMatch) {
6878
+ const items = (coversMatch[1] ?? "").replace(/[·•]/g, ",").split(",").map((s) => s.trim()).filter(Boolean);
6879
+ scope.push(...items);
6173
6880
  }
6881
+ return { trigger, scope };
6174
6882
  }
6175
-
6176
- // src/core/streaming-response-builder.ts
6177
6883
  var STREAM_DRAIN_TIMEOUT_MS = 500;
6178
6884
  function buildResponse(state) {
6179
6885
  const content = [];
@@ -6530,191 +7236,48 @@ async function runProviderWithRetry(opts) {
6530
7236
  }
6531
7237
  await new Promise((resolve5, reject) => {
6532
7238
  let settled = false;
6533
- const onAbort = () => {
6534
- if (settled) return;
6535
- settled = true;
6536
- clearTimeout(t);
6537
- reject(new Error("aborted"));
6538
- };
6539
- const t = setTimeout(() => {
6540
- if (settled) return;
6541
- settled = true;
6542
- clearTimeout(t);
6543
- signal.removeEventListener("abort", onAbort);
6544
- resolve5();
6545
- }, delay);
6546
- if (signal.aborted) {
6547
- onAbort();
6548
- return;
6549
- }
6550
- signal.addEventListener("abort", onAbort, { once: true });
6551
- });
6552
- attempt++;
6553
- }
6554
- }
6555
- }
6556
-
6557
- // src/execution/provider-runner-impl.ts
6558
- var DefaultProviderRunner = class {
6559
- async run(opts) {
6560
- return runProviderWithRetry(opts);
6561
- }
6562
- };
6563
-
6564
- // src/utils/token-estimate.ts
6565
- var RoughTokenEstimate = (text, charsPerToken = 3.5) => Math.max(1, Math.ceil(text.length / charsPerToken));
6566
- var CALIBRATION_GLOBAL_KEY = "__global__";
6567
- var _cals = /* @__PURE__ */ new Map();
6568
- function calState(key) {
6569
- let state = _cals.get(key);
6570
- if (!state) {
6571
- state = { ratio: 1, count: 0, prevEst: 0 };
6572
- _cals.set(key, state);
6573
- }
6574
- return state;
6575
- }
6576
- var MIN_SAMPLES_FOR_CALIBRATION = 3;
6577
- var ESTIMATE_CACHE = /* @__PURE__ */ new Map();
6578
- var ESTIMATE_CACHE_MAX_SIZE = 1e4;
6579
- function getCachedEstimate(key, compute) {
6580
- const existing = ESTIMATE_CACHE.get(key);
6581
- if (existing !== void 0) return existing;
6582
- if (ESTIMATE_CACHE.size >= ESTIMATE_CACHE_MAX_SIZE) {
6583
- let evicted = 0;
6584
- const maxEvict = Math.floor(ESTIMATE_CACHE_MAX_SIZE / 4);
6585
- for (const k of ESTIMATE_CACHE.keys()) {
6586
- if (evicted >= maxEvict) break;
6587
- ESTIMATE_CACHE.delete(k);
6588
- evicted++;
6589
- }
6590
- }
6591
- const estimate = compute(key);
6592
- ESTIMATE_CACHE.set(key, estimate);
6593
- return estimate;
6594
- }
6595
- function estimateToolInputTokens(input) {
6596
- if (typeof input === "string") return RoughTokenEstimate(input);
6597
- if (input === null || typeof input !== "object") {
6598
- return RoughTokenEstimate(String(input));
6599
- }
6600
- return getCachedEstimate(JSON.stringify(input), (key) => RoughTokenEstimate(key));
6601
- }
6602
- function estimateToolResultTokens(content) {
6603
- if (typeof content === "string") return RoughTokenEstimate(content);
6604
- return getCachedEstimate(JSON.stringify(content), (key) => RoughTokenEstimate(key));
6605
- }
6606
- function estimateTextTokens(text) {
6607
- return RoughTokenEstimate(text);
6608
- }
6609
- function computeMessageTokens(msg) {
6610
- if (typeof msg.content === "string") return estimateTextTokens(msg.content);
6611
- let total = 0;
6612
- for (const b of msg.content) {
6613
- if (b.type === "text") total += estimateTextTokens(b.text);
6614
- else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
6615
- else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
6616
- else total += RoughTokenEstimate(JSON.stringify(b));
6617
- }
6618
- return total;
6619
- }
6620
- function estimateMessageTokens(messages) {
6621
- let total = 0;
6622
- for (const m of messages) {
6623
- if (typeof m._estTokens === "number" && m._estTokens > 0) {
6624
- total += m._estTokens;
6625
- continue;
6626
- }
6627
- total += computeMessageTokens(m);
6628
- }
6629
- return total;
6630
- }
6631
- function estimateToolDefTokens(tool) {
6632
- const cached = tool._estDefTokens;
6633
- if (typeof cached === "number" && cached > 0) return cached;
6634
- return RoughTokenEstimate(tool.name) + RoughTokenEstimate(tool.description ?? "") + RoughTokenEstimate(JSON.stringify(tool.inputSchema));
6635
- }
6636
- function estimateRequestTokens(messages, systemPrompt, tools, calibrationKey = CALIBRATION_GLOBAL_KEY) {
6637
- let messagesTokens = 0;
6638
- if (typeof messages === "string") {
6639
- messagesTokens = RoughTokenEstimate(messages);
6640
- } else if (Array.isArray(messages)) {
6641
- for (const m of messages) {
6642
- if (typeof m === "object" && m !== null && "content" in m) {
6643
- const cached = m._estTokens;
6644
- if (typeof cached === "number" && cached > 0) {
6645
- messagesTokens += cached;
6646
- continue;
6647
- }
6648
- const content = m.content;
6649
- if (typeof content === "string") {
6650
- messagesTokens += RoughTokenEstimate(content);
6651
- } else if (Array.isArray(content)) {
6652
- for (const b of content) {
6653
- if (typeof b === "object" && b !== null) {
6654
- if (b.type === "text") {
6655
- messagesTokens += RoughTokenEstimate(b.text);
6656
- } else {
6657
- messagesTokens += RoughTokenEstimate(JSON.stringify(b));
6658
- }
6659
- }
6660
- }
6661
- }
6662
- }
6663
- }
6664
- }
6665
- let systemTokens = 0;
6666
- if (typeof systemPrompt === "string") {
6667
- systemTokens = RoughTokenEstimate(systemPrompt);
6668
- } else if (Array.isArray(systemPrompt)) {
6669
- for (const b of systemPrompt) {
6670
- if (typeof b === "object" && b !== null && b.type === "text") {
6671
- systemTokens += RoughTokenEstimate(b.text);
6672
- }
6673
- }
6674
- }
6675
- let toolsTokens = 0;
6676
- for (const t of tools) {
6677
- toolsTokens += estimateToolDefTokens(t);
6678
- }
6679
- const total = messagesTokens + systemTokens + toolsTokens;
6680
- calState(calibrationKey).prevEst = total;
6681
- return {
6682
- messages: messagesTokens,
6683
- systemPrompt: systemTokens,
6684
- tools: toolsTokens,
6685
- total
6686
- };
6687
- }
6688
- function getCalibrationState(calibrationKey = CALIBRATION_GLOBAL_KEY) {
6689
- const cal = calState(calibrationKey);
6690
- return {
6691
- ratio: cal.ratio,
6692
- count: cal.count,
6693
- calibrated: cal.count >= MIN_SAMPLES_FOR_CALIBRATION
6694
- };
6695
- }
6696
- function estimateRequestTokensCalibrated(messages, systemPrompt, tools, calibrationKey = CALIBRATION_GLOBAL_KEY) {
6697
- const result = estimateRequestTokens(messages, systemPrompt, tools, calibrationKey);
6698
- const cal = calState(calibrationKey);
6699
- if (cal.count >= MIN_SAMPLES_FOR_CALIBRATION) {
6700
- const safeRatio = Math.min(1.5, Math.max(0.5, cal.ratio));
6701
- return {
6702
- messages: Math.round(result.messages * safeRatio),
6703
- systemPrompt: Math.round(result.systemPrompt * safeRatio),
6704
- tools: Math.round(result.tools * safeRatio),
6705
- total: Math.round(result.total * safeRatio)
6706
- };
7239
+ const onAbort = () => {
7240
+ if (settled) return;
7241
+ settled = true;
7242
+ clearTimeout(t);
7243
+ reject(new Error("aborted"));
7244
+ };
7245
+ const t = setTimeout(() => {
7246
+ if (settled) return;
7247
+ settled = true;
7248
+ clearTimeout(t);
7249
+ signal.removeEventListener("abort", onAbort);
7250
+ resolve5();
7251
+ }, delay);
7252
+ if (signal.aborted) {
7253
+ onAbort();
7254
+ return;
7255
+ }
7256
+ signal.addEventListener("abort", onAbort, { once: true });
7257
+ });
7258
+ attempt++;
7259
+ }
6707
7260
  }
6708
- return result;
6709
7261
  }
6710
7262
 
7263
+ // src/execution/provider-runner-impl.ts
7264
+ var DefaultProviderRunner = class {
7265
+ async run(opts) {
7266
+ return runProviderWithRetry(opts);
7267
+ }
7268
+ };
7269
+
6711
7270
  // src/types/blocks.ts
6712
7271
  function isTextBlock(b) {
6713
7272
  return b.type === "text";
6714
7273
  }
6715
7274
 
6716
7275
  // src/execution/compaction-core.ts
7276
+ function compactionDebugEnabled() {
7277
+ return process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1";
7278
+ }
6717
7279
  function emitCompactionMetrics(event, metrics) {
7280
+ if (!compactionDebugEnabled()) return;
6718
7281
  console.log(
6719
7282
  JSON.stringify({
6720
7283
  level: "debug",
@@ -6769,18 +7332,20 @@ function findPreserveStart(messages, preserveK) {
6769
7332
  }
6770
7333
  }
6771
7334
  }
6772
- console.log(
6773
- JSON.stringify({
6774
- level: "debug",
6775
- event: "compaction.find_preserve_start.ended",
6776
- messageCount: messages.length,
6777
- preserveK,
6778
- preserveStart,
6779
- forwardWalkIterations,
6780
- forwardWalkInnerIterations,
6781
- forwardWalkInnerPerOuter: forwardWalkIterations > 0 ? forwardWalkInnerIterations / forwardWalkIterations : 0
6782
- })
6783
- );
7335
+ if (compactionDebugEnabled()) {
7336
+ console.log(
7337
+ JSON.stringify({
7338
+ level: "debug",
7339
+ event: "compaction.find_preserve_start.ended",
7340
+ messageCount: messages.length,
7341
+ preserveK,
7342
+ preserveStart,
7343
+ forwardWalkIterations,
7344
+ forwardWalkInnerIterations,
7345
+ forwardWalkInnerPerOuter: forwardWalkIterations > 0 ? forwardWalkInnerIterations / forwardWalkIterations : 0
7346
+ })
7347
+ );
7348
+ }
6784
7349
  return preserveStart;
6785
7350
  }
6786
7351
  function eliseOldToolResults(messages, opts) {
@@ -6847,7 +7412,7 @@ function eliseOldToolResults(messages, opts) {
6847
7412
  changed = true;
6848
7413
  }
6849
7414
  fullPassInnerIterations += original.length;
6850
- if (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1") {
7415
+ if (compactionDebugEnabled()) {
6851
7416
  const ratio = fullPassInnerIterations / fullPassIterations;
6852
7417
  if (ratio > 10) {
6853
7418
  console.error(
@@ -7254,7 +7819,12 @@ var IntelligentCompactor = class {
7254
7819
  };
7255
7820
  const ac = ctx.signal ? void 0 : new AbortController();
7256
7821
  const signal = ctx.signal ?? ac?.signal;
7257
- const res = await this.provider.complete(req, { signal });
7822
+ let res;
7823
+ try {
7824
+ res = await this.provider.complete(req, { signal });
7825
+ } finally {
7826
+ ac?.abort();
7827
+ }
7258
7828
  const textBlocks = res.content.filter(isTextBlock);
7259
7829
  return textBlocks.map((b) => b.text).join("\n").trim() || "(empty summary)";
7260
7830
  }
@@ -7298,9 +7868,9 @@ Rules:
7298
7868
  - If unsure, keep rather than collapse (errors are more costly than waste)
7299
7869
 
7300
7870
  Return ONLY the JSON object, no markdown, no explanation outside the JSON.`;
7301
- function formatMessages(messages, maxChars = 8e3) {
7871
+ function formatMessages(messages, maxTokens = 2048) {
7302
7872
  const lines = [];
7303
- let used = 0;
7873
+ let usedTokens = 0;
7304
7874
  for (let i = 0; i < messages.length; i++) {
7305
7875
  const m = expectDefined(messages[i]);
7306
7876
  const role = m.role.padEnd(10, " ");
@@ -7312,13 +7882,14 @@ function formatMessages(messages, maxChars = 8e3) {
7312
7882
  text = content.filter(isTextBlock).map((b) => b.text).join(" ");
7313
7883
  const toolUses = content.filter((b) => b.type === "tool_use");
7314
7884
  if (toolUses.length > 0) {
7315
- text += ` [tools: ${toolUses.map((b) => b.name).join(", ")}]`;
7885
+ text += ` [tools: ${toolUses.map((b) => b.name).filter(Boolean).join(", ")}]`;
7316
7886
  }
7317
7887
  }
7318
7888
  const line = `[${i}][${role}]: ${text}`;
7319
- if (used + line.length > maxChars) break;
7889
+ const lineTokens = estimateTextTokens(line);
7890
+ if (usedTokens + lineTokens > maxTokens) break;
7320
7891
  lines.push(line);
7321
- used += line.length;
7892
+ usedTokens += lineTokens;
7322
7893
  }
7323
7894
  return lines.join("\n");
7324
7895
  }
@@ -7327,20 +7898,29 @@ var LLMSelector = class {
7327
7898
  model;
7328
7899
  maxContextTokens;
7329
7900
  systemPrompt;
7901
+ maxOutputTokens;
7330
7902
  constructor(opts) {
7331
7903
  this.provider = opts.provider;
7332
7904
  this.model = opts.model ?? "unknown";
7905
+ if (this.model === "unknown" && (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1")) {
7906
+ console.warn(
7907
+ "[LLMSelector] model not set \u2014 selector will use the provider default. Set `model` explicitly in LLMSelectorOptions to silence this warning."
7908
+ );
7909
+ }
7333
7910
  this.maxContextTokens = opts.maxContextTokens ?? 4e4;
7334
7911
  this.systemPrompt = opts.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
7912
+ this.maxOutputTokens = opts.maxOutputTokens ?? 1024;
7335
7913
  }
7336
7914
  async select(messages, maxToKeep) {
7337
7915
  const effectiveBudget = Math.min(maxToKeep, this.maxContextTokens);
7338
- const historyText = formatMessages(messages);
7339
7916
  const totalTokens = estimateMessageTokens(messages);
7340
7917
  const systemText = `${this.systemPrompt}
7341
7918
 
7342
7919
  Conversation (${messages.length} messages, ~${totalTokens} tokens, budget: ${effectiveBudget}):
7343
7920
  `;
7921
+ const systemTokens = estimateTextTokens(systemText);
7922
+ const historyBudget = Math.max(512, effectiveBudget - systemTokens - this.maxOutputTokens);
7923
+ const historyText = formatMessages(messages, historyBudget);
7344
7924
  const budgetInstruction = totalTokens > effectiveBudget ? `
7345
7925
 
7346
7926
  IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiveBudget}). You MUST collapse enough to fit. Prefer collapsing older/lower-importance ranges.` : "";
@@ -7348,18 +7928,26 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
7348
7928
  model: this.model,
7349
7929
  system: [{ type: "text", text: systemText + budgetInstruction }],
7350
7930
  messages: [{ role: "user", content: historyText }],
7351
- maxTokens: 1024
7931
+ maxTokens: this.maxOutputTokens
7352
7932
  };
7353
7933
  let raw;
7934
+ const ac = new AbortController();
7354
7935
  try {
7355
- const ac = new AbortController();
7356
- const res = await this.provider.complete(req, { signal: ac.signal });
7936
+ const timeoutSignal = AbortSignal.timeout(3e4);
7937
+ const res = await this.provider.complete(req, {
7938
+ signal: AbortSignal.any([ac.signal, timeoutSignal])
7939
+ });
7357
7940
  const textBlocks = res.content.filter(isTextBlock);
7358
7941
  raw = textBlocks.map((b) => b.text).join("\n").trim();
7359
- } catch (_err) {
7942
+ } catch (err) {
7943
+ if (err instanceof Error) {
7944
+ console.warn("[LLMSelector] selector call failed, using recency fallback:", err.message);
7945
+ }
7360
7946
  return this.fallbackSelect(messages, effectiveBudget);
7947
+ } finally {
7948
+ ac.abort();
7361
7949
  }
7362
- return this.parseSelectorOutput(raw, messages.length);
7950
+ return this.parseSelectorOutput(raw, messages);
7363
7951
  }
7364
7952
  fallbackSelect(messages, budget) {
7365
7953
  const toKeep = [];
@@ -7386,34 +7974,63 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
7386
7974
  reasoning: `Fallback: kept last ${messages.length - startIdx} messages within ${budget} token budget`
7387
7975
  };
7388
7976
  }
7389
- parseSelectorOutput(raw, messageCount) {
7977
+ /**
7978
+ * Parse and validate the raw LLM output into a SelectorResult.
7979
+ * Falls back to recency-based selection if the LLM output is malformed,
7980
+ * out-of-bounds, or internally inconsistent.
7981
+ */
7982
+ parseSelectorOutput(raw, messages) {
7983
+ const messageCount = messages.length;
7984
+ if (messageCount === 0) {
7985
+ return { kept: [], collapsed: [], reasoning: "empty session" };
7986
+ }
7390
7987
  const jsonStart = raw.indexOf("{");
7391
7988
  const jsonEnd = raw.lastIndexOf("}");
7392
7989
  if (jsonStart === -1 || jsonEnd === -1) {
7393
- return this.fallbackSelect(
7394
- Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
7395
- this.maxContextTokens
7396
- );
7990
+ return this.fallbackSelect(messages, this.maxContextTokens);
7397
7991
  }
7398
7992
  let parsed;
7399
7993
  try {
7400
7994
  parsed = JSON.parse(raw.slice(jsonStart, jsonEnd + 1));
7401
7995
  } catch {
7402
- return this.fallbackSelect(
7403
- Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
7404
- this.maxContextTokens
7405
- );
7996
+ return this.fallbackSelect(messages, this.maxContextTokens);
7406
7997
  }
7407
7998
  const obj = parsed;
7408
- const kept = obj.kept ?? [];
7409
- const collapsed = obj.collapsed ?? [];
7410
- return {
7411
- kept: kept.map((k) => ({
7999
+ const keptRaw = obj.kept ?? [];
8000
+ const collapsedRaw = obj.collapsed ?? [];
8001
+ const kept = [];
8002
+ for (const k of keptRaw) {
8003
+ if (typeof k.from !== "number" || typeof k.to !== "number" || k.from < 0 || k.to >= messageCount || k.from > k.to) {
8004
+ return this.fallbackSelect(messages, this.maxContextTokens);
8005
+ }
8006
+ kept.push({
7412
8007
  from: k.from,
7413
8008
  to: k.to,
7414
8009
  importance: k.importance ?? "medium"
7415
- })),
7416
- collapsed: collapsed.map((c) => ({ from: c.from, to: c.to, summary: c.summary })),
8010
+ });
8011
+ }
8012
+ const collapsed = [];
8013
+ for (const c of collapsedRaw) {
8014
+ if (typeof c.from !== "number" || typeof c.to !== "number" || c.from < 0 || c.to >= messageCount || c.from > c.to) {
8015
+ return this.fallbackSelect(messages, this.maxContextTokens);
8016
+ }
8017
+ collapsed.push({ from: c.from, to: c.to, summary: c.summary });
8018
+ }
8019
+ const allRanges = [...kept, ...collapsed];
8020
+ for (let i = 0; i < allRanges.length; i++) {
8021
+ const a = allRanges[i];
8022
+ if (!a) continue;
8023
+ for (let j = i + 1; j < allRanges.length; j++) {
8024
+ const b = allRanges[j];
8025
+ if (!b) continue;
8026
+ if (a.from <= b.to && a.to >= b.from) {
8027
+ return this.fallbackSelect(messages, this.maxContextTokens);
8028
+ }
8029
+ }
8030
+ }
8031
+ return {
8032
+ kept,
8033
+ collapsed,
7417
8034
  reasoning: typeof obj.reasoning === "string" ? obj.reasoning : ""
7418
8035
  };
7419
8036
  }
@@ -7433,7 +8050,7 @@ var SelectiveCompactor = class {
7433
8050
  summarizerPrompt;
7434
8051
  constructor(opts) {
7435
8052
  this.provider = opts.provider;
7436
- this.selector = opts.selector ?? new LLMSelector({ provider: opts.provider, model: opts.selectorModel });
8053
+ this.selector = opts.selector ?? new LLMSelector({ provider: opts.provider, model: opts.selectorModel, maxOutputTokens: opts.selectorMaxOutputTokens });
7437
8054
  this.warnThreshold = opts.warnThreshold ?? 0.6;
7438
8055
  this.softThreshold = opts.softThreshold ?? 0.75;
7439
8056
  this.hardThreshold = opts.hardThreshold ?? 0.9;
@@ -7441,6 +8058,11 @@ var SelectiveCompactor = class {
7441
8058
  this.preserveK = opts.preserveK ?? 4;
7442
8059
  this.eliseThreshold = opts.eliseThreshold ?? 500;
7443
8060
  this.summarizerModel = opts.summarizerModel ?? opts.selectorModel ?? "unknown";
8061
+ if (this.summarizerModel === "unknown" && (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1")) {
8062
+ console.warn(
8063
+ "[SelectiveCompactor] summarizerModel not set \u2014 will use provider default. Set `summarizerModel` explicitly to silence this warning."
8064
+ );
8065
+ }
7444
8066
  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.";
7445
8067
  }
7446
8068
  async compact(ctx, opts = {}) {
@@ -7552,8 +8174,9 @@ Summarize the following message range:`;
7552
8174
  maxTokens: 512
7553
8175
  };
7554
8176
  try {
8177
+ const timeoutSignal = AbortSignal.timeout(3e4);
7555
8178
  const res = await this.provider.complete(req, {
7556
- signal: ctx.signal ?? new AbortController().signal
8179
+ signal: AbortSignal.any([ctx.signal, timeoutSignal])
7557
8180
  });
7558
8181
  return res.content.filter(isTextBlock).map((b) => b.text).join("\n").trim() || "(empty)";
7559
8182
  } catch {
@@ -7687,6 +8310,7 @@ var ProviderBackedCompactor = class {
7687
8310
  return new SelectiveCompactor({
7688
8311
  ...common,
7689
8312
  selectorModel: this.opts.summarizerModel,
8313
+ selectorMaxOutputTokens: this.opts.selectorMaxOutputTokens,
7690
8314
  summarizerModel: this.opts.summarizerModel
7691
8315
  });
7692
8316
  }
@@ -7721,6 +8345,12 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7721
8345
  hardThreshold;
7722
8346
  /** Writable so model-switch can update the denominator without re-registering the middleware. */
7723
8347
  _maxContext;
8348
+ /**
8349
+ * Runtime on/off gate. The middleware is always installed in the pipeline so
8350
+ * auto-compaction can be toggled live from the TUI `/settings` picker; when
8351
+ * disabled the handler is a pass-through. Defaults to enabled.
8352
+ */
8353
+ _enabled = true;
7724
8354
  aggressiveOn;
7725
8355
  events;
7726
8356
  failureMode;
@@ -7778,8 +8408,19 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7778
8408
  setMaxContext(maxContext) {
7779
8409
  this._maxContext = maxContext;
7780
8410
  }
8411
+ /** Whether auto-compaction is currently active. */
8412
+ get enabled() {
8413
+ return this._enabled;
8414
+ }
8415
+ /** Toggle auto-compaction on a live session (TUI `/settings`). When disabled
8416
+ * the middleware passes every iteration straight through without estimating
8417
+ * tokens or compacting. */
8418
+ setEnabled(enabled) {
8419
+ this._enabled = enabled;
8420
+ }
7781
8421
  handler() {
7782
8422
  return async (ctx, next) => {
8423
+ if (!this._enabled) return next(ctx);
7783
8424
  const msgCount = ctx.messages.length;
7784
8425
  const toolCount = (ctx.tools ?? []).length;
7785
8426
  let tokens;
@@ -7921,144 +8562,6 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7921
8562
  }
7922
8563
  };
7923
8564
 
7924
- // src/utils/json-schema-validate.ts
7925
- function validateAgainstSchema(value, schema) {
7926
- const errors = [];
7927
- walk2(value, schema, "", errors);
7928
- return { ok: errors.length === 0, errors };
7929
- }
7930
- function walk2(value, schema, path19, errors) {
7931
- if (schema.enum !== void 0) {
7932
- if (!schema.enum.some((e) => deepEqual(e, value))) {
7933
- errors.push({
7934
- path: path19 || "<root>",
7935
- message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
7936
- });
7937
- return;
7938
- }
7939
- }
7940
- if (typeof schema.type === "string") {
7941
- if (!checkType(value, schema.type)) {
7942
- errors.push({
7943
- path: path19 || "<root>",
7944
- message: `expected ${schema.type}, got ${describeType(value)}`
7945
- });
7946
- return;
7947
- }
7948
- }
7949
- if (schema.type === "object" && isPlainObject(value)) {
7950
- const obj = value;
7951
- for (const req of schema.required ?? []) {
7952
- if (!(req in obj)) {
7953
- errors.push({ path: joinPath(path19, req), message: "required property missing" });
7954
- }
7955
- }
7956
- if (schema.properties) {
7957
- for (const [key, subSchema] of Object.entries(schema.properties)) {
7958
- if (key in obj) {
7959
- walk2(obj[key], subSchema, joinPath(path19, key), errors);
7960
- }
7961
- }
7962
- }
7963
- }
7964
- if (schema.type === "array" && Array.isArray(value) && schema.items) {
7965
- value.forEach((item, i) => walk2(item, schema.items, `${path19}[${i}]`, errors));
7966
- }
7967
- }
7968
- function checkType(value, type) {
7969
- switch (type) {
7970
- case "string":
7971
- return typeof value === "string";
7972
- case "number":
7973
- return typeof value === "number" && !Number.isNaN(value);
7974
- case "integer":
7975
- return typeof value === "number" && Number.isInteger(value);
7976
- case "boolean":
7977
- return typeof value === "boolean";
7978
- case "null":
7979
- return value === null;
7980
- case "array":
7981
- return Array.isArray(value);
7982
- case "object":
7983
- return isPlainObject(value);
7984
- default:
7985
- return true;
7986
- }
7987
- }
7988
- function isPlainObject(v) {
7989
- return typeof v === "object" && v !== null && !Array.isArray(v);
7990
- }
7991
- function describeType(v) {
7992
- if (v === null) return "null";
7993
- if (Array.isArray(v)) return "array";
7994
- return typeof v;
7995
- }
7996
- function joinPath(parent, key) {
7997
- if (!parent) return key;
7998
- return `${parent}.${key}`;
7999
- }
8000
- function deepEqual(a, b) {
8001
- if (a === b) return true;
8002
- if (typeof a !== typeof b) return false;
8003
- if (a === null || b === null) return a === b;
8004
- if (Array.isArray(a) && Array.isArray(b)) {
8005
- return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
8006
- }
8007
- if (typeof a === "object" && typeof b === "object") {
8008
- const ak = Object.keys(a);
8009
- const bk = Object.keys(b);
8010
- if (ak.length !== bk.length) return false;
8011
- return ak.every(
8012
- (k) => deepEqual(a[k], b[k])
8013
- );
8014
- }
8015
- return false;
8016
- }
8017
-
8018
- // src/utils/tool-output-serializer.ts
8019
- function createToolOutputSerializer(opts = {}) {
8020
- const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
8021
- function serialize(value) {
8022
- if (typeof value === "string") return value;
8023
- if (value === null || value === void 0) return "";
8024
- if (typeof value === "object") {
8025
- if (Array.isArray(value)) return value.map(serialize).join("\n");
8026
- if ("text" in value) {
8027
- const t = value.text;
8028
- return typeof t === "string" ? t : JSON.stringify(value, null, 2);
8029
- }
8030
- try {
8031
- return JSON.stringify(value, null, 2);
8032
- } catch {
8033
- return String(value);
8034
- }
8035
- }
8036
- return String(value);
8037
- }
8038
- function enforceCap(text, remainingBudget) {
8039
- if (remainingBudget <= 0) {
8040
- return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
8041
- }
8042
- const textBytes = Buffer.byteLength(text, "utf8");
8043
- if (textBytes <= remainingBudget) {
8044
- return { text, newBudget: remainingBudget - textBytes };
8045
- }
8046
- const marker = `
8047
- \u2026[truncated ${textBytes - remainingBudget} bytes]\u2026
8048
- `;
8049
- const markerBytes = Buffer.byteLength(marker, "utf8");
8050
- const available = remainingBudget - markerBytes;
8051
- if (available <= 0) {
8052
- return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
8053
- }
8054
- const half = Math.floor(available / 2);
8055
- const first = text.slice(0, half);
8056
- const second = text.slice(text.length - half);
8057
- return { text: `${first}${marker}${second}`, newBudget: 0 };
8058
- }
8059
- return { serialize, enforceCap, capBytes };
8060
- }
8061
-
8062
8565
  // src/execution/tool-executor.ts
8063
8566
  var ToolExecutor = class _ToolExecutor {
8064
8567
  constructor(registry, opts) {
@@ -8222,7 +8725,7 @@ ${post.additionalContext}`;
8222
8725
  );
8223
8726
  return { result, tool, durationMs: Date.now() - start };
8224
8727
  } catch (err) {
8225
- const msg = err instanceof Error ? err.message : String(err);
8728
+ const msg = toErrorMessage(err);
8226
8729
  const scrubbed = this.opts.secretScrubber.scrub(msg);
8227
8730
  this.opts.renderer?.writeToolResult(tool.name, scrubbed, true);
8228
8731
  const result = {
@@ -8243,7 +8746,7 @@ ${post.additionalContext}`;
8243
8746
  try {
8244
8747
  return await runOne(use);
8245
8748
  } catch (err) {
8246
- const msg = err instanceof Error ? err.message : String(err);
8749
+ const msg = toErrorMessage(err);
8247
8750
  const scrubbed = this.opts.secretScrubber.scrub(msg);
8248
8751
  const result = {
8249
8752
  type: "tool_result",
@@ -8526,15 +9029,6 @@ function extractMalformedRaw(input) {
8526
9029
  }
8527
9030
  }
8528
9031
 
8529
- // src/utils/assert-never.ts
8530
- function assertNever(x, message) {
8531
- const err = new Error(
8532
- `Unhandled case: ${JSON.stringify(x)}`
8533
- );
8534
- err.name = "AssertNeverError";
8535
- throw err;
8536
- }
8537
-
8538
9032
  // src/execution/autonomous-runner.ts
8539
9033
  var DoneConditionChecker = class {
8540
9034
  constructor(condition) {
@@ -8714,64 +9208,6 @@ var AutonomousRunner = class {
8714
9208
 
8715
9209
  // src/storage/goal-store.ts
8716
9210
  init_atomic_write();
8717
- function projectHash(absRoot) {
8718
- return createHash("sha256").update(path11.resolve(absRoot)).digest("hex").slice(0, 12);
8719
- }
8720
- function projectSlug(absRoot) {
8721
- const base = slugify(path11.basename(absRoot));
8722
- const hash = createHash("sha256").update(path11.resolve(absRoot)).digest("hex").slice(0, 6);
8723
- return `${base}-${hash}`;
8724
- }
8725
- function slugify(name) {
8726
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
8727
- }
8728
- function wstackGlobalRoot() {
8729
- const fromEnv = process.env["WRONGSTACK_HOME"];
8730
- if (fromEnv && fromEnv.trim().length > 0) return path11.resolve(fromEnv);
8731
- return path11.join(os.homedir(), ".wrongstack");
8732
- }
8733
- function resolveWstackPaths(opts) {
8734
- const globalRoot = opts.globalRoot ?? (opts.userHome ? path11.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
8735
- const hash = projectHash(opts.projectRoot);
8736
- const slug = projectSlug(opts.projectRoot);
8737
- const projectDir = path11.join(globalRoot, "projects", slug);
8738
- return {
8739
- globalRoot,
8740
- configDir: globalRoot,
8741
- globalConfig: path11.join(globalRoot, "config.json"),
8742
- secretsKey: path11.join(globalRoot, ".key"),
8743
- globalMemory: path11.join(globalRoot, "memory.md"),
8744
- globalSkills: path11.join(globalRoot, "skills"),
8745
- globalPrompts: path11.join(globalRoot, "prompts"),
8746
- cacheDir: path11.join(globalRoot, "cache"),
8747
- modelsCache: path11.join(globalRoot, "cache", "models.dev.json"),
8748
- modelsOverlayCache: path11.join(globalRoot, "cache", "models-overlay.json"),
8749
- historyFile: path11.join(globalRoot, "history"),
8750
- logFile: path11.join(globalRoot, "logs", "wrongstack.log"),
8751
- projectDir,
8752
- projectCodebaseIndex: path11.join(projectDir, "codebase-index"),
8753
- projectMemory: path11.join(projectDir, "memory.md"),
8754
- projectSessions: path11.join(projectDir, "sessions"),
8755
- projectTrust: path11.join(projectDir, "trust.json"),
8756
- projectMeta: path11.join(projectDir, "meta.json"),
8757
- projectLocalConfig: path11.join(projectDir, "config.local.json"),
8758
- inProjectConfig: path11.join(opts.projectRoot, ".wrongstack", "config.json"),
8759
- inProjectAgentsFile: path11.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
8760
- inProjectSkills: path11.join(opts.projectRoot, ".wrongstack", "skills"),
8761
- inProjectWorktrees: path11.join(opts.projectRoot, ".wrongstack", "worktrees"),
8762
- projectHash: hash,
8763
- projectSlug: slug,
8764
- projectGoal: path11.join(projectDir, "goal.json"),
8765
- projectSpecs: path11.join(projectDir, "specs"),
8766
- projectTaskGraphs: path11.join(projectDir, "task-graphs"),
8767
- projectSddSession: path11.join(projectDir, "sdd-session.json"),
8768
- projectPlan: path11.join(projectDir, "plan.json"),
8769
- projectAutophase: path11.join(projectDir, "autophase"),
8770
- syncConfig: path11.join(globalRoot, "sync.json")
8771
- };
8772
- }
8773
-
8774
- // src/storage/goal-store.ts
8775
9211
  var MAX_JOURNAL_ENTRIES = 500;
8776
9212
  function goalFilePath(projectRoot) {
8777
9213
  return resolveWstackPaths({ projectRoot }).projectGoal;
@@ -8780,7 +9216,7 @@ async function loadGoal(filePath, events) {
8780
9216
  const t0 = Date.now();
8781
9217
  let raw;
8782
9218
  try {
8783
- raw = await fsp.readFile(filePath, "utf8");
9219
+ raw = await fsp2.readFile(filePath, "utf8");
8784
9220
  } catch (err) {
8785
9221
  const code = err.code;
8786
9222
  if (code === "ENOENT") {
@@ -8799,7 +9235,7 @@ async function loadGoal(filePath, events) {
8799
9235
  store: "goal",
8800
9236
  filePath,
8801
9237
  operation: "load",
8802
- error: err instanceof Error ? err.message : String(err),
9238
+ error: toErrorMessage(err),
8803
9239
  recoverable: false
8804
9240
  });
8805
9241
  throw err;
@@ -8872,11 +9308,11 @@ async function saveGoal(filePath, goal, events) {
8872
9308
  store: "goal",
8873
9309
  filePath,
8874
9310
  operation: "save",
8875
- error: err instanceof Error ? err.message : String(err),
9311
+ error: toErrorMessage(err),
8876
9312
  recoverable: false
8877
9313
  });
8878
9314
  throw new FsError({
8879
- message: err instanceof Error ? err.message : String(err),
9315
+ message: toErrorMessage(err),
8880
9316
  code: ERROR_CODES.FS_ATOMIC_WRITE_FAILED,
8881
9317
  path: filePath,
8882
9318
  cause: err
@@ -8930,11 +9366,6 @@ function computeTrend(history) {
8930
9366
  return "steady";
8931
9367
  }
8932
9368
 
8933
- // src/utils/sleep.ts
8934
- function sleep(ms) {
8935
- return new Promise((resolve5) => setTimeout(resolve5, ms));
8936
- }
8937
-
8938
9369
  // src/execution/autonomy-brain.ts
8939
9370
  function formatDecisionSummary(decision, request) {
8940
9371
  const question = request.question.length > 80 ? request.question.slice(0, 77) + "\u2026" : request.question;
@@ -8985,7 +9416,7 @@ var EternalAutonomyEngine = class {
8985
9416
  console.error(JSON.stringify({
8986
9417
  level: "error",
8987
9418
  event: "engine.persist_state_failed",
8988
- message: err instanceof Error ? err.message : String(err),
9419
+ message: toErrorMessage(err),
8989
9420
  context: { expectedState: "stopped" },
8990
9421
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
8991
9422
  }));
@@ -9018,7 +9449,7 @@ var EternalAutonomyEngine = class {
9018
9449
  } catch (err) {
9019
9450
  this.consecutiveFailures++;
9020
9451
  this.opts.onError?.(err instanceof Error ? err : new Error(String(err)), this.consecutiveFailures);
9021
- await this.appendFailure("engine error", err instanceof Error ? err.message : String(err));
9452
+ await this.appendFailure("engine error", toErrorMessage(err));
9022
9453
  }
9023
9454
  if (iterationOk) {
9024
9455
  this.consecutiveFailures = 0;
@@ -9119,7 +9550,7 @@ var EternalAutonomyEngine = class {
9119
9550
  } catch (err) {
9120
9551
  const isAbort = err instanceof Error && (err.name === "AbortError" || err.message.includes("abort"));
9121
9552
  status = isAbort ? "aborted" : "failure";
9122
- note = err instanceof Error ? err.message : String(err);
9553
+ note = toErrorMessage(err);
9123
9554
  if (!isAbort && typeof err?.recoverable === "boolean") {
9124
9555
  isTransientFailure = err.recoverable;
9125
9556
  }
@@ -9609,6 +10040,7 @@ ${recentJournal}` : ""
9609
10040
 
9610
10041
  // src/coordination/subagent-budget.ts
9611
10042
  var TIMEOUT_PREEMPT_FRACTION = 0.85;
10043
+ var DECISION_TIMEOUT_MS = 6e4;
9612
10044
  var BudgetExceededError = class extends Error {
9613
10045
  kind;
9614
10046
  limit;
@@ -9638,6 +10070,31 @@ var BudgetThresholdSignal = class extends Error {
9638
10070
  };
9639
10071
  var SubagentBudget = class _SubagentBudget {
9640
10072
  limits;
10073
+ /** Patch one or more budget limits in-place after construction.
10074
+ * Used by the coordinator watchdog when granting an extension.
10075
+ * All fields are optional — only provided fields are updated.
10076
+ * This is the single write path for limit mutations so that future
10077
+ * validation or side-effects live in one place (M1). */
10078
+ patchLimits(ext) {
10079
+ if (ext.maxIterations !== void 0) {
10080
+ this.limits.maxIterations = ext.maxIterations;
10081
+ }
10082
+ if (ext.maxToolCalls !== void 0) {
10083
+ this.limits.maxToolCalls = ext.maxToolCalls;
10084
+ }
10085
+ if (ext.maxTokens !== void 0) {
10086
+ this.limits.maxTokens = ext.maxTokens;
10087
+ }
10088
+ if (ext.maxCostUsd !== void 0) {
10089
+ this.limits.maxCostUsd = ext.maxCostUsd;
10090
+ }
10091
+ if (ext.timeoutMs !== void 0) {
10092
+ this.limits.timeoutMs = ext.timeoutMs;
10093
+ }
10094
+ if (ext.idleTimeoutMs !== void 0) {
10095
+ this.limits.idleTimeoutMs = ext.idleTimeoutMs;
10096
+ }
10097
+ }
9641
10098
  iterations = 0;
9642
10099
  toolCalls = 0;
9643
10100
  tokenInput = 0;
@@ -9658,12 +10115,44 @@ var SubagentBudget = class _SubagentBudget {
9658
10115
  * or hung listener (Director not built / event filter detached mid-run)
9659
10116
  * leaves the budget over-limit and never enforces anything.
9660
10117
  */
9661
- static DECISION_TIMEOUT_MS = 6e4;
10118
+ static DECISION_TIMEOUT_MS = DECISION_TIMEOUT_MS;
9662
10119
  /**
9663
10120
  * Injected by the runner when wiring the budget to its EventBus.
9664
10121
  * Used to emit `budget.threshold_reached` events in `'auto'` mode.
9665
10122
  */
9666
- _events;
10123
+ _events;
10124
+ /**
10125
+ * Guard against dual-path races between the coordinator watchdog
10126
+ * (`executeWithTimeout`) and the budget's own `checkTimeout()`.
10127
+ * Both paths detect `elapsed >= timeoutMs` and can emit
10128
+ * `budget.threshold_reached` for kind `'timeout'` simultaneously.
10129
+ * Set to the current `timeoutMs` ceiling by the coordinator BEFORE
10130
+ * calling `onThreshold`, and cleared after the negotiation resolves.
10131
+ * `checkTimeout()` skips its wall-clock check while this is set so
10132
+ * the coordinator's watchdog is the sole source of wall-clock timeout
10133
+ * events — `checkTimeout()` focuses exclusively on `idle_timeout`.
10134
+ */
10135
+ _watchdogActive;
10136
+ /** Returns the timeout ceiling currently being negotiated by the watchdog,
10137
+ * or `undefined` when no wall-clock negotiation is in flight.
10138
+ * Used by `executeWithTimeout` to detect a stale lock (M3). */
10139
+ get watchdogActive() {
10140
+ return this._watchdogActive;
10141
+ }
10142
+ /** Called by the coordinator watchdog BEFORE calling `onThreshold` so that
10143
+ * `checkTimeout()` skips its wall-clock check for this ceiling. Prevents
10144
+ * the budget's own `checkTimeout()` from emitting a second
10145
+ * `budget.threshold_reached` event while the watchdog is already
10146
+ * negotiating the same wall-clock deadline (C1). */
10147
+ setWatchdogNegotiation(timeoutMs) {
10148
+ this._watchdogActive = timeoutMs;
10149
+ }
10150
+ /** Clears the watchdog guard after negotiation resolves. Called in the
10151
+ * `finally` block of both the pre-empt and deadline branches so it fires
10152
+ * on every exit path: grant, deny, throw, or error. */
10153
+ clearWatchdogNegotiation() {
10154
+ this._watchdogActive = void 0;
10155
+ }
9667
10156
  /**
9668
10157
  * Negotiation mode — controls whether a threshold hit tries to emit
9669
10158
  * `budget.threshold_reached` and wait for a coordinator decision, or
@@ -9764,7 +10253,8 @@ var SubagentBudget = class _SubagentBudget {
9764
10253
  if (this.limits.idleTimeoutMs !== void 0 && idle > this.limits.idleTimeoutMs) {
9765
10254
  exceeded.push({ kind: "idle_timeout", used: idle, limit: this.limits.idleTimeoutMs });
9766
10255
  }
9767
- if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs) {
10256
+ const wallOwnedByWatchdog = this._onThreshold !== void 0 && this._watchdogActive === this.limits.timeoutMs;
10257
+ if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs && !wallOwnedByWatchdog) {
9768
10258
  exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
9769
10259
  }
9770
10260
  }
@@ -9778,19 +10268,99 @@ var SubagentBudget = class _SubagentBudget {
9778
10268
  throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
9779
10269
  }
9780
10270
  const bus = this._events;
9781
- if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
10271
+ if (!bus) {
9782
10272
  const first2 = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
9783
10273
  throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
9784
10274
  }
10275
+ const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
10276
+ if (bus.hasListenerFor("budget.threshold_reached")) {
10277
+ for (const entry of exceeded) {
10278
+ if (this._pendingNegotiations.has(entry.kind)) continue;
10279
+ this._pendingNegotiations.set(entry.kind, this._negotiateExtension(entry));
10280
+ }
10281
+ const decision = this._pendingNegotiations.get(first.kind);
10282
+ if (!decision) throw new Error(`No pending negotiation for ${first.kind}`);
10283
+ throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
10284
+ }
10285
+ let hardStop = null;
9785
10286
  for (const entry of exceeded) {
9786
10287
  if (this._pendingNegotiations.has(entry.kind)) continue;
9787
- const decision2 = this._negotiateExtension(entry.kind, exceeded);
9788
- this._pendingNegotiations.set(entry.kind, decision2);
10288
+ const marker = Promise.resolve("stop");
10289
+ this._pendingNegotiations.set(entry.kind, marker);
10290
+ void marker.finally(() => this._pendingNegotiations.delete(entry.kind));
10291
+ const sync = this._invokeHandlerSync(entry);
10292
+ if (!sync) hardStop ??= new BudgetExceededError(entry.kind, entry.limit, entry.used);
9789
10293
  }
9790
- const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
9791
- const decision = this._pendingNegotiations.get(first.kind);
9792
- if (!decision) throw new Error(`No pending negotiation for ${first.kind}`);
9793
- throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
10294
+ if (hardStop) throw hardStop;
10295
+ return exceeded;
10296
+ }
10297
+ /**
10298
+ * Invoke `onThreshold` once for `entry` on the NO-LISTENER path and report
10299
+ * whether it decided synchronously. Returns `true` when the handler returned
10300
+ * a synchronous decision (already honored — an `extend` patched the limits),
10301
+ * or `false` when it returned a Promise (async; the caller hard-stops, since
10302
+ * there is no listener to resolve the negotiation). The handler is given the
10303
+ * full info shape (`requestDecision` plus direct `extend`/`deny`) so both
10304
+ * recording handlers and policy handlers work without a wired listener.
10305
+ */
10306
+ _invokeHandlerSync(entry) {
10307
+ const handler = this._onThreshold;
10308
+ if (!handler) return false;
10309
+ let extendArg;
10310
+ const result = handler({
10311
+ kind: entry.kind,
10312
+ used: entry.used,
10313
+ limit: entry.limit,
10314
+ requestDecision: () => this._busRequestDecision(entry),
10315
+ // Direct hooks for synchronous policy/recording handlers.
10316
+ extend: (extra) => {
10317
+ extendArg = extra;
10318
+ },
10319
+ deny: () => {
10320
+ }
10321
+ });
10322
+ if (result && typeof result.then === "function") return false;
10323
+ if (result === "throw") return false;
10324
+ if (result && typeof result === "object" && "extend" in result) {
10325
+ extendArg = result.extend;
10326
+ }
10327
+ if (extendArg) this.patchLimits(extendArg);
10328
+ return true;
10329
+ }
10330
+ /**
10331
+ * Emit `budget.threshold_reached` and resolve to the listener's verdict.
10332
+ * Resolves to `'stop'` immediately when there is no listener (or no bus) so
10333
+ * no negotiation can hang and no fallback timer leaks. Mirrors the
10334
+ * coordinator watchdog's own request path so both agree on the no-listener
10335
+ * default.
10336
+ */
10337
+ _busRequestDecision(entry) {
10338
+ const bus = this._events;
10339
+ if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
10340
+ return Promise.resolve("stop");
10341
+ }
10342
+ return new Promise((resolve5) => {
10343
+ let resolved = false;
10344
+ const respond = (d) => {
10345
+ if (resolved) return;
10346
+ resolved = true;
10347
+ clearTimeout(fallback);
10348
+ resolve5(d);
10349
+ };
10350
+ const fallback = setTimeout(() => respond("stop"), _SubagentBudget.DECISION_TIMEOUT_MS);
10351
+ bus.emit("budget.threshold_reached", {
10352
+ kind: entry.kind,
10353
+ used: entry.used,
10354
+ limit: entry.limit,
10355
+ timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
10356
+ // deny() wins over a same-dispatch extend(): a listener that both grants
10357
+ // and denies (or two listeners disagreeing) is resolved as a stop. The
10358
+ // grant is deferred a microtask so a synchronous deny in the same emit
10359
+ // pre-empts it; async grants still resolve normally.
10360
+ extend: (extra) => queueMicrotask(() => respond({ extend: extra })),
10361
+ deny: () => respond("stop")
10362
+ });
10363
+ });
9794
10364
  }
9795
10365
  /**
9796
10366
  * Per-kind in-flight negotiation Promises. Each budget kind can have its
@@ -9810,77 +10380,33 @@ var SubagentBudget = class _SubagentBudget {
9810
10380
  * `{ extend: {} }` — keep going without patching; next overrun fires
9811
10381
  * a fresh signal.
9812
10382
  */
9813
- async _negotiateExtension(kind, exceeded) {
10383
+ async _negotiateExtension(entry) {
9814
10384
  if (!this._onThreshold) {
9815
10385
  return "stop";
9816
10386
  }
9817
10387
  try {
9818
- const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
9819
10388
  const result = this._onThreshold({
9820
- kind: first.kind,
9821
- used: first.used,
9822
- limit: first.limit,
9823
- requestDecision: () => {
9824
- const bus = this._events;
9825
- if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
9826
- return Promise.resolve("stop");
9827
- }
9828
- return new Promise((resolve5) => {
9829
- let resolved = false;
9830
- const respond = (d) => {
9831
- if (resolved) return;
9832
- resolved = true;
9833
- resolve5(d);
9834
- };
9835
- const fallback = setTimeout(
9836
- () => respond("stop"),
9837
- _SubagentBudget.DECISION_TIMEOUT_MS
9838
- );
9839
- for (const { kind: kind2, used, limit } of exceeded) {
9840
- bus.emit("budget.threshold_reached", {
9841
- kind: kind2,
9842
- used,
9843
- limit,
9844
- timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
9845
- extend: (extra) => {
9846
- clearTimeout(fallback);
9847
- respond({ extend: extra });
9848
- },
9849
- deny: () => {
9850
- clearTimeout(fallback);
9851
- respond("stop");
9852
- }
9853
- });
9854
- }
9855
- });
10389
+ kind: entry.kind,
10390
+ used: entry.used,
10391
+ limit: entry.limit,
10392
+ // One event for THIS kind only — each exceeded kind has its own
10393
+ // negotiation (and its own resolve), so there is no cross-kind
10394
+ // first-wins drop and no O(N^2) re-emission.
10395
+ requestDecision: () => this._busRequestDecision(entry),
10396
+ extend: (extra) => {
10397
+ this.patchLimits(extra);
10398
+ },
10399
+ deny: () => {
9856
10400
  }
9857
10401
  });
9858
10402
  if (result === "throw") return "stop";
9859
10403
  if (result === "continue") return { extend: {} };
9860
10404
  const decision = await result;
9861
10405
  if (decision === "stop") return "stop";
9862
- const ext = decision.extend;
9863
- if (ext.maxIterations !== void 0) {
9864
- this.limits.maxIterations = ext.maxIterations;
9865
- }
9866
- if (ext.maxToolCalls !== void 0) {
9867
- this.limits.maxToolCalls = ext.maxToolCalls;
9868
- }
9869
- if (ext.maxTokens !== void 0) {
9870
- this.limits.maxTokens = ext.maxTokens;
9871
- }
9872
- if (ext.maxCostUsd !== void 0) {
9873
- this.limits.maxCostUsd = ext.maxCostUsd;
9874
- }
9875
- if (ext.timeoutMs !== void 0) {
9876
- this.limits.timeoutMs = ext.timeoutMs;
9877
- }
9878
- if (ext.idleTimeoutMs !== void 0) {
9879
- this.limits.idleTimeoutMs = ext.idleTimeoutMs;
9880
- }
10406
+ this.patchLimits(decision.extend);
9881
10407
  return decision;
9882
10408
  } finally {
9883
- this._pendingNegotiations.delete(kind);
10409
+ this._pendingNegotiations.delete(entry.kind);
9884
10410
  }
9885
10411
  }
9886
10412
  recordIteration() {
@@ -9923,7 +10449,8 @@ var SubagentBudget = class _SubagentBudget {
9923
10449
  const { timeoutMs, idleTimeoutMs } = this.limits;
9924
10450
  if (timeoutMs === void 0 && idleTimeoutMs === void 0) return;
9925
10451
  const elapsed = Date.now() - this.startTime;
9926
- const wallTripped = timeoutMs !== void 0 && elapsed > timeoutMs;
10452
+ const wallSkipped = this._onThreshold !== void 0 && this._watchdogActive !== void 0 && timeoutMs !== void 0 && this._watchdogActive === timeoutMs;
10453
+ const wallTripped = wallSkipped ? false : timeoutMs !== void 0 && elapsed > timeoutMs;
9927
10454
  const idleTripped = idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs;
9928
10455
  if (!wallTripped && !idleTripped) return;
9929
10456
  void this.checkLimits(elapsed);
@@ -12916,7 +13443,7 @@ function classifySubagentError(err, hints = {}) {
12916
13443
  const baseMessage2 = err.describe();
12917
13444
  return providerErrorToSubagentError(err, baseMessage2, cause);
12918
13445
  }
12919
- const baseMessage = err instanceof Error ? err.message : String(err);
13446
+ const baseMessage = toErrorMessage(err);
12920
13447
  if (err instanceof BudgetExceededError) {
12921
13448
  const map = {
12922
13449
  iterations: "budget_iterations",
@@ -13431,6 +13958,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
13431
13958
  terminating = /* @__PURE__ */ new Set();
13432
13959
  constructor(config, options = {}) {
13433
13960
  super();
13961
+ this.setMaxListeners(0);
13434
13962
  this.coordinatorId = config.coordinatorId;
13435
13963
  this.config = config;
13436
13964
  this.runner = options.runner;
@@ -13825,7 +14353,13 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
13825
14353
  let result;
13826
14354
  budget.start();
13827
14355
  try {
13828
- const outcome = await this.executeWithTimeout(this.runner, task, runCtx, budget);
14356
+ const outcome = await this.executeWithTimeout(
14357
+ this.runner,
14358
+ task,
14359
+ runCtx,
14360
+ budget,
14361
+ subagent.config.preemptFraction
14362
+ );
13829
14363
  result = {
13830
14364
  subagentId,
13831
14365
  taskId: task.id,
@@ -13852,7 +14386,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
13852
14386
  }
13853
14387
  this.recordCompletion(result);
13854
14388
  }
13855
- async executeWithTimeout(runner, task, ctx, budget) {
14389
+ async executeWithTimeout(runner, task, ctx, budget, preemptFraction = TIMEOUT_PREEMPT_FRACTION) {
13856
14390
  const initialTimeoutMs = budget.limits.timeoutMs;
13857
14391
  const idleLimitMs = budget.limits.idleTimeoutMs;
13858
14392
  if (initialTimeoutMs === void 0 && idleLimitMs === void 0) {
@@ -13860,8 +14394,21 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
13860
14394
  }
13861
14395
  const start = Date.now();
13862
14396
  let timer = null;
13863
- let preemptedForLimit = null;
14397
+ let PreemptState;
14398
+ ((PreemptState2) => {
14399
+ PreemptState2["ACTIVE"] = "active";
14400
+ PreemptState2["LOCKED"] = "locked";
14401
+ })(PreemptState || (PreemptState = {}));
14402
+ let preemptedCeiling = null;
14403
+ let preemptState = "active" /* ACTIVE */;
14404
+ let lastGrantActivityTs = -1;
13864
14405
  const timeoutPromise = new Promise((_, reject) => {
14406
+ const terminate = (kind, limit, used) => {
14407
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
14408
+ reject(
14409
+ budget._events?.hasListenerFor("budget.threshold_reached") ? new Error(`subagent stopped: budget ${kind} (limit=${limit}, used=${used})`) : new BudgetExceededError(kind, limit, used)
14410
+ );
14411
+ };
13865
14412
  const armFor = (ms) => {
13866
14413
  if (timer) clearTimeout(timer);
13867
14414
  timer = setTimeout(onTick, Math.max(0, ms));
@@ -13870,7 +14417,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
13870
14417
  const wallLimit = budget.limits.timeoutMs ?? initialTimeoutMs;
13871
14418
  const wallRemaining = initialTimeoutMs === void 0 ? Number.POSITIVE_INFINITY : wallLimit - (Date.now() - start);
13872
14419
  const idleRemaining = idleLimitMs === void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
13873
- const preemptRemaining = initialTimeoutMs === void 0 || preemptedForLimit === wallLimit ? Number.POSITIVE_INFINITY : wallLimit * TIMEOUT_PREEMPT_FRACTION - (Date.now() - start);
14420
+ const preemptRemaining = initialTimeoutMs === void 0 || preemptedCeiling === wallLimit ? Number.POSITIVE_INFINITY : wallLimit * preemptFraction - (Date.now() - start);
13874
14421
  armFor(Math.max(25, Math.min(wallRemaining, idleRemaining, preemptRemaining)));
13875
14422
  };
13876
14423
  const negotiateTimeout = async (used, limit) => {
@@ -13880,16 +14427,42 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
13880
14427
  kind: "timeout",
13881
14428
  used,
13882
14429
  limit,
13883
- requestDecision: () => new Promise((resolveDecision) => {
13884
- budget._events?.emit("budget.threshold_reached", {
13885
- kind: "timeout",
13886
- used,
13887
- limit,
13888
- timeoutMs: 6e4,
13889
- extend: (extra) => resolveDecision({ extend: extra }),
13890
- deny: () => resolveDecision("stop")
14430
+ requestDecision: () => {
14431
+ if (!budget._events?.hasListenerFor("budget.threshold_reached")) {
14432
+ return Promise.resolve("stop");
14433
+ }
14434
+ return new Promise((resolveDecision) => {
14435
+ let settled = false;
14436
+ const resolve5 = (d) => {
14437
+ if (settled) return;
14438
+ settled = true;
14439
+ resolveDecision(d);
14440
+ };
14441
+ const fallback = setTimeout(() => resolve5("stop"), DECISION_TIMEOUT_MS);
14442
+ budget._events?.emit("budget.threshold_reached", {
14443
+ kind: "timeout",
14444
+ used,
14445
+ limit,
14446
+ // Informational: the budget's own decision deadline. Listeners may use
14447
+ // this to display a countdown. The coordinator does NOT enforce it —
14448
+ // it is the budget's own `setTimeout(fallback)` that races against
14449
+ // the listener's `extend()`/`deny()` call to guarantee progress.
14450
+ timeoutMs: DECISION_TIMEOUT_MS,
14451
+ // deny() wins over a same-dispatch extend(): defer the grant a
14452
+ // microtask so a synchronous deny in the same emit pre-empts it
14453
+ // (a listener that both grants and denies, or two listeners
14454
+ // disagreeing, resolves as a stop). Async grants still resolve.
14455
+ extend: (extra) => {
14456
+ clearTimeout(fallback);
14457
+ queueMicrotask(() => resolve5({ extend: extra }));
14458
+ },
14459
+ deny: () => {
14460
+ clearTimeout(fallback);
14461
+ resolve5("stop");
14462
+ }
14463
+ });
13891
14464
  });
13892
- })
14465
+ }
13893
14466
  });
13894
14467
  return typeof result === "string" ? result : await result;
13895
14468
  };
@@ -13900,21 +14473,45 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
13900
14473
  const wallExceeded = wallLimit !== void 0 && elapsed >= wallLimit;
13901
14474
  const idleExceeded = idleLimit !== void 0 && budget.idleMs() >= idleLimit;
13902
14475
  if (idleExceeded && !wallExceeded) {
14476
+ budget._events?.emit("budget.threshold_reached", {
14477
+ kind: "idle_timeout",
14478
+ used: budget.idleMs(),
14479
+ limit: idleLimit ?? 0,
14480
+ timeoutMs: DECISION_TIMEOUT_MS,
14481
+ extend: () => {
14482
+ },
14483
+ deny: () => {
14484
+ }
14485
+ });
13903
14486
  this.subagents.get(ctx.subagentId)?.abortController.abort();
13904
- reject(new BudgetExceededError("timeout", idleLimit ?? 0, budget.idleMs()));
14487
+ reject(new BudgetExceededError("idle_timeout", idleLimit ?? 0, budget.idleMs()));
13905
14488
  return;
13906
14489
  }
13907
- if (wallLimit !== void 0 && !wallExceeded && budget.onThreshold && preemptedForLimit !== wallLimit && elapsed >= wallLimit * TIMEOUT_PREEMPT_FRACTION) {
14490
+ if (wallLimit !== void 0 && !wallExceeded && budget.onThreshold && preemptState === "active" /* ACTIVE */ && elapsed >= wallLimit * preemptFraction) {
14491
+ const activityTs = Date.now() - budget.idleMs();
14492
+ if (activityTs <= lastGrantActivityTs) {
14493
+ preemptState = "locked" /* LOCKED */;
14494
+ preemptedCeiling = wallLimit;
14495
+ scheduleNext();
14496
+ return;
14497
+ }
14498
+ budget.setWatchdogNegotiation(wallLimit);
13908
14499
  try {
13909
14500
  const decision = await negotiateTimeout(elapsed, wallLimit);
13910
14501
  if (typeof decision !== "string" && decision.extend.timeoutMs !== void 0) {
13911
- budget.limits.timeoutMs = decision.extend.timeoutMs;
13912
- preemptedForLimit = null;
14502
+ budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
14503
+ lastGrantActivityTs = Date.now() - budget.idleMs();
14504
+ preemptState = "active" /* ACTIVE */;
14505
+ preemptedCeiling = null;
13913
14506
  } else {
13914
- preemptedForLimit = wallLimit;
14507
+ preemptState = "locked" /* LOCKED */;
14508
+ preemptedCeiling = wallLimit;
13915
14509
  }
13916
14510
  } catch {
13917
- preemptedForLimit = wallLimit;
14511
+ preemptState = "locked" /* LOCKED */;
14512
+ preemptedCeiling = wallLimit;
14513
+ } finally {
14514
+ budget.clearWatchdogNegotiation();
13918
14515
  }
13919
14516
  scheduleNext();
13920
14517
  return;
@@ -13929,26 +14526,41 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
13929
14526
  reject(new BudgetExceededError("timeout", limit, elapsed));
13930
14527
  return;
13931
14528
  }
14529
+ budget.setWatchdogNegotiation(limit);
13932
14530
  try {
13933
14531
  const decision = await negotiateTimeout(elapsed, limit);
13934
- if (decision === "continue" || decision === "throw" || decision === "stop") {
13935
- preemptedForLimit = null;
14532
+ if (decision === "throw") {
14533
+ terminate("timeout", limit, elapsed);
14534
+ return;
14535
+ }
14536
+ if (decision === "continue") {
14537
+ preemptState = "locked" /* LOCKED */;
14538
+ preemptedCeiling = wallLimit;
13936
14539
  armFor(Math.max(1e3, limit));
13937
14540
  return;
13938
14541
  }
14542
+ if (decision === "stop") {
14543
+ terminate("timeout", limit, elapsed);
14544
+ return;
14545
+ }
13939
14546
  if (decision.extend.timeoutMs !== void 0) {
13940
- budget.limits.timeoutMs = decision.extend.timeoutMs;
13941
- preemptedForLimit = null;
14547
+ budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
14548
+ lastGrantActivityTs = Date.now() - budget.idleMs();
14549
+ preemptState = "active" /* ACTIVE */;
14550
+ preemptedCeiling = null;
13942
14551
  scheduleNext();
13943
14552
  return;
13944
14553
  }
13945
- this.subagents.get(ctx.subagentId)?.abortController.abort();
13946
- reject(new BudgetExceededError("timeout", limit, elapsed));
14554
+ terminate("timeout", limit, elapsed);
14555
+ return;
13947
14556
  } catch (err) {
13948
14557
  this.subagents.get(ctx.subagentId)?.abortController.abort();
13949
14558
  reject(
13950
14559
  err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
13951
14560
  );
14561
+ return;
14562
+ } finally {
14563
+ budget.clearWatchdogNegotiation();
13952
14564
  }
13953
14565
  };
13954
14566
  scheduleNext();
@@ -14111,7 +14723,7 @@ var ParallelEternalEngine = class {
14111
14723
  console.error(JSON.stringify({
14112
14724
  level: "error",
14113
14725
  event: "engine.persist_state_failed",
14114
- message: err instanceof Error ? err.message : String(err),
14726
+ message: toErrorMessage(err),
14115
14727
  context: { expectedState: "stopped" },
14116
14728
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
14117
14729
  }));
@@ -14147,7 +14759,7 @@ var ParallelEternalEngine = class {
14147
14759
  );
14148
14760
  await this.appendFailure(
14149
14761
  "engine error",
14150
- err instanceof Error ? err.message : String(err)
14762
+ toErrorMessage(err)
14151
14763
  );
14152
14764
  }
14153
14765
  if (this.stopRequested) break;
@@ -14794,123 +15406,6 @@ function createMessage(type, from, payload, to) {
14794
15406
  priority: "normal"
14795
15407
  };
14796
15408
  }
14797
- var GLOB_CHARS = /* @__PURE__ */ new Set(["*", "?", "["]);
14798
- var IS_WINDOWS = process.platform === "win32";
14799
- var SEP = IS_WINDOWS ? "\\" : "/";
14800
- function isGlob(p) {
14801
- for (const c of p) {
14802
- if (GLOB_CHARS.has(c)) return true;
14803
- }
14804
- return false;
14805
- }
14806
- function globToRegex(pat) {
14807
- let i = 0;
14808
- let re = "^";
14809
- while (i < pat.length) {
14810
- const c = expectDefined(pat[i]);
14811
- if (c === "*") {
14812
- if (pat[i + 1] === "*") {
14813
- re += ".*";
14814
- i += 2;
14815
- if (pat[i] === "/") i++;
14816
- } else {
14817
- re += "[^/\\\\]*";
14818
- i++;
14819
- }
14820
- } else if (c === "?") {
14821
- re += "[^/\\\\]";
14822
- i++;
14823
- } else if (c === "[") {
14824
- let cls = "[";
14825
- i++;
14826
- if (pat[i] === "!" || pat[i] === "^") {
14827
- cls += "^";
14828
- i++;
14829
- }
14830
- while (i < pat.length && pat[i] !== "]") {
14831
- const ch = pat[i] ?? "";
14832
- if (ch === "\\") cls += "\\\\";
14833
- else if (ch === "]" || ch === "^") cls += `\\${ch}`;
14834
- else cls += ch;
14835
- i++;
14836
- }
14837
- cls += "]";
14838
- re += cls;
14839
- i++;
14840
- } else {
14841
- re += c.replace(/[.+^${}()|\\]/g, "\\$&");
14842
- i++;
14843
- }
14844
- }
14845
- return new RegExp(re + "$");
14846
- }
14847
- function baseDir(pat) {
14848
- let i = pat.length - 1;
14849
- while (i >= 0 && !GLOB_CHARS.has(expectDefined(pat[i])) && pat[i] !== SEP && pat[i] !== "/") i--;
14850
- const cut = i >= 0 ? pat.lastIndexOf(SEP, i) : pat.lastIndexOf("/", i);
14851
- return cut < 0 ? "." : pat.slice(0, cut);
14852
- }
14853
- async function expandGlob(pattern) {
14854
- if (!isGlob(pattern)) return [pattern];
14855
- const results = /* @__PURE__ */ new Set();
14856
- const abs = isAbsolute(pattern);
14857
- const base = abs ? baseDir(pattern) : baseDir(pattern);
14858
- const relPat = base === "." ? pattern : pattern.slice(base.length + 1);
14859
- async function walk3(dir, pat) {
14860
- let entries;
14861
- try {
14862
- entries = await fsp.readdir(dir);
14863
- } catch {
14864
- return;
14865
- }
14866
- const firstGlob = pat.search(/[*?[[]/);
14867
- if (firstGlob < 0) {
14868
- const re = globToRegex(pat);
14869
- for (const e of entries) {
14870
- if (re.test(e)) {
14871
- const full = `${dir}${SEP}${e}`;
14872
- results.add(abs ? resolve(full) : full);
14873
- }
14874
- }
14875
- return;
14876
- }
14877
- const before = pat.slice(0, firstGlob);
14878
- const rest = pat.slice(firstGlob);
14879
- if (before.endsWith("**")) {
14880
- await walk3(dir, rest);
14881
- for (const e of entries) {
14882
- const full = `${dir}${SEP}${e}`;
14883
- try {
14884
- const stat6 = await fsp.stat(full);
14885
- if (stat6.isDirectory()) await walk3(full, rest);
14886
- } catch {
14887
- }
14888
- }
14889
- } else if (before === "") {
14890
- const re = globToRegex(rest);
14891
- for (const e of entries) {
14892
- if (re.test(e)) {
14893
- const full = `${dir}${SEP}${e}`;
14894
- results.add(abs ? resolve(full) : full);
14895
- }
14896
- }
14897
- } else {
14898
- const seg = before.replace(/[*?[\]]/g, "").replace(/\/$/, "");
14899
- if (entries.includes(seg)) {
14900
- const full = `${dir}${SEP}${seg}`;
14901
- try {
14902
- const stat6 = await fsp.stat(full);
14903
- if (stat6.isDirectory()) await walk3(full, rest);
14904
- } catch {
14905
- }
14906
- }
14907
- }
14908
- }
14909
- await walk3(base === "." ? "." : base, relPat);
14910
- return [...results];
14911
- }
14912
-
14913
- // src/coordination/collab-debug.ts
14914
15409
  var DEFAULT_MAX_TARGET_FILES = 30;
14915
15410
  var CollabSession = class extends EventEmitter {
14916
15411
  sessionId;
@@ -15000,8 +15495,8 @@ var CollabSession = class extends EventEmitter {
15000
15495
  for (const filePath of allFiles) {
15001
15496
  try {
15002
15497
  const [content, stat6] = await Promise.all([
15003
- fsp.readFile(filePath, "utf8"),
15004
- fsp.stat(filePath)
15498
+ fsp2.readFile(filePath, "utf8"),
15499
+ fsp2.stat(filePath)
15005
15500
  ]);
15006
15501
  const ext = filePath.split(".").pop() ?? "";
15007
15502
  const language = ext === "ts" || ext === "tsx" ? "typescript" : ext === "js" || ext === "jsx" ? "javascript" : ext === "md" ? "markdown" : ext === "json" ? "json" : void 0;
@@ -15423,7 +15918,7 @@ Emit each evaluation immediately. Do not wait until you have read all reports.`;
15423
15918
  for (const file of this.snapshot.files) {
15424
15919
  if (file.snapshotMtimeMs === void 0 && file.snapshotSizeBytes === void 0) continue;
15425
15920
  try {
15426
- const stat6 = await fsp.stat(file.path);
15921
+ const stat6 = await fsp2.stat(file.path);
15427
15922
  const mtimeChanged = file.snapshotMtimeMs !== void 0 && stat6.mtimeMs > file.snapshotMtimeMs + 1;
15428
15923
  const sizeChanged = file.snapshotSizeBytes !== void 0 && stat6.size !== file.snapshotSizeBytes;
15429
15924
  if (mtimeChanged || sizeChanged) {
@@ -15706,7 +16201,7 @@ function makeSpawnTool(director, roster) {
15706
16201
  if (err instanceof FleetCostCapError) {
15707
16202
  return { error: err.message, kind: err.kind, limit: err.limit, observed: err.observed };
15708
16203
  }
15709
- return { error: err instanceof Error ? err.message : String(err) };
16204
+ return { error: toErrorMessage(err) };
15710
16205
  }
15711
16206
  }
15712
16207
  };
@@ -15788,7 +16283,7 @@ function makeAskTool(director) {
15788
16283
  _hint: "Response was large and stored. Use ask_result with the key to retrieve it."
15789
16284
  };
15790
16285
  } catch (err) {
15791
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
16286
+ return { ok: false, error: toErrorMessage(err) };
15792
16287
  }
15793
16288
  }
15794
16289
  };
@@ -16014,7 +16509,7 @@ function makeCollabDebugTool(director) {
16014
16509
  evaluations: report.evaluations
16015
16510
  };
16016
16511
  } catch (err) {
16017
- const msg = err instanceof Error ? err.message : String(err);
16512
+ const msg = toErrorMessage(err);
16018
16513
  return { error: "collab_debug failed: " + msg };
16019
16514
  }
16020
16515
  }
@@ -16588,7 +17083,7 @@ var Director = class _Director {
16588
17083
  ) : null;
16589
17084
  this.fleetManager = opts.fleetManager;
16590
17085
  if (this.sharedScratchpadPath) {
16591
- void fsp.mkdir(this.sharedScratchpadPath, { recursive: true }).catch((err) => this.logShutdownError("shared_scratchpad_mkdir", err));
17086
+ void fsp2.mkdir(this.sharedScratchpadPath, { recursive: true }).catch((err) => this.logShutdownError("shared_scratchpad_mkdir", err));
16592
17087
  }
16593
17088
  this.transport = new InMemoryBridgeTransport();
16594
17089
  this.bridge = new InMemoryAgentBridge(
@@ -17180,7 +17675,7 @@ var Director = class _Director {
17180
17675
  })),
17181
17676
  usage: this.usage.snapshot()
17182
17677
  };
17183
- await fsp.mkdir(path11.dirname(this.manifestPath), { recursive: true });
17678
+ await fsp2.mkdir(path3.dirname(this.manifestPath), { recursive: true });
17184
17679
  await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
17185
17680
  return this.manifestPath;
17186
17681
  }
@@ -17230,7 +17725,7 @@ var Director = class _Director {
17230
17725
  * listener for structured collection, and never affects exit code.
17231
17726
  */
17232
17727
  logShutdownError(phase, err) {
17233
- const detail = err instanceof Error ? err.message : String(err);
17728
+ const detail = toErrorMessage(err);
17234
17729
  process.emitWarning(
17235
17730
  `Director shutdown phase "${phase}" failed: ${detail}`,
17236
17731
  "DirectorShutdownWarning"
@@ -17394,10 +17889,10 @@ var Director = class _Director {
17394
17889
  */
17395
17890
  async readSession(subagentId, tail) {
17396
17891
  if (!this.sessionsRoot) return null;
17397
- const filePath = path11.join(this.sessionsRoot, this.directorRunId, `${subagentId}.jsonl`);
17892
+ const filePath = path3.join(this.sessionsRoot, this.directorRunId, `${subagentId}.jsonl`);
17398
17893
  let raw;
17399
17894
  try {
17400
- raw = await fsp.readFile(filePath, "utf8");
17895
+ raw = await fsp2.readFile(filePath, "utf8");
17401
17896
  } catch {
17402
17897
  return null;
17403
17898
  }
@@ -17823,7 +18318,7 @@ function createDelegateTool(opts) {
17823
18318
  summary
17824
18319
  };
17825
18320
  } catch (err) {
17826
- const message = err instanceof Error ? err.message : String(err);
18321
+ const message = toErrorMessage(err);
17827
18322
  opts.events?.emit("delegate.completed", {
17828
18323
  target,
17829
18324
  task: i.task,
@@ -17924,13 +18419,13 @@ async function readSubagentPartial(opts, subagentId) {
17924
18419
  if (!opts.sessionsRoot) return void 0;
17925
18420
  const candidates = [];
17926
18421
  if (opts.directorRunId) {
17927
- candidates.push(path11.join(opts.sessionsRoot, opts.directorRunId, `${subagentId}.jsonl`));
18422
+ candidates.push(path3.join(opts.sessionsRoot, opts.directorRunId, `${subagentId}.jsonl`));
17928
18423
  } else {
17929
18424
  try {
17930
- const entries = await fsp.readdir(opts.sessionsRoot, { withFileTypes: true });
18425
+ const entries = await fsp2.readdir(opts.sessionsRoot, { withFileTypes: true });
17931
18426
  for (const entry of entries) {
17932
18427
  if (entry.isDirectory()) {
17933
- candidates.push(path11.join(opts.sessionsRoot, entry.name, `${subagentId}.jsonl`));
18428
+ candidates.push(path3.join(opts.sessionsRoot, entry.name, `${subagentId}.jsonl`));
17934
18429
  }
17935
18430
  }
17936
18431
  } catch {
@@ -17940,7 +18435,7 @@ async function readSubagentPartial(opts, subagentId) {
17940
18435
  for (const file of candidates) {
17941
18436
  let raw;
17942
18437
  try {
17943
- raw = await fsp.readFile(file, "utf8");
18438
+ raw = await fsp2.readFile(file, "utf8");
17944
18439
  } catch {
17945
18440
  continue;
17946
18441
  }
@@ -17980,9 +18475,9 @@ function makeDirectorSessionFactory(opts) {
17980
18475
  let dir;
17981
18476
  if (opts.store) {
17982
18477
  store = opts.store;
17983
- dir = opts.sessionsRoot ? path11.join(opts.sessionsRoot, runId) : "(caller-managed)";
18478
+ dir = opts.sessionsRoot ? path3.join(opts.sessionsRoot, runId) : "(caller-managed)";
17984
18479
  } else if (opts.sessionsRoot) {
17985
- dir = path11.join(opts.sessionsRoot, runId);
18480
+ dir = path3.join(opts.sessionsRoot, runId);
17986
18481
  store = new DefaultSessionStore({ dir });
17987
18482
  } else {
17988
18483
  throw new Error("makeDirectorSessionFactory requires either `store` or `sessionsRoot`");
@@ -18028,6 +18523,7 @@ function attachAutoExtend(events, policy = {}) {
18028
18523
  const extendCounts = /* @__PURE__ */ new Map();
18029
18524
  let progress = 0;
18030
18525
  let lastTimeoutProgress = -1;
18526
+ let lastSeenKey = null;
18031
18527
  const unsubs = [
18032
18528
  events.on("tool.executed", () => {
18033
18529
  progress++;
@@ -18037,6 +18533,9 @@ function attachAutoExtend(events, policy = {}) {
18037
18533
  }),
18038
18534
  events.on("budget.threshold_reached", (e) => {
18039
18535
  const { kind, limit, extend, deny } = e;
18536
+ const key = `${kind}:${limit}`;
18537
+ if (key === lastSeenKey) return;
18538
+ lastSeenKey = key;
18040
18539
  if (kind === "timeout" || kind === "idle_timeout") {
18041
18540
  if (progress > lastTimeoutProgress) {
18042
18541
  lastTimeoutProgress = progress;
@@ -18072,71 +18571,6 @@ var NULL_FLEET_BUS = new FleetBus();
18072
18571
 
18073
18572
  // src/models/models-registry.ts
18074
18573
  init_atomic_write();
18075
-
18076
- // src/utils/merge-models-payload.ts
18077
- function mergeModelsPayload(base, overlay) {
18078
- const out = {};
18079
- for (const [id, provider] of Object.entries(base)) {
18080
- out[id] = cloneProvider(provider);
18081
- }
18082
- for (const [id, ovProvider] of Object.entries(overlay)) {
18083
- const existing = out[id];
18084
- out[id] = existing ? mergeProvider(existing, ovProvider) : cloneProvider(ovProvider);
18085
- }
18086
- return out;
18087
- }
18088
- function mergeProvider(base, overlay) {
18089
- const models = {};
18090
- for (const [mid, m] of Object.entries(base.models ?? {})) {
18091
- models[mid] = { ...m };
18092
- }
18093
- for (const [mid, ovModel] of Object.entries(overlay.models ?? {})) {
18094
- const existing = models[mid];
18095
- models[mid] = existing ? mergeModel(existing, ovModel) : { ...ovModel };
18096
- }
18097
- return {
18098
- ...base,
18099
- // Overlay scalar fields win when explicitly provided; otherwise keep base.
18100
- ...stripUndefined({
18101
- id: overlay.id,
18102
- name: overlay.name,
18103
- npm: overlay.npm,
18104
- api: overlay.api,
18105
- env: overlay.env,
18106
- doc: overlay.doc
18107
- }),
18108
- models
18109
- };
18110
- }
18111
- function mergeModel(base, overlay) {
18112
- const merged = { ...base, ...overlay };
18113
- if (base.limit || overlay.limit) {
18114
- merged.limit = { ...base.limit, ...overlay.limit };
18115
- }
18116
- if (base.cost || overlay.cost) {
18117
- merged.cost = { ...base.cost, ...overlay.cost };
18118
- }
18119
- if (base.modalities || overlay.modalities) {
18120
- merged.modalities = { ...base.modalities, ...overlay.modalities };
18121
- }
18122
- return merged;
18123
- }
18124
- function cloneProvider(p) {
18125
- const models = {};
18126
- for (const [mid, m] of Object.entries(p.models ?? {})) {
18127
- models[mid] = { ...m };
18128
- }
18129
- return { ...p, models };
18130
- }
18131
- function stripUndefined(obj) {
18132
- const out = {};
18133
- for (const [k, v] of Object.entries(obj)) {
18134
- if (v !== void 0) out[k] = v;
18135
- }
18136
- return out;
18137
- }
18138
-
18139
- // src/models/models-registry.ts
18140
18574
  var DEFAULT_URL = "https://models.dev/api.json";
18141
18575
  var DEFAULT_TTL_SECONDS = 24 * 3600;
18142
18576
  var DEFAULT_REFRESH_TIMEOUT_MS = 15e3;
@@ -18193,7 +18627,7 @@ var DefaultModelsRegistry = class {
18193
18627
  this.overlay = opts.overlay;
18194
18628
  this.overlayUrl = opts.overlayUrl;
18195
18629
  this.overlayFile = opts.overlayFile;
18196
- this.overlayCacheFile = opts.overlayCacheFile ?? (opts.overlayUrl ? path11.join(path11.dirname(opts.cacheFile), "models-overlay-cache.json") : void 0);
18630
+ this.overlayCacheFile = opts.overlayCacheFile ?? (opts.overlayUrl ? path3.join(path3.dirname(opts.cacheFile), "models-overlay-cache.json") : void 0);
18197
18631
  }
18198
18632
  async load(opts = {}) {
18199
18633
  if (this.payload && !opts.force) return this.payload;
@@ -18232,7 +18666,7 @@ var DefaultModelsRegistry = class {
18232
18666
  }
18233
18667
  if (overlayAvailable) {
18234
18668
  console.warn(
18235
- `ModelsRegistry: models.dev unavailable (${err instanceof Error ? err.message : String(err)}); serving curated overlay only.`
18669
+ `ModelsRegistry: models.dev unavailable (${toErrorMessage(err)}); serving curated overlay only.`
18236
18670
  );
18237
18671
  return {};
18238
18672
  }
@@ -18320,7 +18754,7 @@ var DefaultModelsRegistry = class {
18320
18754
  async readOverlayFile() {
18321
18755
  if (!this.overlayFile) return void 0;
18322
18756
  try {
18323
- const raw = await fsp.readFile(this.overlayFile, "utf8");
18757
+ const raw = await fsp2.readFile(this.overlayFile, "utf8");
18324
18758
  return JSON.parse(raw);
18325
18759
  } catch {
18326
18760
  return void 0;
@@ -18398,7 +18832,7 @@ var DefaultModelsRegistry = class {
18398
18832
  }
18399
18833
  async readCacheAt(file) {
18400
18834
  try {
18401
- const raw = await fsp.readFile(file, "utf8");
18835
+ const raw = await fsp2.readFile(file, "utf8");
18402
18836
  return JSON.parse(raw);
18403
18837
  } catch {
18404
18838
  return void 0;
@@ -18406,7 +18840,7 @@ var DefaultModelsRegistry = class {
18406
18840
  }
18407
18841
  /** Used by `wstack models refresh` to expose where the cache lives. */
18408
18842
  cacheLocation() {
18409
- return path11.resolve(this.cacheFile);
18843
+ return path3.resolve(this.cacheFile);
18410
18844
  }
18411
18845
  };
18412
18846
  function hasEntries(payload) {
@@ -18773,8 +19207,8 @@ var DefaultModeStore = class {
18773
19207
  }
18774
19208
  async loadActiveMode() {
18775
19209
  try {
18776
- const configPath = path11.join(this.configDir, "mode.json");
18777
- const content = await fsp.readFile(configPath, "utf8");
19210
+ const configPath = path3.join(this.configDir, "mode.json");
19211
+ const content = await fsp2.readFile(configPath, "utf8");
18778
19212
  const data = JSON.parse(content);
18779
19213
  this.activeModeId = data.activeMode ?? null;
18780
19214
  } catch {
@@ -18783,8 +19217,8 @@ var DefaultModeStore = class {
18783
19217
  }
18784
19218
  async saveActiveMode() {
18785
19219
  try {
18786
- await fsp.mkdir(this.configDir, { recursive: true });
18787
- const configPath = path11.join(this.configDir, "mode.json");
19220
+ await fsp2.mkdir(this.configDir, { recursive: true });
19221
+ const configPath = path3.join(this.configDir, "mode.json");
18788
19222
  await atomicWrite(
18789
19223
  configPath,
18790
19224
  JSON.stringify({ activeMode: this.activeModeId }, null, 2)
@@ -18796,14 +19230,14 @@ var DefaultModeStore = class {
18796
19230
  async function loadProjectModes(modesDir) {
18797
19231
  const modes = [];
18798
19232
  try {
18799
- const entries = await fsp.readdir(modesDir);
19233
+ const entries = await fsp2.readdir(modesDir);
18800
19234
  for (const entry of entries) {
18801
19235
  if (!entry.endsWith(".md") && !entry.endsWith(".txt")) continue;
18802
- const filePath = path11.join(modesDir, entry);
18803
- const stat6 = await fsp.stat(filePath);
19236
+ const filePath = path3.join(modesDir, entry);
19237
+ const stat6 = await fsp2.stat(filePath);
18804
19238
  if (!stat6.isFile()) continue;
18805
- const content = await fsp.readFile(filePath, "utf8");
18806
- const id = path11.basename(entry, path11.extname(entry));
19239
+ const content = await fsp2.readFile(filePath, "utf8");
19240
+ const id = path3.basename(entry, path3.extname(entry));
18807
19241
  modes.push({
18808
19242
  id,
18809
19243
  name: id.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
@@ -18819,8 +19253,8 @@ async function loadProjectModes(modesDir) {
18819
19253
  async function loadUserModes(modesDir) {
18820
19254
  const modes = [];
18821
19255
  try {
18822
- const manifestPath = path11.join(modesDir, "modes.json");
18823
- const content = await fsp.readFile(manifestPath, "utf8");
19256
+ const manifestPath = path3.join(modesDir, "modes.json");
19257
+ const content = await fsp2.readFile(manifestPath, "utf8");
18824
19258
  const manifest = JSON.parse(content);
18825
19259
  for (const mode of manifest.modes) {
18826
19260
  modes.push(mode);
@@ -19546,7 +19980,7 @@ var TaskTracker = class {
19546
19980
  this.opts.onPersistError ? this.opts.onPersistError(err) : console.warn(JSON.stringify({
19547
19981
  level: "warn",
19548
19982
  event: "task_tracker.save_graph_failed",
19549
- message: err instanceof Error ? err.message : String(err),
19983
+ message: toErrorMessage(err),
19550
19984
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
19551
19985
  }));
19552
19986
  });
@@ -19771,7 +20205,7 @@ var SpecStore = class {
19771
20205
  indexPath;
19772
20206
  constructor(opts) {
19773
20207
  this.baseDir = opts.baseDir;
19774
- this.indexPath = path11.join(this.baseDir, "_index.json");
20208
+ this.indexPath = path3.join(this.baseDir, "_index.json");
19775
20209
  }
19776
20210
  async save(spec) {
19777
20211
  await ensureDir(this.baseDir);
@@ -19781,7 +20215,7 @@ var SpecStore = class {
19781
20215
  }
19782
20216
  async load(id) {
19783
20217
  try {
19784
- const raw = await fsp.readFile(this.filePath(id), "utf8");
20218
+ const raw = await fsp2.readFile(this.filePath(id), "utf8");
19785
20219
  return JSON.parse(raw);
19786
20220
  } catch {
19787
20221
  return null;
@@ -19793,7 +20227,7 @@ var SpecStore = class {
19793
20227
  }
19794
20228
  async delete(id) {
19795
20229
  try {
19796
- await fsp.unlink(this.filePath(id));
20230
+ await fsp2.unlink(this.filePath(id));
19797
20231
  await this.removeFromIndex(id);
19798
20232
  return true;
19799
20233
  } catch {
@@ -19802,7 +20236,7 @@ var SpecStore = class {
19802
20236
  }
19803
20237
  async exists(id) {
19804
20238
  try {
19805
- await fsp.access(this.filePath(id));
20239
+ await fsp2.access(this.filePath(id));
19806
20240
  return true;
19807
20241
  } catch {
19808
20242
  return false;
@@ -19840,11 +20274,11 @@ var SpecStore = class {
19840
20274
  return updated;
19841
20275
  }
19842
20276
  filePath(id) {
19843
- return path11.join(this.baseDir, `${id}.json`);
20277
+ return path3.join(this.baseDir, `${id}.json`);
19844
20278
  }
19845
20279
  async readIndex() {
19846
20280
  try {
19847
- const raw = await fsp.readFile(this.indexPath, "utf8");
20281
+ const raw = await fsp2.readFile(this.indexPath, "utf8");
19848
20282
  const parsed = JSON.parse(raw);
19849
20283
  if (parsed?.version === 1) return parsed;
19850
20284
  } catch {
@@ -19897,7 +20331,7 @@ var TaskGraphStore = class {
19897
20331
  indexPath;
19898
20332
  constructor(opts) {
19899
20333
  this.baseDir = opts.baseDir;
19900
- this.indexPath = path11.join(this.baseDir, "_index.json");
20334
+ this.indexPath = path3.join(this.baseDir, "_index.json");
19901
20335
  }
19902
20336
  async save(graph) {
19903
20337
  await ensureDir(this.baseDir);
@@ -19907,7 +20341,7 @@ var TaskGraphStore = class {
19907
20341
  }
19908
20342
  async load(id) {
19909
20343
  try {
19910
- const raw = await fsp.readFile(this.filePath(id), "utf8");
20344
+ const raw = await fsp2.readFile(this.filePath(id), "utf8");
19911
20345
  return graphFromJSON(raw);
19912
20346
  } catch {
19913
20347
  return null;
@@ -19919,7 +20353,7 @@ var TaskGraphStore = class {
19919
20353
  }
19920
20354
  async delete(id) {
19921
20355
  try {
19922
- await fsp.unlink(this.filePath(id));
20356
+ await fsp2.unlink(this.filePath(id));
19923
20357
  await this.removeFromIndex(id);
19924
20358
  return true;
19925
20359
  } catch {
@@ -19928,18 +20362,18 @@ var TaskGraphStore = class {
19928
20362
  }
19929
20363
  async exists(id) {
19930
20364
  try {
19931
- await fsp.access(this.filePath(id));
20365
+ await fsp2.access(this.filePath(id));
19932
20366
  return true;
19933
20367
  } catch {
19934
20368
  return false;
19935
20369
  }
19936
20370
  }
19937
20371
  filePath(id) {
19938
- return path11.join(this.baseDir, `${id}.json`);
20372
+ return path3.join(this.baseDir, `${id}.json`);
19939
20373
  }
19940
20374
  async readIndex() {
19941
20375
  try {
19942
- const raw = await fsp.readFile(this.indexPath, "utf8");
20376
+ const raw = await fsp2.readFile(this.indexPath, "utf8");
19943
20377
  const parsed = JSON.parse(raw);
19944
20378
  if (parsed?.version === 1) return parsed;
19945
20379
  } catch {
@@ -20216,7 +20650,7 @@ var AISpecBuilder = class {
20216
20650
  * ENOSPC / EACCES doesn't silently strand session edits in memory. */
20217
20651
  autoSave() {
20218
20652
  this.saveSession().catch((err) => {
20219
- const detail = err instanceof Error ? err.message : String(err);
20653
+ const detail = toErrorMessage(err);
20220
20654
  process.emitWarning(
20221
20655
  `SpecBuilder autoSave failed: ${detail}`,
20222
20656
  "SpecBuilderWarning"
@@ -21535,7 +21969,7 @@ var SddParallelRun = class {
21535
21969
  const failCount = results.length - successCount;
21536
21970
  for (let i = 0; i < results.length; i++) {
21537
21971
  const result = expectDefined(results[i]);
21538
- const taskId = expectDefined(taskIds[i]);
21972
+ const taskId = expectDefined(tasks[i]).id;
21539
21973
  if (result.status === "success") {
21540
21974
  this.opts.tracker.updateNodeStatus(taskId, "completed");
21541
21975
  this.retryMap.delete(taskId);
@@ -21749,7 +22183,7 @@ var DefaultHealthRegistry = class {
21749
22183
  try {
21750
22184
  return await Promise.race([check.check(), timeout]);
21751
22185
  } catch (err) {
21752
- return { status: "unhealthy", detail: err instanceof Error ? err.message : String(err) };
22186
+ return { status: "unhealthy", detail: toErrorMessage(err) };
21753
22187
  } finally {
21754
22188
  if (timer) clearTimeout(timer);
21755
22189
  }
@@ -21942,7 +22376,7 @@ async function startMetricsServer(opts) {
21942
22376
  } catch (err) {
21943
22377
  res.statusCode = 500;
21944
22378
  res.setHeader("content-type", "text/plain; charset=utf-8");
21945
- res.end(`metrics render failed: ${err instanceof Error ? err.message : String(err)}`);
22379
+ res.end(`metrics render failed: ${toErrorMessage(err)}`);
21946
22380
  return;
21947
22381
  }
21948
22382
  res.statusCode = 200;
@@ -21960,7 +22394,7 @@ async function startMetricsServer(opts) {
21960
22394
  (err) => {
21961
22395
  res.statusCode = 500;
21962
22396
  res.setHeader("content-type", "text/plain; charset=utf-8");
21963
- res.end(`health run failed: ${err instanceof Error ? err.message : String(err)}`);
22397
+ res.end(`health run failed: ${toErrorMessage(err)}`);
21964
22398
  }
21965
22399
  );
21966
22400
  return;
@@ -22316,11 +22750,12 @@ function createContextManagerTool(opts = {}) {
22316
22750
  const applyMessages = (next) => {
22317
22751
  const repaired = repairToolUseAdjacency(next);
22318
22752
  const finalMessages = repaired.messages;
22753
+ if (finalMessages === messages) return repaired.report;
22319
22754
  if (ctx.state) {
22320
22755
  ctx.state.replaceMessages(finalMessages);
22321
22756
  } else {
22322
22757
  messages.length = 0;
22323
- messages.splice(0, 0, ...finalMessages);
22758
+ messages.push(...finalMessages);
22324
22759
  }
22325
22760
  return repaired.report;
22326
22761
  };
@@ -22395,9 +22830,15 @@ function createContextManagerTool(opts = {}) {
22395
22830
  }
22396
22831
  const report = await opts.compactor.compact(ctx);
22397
22832
  ctx.clearFileTracking();
22398
- const repair = applyMessages([...ctx.messages]);
22399
- const afterTokens = repair.changed ? roughEstimate(ctx.messages) : report.after;
22400
- const repaired = report.repaired ?? (repair.changed ? repair : void 0);
22833
+ let repaired = report.repaired;
22834
+ let afterTokens;
22835
+ if (!ctx.state) {
22836
+ const repair = applyMessages([...ctx.messages]);
22837
+ repaired = report.repaired ?? (repair.changed ? repair : void 0);
22838
+ afterTokens = repair.changed ? roughEstimate(ctx.messages) : report.after;
22839
+ } else {
22840
+ afterTokens = report.after;
22841
+ }
22401
22842
  const reduced = report.fullRequestTokensBefore > report.fullRequestTokensAfter;
22402
22843
  const repairedSomething = !!report.repaired;
22403
22844
  if (reduced || repairedSomething) {