@wrongstack/core 0.264.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 (87) hide show
  1. package/dist/{agent-bridge-D8sa1vtv.d.ts → agent-bridge-DrkBxszZ.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-c9DLkaas.d.ts → agent-subagent-runner-DM2pP-B6.d.ts} +113 -11
  3. package/dist/{brain-O1IdKPaK.d.ts → brain-BXd_61kQ.d.ts} +31 -2
  4. package/dist/{compactor-BBy0rCtB.d.ts → compactor-B8pOf45Y.d.ts} +1 -1
  5. package/dist/{config-Dz2F3H2K.d.ts → config-BMCj_XDs.d.ts} +80 -12
  6. package/dist/{context-BGSpZNSE.d.ts → context-MRk5PhNv.d.ts} +26 -12
  7. package/dist/coordination/index.d.ts +77 -21
  8. package/dist/coordination/index.js +557 -159
  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 +609 -195
  13. package/dist/defaults/index.js.map +1 -1
  14. package/dist/execution/index.d.ts +16 -16
  15. package/dist/execution/index.js +394 -155
  16. package/dist/execution/index.js.map +1 -1
  17. package/dist/execution/prompt-enhancer.d.ts +2 -2
  18. package/dist/execution/prompt-enhancer.js +1 -1
  19. package/dist/execution/prompt-enhancer.js.map +1 -1
  20. package/dist/extension/index.d.ts +6 -6
  21. package/dist/{goal-preamble-DzjFuN3p.d.ts → goal-preamble-DvHDSKSe.d.ts} +14 -10
  22. package/dist/{goal-store-CxWmCGbH.d.ts → goal-store-DtLMySNb.d.ts} +1 -1
  23. package/dist/{index-CYIQrXVF.d.ts → index-B-ch8K9C.d.ts} +8 -8
  24. package/dist/{index-CbLSI66_.d.ts → index-CEDeNodM.d.ts} +5 -5
  25. package/dist/index.d.ts +183 -52
  26. package/dist/index.js +1779 -673
  27. package/dist/index.js.map +1 -1
  28. package/dist/infrastructure/index.d.ts +6 -6
  29. package/dist/infrastructure/index.js +12 -8
  30. package/dist/infrastructure/index.js.map +1 -1
  31. package/dist/kernel/index.d.ts +9 -9
  32. package/dist/kernel/index.js +1 -1
  33. package/dist/kernel/index.js.map +1 -1
  34. package/dist/{llm-selector-DzxuZnNz.d.ts → llm-selector-C0tfTCUe.d.ts} +14 -2
  35. package/dist/{mcp-servers-DC4QRPUI.d.ts → mcp-servers-2x4w6Jn9.d.ts} +3 -3
  36. package/dist/models/index.d.ts +5 -5
  37. package/dist/models/index.js +74 -30
  38. package/dist/models/index.js.map +1 -1
  39. package/dist/{models-registry-B_siPxqN.d.ts → models-registry-DmJlKuNp.d.ts} +1 -1
  40. package/dist/{multi-agent-coordinator-CK5Jdj9K.d.ts → multi-agent-coordinator-DyCkCZnU.d.ts} +1 -1
  41. package/dist/{null-fleet-bus-DgvD4SCO.d.ts → null-fleet-bus-CG9QY2aP.d.ts} +6 -6
  42. package/dist/observability/index.d.ts +2 -2
  43. package/dist/{parallel-eternal-engine-bK0JQBR_.d.ts → parallel-eternal-engine-Jw9uhEoT.d.ts} +9 -9
  44. package/dist/{path-resolver-BPEDlN38.d.ts → path-resolver-Dy2ej-gE.d.ts} +3 -3
  45. package/dist/{permission-4yvGmMRB.d.ts → permission-B9SB45lp.d.ts} +1 -1
  46. package/dist/{permission-policy-C6XpsBOy.d.ts → permission-policy-CkjSXabK.d.ts} +2 -2
  47. package/dist/{pipeline-CXCeMz8J.d.ts → pipeline-DPDxH_7m.d.ts} +3 -3
  48. package/dist/{plan-templates-BvzRBkJc.d.ts → plan-templates-CzD9GnAU.d.ts} +32 -8
  49. package/dist/{provider-runner-C5aQpDWE.d.ts → provider-runner-DMa70ODu.d.ts} +3 -3
  50. package/dist/{retry-policy-CFhdtRzz.d.ts → retry-policy-CN0khdlj.d.ts} +1 -1
  51. package/dist/sdd/index.d.ts +8 -8
  52. package/dist/sdd/index.js +274 -93
  53. package/dist/sdd/index.js.map +1 -1
  54. package/dist/{secret-vault-CxiVLbt1.d.ts → secret-vault-B2yw84VT.d.ts} +43 -4
  55. package/dist/secret-vault-BAKpgFw_.d.ts +57 -0
  56. package/dist/security/index.d.ts +5 -5
  57. package/dist/security/index.js +204 -23
  58. package/dist/security/index.js.map +1 -1
  59. package/dist/{selector-gIuhRTkN.d.ts → selector-CzHh_igB.d.ts} +1 -1
  60. package/dist/{session-event-bridge-DkvvrpDt.d.ts → session-event-bridge-BUI6Jf-4.d.ts} +1 -1
  61. package/dist/{session-reader-KdfVwkKP.d.ts → session-reader-CMgdMSRP.d.ts} +1 -1
  62. package/dist/storage/index.d.ts +112 -15
  63. package/dist/storage/index.js +419 -81
  64. package/dist/storage/index.js.map +1 -1
  65. package/dist/tools/index.d.ts +2 -2
  66. package/dist/types/index.d.ts +21 -21
  67. package/dist/types/index.js +261 -53
  68. package/dist/types/index.js.map +1 -1
  69. package/dist/utils/index.d.ts +3 -3
  70. package/dist/utils/index.js +3 -5
  71. package/dist/utils/index.js.map +1 -1
  72. package/dist/{wstack-paths-CJjEwPXn.d.ts → wstack-paths-hOpNLmvf.d.ts} +2 -0
  73. package/package.json +1 -1
  74. package/skills/api-design/SKILL.md +1 -1
  75. package/skills/audit-log/SKILL.md +6 -6
  76. package/skills/bug-hunter/SKILL.md +5 -5
  77. package/skills/chimera/SKILL.md +4 -4
  78. package/skills/docker-deploy/SKILL.md +1 -1
  79. package/skills/git-flow/SKILL.md +3 -3
  80. package/skills/multi-agent/SKILL.md +3 -3
  81. package/skills/node-modern/SKILL.md +1 -0
  82. package/skills/observability/SKILL.md +2 -2
  83. package/skills/output-standards/SKILL.md +51 -28
  84. package/skills/refactor-planner/SKILL.md +3 -3
  85. package/skills/security-scanner/SKILL.md +4 -3
  86. package/skills/tech-stack/SKILL.md +1 -2
  87. package/dist/secret-vault-BJDY28ev.d.ts +0 -25
@@ -3,7 +3,7 @@ import { randomBytes, createCipheriv, createDecipheriv, randomUUID, createHash }
3
3
  import * as fsp2 from 'fs/promises';
4
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';
@@ -234,7 +234,7 @@ var DefaultLogger = class _DefaultLogger {
234
234
  this.maxFileBytes = opts.maxFileBytes ?? 10 * 1024 * 1024;
235
235
  if (this.file) {
236
236
  try {
237
- fs.mkdirSync(path3.dirname(this.file), { recursive: true });
237
+ fs4.mkdirSync(path3.dirname(this.file), { recursive: true });
238
238
  } catch {
239
239
  }
240
240
  }
@@ -275,10 +275,10 @@ var DefaultLogger = class _DefaultLogger {
275
275
  maybeRotate(file) {
276
276
  if (this.writesSinceRotateCheck++ % _DefaultLogger.ROTATE_CHECK_EVERY !== 0) return;
277
277
  try {
278
- const st = fs.statSync(file);
278
+ const st = fs4.statSync(file);
279
279
  if (st.size < this.maxFileBytes) return;
280
- fs.rmSync(`${file}.1`, { force: true });
281
- fs.renameSync(file, `${file}.1`);
280
+ fs4.rmSync(`${file}.1`, { force: true });
281
+ fs4.renameSync(file, `${file}.1`);
282
282
  } catch {
283
283
  }
284
284
  }
@@ -294,7 +294,7 @@ var DefaultLogger = class _DefaultLogger {
294
294
  if (this.file) {
295
295
  try {
296
296
  this.maybeRotate(this.file);
297
- fs.appendFileSync(this.file, `${JSON.stringify(entry)}
297
+ fs4.appendFileSync(this.file, `${JSON.stringify(entry)}
298
298
  `);
299
299
  } catch {
300
300
  }
@@ -592,7 +592,8 @@ function resolveWstackPaths(opts) {
592
592
  projectSddSession: path3.join(projectDir, "sdd-session.json"),
593
593
  projectPlan: path3.join(projectDir, "plan.json"),
594
594
  projectAutophase: path3.join(projectDir, "autophase"),
595
- syncConfig: path3.join(globalRoot, "sync.json")
595
+ syncConfig: path3.join(globalRoot, "sync.json"),
596
+ projectStatus: (projectHash2) => path3.join(globalRoot, "projects", projectHash2, "status.json")
596
597
  };
597
598
  }
598
599
 
@@ -732,12 +733,9 @@ function getCachedEstimate(key, compute) {
732
733
  const existing = ESTIMATE_CACHE.get(key);
733
734
  if (existing !== void 0) return existing;
734
735
  if (ESTIMATE_CACHE.size >= ESTIMATE_CACHE_MAX_SIZE) {
735
- let evicted = 0;
736
- const maxEvict = Math.floor(ESTIMATE_CACHE_MAX_SIZE / 4);
737
736
  for (const k of ESTIMATE_CACHE.keys()) {
738
- if (evicted >= maxEvict) break;
737
+ if (ESTIMATE_CACHE.size <= Math.floor(ESTIMATE_CACHE_MAX_SIZE / 2)) break;
739
738
  ESTIMATE_CACHE.delete(k);
740
- evicted++;
741
739
  }
742
740
  }
743
741
  const estimate = compute(key);
@@ -1277,11 +1275,34 @@ var DefaultSessionStore = class _DefaultSessionStore {
1277
1275
  dir;
1278
1276
  events;
1279
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;
1280
1290
  constructor(opts) {
1281
1291
  this.dir = opts.dir;
1282
1292
  this.events = opts.events;
1283
1293
  this.secretScrubber = opts.secretScrubber;
1284
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
+ }
1285
1306
  // ── Storage event helpers ───────────────────────────────────────────────────
1286
1307
  emitRead(sessionId, filePath, operation, outcome, durationMs, error) {
1287
1308
  this.events?.emit("storage.read", {
@@ -1424,7 +1445,20 @@ var DefaultSessionStore = class _DefaultSessionStore {
1424
1445
  const t0 = Date.now();
1425
1446
  let outcome = "success";
1426
1447
  let errorMsg;
1448
+ let cacheHit = false;
1427
1449
  try {
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
+ }
1428
1462
  const raw = await fsp2.readFile(file, "utf8");
1429
1463
  const lines = raw.split("\n").filter((l) => l.trim());
1430
1464
  const events = [];
@@ -1440,13 +1474,30 @@ var DefaultSessionStore = class _DefaultSessionStore {
1440
1474
  const meta = this.metaFromEvents(id, events);
1441
1475
  const { messages, usage } = this.replay(events, id);
1442
1476
  const toolCallEnds = extractToolCallEnds(events);
1443
- 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;
1444
1486
  } catch (err) {
1445
1487
  outcome = "failure";
1446
1488
  errorMsg = toErrorMessage(err);
1447
1489
  throw err;
1448
1490
  } finally {
1449
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
+ }
1450
1501
  }
1451
1502
  }
1452
1503
  async list(limit = 20) {
@@ -3663,7 +3714,10 @@ function deepFreeze(obj) {
3663
3714
  }
3664
3715
 
3665
3716
  // src/types/secret-vault.ts
3666
- var ENCRYPTED_PREFIX = "enc:v1:";
3717
+ var ENCRYPTED_PREFIX_PATTERN = /^enc:v(\d+):/;
3718
+ function encryptedPrefixForVersion(version) {
3719
+ return `enc:v${version}:`;
3720
+ }
3667
3721
 
3668
3722
  // src/security/secret-vault.ts
3669
3723
  init_atomic_write();
@@ -3672,10 +3726,12 @@ var IV_BYTES = 12;
3672
3726
  var TAG_BYTES = 16;
3673
3727
  var ALGO = "aes-256-gcm";
3674
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;
3675
3731
  function checkKeyFilePermissions(keyFile) {
3676
3732
  if (process.platform === "win32") return;
3677
3733
  try {
3678
- const stat6 = fs.statSync(keyFile);
3734
+ const stat6 = fs4.statSync(keyFile);
3679
3735
  const actualMode = stat6.mode & 511;
3680
3736
  if (actualMode !== KEY_FILE_MODE) {
3681
3737
  console.warn(JSON.stringify({
@@ -3694,11 +3750,17 @@ function checkKeyFilePermissions(keyFile) {
3694
3750
  var DefaultSecretVault = class {
3695
3751
  keyFile;
3696
3752
  key;
3753
+ _keyVersion = 1;
3697
3754
  constructor(opts) {
3698
3755
  this.keyFile = opts.keyFile;
3699
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
+ }
3700
3762
  isEncrypted(value) {
3701
- return typeof value === "string" && value.startsWith(ENCRYPTED_PREFIX);
3763
+ return typeof value === "string" && ENCRYPTED_PREFIX_PATTERN.test(value);
3702
3764
  }
3703
3765
  encrypt(plaintext) {
3704
3766
  if (this.isEncrypted(plaintext)) return plaintext;
@@ -3707,11 +3769,20 @@ var DefaultSecretVault = class {
3707
3769
  const cipher = createCipheriv(ALGO, key, iv);
3708
3770
  const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
3709
3771
  const tag = cipher.getAuthTag();
3710
- 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")}`;
3711
3774
  }
3712
3775
  decrypt(value) {
3713
3776
  if (!this.isEncrypted(value)) return value;
3714
- 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);
3715
3786
  const parts = rest.split(":");
3716
3787
  if (parts.length !== 3) {
3717
3788
  throw new ConfigError({
@@ -3740,42 +3811,104 @@ var DefaultSecretVault = class {
3740
3811
  const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
3741
3812
  return pt.toString("utf8");
3742
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
+ }
3743
3834
  loadOrCreateKey() {
3744
3835
  if (this.key) return this.key;
3745
3836
  try {
3746
- const buf = fs.readFileSync(this.keyFile);
3747
- if (buf.length !== KEY_BYTES) {
3748
- throw new ConfigError({
3749
- message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`,
3750
- code: ERROR_CODES.CONFIG_INVALID,
3751
- context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
3752
- });
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;
3753
3866
  }
3754
- this.key = buf;
3755
- checkKeyFilePermissions(this.keyFile);
3756
- 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
+ });
3757
3872
  } catch (err) {
3758
3873
  if (err.code !== "ENOENT") throw err;
3759
3874
  }
3760
- fs.mkdirSync(path3.dirname(this.keyFile), { recursive: true });
3875
+ fs4.mkdirSync(path3.dirname(this.keyFile), { recursive: true });
3761
3876
  const key = randomBytes(KEY_BYTES);
3762
3877
  try {
3763
- fs.writeFileSync(this.keyFile, key, { mode: 384, flag: "wx" });
3878
+ fs4.writeFileSync(this.keyFile, key, { mode: 384, flag: "wx" });
3764
3879
  } catch (err) {
3765
3880
  if (err.code !== "EEXIST") throw err;
3766
- const buf = fs.readFileSync(this.keyFile);
3767
- if (buf.length !== KEY_BYTES) {
3768
- throw new ConfigError({
3769
- message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`,
3770
- code: ERROR_CODES.CONFIG_INVALID,
3771
- context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
3772
- });
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;
3773
3903
  }
3774
- this.key = buf;
3775
- checkKeyFilePermissions(this.keyFile);
3776
- 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
+ });
3777
3909
  }
3778
3910
  this.key = key;
3911
+ this._keyVersion = 1;
3779
3912
  return key;
3780
3913
  }
3781
3914
  };
@@ -3971,7 +4104,8 @@ var DEFAULT_TOOLS_CONFIG = Object.freeze({
3971
4104
  iterationTimeoutMs: 3e5,
3972
4105
  sessionTimeoutMs: 18e5,
3973
4106
  perIterationOutputCapBytes: 1e5,
3974
- autoExtendLimit: true
4107
+ autoExtendLimit: true,
4108
+ restrictToProjectRoot: false
3975
4109
  });
3976
4110
  var DEFAULT_CONTEXT_CONFIG = Object.freeze({
3977
4111
  preserveK: 10,
@@ -4014,7 +4148,8 @@ var BEHAVIOR_DEFAULTS = {
4014
4148
  iterationTimeoutMs: DEFAULT_TOOLS_CONFIG.iterationTimeoutMs,
4015
4149
  sessionTimeoutMs: DEFAULT_TOOLS_CONFIG.sessionTimeoutMs,
4016
4150
  perIterationOutputCapBytes: DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
4017
- autoExtendLimit: DEFAULT_TOOLS_CONFIG.autoExtendLimit
4151
+ autoExtendLimit: DEFAULT_TOOLS_CONFIG.autoExtendLimit,
4152
+ restrictToProjectRoot: DEFAULT_TOOLS_CONFIG.restrictToProjectRoot
4018
4153
  },
4019
4154
  log: { level: "info" },
4020
4155
  features: {
@@ -5283,6 +5418,7 @@ async function savePlan(filePath, plan, events) {
5283
5418
  outcome: "success",
5284
5419
  durationMs: Date.now() - t0
5285
5420
  });
5421
+ return true;
5286
5422
  } catch (err) {
5287
5423
  events?.emit("storage.error", {
5288
5424
  sessionId: "~boot~",
@@ -5296,6 +5432,7 @@ async function savePlan(filePath, plan, events) {
5296
5432
  "[plan-store] save failed:",
5297
5433
  toErrorMessage(err)
5298
5434
  );
5435
+ return false;
5299
5436
  }
5300
5437
  }
5301
5438
  function emptyPlan(sessionId, title) {
@@ -5732,6 +5869,27 @@ var PATTERNS = [
5732
5869
  { type: "postgres_uri", regex: /postgres(?:ql)?:\/\/[^\s"'`]+/g },
5733
5870
  { type: "mysql_uri", regex: /mysql:\/\/[^\s"'`]+/g },
5734
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
+ },
5735
5893
  {
5736
5894
  type: "bearer_token",
5737
5895
  // Anchored with alternation instead of negative lookahead — avoids V8
@@ -5765,6 +5923,10 @@ function hasCredentialAnchors(text) {
5765
5923
  text.includes("xox") || // Slack token (xoxa/xoxb/xoxp/xoxo/xoxs)
5766
5924
  text.includes("Bearer ") || // Bearer token (space suffix reduces false positives)
5767
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
5768
5930
  text.includes("_KEY=") || // High-entropy env vars: API_KEY=, SECRET_KEY=, ...
5769
5931
  text.includes("_TOKEN=") || // ACCESS_TOKEN=, AUTH_TOKEN=, ...
5770
5932
  text.includes("_SECRET=") || // API_SECRET=, CLIENT_SECRET=, ...
@@ -6605,12 +6767,12 @@ var DefaultSkillLoader = class {
6605
6767
  }
6606
6768
  async find(name) {
6607
6769
  const all = await this.list();
6608
- return all.find((s) => s.name === name);
6770
+ const lower = name.toLowerCase();
6771
+ return all.find((s) => s.name.toLowerCase() === lower);
6609
6772
  }
6610
6773
  async manifestText() {
6611
- const skills = await this.list();
6612
- if (skills.length === 0) return "";
6613
6774
  const entries = await this.listEntries();
6775
+ if (entries.length === 0) return "";
6614
6776
  const lines = ["## Available skills"];
6615
6777
  for (const e of entries) {
6616
6778
  const scopeTag = e.scope.length > 0 ? ` \u2014 ${e.scope.slice(0, 3).join(", ")}` : "";
@@ -6624,12 +6786,8 @@ var DefaultSkillLoader = class {
6624
6786
  const skills = await this.list();
6625
6787
  const entries = [];
6626
6788
  for (const s of skills) {
6627
- try {
6628
- const raw = await fsp2.readFile(s.path, "utf8");
6629
- const { trigger, scope } = parseDescription(raw);
6630
- entries.push({ name: s.name, trigger, scope, source: s.source, path: s.path });
6631
- } catch {
6632
- }
6789
+ const { trigger, scope } = parseDescriptionFromText(s.description ?? "");
6790
+ entries.push({ name: s.name, trigger, scope, source: s.source, path: s.path });
6633
6791
  }
6634
6792
  this.entriesCache = entries;
6635
6793
  return entries;
@@ -6640,16 +6798,17 @@ var DefaultSkillLoader = class {
6640
6798
  this.bodyCache.clear();
6641
6799
  }
6642
6800
  async readBody(name) {
6643
- const cached = this.bodyCache.get(name);
6801
+ const key = name.toLowerCase();
6802
+ const cached = this.bodyCache.get(key);
6644
6803
  if (cached !== void 0) return cached;
6645
6804
  const m = await this.find(name);
6646
6805
  if (!m) throw new Error(`Skill "${name}" not found`);
6647
6806
  const body = await fsp2.readFile(m.path, "utf8");
6648
- this.bodyCache.set(name, body);
6807
+ this.bodyCache.set(key, body);
6649
6808
  return body;
6650
6809
  }
6651
6810
  async readSaveBody(name) {
6652
- const key = `save:${name}`;
6811
+ const key = `save:${name.toLowerCase()}`;
6653
6812
  const cached = this.bodyCache.get(key);
6654
6813
  if (cached !== void 0) return cached;
6655
6814
  const m = await this.find(name);
@@ -6710,9 +6869,7 @@ function parseFrontmatter(raw) {
6710
6869
  flush();
6711
6870
  return out;
6712
6871
  }
6713
- function parseDescription(raw) {
6714
- const fm = parseFrontmatter(raw);
6715
- const desc = fm.description ?? "";
6872
+ function parseDescriptionFromText(desc) {
6716
6873
  const firstSentenceEnd = desc.indexOf(". ");
6717
6874
  const trigger = firstSentenceEnd !== -1 ? desc.slice(0, firstSentenceEnd + 1).trim() : desc.trim().split("\n")[0] ?? "";
6718
6875
  const scope = [];
@@ -7116,7 +7273,11 @@ function isTextBlock(b) {
7116
7273
  }
7117
7274
 
7118
7275
  // src/execution/compaction-core.ts
7276
+ function compactionDebugEnabled() {
7277
+ return process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1";
7278
+ }
7119
7279
  function emitCompactionMetrics(event, metrics) {
7280
+ if (!compactionDebugEnabled()) return;
7120
7281
  console.log(
7121
7282
  JSON.stringify({
7122
7283
  level: "debug",
@@ -7171,18 +7332,20 @@ function findPreserveStart(messages, preserveK) {
7171
7332
  }
7172
7333
  }
7173
7334
  }
7174
- console.log(
7175
- JSON.stringify({
7176
- level: "debug",
7177
- event: "compaction.find_preserve_start.ended",
7178
- messageCount: messages.length,
7179
- preserveK,
7180
- preserveStart,
7181
- forwardWalkIterations,
7182
- forwardWalkInnerIterations,
7183
- forwardWalkInnerPerOuter: forwardWalkIterations > 0 ? forwardWalkInnerIterations / forwardWalkIterations : 0
7184
- })
7185
- );
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
+ }
7186
7349
  return preserveStart;
7187
7350
  }
7188
7351
  function eliseOldToolResults(messages, opts) {
@@ -7249,7 +7412,7 @@ function eliseOldToolResults(messages, opts) {
7249
7412
  changed = true;
7250
7413
  }
7251
7414
  fullPassInnerIterations += original.length;
7252
- if (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1") {
7415
+ if (compactionDebugEnabled()) {
7253
7416
  const ratio = fullPassInnerIterations / fullPassIterations;
7254
7417
  if (ratio > 10) {
7255
7418
  console.error(
@@ -7656,7 +7819,12 @@ var IntelligentCompactor = class {
7656
7819
  };
7657
7820
  const ac = ctx.signal ? void 0 : new AbortController();
7658
7821
  const signal = ctx.signal ?? ac?.signal;
7659
- 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
+ }
7660
7828
  const textBlocks = res.content.filter(isTextBlock);
7661
7829
  return textBlocks.map((b) => b.text).join("\n").trim() || "(empty summary)";
7662
7830
  }
@@ -7700,9 +7868,9 @@ Rules:
7700
7868
  - If unsure, keep rather than collapse (errors are more costly than waste)
7701
7869
 
7702
7870
  Return ONLY the JSON object, no markdown, no explanation outside the JSON.`;
7703
- function formatMessages(messages, maxChars = 8e3) {
7871
+ function formatMessages(messages, maxTokens = 2048) {
7704
7872
  const lines = [];
7705
- let used = 0;
7873
+ let usedTokens = 0;
7706
7874
  for (let i = 0; i < messages.length; i++) {
7707
7875
  const m = expectDefined(messages[i]);
7708
7876
  const role = m.role.padEnd(10, " ");
@@ -7714,13 +7882,14 @@ function formatMessages(messages, maxChars = 8e3) {
7714
7882
  text = content.filter(isTextBlock).map((b) => b.text).join(" ");
7715
7883
  const toolUses = content.filter((b) => b.type === "tool_use");
7716
7884
  if (toolUses.length > 0) {
7717
- text += ` [tools: ${toolUses.map((b) => b.name).join(", ")}]`;
7885
+ text += ` [tools: ${toolUses.map((b) => b.name).filter(Boolean).join(", ")}]`;
7718
7886
  }
7719
7887
  }
7720
7888
  const line = `[${i}][${role}]: ${text}`;
7721
- if (used + line.length > maxChars) break;
7889
+ const lineTokens = estimateTextTokens(line);
7890
+ if (usedTokens + lineTokens > maxTokens) break;
7722
7891
  lines.push(line);
7723
- used += line.length;
7892
+ usedTokens += lineTokens;
7724
7893
  }
7725
7894
  return lines.join("\n");
7726
7895
  }
@@ -7729,20 +7898,29 @@ var LLMSelector = class {
7729
7898
  model;
7730
7899
  maxContextTokens;
7731
7900
  systemPrompt;
7901
+ maxOutputTokens;
7732
7902
  constructor(opts) {
7733
7903
  this.provider = opts.provider;
7734
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
+ }
7735
7910
  this.maxContextTokens = opts.maxContextTokens ?? 4e4;
7736
7911
  this.systemPrompt = opts.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
7912
+ this.maxOutputTokens = opts.maxOutputTokens ?? 1024;
7737
7913
  }
7738
7914
  async select(messages, maxToKeep) {
7739
7915
  const effectiveBudget = Math.min(maxToKeep, this.maxContextTokens);
7740
- const historyText = formatMessages(messages);
7741
7916
  const totalTokens = estimateMessageTokens(messages);
7742
7917
  const systemText = `${this.systemPrompt}
7743
7918
 
7744
7919
  Conversation (${messages.length} messages, ~${totalTokens} tokens, budget: ${effectiveBudget}):
7745
7920
  `;
7921
+ const systemTokens = estimateTextTokens(systemText);
7922
+ const historyBudget = Math.max(512, effectiveBudget - systemTokens - this.maxOutputTokens);
7923
+ const historyText = formatMessages(messages, historyBudget);
7746
7924
  const budgetInstruction = totalTokens > effectiveBudget ? `
7747
7925
 
7748
7926
  IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiveBudget}). You MUST collapse enough to fit. Prefer collapsing older/lower-importance ranges.` : "";
@@ -7750,18 +7928,26 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
7750
7928
  model: this.model,
7751
7929
  system: [{ type: "text", text: systemText + budgetInstruction }],
7752
7930
  messages: [{ role: "user", content: historyText }],
7753
- maxTokens: 1024
7931
+ maxTokens: this.maxOutputTokens
7754
7932
  };
7755
7933
  let raw;
7934
+ const ac = new AbortController();
7756
7935
  try {
7757
- const ac = new AbortController();
7758
- 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
+ });
7759
7940
  const textBlocks = res.content.filter(isTextBlock);
7760
7941
  raw = textBlocks.map((b) => b.text).join("\n").trim();
7761
- } catch (_err) {
7942
+ } catch (err) {
7943
+ if (err instanceof Error) {
7944
+ console.warn("[LLMSelector] selector call failed, using recency fallback:", err.message);
7945
+ }
7762
7946
  return this.fallbackSelect(messages, effectiveBudget);
7947
+ } finally {
7948
+ ac.abort();
7763
7949
  }
7764
- return this.parseSelectorOutput(raw, messages.length);
7950
+ return this.parseSelectorOutput(raw, messages);
7765
7951
  }
7766
7952
  fallbackSelect(messages, budget) {
7767
7953
  const toKeep = [];
@@ -7788,34 +7974,63 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
7788
7974
  reasoning: `Fallback: kept last ${messages.length - startIdx} messages within ${budget} token budget`
7789
7975
  };
7790
7976
  }
7791
- 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
+ }
7792
7987
  const jsonStart = raw.indexOf("{");
7793
7988
  const jsonEnd = raw.lastIndexOf("}");
7794
7989
  if (jsonStart === -1 || jsonEnd === -1) {
7795
- return this.fallbackSelect(
7796
- Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
7797
- this.maxContextTokens
7798
- );
7990
+ return this.fallbackSelect(messages, this.maxContextTokens);
7799
7991
  }
7800
7992
  let parsed;
7801
7993
  try {
7802
7994
  parsed = JSON.parse(raw.slice(jsonStart, jsonEnd + 1));
7803
7995
  } catch {
7804
- return this.fallbackSelect(
7805
- Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
7806
- this.maxContextTokens
7807
- );
7996
+ return this.fallbackSelect(messages, this.maxContextTokens);
7808
7997
  }
7809
7998
  const obj = parsed;
7810
- const kept = obj.kept ?? [];
7811
- const collapsed = obj.collapsed ?? [];
7812
- return {
7813
- 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({
7814
8007
  from: k.from,
7815
8008
  to: k.to,
7816
8009
  importance: k.importance ?? "medium"
7817
- })),
7818
- 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,
7819
8034
  reasoning: typeof obj.reasoning === "string" ? obj.reasoning : ""
7820
8035
  };
7821
8036
  }
@@ -7835,7 +8050,7 @@ var SelectiveCompactor = class {
7835
8050
  summarizerPrompt;
7836
8051
  constructor(opts) {
7837
8052
  this.provider = opts.provider;
7838
- 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 });
7839
8054
  this.warnThreshold = opts.warnThreshold ?? 0.6;
7840
8055
  this.softThreshold = opts.softThreshold ?? 0.75;
7841
8056
  this.hardThreshold = opts.hardThreshold ?? 0.9;
@@ -7843,6 +8058,11 @@ var SelectiveCompactor = class {
7843
8058
  this.preserveK = opts.preserveK ?? 4;
7844
8059
  this.eliseThreshold = opts.eliseThreshold ?? 500;
7845
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
+ }
7846
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.";
7847
8067
  }
7848
8068
  async compact(ctx, opts = {}) {
@@ -7954,8 +8174,9 @@ Summarize the following message range:`;
7954
8174
  maxTokens: 512
7955
8175
  };
7956
8176
  try {
8177
+ const timeoutSignal = AbortSignal.timeout(3e4);
7957
8178
  const res = await this.provider.complete(req, {
7958
- signal: ctx.signal ?? new AbortController().signal
8179
+ signal: AbortSignal.any([ctx.signal, timeoutSignal])
7959
8180
  });
7960
8181
  return res.content.filter(isTextBlock).map((b) => b.text).join("\n").trim() || "(empty)";
7961
8182
  } catch {
@@ -8089,6 +8310,7 @@ var ProviderBackedCompactor = class {
8089
8310
  return new SelectiveCompactor({
8090
8311
  ...common,
8091
8312
  selectorModel: this.opts.summarizerModel,
8313
+ selectorMaxOutputTokens: this.opts.selectorMaxOutputTokens,
8092
8314
  summarizerModel: this.opts.summarizerModel
8093
8315
  });
8094
8316
  }
@@ -9818,6 +10040,7 @@ ${recentJournal}` : ""
9818
10040
 
9819
10041
  // src/coordination/subagent-budget.ts
9820
10042
  var TIMEOUT_PREEMPT_FRACTION = 0.85;
10043
+ var DECISION_TIMEOUT_MS = 6e4;
9821
10044
  var BudgetExceededError = class extends Error {
9822
10045
  kind;
9823
10046
  limit;
@@ -9847,6 +10070,31 @@ var BudgetThresholdSignal = class extends Error {
9847
10070
  };
9848
10071
  var SubagentBudget = class _SubagentBudget {
9849
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
+ }
9850
10098
  iterations = 0;
9851
10099
  toolCalls = 0;
9852
10100
  tokenInput = 0;
@@ -9867,12 +10115,44 @@ var SubagentBudget = class _SubagentBudget {
9867
10115
  * or hung listener (Director not built / event filter detached mid-run)
9868
10116
  * leaves the budget over-limit and never enforces anything.
9869
10117
  */
9870
- static DECISION_TIMEOUT_MS = 6e4;
10118
+ static DECISION_TIMEOUT_MS = DECISION_TIMEOUT_MS;
9871
10119
  /**
9872
10120
  * Injected by the runner when wiring the budget to its EventBus.
9873
10121
  * Used to emit `budget.threshold_reached` events in `'auto'` mode.
9874
10122
  */
9875
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
+ }
9876
10156
  /**
9877
10157
  * Negotiation mode — controls whether a threshold hit tries to emit
9878
10158
  * `budget.threshold_reached` and wait for a coordinator decision, or
@@ -9973,7 +10253,8 @@ var SubagentBudget = class _SubagentBudget {
9973
10253
  if (this.limits.idleTimeoutMs !== void 0 && idle > this.limits.idleTimeoutMs) {
9974
10254
  exceeded.push({ kind: "idle_timeout", used: idle, limit: this.limits.idleTimeoutMs });
9975
10255
  }
9976
- 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) {
9977
10258
  exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
9978
10259
  }
9979
10260
  }
@@ -9987,19 +10268,99 @@ var SubagentBudget = class _SubagentBudget {
9987
10268
  throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
9988
10269
  }
9989
10270
  const bus = this._events;
9990
- if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
10271
+ if (!bus) {
9991
10272
  const first2 = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
9992
10273
  throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
9993
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;
9994
10286
  for (const entry of exceeded) {
9995
10287
  if (this._pendingNegotiations.has(entry.kind)) continue;
9996
- const decision2 = this._negotiateExtension(entry.kind, exceeded);
9997
- 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);
9998
10293
  }
9999
- const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
10000
- const decision = this._pendingNegotiations.get(first.kind);
10001
- if (!decision) throw new Error(`No pending negotiation for ${first.kind}`);
10002
- throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
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
+ });
10003
10364
  }
10004
10365
  /**
10005
10366
  * Per-kind in-flight negotiation Promises. Each budget kind can have its
@@ -10019,77 +10380,33 @@ var SubagentBudget = class _SubagentBudget {
10019
10380
  * `{ extend: {} }` — keep going without patching; next overrun fires
10020
10381
  * a fresh signal.
10021
10382
  */
10022
- async _negotiateExtension(kind, exceeded) {
10383
+ async _negotiateExtension(entry) {
10023
10384
  if (!this._onThreshold) {
10024
10385
  return "stop";
10025
10386
  }
10026
10387
  try {
10027
- const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
10028
10388
  const result = this._onThreshold({
10029
- kind: first.kind,
10030
- used: first.used,
10031
- limit: first.limit,
10032
- requestDecision: () => {
10033
- const bus = this._events;
10034
- if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
10035
- return Promise.resolve("stop");
10036
- }
10037
- return new Promise((resolve5) => {
10038
- let resolved = false;
10039
- const respond = (d) => {
10040
- if (resolved) return;
10041
- resolved = true;
10042
- resolve5(d);
10043
- };
10044
- const fallback = setTimeout(
10045
- () => respond("stop"),
10046
- _SubagentBudget.DECISION_TIMEOUT_MS
10047
- );
10048
- for (const { kind: kind2, used, limit } of exceeded) {
10049
- bus.emit("budget.threshold_reached", {
10050
- kind: kind2,
10051
- used,
10052
- limit,
10053
- timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
10054
- extend: (extra) => {
10055
- clearTimeout(fallback);
10056
- respond({ extend: extra });
10057
- },
10058
- deny: () => {
10059
- clearTimeout(fallback);
10060
- respond("stop");
10061
- }
10062
- });
10063
- }
10064
- });
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: () => {
10065
10400
  }
10066
10401
  });
10067
10402
  if (result === "throw") return "stop";
10068
10403
  if (result === "continue") return { extend: {} };
10069
10404
  const decision = await result;
10070
10405
  if (decision === "stop") return "stop";
10071
- const ext = decision.extend;
10072
- if (ext.maxIterations !== void 0) {
10073
- this.limits.maxIterations = ext.maxIterations;
10074
- }
10075
- if (ext.maxToolCalls !== void 0) {
10076
- this.limits.maxToolCalls = ext.maxToolCalls;
10077
- }
10078
- if (ext.maxTokens !== void 0) {
10079
- this.limits.maxTokens = ext.maxTokens;
10080
- }
10081
- if (ext.maxCostUsd !== void 0) {
10082
- this.limits.maxCostUsd = ext.maxCostUsd;
10083
- }
10084
- if (ext.timeoutMs !== void 0) {
10085
- this.limits.timeoutMs = ext.timeoutMs;
10086
- }
10087
- if (ext.idleTimeoutMs !== void 0) {
10088
- this.limits.idleTimeoutMs = ext.idleTimeoutMs;
10089
- }
10406
+ this.patchLimits(decision.extend);
10090
10407
  return decision;
10091
10408
  } finally {
10092
- this._pendingNegotiations.delete(kind);
10409
+ this._pendingNegotiations.delete(entry.kind);
10093
10410
  }
10094
10411
  }
10095
10412
  recordIteration() {
@@ -10132,7 +10449,8 @@ var SubagentBudget = class _SubagentBudget {
10132
10449
  const { timeoutMs, idleTimeoutMs } = this.limits;
10133
10450
  if (timeoutMs === void 0 && idleTimeoutMs === void 0) return;
10134
10451
  const elapsed = Date.now() - this.startTime;
10135
- 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;
10136
10454
  const idleTripped = idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs;
10137
10455
  if (!wallTripped && !idleTripped) return;
10138
10456
  void this.checkLimits(elapsed);
@@ -13640,6 +13958,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
13640
13958
  terminating = /* @__PURE__ */ new Set();
13641
13959
  constructor(config, options = {}) {
13642
13960
  super();
13961
+ this.setMaxListeners(0);
13643
13962
  this.coordinatorId = config.coordinatorId;
13644
13963
  this.config = config;
13645
13964
  this.runner = options.runner;
@@ -14034,7 +14353,13 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
14034
14353
  let result;
14035
14354
  budget.start();
14036
14355
  try {
14037
- 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
+ );
14038
14363
  result = {
14039
14364
  subagentId,
14040
14365
  taskId: task.id,
@@ -14061,7 +14386,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
14061
14386
  }
14062
14387
  this.recordCompletion(result);
14063
14388
  }
14064
- async executeWithTimeout(runner, task, ctx, budget) {
14389
+ async executeWithTimeout(runner, task, ctx, budget, preemptFraction = TIMEOUT_PREEMPT_FRACTION) {
14065
14390
  const initialTimeoutMs = budget.limits.timeoutMs;
14066
14391
  const idleLimitMs = budget.limits.idleTimeoutMs;
14067
14392
  if (initialTimeoutMs === void 0 && idleLimitMs === void 0) {
@@ -14069,8 +14394,21 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
14069
14394
  }
14070
14395
  const start = Date.now();
14071
14396
  let timer = null;
14072
- 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;
14073
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
+ };
14074
14412
  const armFor = (ms) => {
14075
14413
  if (timer) clearTimeout(timer);
14076
14414
  timer = setTimeout(onTick, Math.max(0, ms));
@@ -14079,7 +14417,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
14079
14417
  const wallLimit = budget.limits.timeoutMs ?? initialTimeoutMs;
14080
14418
  const wallRemaining = initialTimeoutMs === void 0 ? Number.POSITIVE_INFINITY : wallLimit - (Date.now() - start);
14081
14419
  const idleRemaining = idleLimitMs === void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
14082
- const preemptRemaining = initialTimeoutMs === void 0 || preemptedForLimit === wallLimit ? Number.POSITIVE_INFINITY : wallLimit * TIMEOUT_PREEMPT_FRACTION - (Date.now() - start);
14420
+ const preemptRemaining = initialTimeoutMs === void 0 || preemptedCeiling === wallLimit ? Number.POSITIVE_INFINITY : wallLimit * preemptFraction - (Date.now() - start);
14083
14421
  armFor(Math.max(25, Math.min(wallRemaining, idleRemaining, preemptRemaining)));
14084
14422
  };
14085
14423
  const negotiateTimeout = async (used, limit) => {
@@ -14089,16 +14427,42 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
14089
14427
  kind: "timeout",
14090
14428
  used,
14091
14429
  limit,
14092
- requestDecision: () => new Promise((resolveDecision) => {
14093
- budget._events?.emit("budget.threshold_reached", {
14094
- kind: "timeout",
14095
- used,
14096
- limit,
14097
- timeoutMs: 6e4,
14098
- extend: (extra) => resolveDecision({ extend: extra }),
14099
- deny: () => resolveDecision("stop")
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
+ });
14100
14464
  });
14101
- })
14465
+ }
14102
14466
  });
14103
14467
  return typeof result === "string" ? result : await result;
14104
14468
  };
@@ -14109,21 +14473,45 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
14109
14473
  const wallExceeded = wallLimit !== void 0 && elapsed >= wallLimit;
14110
14474
  const idleExceeded = idleLimit !== void 0 && budget.idleMs() >= idleLimit;
14111
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
+ });
14112
14486
  this.subagents.get(ctx.subagentId)?.abortController.abort();
14113
- reject(new BudgetExceededError("timeout", idleLimit ?? 0, budget.idleMs()));
14487
+ reject(new BudgetExceededError("idle_timeout", idleLimit ?? 0, budget.idleMs()));
14114
14488
  return;
14115
14489
  }
14116
- 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);
14117
14499
  try {
14118
14500
  const decision = await negotiateTimeout(elapsed, wallLimit);
14119
14501
  if (typeof decision !== "string" && decision.extend.timeoutMs !== void 0) {
14120
- budget.limits.timeoutMs = decision.extend.timeoutMs;
14121
- preemptedForLimit = null;
14502
+ budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
14503
+ lastGrantActivityTs = Date.now() - budget.idleMs();
14504
+ preemptState = "active" /* ACTIVE */;
14505
+ preemptedCeiling = null;
14122
14506
  } else {
14123
- preemptedForLimit = wallLimit;
14507
+ preemptState = "locked" /* LOCKED */;
14508
+ preemptedCeiling = wallLimit;
14124
14509
  }
14125
14510
  } catch {
14126
- preemptedForLimit = wallLimit;
14511
+ preemptState = "locked" /* LOCKED */;
14512
+ preemptedCeiling = wallLimit;
14513
+ } finally {
14514
+ budget.clearWatchdogNegotiation();
14127
14515
  }
14128
14516
  scheduleNext();
14129
14517
  return;
@@ -14138,26 +14526,41 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
14138
14526
  reject(new BudgetExceededError("timeout", limit, elapsed));
14139
14527
  return;
14140
14528
  }
14529
+ budget.setWatchdogNegotiation(limit);
14141
14530
  try {
14142
14531
  const decision = await negotiateTimeout(elapsed, limit);
14143
- if (decision === "continue" || decision === "throw" || decision === "stop") {
14144
- 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;
14145
14539
  armFor(Math.max(1e3, limit));
14146
14540
  return;
14147
14541
  }
14542
+ if (decision === "stop") {
14543
+ terminate("timeout", limit, elapsed);
14544
+ return;
14545
+ }
14148
14546
  if (decision.extend.timeoutMs !== void 0) {
14149
- budget.limits.timeoutMs = decision.extend.timeoutMs;
14150
- preemptedForLimit = null;
14547
+ budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
14548
+ lastGrantActivityTs = Date.now() - budget.idleMs();
14549
+ preemptState = "active" /* ACTIVE */;
14550
+ preemptedCeiling = null;
14151
14551
  scheduleNext();
14152
14552
  return;
14153
14553
  }
14154
- this.subagents.get(ctx.subagentId)?.abortController.abort();
14155
- reject(new BudgetExceededError("timeout", limit, elapsed));
14554
+ terminate("timeout", limit, elapsed);
14555
+ return;
14156
14556
  } catch (err) {
14157
14557
  this.subagents.get(ctx.subagentId)?.abortController.abort();
14158
14558
  reject(
14159
14559
  err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
14160
14560
  );
14561
+ return;
14562
+ } finally {
14563
+ budget.clearWatchdogNegotiation();
14161
14564
  }
14162
14565
  };
14163
14566
  scheduleNext();
@@ -18120,6 +18523,7 @@ function attachAutoExtend(events, policy = {}) {
18120
18523
  const extendCounts = /* @__PURE__ */ new Map();
18121
18524
  let progress = 0;
18122
18525
  let lastTimeoutProgress = -1;
18526
+ let lastSeenKey = null;
18123
18527
  const unsubs = [
18124
18528
  events.on("tool.executed", () => {
18125
18529
  progress++;
@@ -18129,6 +18533,9 @@ function attachAutoExtend(events, policy = {}) {
18129
18533
  }),
18130
18534
  events.on("budget.threshold_reached", (e) => {
18131
18535
  const { kind, limit, extend, deny } = e;
18536
+ const key = `${kind}:${limit}`;
18537
+ if (key === lastSeenKey) return;
18538
+ lastSeenKey = key;
18132
18539
  if (kind === "timeout" || kind === "idle_timeout") {
18133
18540
  if (progress > lastTimeoutProgress) {
18134
18541
  lastTimeoutProgress = progress;
@@ -22343,11 +22750,12 @@ function createContextManagerTool(opts = {}) {
22343
22750
  const applyMessages = (next) => {
22344
22751
  const repaired = repairToolUseAdjacency(next);
22345
22752
  const finalMessages = repaired.messages;
22753
+ if (finalMessages === messages) return repaired.report;
22346
22754
  if (ctx.state) {
22347
22755
  ctx.state.replaceMessages(finalMessages);
22348
22756
  } else {
22349
22757
  messages.length = 0;
22350
- messages.splice(0, 0, ...finalMessages);
22758
+ messages.push(...finalMessages);
22351
22759
  }
22352
22760
  return repaired.report;
22353
22761
  };
@@ -22422,9 +22830,15 @@ function createContextManagerTool(opts = {}) {
22422
22830
  }
22423
22831
  const report = await opts.compactor.compact(ctx);
22424
22832
  ctx.clearFileTracking();
22425
- const repair = applyMessages([...ctx.messages]);
22426
- const afterTokens = repair.changed ? roughEstimate(ctx.messages) : report.after;
22427
- const repaired = report.repaired ?? (repair.changed ? repair : void 0);
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
+ }
22428
22842
  const reduced = report.fullRequestTokensBefore > report.fullRequestTokensAfter;
22429
22843
  const repairedSomething = !!report.repaired;
22430
22844
  if (reduced || repairedSomething) {