@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
@@ -25,12 +25,9 @@ function getCachedEstimate(key, compute) {
25
25
  const existing = ESTIMATE_CACHE.get(key);
26
26
  if (existing !== void 0) return existing;
27
27
  if (ESTIMATE_CACHE.size >= ESTIMATE_CACHE_MAX_SIZE) {
28
- let evicted = 0;
29
- const maxEvict = Math.floor(ESTIMATE_CACHE_MAX_SIZE / 4);
30
28
  for (const k of ESTIMATE_CACHE.keys()) {
31
- if (evicted >= maxEvict) break;
29
+ if (ESTIMATE_CACHE.size <= Math.floor(ESTIMATE_CACHE_MAX_SIZE / 2)) break;
32
30
  ESTIMATE_CACHE.delete(k);
33
- evicted++;
34
31
  }
35
32
  }
36
33
  const estimate = compute(key);
@@ -261,7 +258,11 @@ function isTextBlock(b) {
261
258
  }
262
259
 
263
260
  // src/execution/compaction-core.ts
261
+ function compactionDebugEnabled() {
262
+ return process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1";
263
+ }
264
264
  function emitCompactionMetrics(event, metrics) {
265
+ if (!compactionDebugEnabled()) return;
265
266
  console.log(
266
267
  JSON.stringify({
267
268
  level: "debug",
@@ -316,18 +317,20 @@ function findPreserveStart(messages, preserveK) {
316
317
  }
317
318
  }
318
319
  }
319
- console.log(
320
- JSON.stringify({
321
- level: "debug",
322
- event: "compaction.find_preserve_start.ended",
323
- messageCount: messages.length,
324
- preserveK,
325
- preserveStart,
326
- forwardWalkIterations,
327
- forwardWalkInnerIterations,
328
- forwardWalkInnerPerOuter: forwardWalkIterations > 0 ? forwardWalkInnerIterations / forwardWalkIterations : 0
329
- })
330
- );
320
+ if (compactionDebugEnabled()) {
321
+ console.log(
322
+ JSON.stringify({
323
+ level: "debug",
324
+ event: "compaction.find_preserve_start.ended",
325
+ messageCount: messages.length,
326
+ preserveK,
327
+ preserveStart,
328
+ forwardWalkIterations,
329
+ forwardWalkInnerIterations,
330
+ forwardWalkInnerPerOuter: forwardWalkIterations > 0 ? forwardWalkInnerIterations / forwardWalkIterations : 0
331
+ })
332
+ );
333
+ }
331
334
  return preserveStart;
332
335
  }
333
336
  function eliseOldToolResults(messages, opts) {
@@ -394,7 +397,7 @@ function eliseOldToolResults(messages, opts) {
394
397
  changed = true;
395
398
  }
396
399
  fullPassInnerIterations += original.length;
397
- if (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1") {
400
+ if (compactionDebugEnabled()) {
398
401
  const ratio = fullPassInnerIterations / fullPassIterations;
399
402
  if (ratio > 10) {
400
403
  console.error(
@@ -801,7 +804,12 @@ var IntelligentCompactor = class {
801
804
  };
802
805
  const ac = ctx.signal ? void 0 : new AbortController();
803
806
  const signal = ctx.signal ?? ac?.signal;
804
- const res = await this.provider.complete(req, { signal });
807
+ let res;
808
+ try {
809
+ res = await this.provider.complete(req, { signal });
810
+ } finally {
811
+ ac?.abort();
812
+ }
805
813
  const textBlocks = res.content.filter(isTextBlock);
806
814
  return textBlocks.map((b) => b.text).join("\n").trim() || "(empty summary)";
807
815
  }
@@ -845,9 +853,9 @@ Rules:
845
853
  - If unsure, keep rather than collapse (errors are more costly than waste)
846
854
 
847
855
  Return ONLY the JSON object, no markdown, no explanation outside the JSON.`;
848
- function formatMessages(messages, maxChars = 8e3) {
856
+ function formatMessages(messages, maxTokens = 2048) {
849
857
  const lines = [];
850
- let used = 0;
858
+ let usedTokens = 0;
851
859
  for (let i = 0; i < messages.length; i++) {
852
860
  const m = expectDefined(messages[i]);
853
861
  const role = m.role.padEnd(10, " ");
@@ -859,13 +867,14 @@ function formatMessages(messages, maxChars = 8e3) {
859
867
  text = content.filter(isTextBlock).map((b) => b.text).join(" ");
860
868
  const toolUses = content.filter((b) => b.type === "tool_use");
861
869
  if (toolUses.length > 0) {
862
- text += ` [tools: ${toolUses.map((b) => b.name).join(", ")}]`;
870
+ text += ` [tools: ${toolUses.map((b) => b.name).filter(Boolean).join(", ")}]`;
863
871
  }
864
872
  }
865
873
  const line = `[${i}][${role}]: ${text}`;
866
- if (used + line.length > maxChars) break;
874
+ const lineTokens = estimateTextTokens(line);
875
+ if (usedTokens + lineTokens > maxTokens) break;
867
876
  lines.push(line);
868
- used += line.length;
877
+ usedTokens += lineTokens;
869
878
  }
870
879
  return lines.join("\n");
871
880
  }
@@ -874,20 +883,29 @@ var LLMSelector = class {
874
883
  model;
875
884
  maxContextTokens;
876
885
  systemPrompt;
886
+ maxOutputTokens;
877
887
  constructor(opts) {
878
888
  this.provider = opts.provider;
879
889
  this.model = opts.model ?? "unknown";
890
+ if (this.model === "unknown" && (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1")) {
891
+ console.warn(
892
+ "[LLMSelector] model not set \u2014 selector will use the provider default. Set `model` explicitly in LLMSelectorOptions to silence this warning."
893
+ );
894
+ }
880
895
  this.maxContextTokens = opts.maxContextTokens ?? 4e4;
881
896
  this.systemPrompt = opts.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
897
+ this.maxOutputTokens = opts.maxOutputTokens ?? 1024;
882
898
  }
883
899
  async select(messages, maxToKeep) {
884
900
  const effectiveBudget = Math.min(maxToKeep, this.maxContextTokens);
885
- const historyText = formatMessages(messages);
886
901
  const totalTokens = estimateMessageTokens(messages);
887
902
  const systemText = `${this.systemPrompt}
888
903
 
889
904
  Conversation (${messages.length} messages, ~${totalTokens} tokens, budget: ${effectiveBudget}):
890
905
  `;
906
+ const systemTokens = estimateTextTokens(systemText);
907
+ const historyBudget = Math.max(512, effectiveBudget - systemTokens - this.maxOutputTokens);
908
+ const historyText = formatMessages(messages, historyBudget);
891
909
  const budgetInstruction = totalTokens > effectiveBudget ? `
892
910
 
893
911
  IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiveBudget}). You MUST collapse enough to fit. Prefer collapsing older/lower-importance ranges.` : "";
@@ -895,18 +913,26 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
895
913
  model: this.model,
896
914
  system: [{ type: "text", text: systemText + budgetInstruction }],
897
915
  messages: [{ role: "user", content: historyText }],
898
- maxTokens: 1024
916
+ maxTokens: this.maxOutputTokens
899
917
  };
900
918
  let raw;
919
+ const ac = new AbortController();
901
920
  try {
902
- const ac = new AbortController();
903
- const res = await this.provider.complete(req, { signal: ac.signal });
921
+ const timeoutSignal = AbortSignal.timeout(3e4);
922
+ const res = await this.provider.complete(req, {
923
+ signal: AbortSignal.any([ac.signal, timeoutSignal])
924
+ });
904
925
  const textBlocks = res.content.filter(isTextBlock);
905
926
  raw = textBlocks.map((b) => b.text).join("\n").trim();
906
- } catch (_err) {
927
+ } catch (err) {
928
+ if (err instanceof Error) {
929
+ console.warn("[LLMSelector] selector call failed, using recency fallback:", err.message);
930
+ }
907
931
  return this.fallbackSelect(messages, effectiveBudget);
932
+ } finally {
933
+ ac.abort();
908
934
  }
909
- return this.parseSelectorOutput(raw, messages.length);
935
+ return this.parseSelectorOutput(raw, messages);
910
936
  }
911
937
  fallbackSelect(messages, budget) {
912
938
  const toKeep = [];
@@ -933,34 +959,63 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
933
959
  reasoning: `Fallback: kept last ${messages.length - startIdx} messages within ${budget} token budget`
934
960
  };
935
961
  }
936
- parseSelectorOutput(raw, messageCount) {
962
+ /**
963
+ * Parse and validate the raw LLM output into a SelectorResult.
964
+ * Falls back to recency-based selection if the LLM output is malformed,
965
+ * out-of-bounds, or internally inconsistent.
966
+ */
967
+ parseSelectorOutput(raw, messages) {
968
+ const messageCount = messages.length;
969
+ if (messageCount === 0) {
970
+ return { kept: [], collapsed: [], reasoning: "empty session" };
971
+ }
937
972
  const jsonStart = raw.indexOf("{");
938
973
  const jsonEnd = raw.lastIndexOf("}");
939
974
  if (jsonStart === -1 || jsonEnd === -1) {
940
- return this.fallbackSelect(
941
- Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
942
- this.maxContextTokens
943
- );
975
+ return this.fallbackSelect(messages, this.maxContextTokens);
944
976
  }
945
977
  let parsed;
946
978
  try {
947
979
  parsed = JSON.parse(raw.slice(jsonStart, jsonEnd + 1));
948
980
  } catch {
949
- return this.fallbackSelect(
950
- Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
951
- this.maxContextTokens
952
- );
981
+ return this.fallbackSelect(messages, this.maxContextTokens);
953
982
  }
954
983
  const obj = parsed;
955
- const kept = obj.kept ?? [];
956
- const collapsed = obj.collapsed ?? [];
957
- return {
958
- kept: kept.map((k) => ({
984
+ const keptRaw = obj.kept ?? [];
985
+ const collapsedRaw = obj.collapsed ?? [];
986
+ const kept = [];
987
+ for (const k of keptRaw) {
988
+ if (typeof k.from !== "number" || typeof k.to !== "number" || k.from < 0 || k.to >= messageCount || k.from > k.to) {
989
+ return this.fallbackSelect(messages, this.maxContextTokens);
990
+ }
991
+ kept.push({
959
992
  from: k.from,
960
993
  to: k.to,
961
994
  importance: k.importance ?? "medium"
962
- })),
963
- collapsed: collapsed.map((c) => ({ from: c.from, to: c.to, summary: c.summary })),
995
+ });
996
+ }
997
+ const collapsed = [];
998
+ for (const c of collapsedRaw) {
999
+ if (typeof c.from !== "number" || typeof c.to !== "number" || c.from < 0 || c.to >= messageCount || c.from > c.to) {
1000
+ return this.fallbackSelect(messages, this.maxContextTokens);
1001
+ }
1002
+ collapsed.push({ from: c.from, to: c.to, summary: c.summary });
1003
+ }
1004
+ const allRanges = [...kept, ...collapsed];
1005
+ for (let i = 0; i < allRanges.length; i++) {
1006
+ const a = allRanges[i];
1007
+ if (!a) continue;
1008
+ for (let j = i + 1; j < allRanges.length; j++) {
1009
+ const b = allRanges[j];
1010
+ if (!b) continue;
1011
+ if (a.from <= b.to && a.to >= b.from) {
1012
+ return this.fallbackSelect(messages, this.maxContextTokens);
1013
+ }
1014
+ }
1015
+ }
1016
+ return {
1017
+ kept,
1018
+ collapsed,
964
1019
  reasoning: typeof obj.reasoning === "string" ? obj.reasoning : ""
965
1020
  };
966
1021
  }
@@ -980,7 +1035,7 @@ var SelectiveCompactor = class {
980
1035
  summarizerPrompt;
981
1036
  constructor(opts) {
982
1037
  this.provider = opts.provider;
983
- this.selector = opts.selector ?? new LLMSelector({ provider: opts.provider, model: opts.selectorModel });
1038
+ this.selector = opts.selector ?? new LLMSelector({ provider: opts.provider, model: opts.selectorModel, maxOutputTokens: opts.selectorMaxOutputTokens });
984
1039
  this.warnThreshold = opts.warnThreshold ?? 0.6;
985
1040
  this.softThreshold = opts.softThreshold ?? 0.75;
986
1041
  this.hardThreshold = opts.hardThreshold ?? 0.9;
@@ -988,6 +1043,11 @@ var SelectiveCompactor = class {
988
1043
  this.preserveK = opts.preserveK ?? 4;
989
1044
  this.eliseThreshold = opts.eliseThreshold ?? 500;
990
1045
  this.summarizerModel = opts.summarizerModel ?? opts.selectorModel ?? "unknown";
1046
+ if (this.summarizerModel === "unknown" && (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1")) {
1047
+ console.warn(
1048
+ "[SelectiveCompactor] summarizerModel not set \u2014 will use provider default. Set `summarizerModel` explicitly to silence this warning."
1049
+ );
1050
+ }
991
1051
  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.";
992
1052
  }
993
1053
  async compact(ctx, opts = {}) {
@@ -1099,8 +1159,9 @@ Summarize the following message range:`;
1099
1159
  maxTokens: 512
1100
1160
  };
1101
1161
  try {
1162
+ const timeoutSignal = AbortSignal.timeout(3e4);
1102
1163
  const res = await this.provider.complete(req, {
1103
- signal: ctx.signal ?? new AbortController().signal
1164
+ signal: AbortSignal.any([ctx.signal, timeoutSignal])
1104
1165
  });
1105
1166
  return res.content.filter(isTextBlock).map((b) => b.text).join("\n").trim() || "(empty)";
1106
1167
  } catch {
@@ -1234,6 +1295,7 @@ var ProviderBackedCompactor = class {
1234
1295
  return new SelectiveCompactor({
1235
1296
  ...common,
1236
1297
  selectorModel: this.opts.summarizerModel,
1298
+ selectorMaxOutputTokens: this.opts.selectorMaxOutputTokens,
1237
1299
  summarizerModel: this.opts.summarizerModel
1238
1300
  });
1239
1301
  }
@@ -1374,7 +1436,8 @@ function resolveWstackPaths(opts) {
1374
1436
  projectSddSession: path2.join(projectDir, "sdd-session.json"),
1375
1437
  projectPlan: path2.join(projectDir, "plan.json"),
1376
1438
  projectAutophase: path2.join(projectDir, "autophase"),
1377
- syncConfig: path2.join(globalRoot, "sync.json")
1439
+ syncConfig: path2.join(globalRoot, "sync.json"),
1440
+ projectStatus: (projectHash2) => path2.join(globalRoot, "projects", projectHash2, "status.json")
1378
1441
  };
1379
1442
  }
1380
1443
 
@@ -3592,6 +3655,7 @@ ${recentJournal}` : ""
3592
3655
 
3593
3656
  // src/coordination/subagent-budget.ts
3594
3657
  var TIMEOUT_PREEMPT_FRACTION = 0.85;
3658
+ var DECISION_TIMEOUT_MS = 6e4;
3595
3659
  var BudgetExceededError = class extends Error {
3596
3660
  kind;
3597
3661
  limit;
@@ -3621,6 +3685,31 @@ var BudgetThresholdSignal = class extends Error {
3621
3685
  };
3622
3686
  var SubagentBudget = class _SubagentBudget {
3623
3687
  limits;
3688
+ /** Patch one or more budget limits in-place after construction.
3689
+ * Used by the coordinator watchdog when granting an extension.
3690
+ * All fields are optional — only provided fields are updated.
3691
+ * This is the single write path for limit mutations so that future
3692
+ * validation or side-effects live in one place (M1). */
3693
+ patchLimits(ext) {
3694
+ if (ext.maxIterations !== void 0) {
3695
+ this.limits.maxIterations = ext.maxIterations;
3696
+ }
3697
+ if (ext.maxToolCalls !== void 0) {
3698
+ this.limits.maxToolCalls = ext.maxToolCalls;
3699
+ }
3700
+ if (ext.maxTokens !== void 0) {
3701
+ this.limits.maxTokens = ext.maxTokens;
3702
+ }
3703
+ if (ext.maxCostUsd !== void 0) {
3704
+ this.limits.maxCostUsd = ext.maxCostUsd;
3705
+ }
3706
+ if (ext.timeoutMs !== void 0) {
3707
+ this.limits.timeoutMs = ext.timeoutMs;
3708
+ }
3709
+ if (ext.idleTimeoutMs !== void 0) {
3710
+ this.limits.idleTimeoutMs = ext.idleTimeoutMs;
3711
+ }
3712
+ }
3624
3713
  iterations = 0;
3625
3714
  toolCalls = 0;
3626
3715
  tokenInput = 0;
@@ -3641,12 +3730,44 @@ var SubagentBudget = class _SubagentBudget {
3641
3730
  * or hung listener (Director not built / event filter detached mid-run)
3642
3731
  * leaves the budget over-limit and never enforces anything.
3643
3732
  */
3644
- static DECISION_TIMEOUT_MS = 6e4;
3733
+ static DECISION_TIMEOUT_MS = DECISION_TIMEOUT_MS;
3645
3734
  /**
3646
3735
  * Injected by the runner when wiring the budget to its EventBus.
3647
3736
  * Used to emit `budget.threshold_reached` events in `'auto'` mode.
3648
3737
  */
3649
3738
  _events;
3739
+ /**
3740
+ * Guard against dual-path races between the coordinator watchdog
3741
+ * (`executeWithTimeout`) and the budget's own `checkTimeout()`.
3742
+ * Both paths detect `elapsed >= timeoutMs` and can emit
3743
+ * `budget.threshold_reached` for kind `'timeout'` simultaneously.
3744
+ * Set to the current `timeoutMs` ceiling by the coordinator BEFORE
3745
+ * calling `onThreshold`, and cleared after the negotiation resolves.
3746
+ * `checkTimeout()` skips its wall-clock check while this is set so
3747
+ * the coordinator's watchdog is the sole source of wall-clock timeout
3748
+ * events — `checkTimeout()` focuses exclusively on `idle_timeout`.
3749
+ */
3750
+ _watchdogActive;
3751
+ /** Returns the timeout ceiling currently being negotiated by the watchdog,
3752
+ * or `undefined` when no wall-clock negotiation is in flight.
3753
+ * Used by `executeWithTimeout` to detect a stale lock (M3). */
3754
+ get watchdogActive() {
3755
+ return this._watchdogActive;
3756
+ }
3757
+ /** Called by the coordinator watchdog BEFORE calling `onThreshold` so that
3758
+ * `checkTimeout()` skips its wall-clock check for this ceiling. Prevents
3759
+ * the budget's own `checkTimeout()` from emitting a second
3760
+ * `budget.threshold_reached` event while the watchdog is already
3761
+ * negotiating the same wall-clock deadline (C1). */
3762
+ setWatchdogNegotiation(timeoutMs) {
3763
+ this._watchdogActive = timeoutMs;
3764
+ }
3765
+ /** Clears the watchdog guard after negotiation resolves. Called in the
3766
+ * `finally` block of both the pre-empt and deadline branches so it fires
3767
+ * on every exit path: grant, deny, throw, or error. */
3768
+ clearWatchdogNegotiation() {
3769
+ this._watchdogActive = void 0;
3770
+ }
3650
3771
  /**
3651
3772
  * Negotiation mode — controls whether a threshold hit tries to emit
3652
3773
  * `budget.threshold_reached` and wait for a coordinator decision, or
@@ -3747,7 +3868,8 @@ var SubagentBudget = class _SubagentBudget {
3747
3868
  if (this.limits.idleTimeoutMs !== void 0 && idle > this.limits.idleTimeoutMs) {
3748
3869
  exceeded.push({ kind: "idle_timeout", used: idle, limit: this.limits.idleTimeoutMs });
3749
3870
  }
3750
- if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs) {
3871
+ const wallOwnedByWatchdog = this._onThreshold !== void 0 && this._watchdogActive === this.limits.timeoutMs;
3872
+ if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs && !wallOwnedByWatchdog) {
3751
3873
  exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
3752
3874
  }
3753
3875
  }
@@ -3761,19 +3883,99 @@ var SubagentBudget = class _SubagentBudget {
3761
3883
  throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
3762
3884
  }
3763
3885
  const bus = this._events;
3764
- if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
3886
+ if (!bus) {
3765
3887
  const first2 = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
3766
3888
  throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
3767
3889
  }
3890
+ const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
3891
+ if (bus.hasListenerFor("budget.threshold_reached")) {
3892
+ for (const entry of exceeded) {
3893
+ if (this._pendingNegotiations.has(entry.kind)) continue;
3894
+ this._pendingNegotiations.set(entry.kind, this._negotiateExtension(entry));
3895
+ }
3896
+ const decision = this._pendingNegotiations.get(first.kind);
3897
+ if (!decision) throw new Error(`No pending negotiation for ${first.kind}`);
3898
+ throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
3899
+ }
3900
+ let hardStop = null;
3768
3901
  for (const entry of exceeded) {
3769
3902
  if (this._pendingNegotiations.has(entry.kind)) continue;
3770
- const decision2 = this._negotiateExtension(entry.kind, exceeded);
3771
- this._pendingNegotiations.set(entry.kind, decision2);
3903
+ const marker = Promise.resolve("stop");
3904
+ this._pendingNegotiations.set(entry.kind, marker);
3905
+ void marker.finally(() => this._pendingNegotiations.delete(entry.kind));
3906
+ const sync = this._invokeHandlerSync(entry);
3907
+ if (!sync) hardStop ??= new BudgetExceededError(entry.kind, entry.limit, entry.used);
3772
3908
  }
3773
- const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
3774
- const decision = this._pendingNegotiations.get(first.kind);
3775
- if (!decision) throw new Error(`No pending negotiation for ${first.kind}`);
3776
- throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
3909
+ if (hardStop) throw hardStop;
3910
+ return exceeded;
3911
+ }
3912
+ /**
3913
+ * Invoke `onThreshold` once for `entry` on the NO-LISTENER path and report
3914
+ * whether it decided synchronously. Returns `true` when the handler returned
3915
+ * a synchronous decision (already honored — an `extend` patched the limits),
3916
+ * or `false` when it returned a Promise (async; the caller hard-stops, since
3917
+ * there is no listener to resolve the negotiation). The handler is given the
3918
+ * full info shape (`requestDecision` plus direct `extend`/`deny`) so both
3919
+ * recording handlers and policy handlers work without a wired listener.
3920
+ */
3921
+ _invokeHandlerSync(entry) {
3922
+ const handler = this._onThreshold;
3923
+ if (!handler) return false;
3924
+ let extendArg;
3925
+ const result = handler({
3926
+ kind: entry.kind,
3927
+ used: entry.used,
3928
+ limit: entry.limit,
3929
+ requestDecision: () => this._busRequestDecision(entry),
3930
+ // Direct hooks for synchronous policy/recording handlers.
3931
+ extend: (extra) => {
3932
+ extendArg = extra;
3933
+ },
3934
+ deny: () => {
3935
+ }
3936
+ });
3937
+ if (result && typeof result.then === "function") return false;
3938
+ if (result === "throw") return false;
3939
+ if (result && typeof result === "object" && "extend" in result) {
3940
+ extendArg = result.extend;
3941
+ }
3942
+ if (extendArg) this.patchLimits(extendArg);
3943
+ return true;
3944
+ }
3945
+ /**
3946
+ * Emit `budget.threshold_reached` and resolve to the listener's verdict.
3947
+ * Resolves to `'stop'` immediately when there is no listener (or no bus) so
3948
+ * no negotiation can hang and no fallback timer leaks. Mirrors the
3949
+ * coordinator watchdog's own request path so both agree on the no-listener
3950
+ * default.
3951
+ */
3952
+ _busRequestDecision(entry) {
3953
+ const bus = this._events;
3954
+ if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
3955
+ return Promise.resolve("stop");
3956
+ }
3957
+ return new Promise((resolve2) => {
3958
+ let resolved = false;
3959
+ const respond = (d) => {
3960
+ if (resolved) return;
3961
+ resolved = true;
3962
+ clearTimeout(fallback);
3963
+ resolve2(d);
3964
+ };
3965
+ const fallback = setTimeout(() => respond("stop"), _SubagentBudget.DECISION_TIMEOUT_MS);
3966
+ bus.emit("budget.threshold_reached", {
3967
+ kind: entry.kind,
3968
+ used: entry.used,
3969
+ limit: entry.limit,
3970
+ timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
3971
+ // deny() wins over a same-dispatch extend(): a listener that both grants
3972
+ // and denies (or two listeners disagreeing) is resolved as a stop. The
3973
+ // grant is deferred a microtask so a synchronous deny in the same emit
3974
+ // pre-empts it; async grants still resolve normally.
3975
+ extend: (extra) => queueMicrotask(() => respond({ extend: extra })),
3976
+ deny: () => respond("stop")
3977
+ });
3978
+ });
3777
3979
  }
3778
3980
  /**
3779
3981
  * Per-kind in-flight negotiation Promises. Each budget kind can have its
@@ -3793,77 +3995,33 @@ var SubagentBudget = class _SubagentBudget {
3793
3995
  * `{ extend: {} }` — keep going without patching; next overrun fires
3794
3996
  * a fresh signal.
3795
3997
  */
3796
- async _negotiateExtension(kind, exceeded) {
3998
+ async _negotiateExtension(entry) {
3797
3999
  if (!this._onThreshold) {
3798
4000
  return "stop";
3799
4001
  }
3800
4002
  try {
3801
- const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
3802
4003
  const result = this._onThreshold({
3803
- kind: first.kind,
3804
- used: first.used,
3805
- limit: first.limit,
3806
- requestDecision: () => {
3807
- const bus = this._events;
3808
- if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
3809
- return Promise.resolve("stop");
3810
- }
3811
- return new Promise((resolve2) => {
3812
- let resolved = false;
3813
- const respond = (d) => {
3814
- if (resolved) return;
3815
- resolved = true;
3816
- resolve2(d);
3817
- };
3818
- const fallback = setTimeout(
3819
- () => respond("stop"),
3820
- _SubagentBudget.DECISION_TIMEOUT_MS
3821
- );
3822
- for (const { kind: kind2, used, limit } of exceeded) {
3823
- bus.emit("budget.threshold_reached", {
3824
- kind: kind2,
3825
- used,
3826
- limit,
3827
- timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
3828
- extend: (extra) => {
3829
- clearTimeout(fallback);
3830
- respond({ extend: extra });
3831
- },
3832
- deny: () => {
3833
- clearTimeout(fallback);
3834
- respond("stop");
3835
- }
3836
- });
3837
- }
3838
- });
4004
+ kind: entry.kind,
4005
+ used: entry.used,
4006
+ limit: entry.limit,
4007
+ // One event for THIS kind only — each exceeded kind has its own
4008
+ // negotiation (and its own resolve), so there is no cross-kind
4009
+ // first-wins drop and no O(N^2) re-emission.
4010
+ requestDecision: () => this._busRequestDecision(entry),
4011
+ extend: (extra) => {
4012
+ this.patchLimits(extra);
4013
+ },
4014
+ deny: () => {
3839
4015
  }
3840
4016
  });
3841
4017
  if (result === "throw") return "stop";
3842
4018
  if (result === "continue") return { extend: {} };
3843
4019
  const decision = await result;
3844
4020
  if (decision === "stop") return "stop";
3845
- const ext = decision.extend;
3846
- if (ext.maxIterations !== void 0) {
3847
- this.limits.maxIterations = ext.maxIterations;
3848
- }
3849
- if (ext.maxToolCalls !== void 0) {
3850
- this.limits.maxToolCalls = ext.maxToolCalls;
3851
- }
3852
- if (ext.maxTokens !== void 0) {
3853
- this.limits.maxTokens = ext.maxTokens;
3854
- }
3855
- if (ext.maxCostUsd !== void 0) {
3856
- this.limits.maxCostUsd = ext.maxCostUsd;
3857
- }
3858
- if (ext.timeoutMs !== void 0) {
3859
- this.limits.timeoutMs = ext.timeoutMs;
3860
- }
3861
- if (ext.idleTimeoutMs !== void 0) {
3862
- this.limits.idleTimeoutMs = ext.idleTimeoutMs;
3863
- }
4021
+ this.patchLimits(decision.extend);
3864
4022
  return decision;
3865
4023
  } finally {
3866
- this._pendingNegotiations.delete(kind);
4024
+ this._pendingNegotiations.delete(entry.kind);
3867
4025
  }
3868
4026
  }
3869
4027
  recordIteration() {
@@ -3906,7 +4064,8 @@ var SubagentBudget = class _SubagentBudget {
3906
4064
  const { timeoutMs, idleTimeoutMs } = this.limits;
3907
4065
  if (timeoutMs === void 0 && idleTimeoutMs === void 0) return;
3908
4066
  const elapsed = Date.now() - this.startTime;
3909
- const wallTripped = timeoutMs !== void 0 && elapsed > timeoutMs;
4067
+ const wallSkipped = this._onThreshold !== void 0 && this._watchdogActive !== void 0 && timeoutMs !== void 0 && this._watchdogActive === timeoutMs;
4068
+ const wallTripped = wallSkipped ? false : timeoutMs !== void 0 && elapsed > timeoutMs;
3910
4069
  const idleTripped = idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs;
3911
4070
  if (!wallTripped && !idleTripped) return;
3912
4071
  void this.checkLimits(elapsed);
@@ -7225,6 +7384,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7225
7384
  terminating = /* @__PURE__ */ new Set();
7226
7385
  constructor(config, options = {}) {
7227
7386
  super();
7387
+ this.setMaxListeners(0);
7228
7388
  this.coordinatorId = config.coordinatorId;
7229
7389
  this.config = config;
7230
7390
  this.runner = options.runner;
@@ -7619,7 +7779,13 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7619
7779
  let result;
7620
7780
  budget.start();
7621
7781
  try {
7622
- const outcome = await this.executeWithTimeout(this.runner, task, runCtx, budget);
7782
+ const outcome = await this.executeWithTimeout(
7783
+ this.runner,
7784
+ task,
7785
+ runCtx,
7786
+ budget,
7787
+ subagent.config.preemptFraction
7788
+ );
7623
7789
  result = {
7624
7790
  subagentId,
7625
7791
  taskId: task.id,
@@ -7646,7 +7812,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7646
7812
  }
7647
7813
  this.recordCompletion(result);
7648
7814
  }
7649
- async executeWithTimeout(runner, task, ctx, budget) {
7815
+ async executeWithTimeout(runner, task, ctx, budget, preemptFraction = TIMEOUT_PREEMPT_FRACTION) {
7650
7816
  const initialTimeoutMs = budget.limits.timeoutMs;
7651
7817
  const idleLimitMs = budget.limits.idleTimeoutMs;
7652
7818
  if (initialTimeoutMs === void 0 && idleLimitMs === void 0) {
@@ -7654,8 +7820,21 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7654
7820
  }
7655
7821
  const start = Date.now();
7656
7822
  let timer = null;
7657
- let preemptedForLimit = null;
7823
+ let PreemptState;
7824
+ ((PreemptState2) => {
7825
+ PreemptState2["ACTIVE"] = "active";
7826
+ PreemptState2["LOCKED"] = "locked";
7827
+ })(PreemptState || (PreemptState = {}));
7828
+ let preemptedCeiling = null;
7829
+ let preemptState = "active" /* ACTIVE */;
7830
+ let lastGrantActivityTs = -1;
7658
7831
  const timeoutPromise = new Promise((_, reject) => {
7832
+ const terminate = (kind, limit, used) => {
7833
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
7834
+ reject(
7835
+ budget._events?.hasListenerFor("budget.threshold_reached") ? new Error(`subagent stopped: budget ${kind} (limit=${limit}, used=${used})`) : new BudgetExceededError(kind, limit, used)
7836
+ );
7837
+ };
7659
7838
  const armFor = (ms) => {
7660
7839
  if (timer) clearTimeout(timer);
7661
7840
  timer = setTimeout(onTick, Math.max(0, ms));
@@ -7664,7 +7843,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7664
7843
  const wallLimit = budget.limits.timeoutMs ?? initialTimeoutMs;
7665
7844
  const wallRemaining = initialTimeoutMs === void 0 ? Number.POSITIVE_INFINITY : wallLimit - (Date.now() - start);
7666
7845
  const idleRemaining = idleLimitMs === void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
7667
- const preemptRemaining = initialTimeoutMs === void 0 || preemptedForLimit === wallLimit ? Number.POSITIVE_INFINITY : wallLimit * TIMEOUT_PREEMPT_FRACTION - (Date.now() - start);
7846
+ const preemptRemaining = initialTimeoutMs === void 0 || preemptedCeiling === wallLimit ? Number.POSITIVE_INFINITY : wallLimit * preemptFraction - (Date.now() - start);
7668
7847
  armFor(Math.max(25, Math.min(wallRemaining, idleRemaining, preemptRemaining)));
7669
7848
  };
7670
7849
  const negotiateTimeout = async (used, limit) => {
@@ -7674,16 +7853,42 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7674
7853
  kind: "timeout",
7675
7854
  used,
7676
7855
  limit,
7677
- requestDecision: () => new Promise((resolveDecision) => {
7678
- budget._events?.emit("budget.threshold_reached", {
7679
- kind: "timeout",
7680
- used,
7681
- limit,
7682
- timeoutMs: 6e4,
7683
- extend: (extra) => resolveDecision({ extend: extra }),
7684
- deny: () => resolveDecision("stop")
7856
+ requestDecision: () => {
7857
+ if (!budget._events?.hasListenerFor("budget.threshold_reached")) {
7858
+ return Promise.resolve("stop");
7859
+ }
7860
+ return new Promise((resolveDecision) => {
7861
+ let settled = false;
7862
+ const resolve2 = (d) => {
7863
+ if (settled) return;
7864
+ settled = true;
7865
+ resolveDecision(d);
7866
+ };
7867
+ const fallback = setTimeout(() => resolve2("stop"), DECISION_TIMEOUT_MS);
7868
+ budget._events?.emit("budget.threshold_reached", {
7869
+ kind: "timeout",
7870
+ used,
7871
+ limit,
7872
+ // Informational: the budget's own decision deadline. Listeners may use
7873
+ // this to display a countdown. The coordinator does NOT enforce it —
7874
+ // it is the budget's own `setTimeout(fallback)` that races against
7875
+ // the listener's `extend()`/`deny()` call to guarantee progress.
7876
+ timeoutMs: DECISION_TIMEOUT_MS,
7877
+ // deny() wins over a same-dispatch extend(): defer the grant a
7878
+ // microtask so a synchronous deny in the same emit pre-empts it
7879
+ // (a listener that both grants and denies, or two listeners
7880
+ // disagreeing, resolves as a stop). Async grants still resolve.
7881
+ extend: (extra) => {
7882
+ clearTimeout(fallback);
7883
+ queueMicrotask(() => resolve2({ extend: extra }));
7884
+ },
7885
+ deny: () => {
7886
+ clearTimeout(fallback);
7887
+ resolve2("stop");
7888
+ }
7889
+ });
7685
7890
  });
7686
- })
7891
+ }
7687
7892
  });
7688
7893
  return typeof result === "string" ? result : await result;
7689
7894
  };
@@ -7694,21 +7899,45 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7694
7899
  const wallExceeded = wallLimit !== void 0 && elapsed >= wallLimit;
7695
7900
  const idleExceeded = idleLimit !== void 0 && budget.idleMs() >= idleLimit;
7696
7901
  if (idleExceeded && !wallExceeded) {
7902
+ budget._events?.emit("budget.threshold_reached", {
7903
+ kind: "idle_timeout",
7904
+ used: budget.idleMs(),
7905
+ limit: idleLimit ?? 0,
7906
+ timeoutMs: DECISION_TIMEOUT_MS,
7907
+ extend: () => {
7908
+ },
7909
+ deny: () => {
7910
+ }
7911
+ });
7697
7912
  this.subagents.get(ctx.subagentId)?.abortController.abort();
7698
- reject(new BudgetExceededError("timeout", idleLimit ?? 0, budget.idleMs()));
7913
+ reject(new BudgetExceededError("idle_timeout", idleLimit ?? 0, budget.idleMs()));
7699
7914
  return;
7700
7915
  }
7701
- if (wallLimit !== void 0 && !wallExceeded && budget.onThreshold && preemptedForLimit !== wallLimit && elapsed >= wallLimit * TIMEOUT_PREEMPT_FRACTION) {
7916
+ if (wallLimit !== void 0 && !wallExceeded && budget.onThreshold && preemptState === "active" /* ACTIVE */ && elapsed >= wallLimit * preemptFraction) {
7917
+ const activityTs = Date.now() - budget.idleMs();
7918
+ if (activityTs <= lastGrantActivityTs) {
7919
+ preemptState = "locked" /* LOCKED */;
7920
+ preemptedCeiling = wallLimit;
7921
+ scheduleNext();
7922
+ return;
7923
+ }
7924
+ budget.setWatchdogNegotiation(wallLimit);
7702
7925
  try {
7703
7926
  const decision = await negotiateTimeout(elapsed, wallLimit);
7704
7927
  if (typeof decision !== "string" && decision.extend.timeoutMs !== void 0) {
7705
- budget.limits.timeoutMs = decision.extend.timeoutMs;
7706
- preemptedForLimit = null;
7928
+ budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
7929
+ lastGrantActivityTs = Date.now() - budget.idleMs();
7930
+ preemptState = "active" /* ACTIVE */;
7931
+ preemptedCeiling = null;
7707
7932
  } else {
7708
- preemptedForLimit = wallLimit;
7933
+ preemptState = "locked" /* LOCKED */;
7934
+ preemptedCeiling = wallLimit;
7709
7935
  }
7710
7936
  } catch {
7711
- preemptedForLimit = wallLimit;
7937
+ preemptState = "locked" /* LOCKED */;
7938
+ preemptedCeiling = wallLimit;
7939
+ } finally {
7940
+ budget.clearWatchdogNegotiation();
7712
7941
  }
7713
7942
  scheduleNext();
7714
7943
  return;
@@ -7723,26 +7952,41 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7723
7952
  reject(new BudgetExceededError("timeout", limit, elapsed));
7724
7953
  return;
7725
7954
  }
7955
+ budget.setWatchdogNegotiation(limit);
7726
7956
  try {
7727
7957
  const decision = await negotiateTimeout(elapsed, limit);
7728
- if (decision === "continue" || decision === "throw" || decision === "stop") {
7729
- preemptedForLimit = null;
7958
+ if (decision === "throw") {
7959
+ terminate("timeout", limit, elapsed);
7960
+ return;
7961
+ }
7962
+ if (decision === "continue") {
7963
+ preemptState = "locked" /* LOCKED */;
7964
+ preemptedCeiling = wallLimit;
7730
7965
  armFor(Math.max(1e3, limit));
7731
7966
  return;
7732
7967
  }
7968
+ if (decision === "stop") {
7969
+ terminate("timeout", limit, elapsed);
7970
+ return;
7971
+ }
7733
7972
  if (decision.extend.timeoutMs !== void 0) {
7734
- budget.limits.timeoutMs = decision.extend.timeoutMs;
7735
- preemptedForLimit = null;
7973
+ budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
7974
+ lastGrantActivityTs = Date.now() - budget.idleMs();
7975
+ preemptState = "active" /* ACTIVE */;
7976
+ preemptedCeiling = null;
7736
7977
  scheduleNext();
7737
7978
  return;
7738
7979
  }
7739
- this.subagents.get(ctx.subagentId)?.abortController.abort();
7740
- reject(new BudgetExceededError("timeout", limit, elapsed));
7980
+ terminate("timeout", limit, elapsed);
7981
+ return;
7741
7982
  } catch (err) {
7742
7983
  this.subagents.get(ctx.subagentId)?.abortController.abort();
7743
7984
  reject(
7744
7985
  err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
7745
7986
  );
7987
+ return;
7988
+ } finally {
7989
+ budget.clearWatchdogNegotiation();
7746
7990
  }
7747
7991
  };
7748
7992
  scheduleNext();
@@ -8620,12 +8864,12 @@ var DefaultSkillLoader = class {
8620
8864
  }
8621
8865
  async find(name) {
8622
8866
  const all = await this.list();
8623
- return all.find((s) => s.name === name);
8867
+ const lower = name.toLowerCase();
8868
+ return all.find((s) => s.name.toLowerCase() === lower);
8624
8869
  }
8625
8870
  async manifestText() {
8626
- const skills = await this.list();
8627
- if (skills.length === 0) return "";
8628
8871
  const entries = await this.listEntries();
8872
+ if (entries.length === 0) return "";
8629
8873
  const lines = ["## Available skills"];
8630
8874
  for (const e of entries) {
8631
8875
  const scopeTag = e.scope.length > 0 ? ` \u2014 ${e.scope.slice(0, 3).join(", ")}` : "";
@@ -8639,12 +8883,8 @@ var DefaultSkillLoader = class {
8639
8883
  const skills = await this.list();
8640
8884
  const entries = [];
8641
8885
  for (const s of skills) {
8642
- try {
8643
- const raw = await fs.readFile(s.path, "utf8");
8644
- const { trigger, scope } = parseDescription(raw);
8645
- entries.push({ name: s.name, trigger, scope, source: s.source, path: s.path });
8646
- } catch {
8647
- }
8886
+ const { trigger, scope } = parseDescriptionFromText(s.description ?? "");
8887
+ entries.push({ name: s.name, trigger, scope, source: s.source, path: s.path });
8648
8888
  }
8649
8889
  this.entriesCache = entries;
8650
8890
  return entries;
@@ -8655,16 +8895,17 @@ var DefaultSkillLoader = class {
8655
8895
  this.bodyCache.clear();
8656
8896
  }
8657
8897
  async readBody(name) {
8658
- const cached = this.bodyCache.get(name);
8898
+ const key = name.toLowerCase();
8899
+ const cached = this.bodyCache.get(key);
8659
8900
  if (cached !== void 0) return cached;
8660
8901
  const m = await this.find(name);
8661
8902
  if (!m) throw new Error(`Skill "${name}" not found`);
8662
8903
  const body = await fs.readFile(m.path, "utf8");
8663
- this.bodyCache.set(name, body);
8904
+ this.bodyCache.set(key, body);
8664
8905
  return body;
8665
8906
  }
8666
8907
  async readSaveBody(name) {
8667
- const key = `save:${name}`;
8908
+ const key = `save:${name.toLowerCase()}`;
8668
8909
  const cached = this.bodyCache.get(key);
8669
8910
  if (cached !== void 0) return cached;
8670
8911
  const m = await this.find(name);
@@ -8725,9 +8966,7 @@ function parseFrontmatter(raw) {
8725
8966
  flush();
8726
8967
  return out;
8727
8968
  }
8728
- function parseDescription(raw) {
8729
- const fm = parseFrontmatter(raw);
8730
- const desc = fm.description ?? "";
8969
+ function parseDescriptionFromText(desc) {
8731
8970
  const firstSentenceEnd = desc.indexOf(". ");
8732
8971
  const trigger = firstSentenceEnd !== -1 ? desc.slice(0, firstSentenceEnd + 1).trim() : desc.trim().split("\n")[0] ?? "";
8733
8972
  const scope = [];