@wrongstack/core 0.260.0 → 0.265.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/{agent-bridge-BbskZ7HH.d.ts → agent-bridge-DrkBxszZ.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-BNIGZx18.d.ts → agent-subagent-runner-DM2pP-B6.d.ts} +116 -12
  3. package/dist/{brain-C2yDd7Lw.d.ts → brain-BXd_61kQ.d.ts} +32 -3
  4. package/dist/{compactor-t0R_AIt_.d.ts → compactor-B8pOf45Y.d.ts} +1 -1
  5. package/dist/{config-FG6As4H5.d.ts → config-BMCj_XDs.d.ts} +86 -12
  6. package/dist/{context-JFOVvu6z.d.ts → context-MRk5PhNv.d.ts} +26 -1
  7. package/dist/coordination/index.d.ts +1737 -15
  8. package/dist/coordination/index.js +3152 -494
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/{default-config-CXsDvOmP.d.ts → default-config-B0cj-Hry.d.ts} +11 -1
  11. package/dist/defaults/index.d.ts +28 -28
  12. package/dist/defaults/index.js +1804 -1363
  13. package/dist/defaults/index.js.map +1 -1
  14. package/dist/dispatcher-types.d-BBeXBQgS.d.ts +66 -0
  15. package/dist/execution/index.d.ts +16 -16
  16. package/dist/execution/index.js +933 -672
  17. package/dist/execution/index.js.map +1 -1
  18. package/dist/execution/prompt-enhancer.d.ts +1 -1
  19. package/dist/execution/prompt-enhancer.js +7 -1
  20. package/dist/execution/prompt-enhancer.js.map +1 -1
  21. package/dist/extension/index.d.ts +6 -6
  22. package/dist/extension/index.js.map +1 -1
  23. package/dist/{goal-preamble-B1IXJtLX.d.ts → goal-preamble-DvHDSKSe.d.ts} +26 -10
  24. package/dist/{goal-store-CPXz6Mml.d.ts → goal-store-DtLMySNb.d.ts} +1 -1
  25. package/dist/{index-CebbJB94.d.ts → index-B-ch8K9C.d.ts} +8 -8
  26. package/dist/{index-BPcg4N3M.d.ts → index-CEDeNodM.d.ts} +5 -5
  27. package/dist/index.d.ts +189 -104
  28. package/dist/index.js +24693 -21162
  29. package/dist/index.js.map +1 -1
  30. package/dist/infrastructure/index.d.ts +6 -6
  31. package/dist/infrastructure/index.js +12 -8
  32. package/dist/infrastructure/index.js.map +1 -1
  33. package/dist/kernel/index.d.ts +9 -9
  34. package/dist/kernel/index.js +7 -2
  35. package/dist/kernel/index.js.map +1 -1
  36. package/dist/{llm-selector-DXxI2tlu.d.ts → llm-selector-C0tfTCUe.d.ts} +14 -2
  37. package/dist/{mcp-servers-OwNHo43-.d.ts → mcp-servers-2x4w6Jn9.d.ts} +3 -3
  38. package/dist/models/index.d.ts +5 -5
  39. package/dist/models/index.js +80 -31
  40. package/dist/models/index.js.map +1 -1
  41. package/dist/{models-registry-Djlmq4uB.d.ts → models-registry-DmJlKuNp.d.ts} +1 -1
  42. package/dist/{multi-agent-coordinator-CEmrSCMJ.d.ts → multi-agent-coordinator-DyCkCZnU.d.ts} +2 -2
  43. package/dist/{null-fleet-bus-DT92xqgJ.d.ts → null-fleet-bus-CG9QY2aP.d.ts} +6 -6
  44. package/dist/observability/index.d.ts +2 -2
  45. package/dist/observability/index.js +8 -3
  46. package/dist/observability/index.js.map +1 -1
  47. package/dist/{parallel-eternal-engine-0SItuq5r.d.ts → parallel-eternal-engine-Jw9uhEoT.d.ts} +9 -9
  48. package/dist/{path-resolver-DKBh6Jlo.d.ts → path-resolver-Dy2ej-gE.d.ts} +3 -3
  49. package/dist/{permission-BJ7eO9Vl.d.ts → permission-B9SB45lp.d.ts} +1 -1
  50. package/dist/{permission-policy-DEXOfnpm.d.ts → permission-policy-CkjSXabK.d.ts} +2 -2
  51. package/dist/{pipeline-zflkI2dp.d.ts → pipeline-DPDxH_7m.d.ts} +59 -4
  52. package/dist/{plan-templates-BFXyRkEK.d.ts → plan-templates-CzD9GnAU.d.ts} +32 -8
  53. package/dist/{provider-runner-BC-uywtT.d.ts → provider-runner-DMa70ODu.d.ts} +3 -3
  54. package/dist/{retry-policy-Cavrzmtk.d.ts → retry-policy-CN0khdlj.d.ts} +1 -1
  55. package/dist/sdd/index.d.ts +8 -8
  56. package/dist/sdd/index.js +313 -122
  57. package/dist/sdd/index.js.map +1 -1
  58. package/dist/{secret-vault-CDvDYXWX.d.ts → secret-vault-B2yw84VT.d.ts} +43 -4
  59. package/dist/secret-vault-BAKpgFw_.d.ts +57 -0
  60. package/dist/security/index.d.ts +5 -5
  61. package/dist/security/index.js +411 -225
  62. package/dist/security/index.js.map +1 -1
  63. package/dist/{selector-B7AivHsu.d.ts → selector-CzHh_igB.d.ts} +1 -1
  64. package/dist/{session-event-bridge-BmIDxdJd.d.ts → session-event-bridge-BUI6Jf-4.d.ts} +8 -2
  65. package/dist/{session-reader-DtofsB-2.d.ts → session-reader-CMgdMSRP.d.ts} +1 -1
  66. package/dist/skills/index.js +67 -64
  67. package/dist/skills/index.js.map +1 -1
  68. package/dist/storage/index.d.ts +132 -16
  69. package/dist/storage/index.js +851 -432
  70. package/dist/storage/index.js.map +1 -1
  71. package/dist/tools/index.d.ts +57 -0
  72. package/dist/tools/index.js +411 -0
  73. package/dist/tools/index.js.map +1 -0
  74. package/dist/types/index.d.ts +21 -21
  75. package/dist/types/index.js +928 -711
  76. package/dist/types/index.js.map +1 -1
  77. package/dist/utils/error.d.ts +7 -0
  78. package/dist/utils/error.js +8 -0
  79. package/dist/utils/error.js.map +1 -0
  80. package/dist/utils/index.d.ts +8 -68
  81. package/dist/utils/index.js +20 -10
  82. package/dist/utils/index.js.map +1 -1
  83. package/dist/{wstack-paths-CJjEwPXn.d.ts → wstack-paths-hOpNLmvf.d.ts} +2 -0
  84. package/package.json +5 -1
  85. package/skills/api-design/SKILL.md +1 -1
  86. package/skills/audit-log/SKILL.md +6 -6
  87. package/skills/bug-hunter/SKILL.md +5 -5
  88. package/skills/chimera/SKILL.md +4 -4
  89. package/skills/docker-deploy/SKILL.md +1 -1
  90. package/skills/git-flow/SKILL.md +3 -3
  91. package/skills/multi-agent/SKILL.md +3 -3
  92. package/skills/node-modern/SKILL.md +1 -0
  93. package/skills/observability/SKILL.md +2 -2
  94. package/skills/output-standards/SKILL.md +51 -28
  95. package/skills/refactor-planner/SKILL.md +3 -3
  96. package/skills/security-scanner/SKILL.md +4 -3
  97. package/skills/tech-stack/SKILL.md +1 -2
  98. package/dist/package-outdated-watcher-C70ag2G9.d.ts +0 -581
  99. package/dist/secret-vault-BJDY28ev.d.ts +0 -25
@@ -1,9 +1,9 @@
1
- import { execFile } from 'child_process';
2
- import { promisify } from 'util';
3
- import * as fs from 'fs/promises';
4
1
  import { randomUUID, randomBytes, createHash } from 'crypto';
2
+ import * as fs from 'fs/promises';
5
3
  import * as path2 from 'path';
6
4
  import * as os from 'os';
5
+ import { execFile } from 'child_process';
6
+ import { promisify } from 'util';
7
7
  import { EventEmitter } from 'events';
8
8
 
9
9
  // src/utils/token-estimate.ts
@@ -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
  }
@@ -1250,168 +1312,503 @@ function readPolicy(ctx) {
1250
1312
  if (typeof candidate.preserveK !== "number" || !candidate.thresholds) return null;
1251
1313
  return candidate;
1252
1314
  }
1253
-
1254
- // src/types/errors.ts
1255
- var ERROR_CODES = {
1256
- // Provider
1257
- PROVIDER_RATE_LIMITED: "PROVIDER_RATE_LIMITED",
1258
- PROVIDER_AUTH_FAILED: "PROVIDER_AUTH_FAILED",
1259
- PROVIDER_OVERLOADED: "PROVIDER_OVERLOADED",
1260
- PROVIDER_INVALID_REQUEST: "PROVIDER_INVALID_REQUEST",
1261
- PROVIDER_SERVER_ERROR: "PROVIDER_SERVER_ERROR",
1262
- PROVIDER_NETWORK_ERROR: "PROVIDER_NETWORK_ERROR",
1263
- // Agent
1264
- AGENT_ITERATION_LIMIT: "AGENT_ITERATION_LIMIT",
1265
- AGENT_CONTEXT_OVERFLOW: "AGENT_CONTEXT_OVERFLOW",
1266
- AGENT_ABORTED: "AGENT_ABORTED",
1267
- AGENT_RUN_FAILED: "AGENT_RUN_FAILED",
1268
- // File system
1269
- FS_READ_FAILED: "FS_READ_FAILED",
1270
- FS_ATOMIC_WRITE_FAILED: "FS_ATOMIC_WRITE_FAILED"};
1271
- var WrongStackError = class extends Error {
1272
- code;
1273
- subsystem;
1274
- severity;
1275
- recoverable;
1276
- context;
1277
- constructor(opts) {
1278
- super(opts.message, { cause: opts.cause });
1279
- this.name = "WrongStackError";
1280
- this.code = opts.code;
1281
- this.subsystem = opts.subsystem;
1282
- this.severity = opts.severity ?? "error";
1283
- this.recoverable = opts.recoverable ?? false;
1284
- this.context = opts.context;
1285
- }
1286
- /**
1287
- * Render a one-line user-facing description.
1288
- * Subclasses should override for domain-specific formatting.
1289
- */
1290
- describe() {
1291
- const ctx = this.context ? ` ${formatContext(this.context)}` : "";
1292
- return `${this.code}: ${this.message}${ctx}`;
1315
+ async function atomicWrite(targetPath, content, opts = {}) {
1316
+ const dir = path2.dirname(targetPath);
1317
+ await fs.mkdir(dir, { recursive: true });
1318
+ const tmp = path2.join(dir, `.${path2.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
1319
+ try {
1320
+ if (typeof content === "string") {
1321
+ await fs.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
1322
+ } else {
1323
+ await fs.writeFile(tmp, content, { flag: "wx" });
1324
+ }
1325
+ try {
1326
+ const fh = await fs.open(tmp, "r+");
1327
+ try {
1328
+ await fh.sync();
1329
+ } finally {
1330
+ await fh.close();
1331
+ }
1332
+ } catch {
1333
+ }
1334
+ let mode;
1335
+ try {
1336
+ const stat2 = await fs.stat(targetPath);
1337
+ mode = stat2.mode & 511;
1338
+ } catch {
1339
+ mode = opts.mode;
1340
+ }
1341
+ if (mode !== void 0) {
1342
+ await fs.chmod(tmp, mode);
1343
+ }
1344
+ await renameWithRetry(tmp, targetPath);
1345
+ } catch (err) {
1346
+ try {
1347
+ await fs.unlink(tmp);
1348
+ } catch {
1349
+ }
1350
+ throw err;
1293
1351
  }
1294
- };
1295
- function formatContext(ctx) {
1296
- const parts = Object.entries(ctx).filter(([, v]) => v !== void 0).slice(0, 3).map(([k, v]) => `${k}=${String(v)}`);
1297
- return parts.length > 0 ? `[${parts.join(" ")}]` : "";
1298
1352
  }
1299
- var AgentError = class extends WrongStackError {
1300
- constructor(opts) {
1301
- super({
1302
- message: opts.message,
1303
- code: opts.code,
1304
- subsystem: "agent",
1305
- severity: opts.code === ERROR_CODES.AGENT_ABORTED ? "warning" : "error",
1306
- recoverable: opts.recoverable ?? opts.code === ERROR_CODES.AGENT_ITERATION_LIMIT,
1307
- context: opts.context,
1308
- cause: opts.cause
1309
- });
1310
- this.name = "AgentError";
1353
+ var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
1354
+ async function renameWithRetry(from, to) {
1355
+ if (process.platform !== "win32") {
1356
+ await fs.rename(from, to);
1357
+ return;
1311
1358
  }
1312
- };
1313
- function toWrongStackError(err, code = ERROR_CODES.AGENT_RUN_FAILED) {
1314
- if (err instanceof WrongStackError) return err;
1315
- const message = err instanceof Error ? err.message : String(err);
1316
- return new AgentError({
1317
- message,
1318
- code: code === "UNKNOWN" ? ERROR_CODES.AGENT_RUN_FAILED : code,
1319
- cause: err
1320
- });
1321
- }
1322
- var FsError = class extends WrongStackError {
1323
- path;
1324
- constructor(opts) {
1325
- super({
1326
- message: opts.message,
1327
- code: opts.code,
1328
- subsystem: "fs",
1329
- severity: "error",
1330
- recoverable: opts.code !== ERROR_CODES.FS_READ_FAILED,
1331
- context: { path: opts.path, ...opts.context },
1332
- cause: opts.cause
1333
- });
1334
- this.name = "FsError";
1335
- this.path = opts.path;
1359
+ const delays = [10, 25, 60, 120, 250];
1360
+ let lastErr;
1361
+ for (let i = 0; i <= delays.length; i++) {
1362
+ try {
1363
+ await fs.rename(from, to);
1364
+ return;
1365
+ } catch (err) {
1366
+ lastErr = err;
1367
+ const code = err?.code;
1368
+ if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
1369
+ throw err;
1370
+ }
1371
+ await new Promise((resolve2) => setTimeout(resolve2, delays[i]));
1372
+ }
1336
1373
  }
1337
- };
1374
+ throw lastErr;
1375
+ }
1338
1376
 
1339
- // src/execution/auto-compaction-middleware.ts
1340
- var LEVEL_RANK = { warn: 0, soft: 1, hard: 2 };
1341
- var MAX_DIGEST_LOG_CHARS = 4e3;
1342
- function truncateDigest(digest) {
1343
- if (digest.length <= MAX_DIGEST_LOG_CHARS) return digest;
1344
- return `${digest.slice(0, MAX_DIGEST_LOG_CHARS)}\u2026 [+${digest.length - MAX_DIGEST_LOG_CHARS} chars; full turns in session log]`;
1377
+ // src/utils/error.ts
1378
+ function toErrorMessage(err) {
1379
+ return err instanceof Error ? err.message : String(err);
1345
1380
  }
1346
- var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
1347
- name = "AutoCompaction";
1348
- compactor;
1349
- /** Deprecated. Kept for backward compat with tests that pass simpleEstimator. */
1350
- _estimator;
1351
- warnThreshold;
1352
- softThreshold;
1353
- hardThreshold;
1354
- /** Writable so model-switch can update the denominator without re-registering the middleware. */
1355
- _maxContext;
1356
- aggressiveOn;
1357
- events;
1358
- failureMode;
1359
- policyProvider;
1360
- sessionBridge;
1361
- /**
1362
- * Once a compaction attempt reduces nothing (preserveK protects everything,
1363
- * no oversized tool_results remain to elide), retrying on every iteration
1364
- * just spams `compaction.fired` events without making progress. We remember
1365
- * the no-op and skip until either the pressure level escalates or context
1366
- * has grown by at least this many tokens since the failed attempt.
1367
- */
1368
- static NOOP_RETRY_DELTA_TOKENS = 2e3;
1369
- /** Tracks the most recent no-op attempt so we can avoid re-firing per turn. */
1370
- lastNoopAttempt = null;
1371
- /**
1372
- * Cached token estimate from the last handler() invocation. When the
1373
- * message count and tool count haven't changed since the last estimate
1374
- * (autonomous idle loops), we skip the expensive O(n) token estimation
1375
- * and reuse this value. Reset to -1 when the context changes.
1376
- */
1377
- _cachedTokens = -1;
1378
- _cachedMsgCount = -1;
1379
- _cachedToolCount = -1;
1380
- /**
1381
- * @param compactor Compactor to use for compaction.
1382
- * @param maxContext Provider's max context window in tokens.
1383
- * @param _estimator Deprecated parameter kept for backward compatibility.
1384
- * The middleware now uses `estimateRequestTokens` internally
1385
- * for accurate full-request token counting (messages +
1386
- * systemPrompt + toolDefs).
1387
- * @param thresholds Threshold fractions (0-1) of maxContext.
1388
- * @param opts Optional behavior. By default, failures at the
1389
- * hard threshold throw AGENT_CONTEXT_OVERFLOW so
1390
- * the agent does not continue into a likely
1391
- * provider context overflow. Warn/soft failures
1392
- * still emit compaction.failed and continue.
1393
- */
1394
- constructor(compactor, maxContext, _estimator, thresholds, optsOrAggressiveOn = {}, events) {
1395
- const opts = typeof optsOrAggressiveOn === "string" ? { aggressiveOn: optsOrAggressiveOn, events } : optsOrAggressiveOn;
1396
- this.compactor = compactor;
1397
- this._maxContext = maxContext;
1398
- this._estimator = _estimator;
1399
- this.warnThreshold = thresholds.warn;
1400
- this.softThreshold = thresholds.soft;
1401
- this.hardThreshold = thresholds.hard;
1402
- this.aggressiveOn = opts.aggressiveOn ?? "soft";
1403
- this.events = opts.events;
1404
- this.failureMode = opts.failureMode ?? "throw_on_hard";
1405
- this.policyProvider = opts.policyProvider;
1406
- this.sessionBridge = opts.sessionBridge;
1407
- }
1408
- /** Allow callers (e.g. model-switch in WebUI) to update the context window
1409
- * denominator when the active model changes. */
1410
- setMaxContext(maxContext) {
1411
- this._maxContext = maxContext;
1412
- }
1413
- handler() {
1381
+
1382
+ // src/utils/string.ts
1383
+ function truncate(s, max) {
1384
+ return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
1385
+ }
1386
+ function projectHash(absRoot) {
1387
+ return createHash("sha256").update(path2.resolve(absRoot)).digest("hex").slice(0, 12);
1388
+ }
1389
+ function projectSlug(absRoot) {
1390
+ const base = slugify(path2.basename(absRoot));
1391
+ const hash = createHash("sha256").update(path2.resolve(absRoot)).digest("hex").slice(0, 6);
1392
+ return `${base}-${hash}`;
1393
+ }
1394
+ function slugify(name) {
1395
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
1396
+ }
1397
+ function wstackGlobalRoot() {
1398
+ const fromEnv = process.env["WRONGSTACK_HOME"];
1399
+ if (fromEnv && fromEnv.trim().length > 0) return path2.resolve(fromEnv);
1400
+ return path2.join(os.homedir(), ".wrongstack");
1401
+ }
1402
+ function resolveWstackPaths(opts) {
1403
+ const globalRoot = opts.globalRoot ?? (opts.userHome ? path2.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
1404
+ const hash = projectHash(opts.projectRoot);
1405
+ const slug = projectSlug(opts.projectRoot);
1406
+ const projectDir = path2.join(globalRoot, "projects", slug);
1407
+ return {
1408
+ globalRoot,
1409
+ configDir: globalRoot,
1410
+ globalConfig: path2.join(globalRoot, "config.json"),
1411
+ secretsKey: path2.join(globalRoot, ".key"),
1412
+ globalMemory: path2.join(globalRoot, "memory.md"),
1413
+ globalSkills: path2.join(globalRoot, "skills"),
1414
+ globalPrompts: path2.join(globalRoot, "prompts"),
1415
+ cacheDir: path2.join(globalRoot, "cache"),
1416
+ modelsCache: path2.join(globalRoot, "cache", "models.dev.json"),
1417
+ modelsOverlayCache: path2.join(globalRoot, "cache", "models-overlay.json"),
1418
+ historyFile: path2.join(globalRoot, "history"),
1419
+ logFile: path2.join(globalRoot, "logs", "wrongstack.log"),
1420
+ projectDir,
1421
+ projectCodebaseIndex: path2.join(projectDir, "codebase-index"),
1422
+ projectMemory: path2.join(projectDir, "memory.md"),
1423
+ projectSessions: path2.join(projectDir, "sessions"),
1424
+ projectTrust: path2.join(projectDir, "trust.json"),
1425
+ projectMeta: path2.join(projectDir, "meta.json"),
1426
+ projectLocalConfig: path2.join(projectDir, "config.local.json"),
1427
+ inProjectConfig: path2.join(opts.projectRoot, ".wrongstack", "config.json"),
1428
+ inProjectAgentsFile: path2.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
1429
+ inProjectSkills: path2.join(opts.projectRoot, ".wrongstack", "skills"),
1430
+ inProjectWorktrees: path2.join(opts.projectRoot, ".wrongstack", "worktrees"),
1431
+ projectHash: hash,
1432
+ projectSlug: slug,
1433
+ projectGoal: path2.join(projectDir, "goal.json"),
1434
+ projectSpecs: path2.join(projectDir, "specs"),
1435
+ projectTaskGraphs: path2.join(projectDir, "task-graphs"),
1436
+ projectSddSession: path2.join(projectDir, "sdd-session.json"),
1437
+ projectPlan: path2.join(projectDir, "plan.json"),
1438
+ projectAutophase: path2.join(projectDir, "autophase"),
1439
+ syncConfig: path2.join(globalRoot, "sync.json"),
1440
+ projectStatus: (projectHash2) => path2.join(globalRoot, "projects", projectHash2, "status.json")
1441
+ };
1442
+ }
1443
+
1444
+ // src/utils/sleep.ts
1445
+ function sleep(ms) {
1446
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1447
+ }
1448
+
1449
+ // src/utils/assert-never.ts
1450
+ function assertNever(x, message) {
1451
+ const err = new Error(
1452
+ `Unhandled case: ${JSON.stringify(x)}`
1453
+ );
1454
+ err.name = "AssertNeverError";
1455
+ throw err;
1456
+ }
1457
+
1458
+ // src/utils/tool-output-serializer.ts
1459
+ function createToolOutputSerializer(opts = {}) {
1460
+ const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
1461
+ function serialize(value) {
1462
+ if (typeof value === "string") return value;
1463
+ if (value === null || value === void 0) return "";
1464
+ if (typeof value === "object") {
1465
+ if (Array.isArray(value)) return value.map(serialize).join("\n");
1466
+ if ("text" in value) {
1467
+ const t = value.text;
1468
+ return typeof t === "string" ? t : JSON.stringify(value, null, 2);
1469
+ }
1470
+ try {
1471
+ return JSON.stringify(value, null, 2);
1472
+ } catch {
1473
+ return String(value);
1474
+ }
1475
+ }
1476
+ return String(value);
1477
+ }
1478
+ function enforceCap(text, remainingBudget) {
1479
+ if (remainingBudget <= 0) {
1480
+ return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
1481
+ }
1482
+ const textBytes = Buffer.byteLength(text, "utf8");
1483
+ if (textBytes <= remainingBudget) {
1484
+ return { text, newBudget: remainingBudget - textBytes };
1485
+ }
1486
+ const marker = `
1487
+ \u2026[truncated ${textBytes - remainingBudget} bytes]\u2026
1488
+ `;
1489
+ const markerBytes = Buffer.byteLength(marker, "utf8");
1490
+ const available = remainingBudget - markerBytes;
1491
+ if (available <= 0) {
1492
+ return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
1493
+ }
1494
+ const half = Math.floor(available / 2);
1495
+ const first = text.slice(0, half);
1496
+ const second = text.slice(text.length - half);
1497
+ return { text: `${first}${marker}${second}`, newBudget: 0 };
1498
+ }
1499
+ return { serialize, enforceCap, capBytes };
1500
+ }
1501
+
1502
+ // src/utils/json-schema-validate.ts
1503
+ function validateAgainstSchema(value, schema) {
1504
+ const errors = [];
1505
+ walk(value, schema, "", errors);
1506
+ return { ok: errors.length === 0, errors };
1507
+ }
1508
+ function walk(value, schema, path4, errors) {
1509
+ if (schema.enum !== void 0) {
1510
+ if (!schema.enum.some((e) => deepEqual(e, value))) {
1511
+ errors.push({
1512
+ path: path4 || "<root>",
1513
+ message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
1514
+ });
1515
+ return;
1516
+ }
1517
+ }
1518
+ if (typeof schema.type === "string") {
1519
+ if (!checkType(value, schema.type)) {
1520
+ errors.push({
1521
+ path: path4 || "<root>",
1522
+ message: `expected ${schema.type}, got ${describeType(value)}`
1523
+ });
1524
+ return;
1525
+ }
1526
+ }
1527
+ if (schema.type === "object" && isPlainObject(value)) {
1528
+ const obj = value;
1529
+ for (const req of schema.required ?? []) {
1530
+ if (!(req in obj)) {
1531
+ errors.push({ path: joinPath(path4, req), message: "required property missing" });
1532
+ }
1533
+ }
1534
+ if (schema.properties) {
1535
+ for (const [key, subSchema] of Object.entries(schema.properties)) {
1536
+ if (key in obj) {
1537
+ walk(obj[key], subSchema, joinPath(path4, key), errors);
1538
+ }
1539
+ }
1540
+ }
1541
+ }
1542
+ if (schema.type === "array" && Array.isArray(value) && schema.items) {
1543
+ for (let i = 0; i < value.length; i++) {
1544
+ walk(value[i], schema.items, `${path4}[${i}]`, errors);
1545
+ }
1546
+ }
1547
+ }
1548
+ function checkType(value, type) {
1549
+ switch (type) {
1550
+ case "string":
1551
+ return typeof value === "string";
1552
+ case "number":
1553
+ return typeof value === "number" && !Number.isNaN(value);
1554
+ case "integer":
1555
+ return typeof value === "number" && Number.isInteger(value);
1556
+ case "boolean":
1557
+ return typeof value === "boolean";
1558
+ case "null":
1559
+ return value === null;
1560
+ case "array":
1561
+ return Array.isArray(value);
1562
+ case "object":
1563
+ return isPlainObject(value);
1564
+ default:
1565
+ return true;
1566
+ }
1567
+ }
1568
+ function isPlainObject(v) {
1569
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1570
+ }
1571
+ function describeType(v) {
1572
+ if (v === null) return "null";
1573
+ if (Array.isArray(v)) return "array";
1574
+ return typeof v;
1575
+ }
1576
+ function joinPath(parent, key) {
1577
+ if (!parent) return key;
1578
+ return `${parent}.${key}`;
1579
+ }
1580
+ function deepEqual(a, b) {
1581
+ if (a === b) return true;
1582
+ if (typeof a !== typeof b) return false;
1583
+ if (a === null || b === null) return a === b;
1584
+ if (Array.isArray(a) && Array.isArray(b)) {
1585
+ return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
1586
+ }
1587
+ if (typeof a === "object" && typeof b === "object") {
1588
+ const ak = Object.keys(a);
1589
+ const bk = Object.keys(b);
1590
+ if (ak.length !== bk.length) return false;
1591
+ return ak.every(
1592
+ (k) => deepEqual(a[k], b[k])
1593
+ );
1594
+ }
1595
+ return false;
1596
+ }
1597
+
1598
+ // src/utils/regex-guard.ts
1599
+ var MAX_PATTERN_LEN = 512;
1600
+ var DANGEROUS_PATTERNS = [
1601
+ /(\([^)]*[+*][^)]*\))[+*]/,
1602
+ // (a+)+, (.*)+, etc
1603
+ /(\(\?:[^)]*[+*][^)]*\))[+*]/
1604
+ // same, with non-capturing group
1605
+ ];
1606
+ function compileUserRegex(pattern, flags) {
1607
+ if (typeof pattern !== "string") {
1608
+ return { ok: false, reason: "pattern must be a string" };
1609
+ }
1610
+ if (pattern.length === 0) {
1611
+ return { ok: false, reason: "pattern is empty" };
1612
+ }
1613
+ if (pattern.length > MAX_PATTERN_LEN) {
1614
+ return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
1615
+ }
1616
+ for (const rx of DANGEROUS_PATTERNS) {
1617
+ if (rx.test(pattern)) {
1618
+ return {
1619
+ ok: false,
1620
+ reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
1621
+ };
1622
+ }
1623
+ }
1624
+ try {
1625
+ return { ok: true, regex: new RegExp(pattern, flags) };
1626
+ } catch (err) {
1627
+ return {
1628
+ ok: false,
1629
+ reason: err instanceof Error ? err.message : "invalid regex"
1630
+ };
1631
+ }
1632
+ }
1633
+
1634
+ // src/types/errors.ts
1635
+ var ERROR_CODES = {
1636
+ // Provider
1637
+ PROVIDER_RATE_LIMITED: "PROVIDER_RATE_LIMITED",
1638
+ PROVIDER_AUTH_FAILED: "PROVIDER_AUTH_FAILED",
1639
+ PROVIDER_OVERLOADED: "PROVIDER_OVERLOADED",
1640
+ PROVIDER_INVALID_REQUEST: "PROVIDER_INVALID_REQUEST",
1641
+ PROVIDER_SERVER_ERROR: "PROVIDER_SERVER_ERROR",
1642
+ PROVIDER_NETWORK_ERROR: "PROVIDER_NETWORK_ERROR",
1643
+ // Agent
1644
+ AGENT_ITERATION_LIMIT: "AGENT_ITERATION_LIMIT",
1645
+ AGENT_CONTEXT_OVERFLOW: "AGENT_CONTEXT_OVERFLOW",
1646
+ AGENT_ABORTED: "AGENT_ABORTED",
1647
+ AGENT_RUN_FAILED: "AGENT_RUN_FAILED",
1648
+ // File system
1649
+ FS_READ_FAILED: "FS_READ_FAILED",
1650
+ FS_ATOMIC_WRITE_FAILED: "FS_ATOMIC_WRITE_FAILED"};
1651
+ var WrongStackError = class extends Error {
1652
+ code;
1653
+ subsystem;
1654
+ severity;
1655
+ recoverable;
1656
+ context;
1657
+ constructor(opts) {
1658
+ super(opts.message, { cause: opts.cause });
1659
+ this.name = "WrongStackError";
1660
+ this.code = opts.code;
1661
+ this.subsystem = opts.subsystem;
1662
+ this.severity = opts.severity ?? "error";
1663
+ this.recoverable = opts.recoverable ?? false;
1664
+ this.context = opts.context;
1665
+ }
1666
+ /**
1667
+ * Render a one-line user-facing description.
1668
+ * Subclasses should override for domain-specific formatting.
1669
+ */
1670
+ describe() {
1671
+ const ctx = this.context ? ` ${formatContext(this.context)}` : "";
1672
+ return `${this.code}: ${this.message}${ctx}`;
1673
+ }
1674
+ };
1675
+ function formatContext(ctx) {
1676
+ const parts = Object.entries(ctx).filter(([, v]) => v !== void 0).slice(0, 3).map(([k, v]) => `${k}=${String(v)}`);
1677
+ return parts.length > 0 ? `[${parts.join(" ")}]` : "";
1678
+ }
1679
+ var AgentError = class extends WrongStackError {
1680
+ constructor(opts) {
1681
+ super({
1682
+ message: opts.message,
1683
+ code: opts.code,
1684
+ subsystem: "agent",
1685
+ severity: opts.code === ERROR_CODES.AGENT_ABORTED ? "warning" : "error",
1686
+ recoverable: opts.recoverable ?? opts.code === ERROR_CODES.AGENT_ITERATION_LIMIT,
1687
+ context: opts.context,
1688
+ cause: opts.cause
1689
+ });
1690
+ this.name = "AgentError";
1691
+ }
1692
+ };
1693
+ function toWrongStackError(err, code = ERROR_CODES.AGENT_RUN_FAILED) {
1694
+ if (err instanceof WrongStackError) return err;
1695
+ const message = toErrorMessage(err);
1696
+ return new AgentError({
1697
+ message,
1698
+ code: code === "UNKNOWN" ? ERROR_CODES.AGENT_RUN_FAILED : code,
1699
+ cause: err
1700
+ });
1701
+ }
1702
+ var FsError = class extends WrongStackError {
1703
+ path;
1704
+ constructor(opts) {
1705
+ super({
1706
+ message: opts.message,
1707
+ code: opts.code,
1708
+ subsystem: "fs",
1709
+ severity: "error",
1710
+ recoverable: opts.code !== ERROR_CODES.FS_READ_FAILED,
1711
+ context: { path: opts.path, ...opts.context },
1712
+ cause: opts.cause
1713
+ });
1714
+ this.name = "FsError";
1715
+ this.path = opts.path;
1716
+ }
1717
+ };
1718
+
1719
+ // src/execution/auto-compaction-middleware.ts
1720
+ var LEVEL_RANK = { warn: 0, soft: 1, hard: 2 };
1721
+ var MAX_DIGEST_LOG_CHARS = 4e3;
1722
+ function truncateDigest(digest) {
1723
+ if (digest.length <= MAX_DIGEST_LOG_CHARS) return digest;
1724
+ return `${digest.slice(0, MAX_DIGEST_LOG_CHARS)}\u2026 [+${digest.length - MAX_DIGEST_LOG_CHARS} chars; full turns in session log]`;
1725
+ }
1726
+ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
1727
+ name = "AutoCompaction";
1728
+ compactor;
1729
+ /** Deprecated. Kept for backward compat with tests that pass simpleEstimator. */
1730
+ _estimator;
1731
+ warnThreshold;
1732
+ softThreshold;
1733
+ hardThreshold;
1734
+ /** Writable so model-switch can update the denominator without re-registering the middleware. */
1735
+ _maxContext;
1736
+ /**
1737
+ * Runtime on/off gate. The middleware is always installed in the pipeline so
1738
+ * auto-compaction can be toggled live from the TUI `/settings` picker; when
1739
+ * disabled the handler is a pass-through. Defaults to enabled.
1740
+ */
1741
+ _enabled = true;
1742
+ aggressiveOn;
1743
+ events;
1744
+ failureMode;
1745
+ policyProvider;
1746
+ sessionBridge;
1747
+ /**
1748
+ * Once a compaction attempt reduces nothing (preserveK protects everything,
1749
+ * no oversized tool_results remain to elide), retrying on every iteration
1750
+ * just spams `compaction.fired` events without making progress. We remember
1751
+ * the no-op and skip until either the pressure level escalates or context
1752
+ * has grown by at least this many tokens since the failed attempt.
1753
+ */
1754
+ static NOOP_RETRY_DELTA_TOKENS = 2e3;
1755
+ /** Tracks the most recent no-op attempt so we can avoid re-firing per turn. */
1756
+ lastNoopAttempt = null;
1757
+ /**
1758
+ * Cached token estimate from the last handler() invocation. When the
1759
+ * message count and tool count haven't changed since the last estimate
1760
+ * (autonomous idle loops), we skip the expensive O(n) token estimation
1761
+ * and reuse this value. Reset to -1 when the context changes.
1762
+ */
1763
+ _cachedTokens = -1;
1764
+ _cachedMsgCount = -1;
1765
+ _cachedToolCount = -1;
1766
+ /**
1767
+ * @param compactor Compactor to use for compaction.
1768
+ * @param maxContext Provider's max context window in tokens.
1769
+ * @param _estimator Deprecated parameter kept for backward compatibility.
1770
+ * The middleware now uses `estimateRequestTokens` internally
1771
+ * for accurate full-request token counting (messages +
1772
+ * systemPrompt + toolDefs).
1773
+ * @param thresholds Threshold fractions (0-1) of maxContext.
1774
+ * @param opts Optional behavior. By default, failures at the
1775
+ * hard threshold throw AGENT_CONTEXT_OVERFLOW so
1776
+ * the agent does not continue into a likely
1777
+ * provider context overflow. Warn/soft failures
1778
+ * still emit compaction.failed and continue.
1779
+ */
1780
+ constructor(compactor, maxContext, _estimator, thresholds, optsOrAggressiveOn = {}, events) {
1781
+ const opts = typeof optsOrAggressiveOn === "string" ? { aggressiveOn: optsOrAggressiveOn, events } : optsOrAggressiveOn;
1782
+ this.compactor = compactor;
1783
+ this._maxContext = maxContext;
1784
+ this._estimator = _estimator;
1785
+ this.warnThreshold = thresholds.warn;
1786
+ this.softThreshold = thresholds.soft;
1787
+ this.hardThreshold = thresholds.hard;
1788
+ this.aggressiveOn = opts.aggressiveOn ?? "soft";
1789
+ this.events = opts.events;
1790
+ this.failureMode = opts.failureMode ?? "throw_on_hard";
1791
+ this.policyProvider = opts.policyProvider;
1792
+ this.sessionBridge = opts.sessionBridge;
1793
+ }
1794
+ /** Allow callers (e.g. model-switch in WebUI) to update the context window
1795
+ * denominator when the active model changes. */
1796
+ setMaxContext(maxContext) {
1797
+ this._maxContext = maxContext;
1798
+ }
1799
+ /** Whether auto-compaction is currently active. */
1800
+ get enabled() {
1801
+ return this._enabled;
1802
+ }
1803
+ /** Toggle auto-compaction on a live session (TUI `/settings`). When disabled
1804
+ * the middleware passes every iteration straight through without estimating
1805
+ * tokens or compacting. */
1806
+ setEnabled(enabled) {
1807
+ this._enabled = enabled;
1808
+ }
1809
+ handler() {
1414
1810
  return async (ctx, next) => {
1811
+ if (!this._enabled) return next(ctx);
1415
1812
  const msgCount = ctx.messages.length;
1416
1813
  const toolCount = (ctx.tools ?? []).length;
1417
1814
  let tokens;
@@ -1597,144 +1994,6 @@ function getDangerousCapabilities(toolOrCaps) {
1597
1994
  );
1598
1995
  }
1599
1996
 
1600
- // src/utils/json-schema-validate.ts
1601
- function validateAgainstSchema(value, schema) {
1602
- const errors = [];
1603
- walk(value, schema, "", errors);
1604
- return { ok: errors.length === 0, errors };
1605
- }
1606
- function walk(value, schema, path4, errors) {
1607
- if (schema.enum !== void 0) {
1608
- if (!schema.enum.some((e) => deepEqual(e, value))) {
1609
- errors.push({
1610
- path: path4 || "<root>",
1611
- message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
1612
- });
1613
- return;
1614
- }
1615
- }
1616
- if (typeof schema.type === "string") {
1617
- if (!checkType(value, schema.type)) {
1618
- errors.push({
1619
- path: path4 || "<root>",
1620
- message: `expected ${schema.type}, got ${describeType(value)}`
1621
- });
1622
- return;
1623
- }
1624
- }
1625
- if (schema.type === "object" && isPlainObject(value)) {
1626
- const obj = value;
1627
- for (const req of schema.required ?? []) {
1628
- if (!(req in obj)) {
1629
- errors.push({ path: joinPath(path4, req), message: "required property missing" });
1630
- }
1631
- }
1632
- if (schema.properties) {
1633
- for (const [key, subSchema] of Object.entries(schema.properties)) {
1634
- if (key in obj) {
1635
- walk(obj[key], subSchema, joinPath(path4, key), errors);
1636
- }
1637
- }
1638
- }
1639
- }
1640
- if (schema.type === "array" && Array.isArray(value) && schema.items) {
1641
- value.forEach((item, i) => walk(item, schema.items, `${path4}[${i}]`, errors));
1642
- }
1643
- }
1644
- function checkType(value, type) {
1645
- switch (type) {
1646
- case "string":
1647
- return typeof value === "string";
1648
- case "number":
1649
- return typeof value === "number" && !Number.isNaN(value);
1650
- case "integer":
1651
- return typeof value === "number" && Number.isInteger(value);
1652
- case "boolean":
1653
- return typeof value === "boolean";
1654
- case "null":
1655
- return value === null;
1656
- case "array":
1657
- return Array.isArray(value);
1658
- case "object":
1659
- return isPlainObject(value);
1660
- default:
1661
- return true;
1662
- }
1663
- }
1664
- function isPlainObject(v) {
1665
- return typeof v === "object" && v !== null && !Array.isArray(v);
1666
- }
1667
- function describeType(v) {
1668
- if (v === null) return "null";
1669
- if (Array.isArray(v)) return "array";
1670
- return typeof v;
1671
- }
1672
- function joinPath(parent, key) {
1673
- if (!parent) return key;
1674
- return `${parent}.${key}`;
1675
- }
1676
- function deepEqual(a, b) {
1677
- if (a === b) return true;
1678
- if (typeof a !== typeof b) return false;
1679
- if (a === null || b === null) return a === b;
1680
- if (Array.isArray(a) && Array.isArray(b)) {
1681
- return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
1682
- }
1683
- if (typeof a === "object" && typeof b === "object") {
1684
- const ak = Object.keys(a);
1685
- const bk = Object.keys(b);
1686
- if (ak.length !== bk.length) return false;
1687
- return ak.every(
1688
- (k) => deepEqual(a[k], b[k])
1689
- );
1690
- }
1691
- return false;
1692
- }
1693
-
1694
- // src/utils/tool-output-serializer.ts
1695
- function createToolOutputSerializer(opts = {}) {
1696
- const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
1697
- function serialize(value) {
1698
- if (typeof value === "string") return value;
1699
- if (value === null || value === void 0) return "";
1700
- if (typeof value === "object") {
1701
- if (Array.isArray(value)) return value.map(serialize).join("\n");
1702
- if ("text" in value) {
1703
- const t = value.text;
1704
- return typeof t === "string" ? t : JSON.stringify(value, null, 2);
1705
- }
1706
- try {
1707
- return JSON.stringify(value, null, 2);
1708
- } catch {
1709
- return String(value);
1710
- }
1711
- }
1712
- return String(value);
1713
- }
1714
- function enforceCap(text, remainingBudget) {
1715
- if (remainingBudget <= 0) {
1716
- return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
1717
- }
1718
- const textBytes = Buffer.byteLength(text, "utf8");
1719
- if (textBytes <= remainingBudget) {
1720
- return { text, newBudget: remainingBudget - textBytes };
1721
- }
1722
- const marker = `
1723
- \u2026[truncated ${textBytes - remainingBudget} bytes]\u2026
1724
- `;
1725
- const markerBytes = Buffer.byteLength(marker, "utf8");
1726
- const available = remainingBudget - markerBytes;
1727
- if (available <= 0) {
1728
- return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
1729
- }
1730
- const half = Math.floor(available / 2);
1731
- const first = text.slice(0, half);
1732
- const second = text.slice(text.length - half);
1733
- return { text: `${first}${marker}${second}`, newBudget: 0 };
1734
- }
1735
- return { serialize, enforceCap, capBytes };
1736
- }
1737
-
1738
1997
  // src/execution/tool-executor.ts
1739
1998
  var ToolExecutor = class _ToolExecutor {
1740
1999
  constructor(registry, opts) {
@@ -1898,7 +2157,7 @@ ${post.additionalContext}`;
1898
2157
  );
1899
2158
  return { result, tool, durationMs: Date.now() - start };
1900
2159
  } catch (err) {
1901
- const msg = err instanceof Error ? err.message : String(err);
2160
+ const msg = toErrorMessage(err);
1902
2161
  const scrubbed = this.opts.secretScrubber.scrub(msg);
1903
2162
  this.opts.renderer?.writeToolResult(tool.name, scrubbed, true);
1904
2163
  const result = {
@@ -1919,7 +2178,7 @@ ${post.additionalContext}`;
1919
2178
  try {
1920
2179
  return await runOne(use);
1921
2180
  } catch (err) {
1922
- const msg = err instanceof Error ? err.message : String(err);
2181
+ const msg = toErrorMessage(err);
1923
2182
  const scrubbed = this.opts.secretScrubber.scrub(msg);
1924
2183
  const result = {
1925
2184
  type: "tool_result",
@@ -2161,89 +2420,44 @@ ${excerpt}`;
2161
2420
  return isPathKey ? normalizePath(v) : escapeGlob(v);
2162
2421
  }
2163
2422
  }
2164
- if (toolName === "bash" && typeof obj.command === "string") {
2165
- return escapeGlob(obj.command);
2166
- }
2167
- if (typeof obj.path === "string") {
2168
- return normalizePath(obj.path);
2169
- }
2170
- if (typeof obj.url === "string") {
2171
- return escapeGlob(obj.url);
2172
- }
2173
- if (typeof obj.name === "string") {
2174
- return escapeGlob(obj.name);
2175
- }
2176
- return void 0;
2177
- }
2178
- };
2179
- function clampTimeoutMs(timeoutMs, maxTimeoutMs) {
2180
- const fallback = 3e5;
2181
- const finiteTimeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : fallback;
2182
- const finiteMax = Number.isFinite(maxTimeoutMs) && maxTimeoutMs > 0 ? maxTimeoutMs : fallback;
2183
- return Math.max(1, Math.min(finiteTimeout, finiteMax));
2184
- }
2185
- var MALFORMED_ARG_MARKERS = ["__raw", "__raw_arguments", "_raw"];
2186
- function hasMalformedArguments(input) {
2187
- if (!input || typeof input !== "object" || Array.isArray(input)) return false;
2188
- const obj = input;
2189
- const keys = Object.keys(obj);
2190
- return keys.length === 1 && MALFORMED_ARG_MARKERS.includes(keys[0]);
2191
- }
2192
- function extractMalformedRaw(input) {
2193
- if (!hasMalformedArguments(input)) return void 0;
2194
- const obj = input;
2195
- const value = obj[expectDefined(Object.keys(obj)[0])];
2196
- if (value === void 0 || value === null) return void 0;
2197
- if (typeof value === "string") return value;
2198
- try {
2199
- return JSON.stringify(value);
2200
- } catch {
2201
- return String(value);
2202
- }
2203
- }
2204
-
2205
- // src/utils/assert-never.ts
2206
- function assertNever(x, message) {
2207
- const err = new Error(
2208
- `Unhandled case: ${JSON.stringify(x)}`
2209
- );
2210
- err.name = "AssertNeverError";
2211
- throw err;
2212
- }
2213
-
2214
- // src/utils/regex-guard.ts
2215
- var MAX_PATTERN_LEN = 512;
2216
- var DANGEROUS_PATTERNS = [
2217
- /(\([^)]*[+*][^)]*\))[+*]/,
2218
- // (a+)+, (.*)+, etc
2219
- /(\(\?:[^)]*[+*][^)]*\))[+*]/
2220
- // same, with non-capturing group
2221
- ];
2222
- function compileUserRegex(pattern, flags) {
2223
- if (typeof pattern !== "string") {
2224
- return { ok: false, reason: "pattern must be a string" };
2225
- }
2226
- if (pattern.length === 0) {
2227
- return { ok: false, reason: "pattern is empty" };
2228
- }
2229
- if (pattern.length > MAX_PATTERN_LEN) {
2230
- return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
2231
- }
2232
- for (const rx of DANGEROUS_PATTERNS) {
2233
- if (rx.test(pattern)) {
2234
- return {
2235
- ok: false,
2236
- reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
2237
- };
2238
- }
2423
+ if (toolName === "bash" && typeof obj.command === "string") {
2424
+ return escapeGlob(obj.command);
2425
+ }
2426
+ if (typeof obj.path === "string") {
2427
+ return normalizePath(obj.path);
2428
+ }
2429
+ if (typeof obj.url === "string") {
2430
+ return escapeGlob(obj.url);
2431
+ }
2432
+ if (typeof obj.name === "string") {
2433
+ return escapeGlob(obj.name);
2434
+ }
2435
+ return void 0;
2239
2436
  }
2437
+ };
2438
+ function clampTimeoutMs(timeoutMs, maxTimeoutMs) {
2439
+ const fallback = 3e5;
2440
+ const finiteTimeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : fallback;
2441
+ const finiteMax = Number.isFinite(maxTimeoutMs) && maxTimeoutMs > 0 ? maxTimeoutMs : fallback;
2442
+ return Math.max(1, Math.min(finiteTimeout, finiteMax));
2443
+ }
2444
+ var MALFORMED_ARG_MARKERS = ["__raw", "__raw_arguments", "_raw"];
2445
+ function hasMalformedArguments(input) {
2446
+ if (!input || typeof input !== "object" || Array.isArray(input)) return false;
2447
+ const obj = input;
2448
+ const keys = Object.keys(obj);
2449
+ return keys.length === 1 && MALFORMED_ARG_MARKERS.includes(keys[0]);
2450
+ }
2451
+ function extractMalformedRaw(input) {
2452
+ if (!hasMalformedArguments(input)) return void 0;
2453
+ const obj = input;
2454
+ const value = obj[expectDefined(Object.keys(obj)[0])];
2455
+ if (value === void 0 || value === null) return void 0;
2456
+ if (typeof value === "string") return value;
2240
2457
  try {
2241
- return { ok: true, regex: new RegExp(pattern, flags) };
2242
- } catch (err) {
2243
- return {
2244
- ok: false,
2245
- reason: err instanceof Error ? err.message : "invalid regex"
2246
- };
2458
+ return JSON.stringify(value);
2459
+ } catch {
2460
+ return String(value);
2247
2461
  }
2248
2462
  }
2249
2463
 
@@ -2423,125 +2637,6 @@ var AutonomousRunner = class {
2423
2637
  this.stopped = true;
2424
2638
  }
2425
2639
  };
2426
- async function atomicWrite(targetPath, content, opts = {}) {
2427
- const dir = path2.dirname(targetPath);
2428
- await fs.mkdir(dir, { recursive: true });
2429
- const tmp = path2.join(dir, `.${path2.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
2430
- try {
2431
- if (typeof content === "string") {
2432
- await fs.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
2433
- } else {
2434
- await fs.writeFile(tmp, content, { flag: "wx" });
2435
- }
2436
- try {
2437
- const fh = await fs.open(tmp, "r+");
2438
- try {
2439
- await fh.sync();
2440
- } finally {
2441
- await fh.close();
2442
- }
2443
- } catch {
2444
- }
2445
- let mode;
2446
- try {
2447
- const stat2 = await fs.stat(targetPath);
2448
- mode = stat2.mode & 511;
2449
- } catch {
2450
- mode = opts.mode;
2451
- }
2452
- if (mode !== void 0) {
2453
- await fs.chmod(tmp, mode);
2454
- }
2455
- await renameWithRetry(tmp, targetPath);
2456
- } catch (err) {
2457
- try {
2458
- await fs.unlink(tmp);
2459
- } catch {
2460
- }
2461
- throw err;
2462
- }
2463
- }
2464
- var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
2465
- async function renameWithRetry(from, to) {
2466
- if (process.platform !== "win32") {
2467
- await fs.rename(from, to);
2468
- return;
2469
- }
2470
- const delays = [10, 25, 60, 120, 250];
2471
- let lastErr;
2472
- for (let i = 0; i <= delays.length; i++) {
2473
- try {
2474
- await fs.rename(from, to);
2475
- return;
2476
- } catch (err) {
2477
- lastErr = err;
2478
- const code = err?.code;
2479
- if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
2480
- throw err;
2481
- }
2482
- await new Promise((resolve2) => setTimeout(resolve2, delays[i]));
2483
- }
2484
- }
2485
- throw lastErr;
2486
- }
2487
- function projectHash(absRoot) {
2488
- return createHash("sha256").update(path2.resolve(absRoot)).digest("hex").slice(0, 12);
2489
- }
2490
- function projectSlug(absRoot) {
2491
- const base = slugify(path2.basename(absRoot));
2492
- const hash = createHash("sha256").update(path2.resolve(absRoot)).digest("hex").slice(0, 6);
2493
- return `${base}-${hash}`;
2494
- }
2495
- function slugify(name) {
2496
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
2497
- }
2498
- function wstackGlobalRoot() {
2499
- const fromEnv = process.env["WRONGSTACK_HOME"];
2500
- if (fromEnv && fromEnv.trim().length > 0) return path2.resolve(fromEnv);
2501
- return path2.join(os.homedir(), ".wrongstack");
2502
- }
2503
- function resolveWstackPaths(opts) {
2504
- const globalRoot = opts.globalRoot ?? (opts.userHome ? path2.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
2505
- const hash = projectHash(opts.projectRoot);
2506
- const slug = projectSlug(opts.projectRoot);
2507
- const projectDir = path2.join(globalRoot, "projects", slug);
2508
- return {
2509
- globalRoot,
2510
- configDir: globalRoot,
2511
- globalConfig: path2.join(globalRoot, "config.json"),
2512
- secretsKey: path2.join(globalRoot, ".key"),
2513
- globalMemory: path2.join(globalRoot, "memory.md"),
2514
- globalSkills: path2.join(globalRoot, "skills"),
2515
- globalPrompts: path2.join(globalRoot, "prompts"),
2516
- cacheDir: path2.join(globalRoot, "cache"),
2517
- modelsCache: path2.join(globalRoot, "cache", "models.dev.json"),
2518
- modelsOverlayCache: path2.join(globalRoot, "cache", "models-overlay.json"),
2519
- historyFile: path2.join(globalRoot, "history"),
2520
- logFile: path2.join(globalRoot, "logs", "wrongstack.log"),
2521
- projectDir,
2522
- projectCodebaseIndex: path2.join(projectDir, "codebase-index"),
2523
- projectMemory: path2.join(projectDir, "memory.md"),
2524
- projectSessions: path2.join(projectDir, "sessions"),
2525
- projectTrust: path2.join(projectDir, "trust.json"),
2526
- projectMeta: path2.join(projectDir, "meta.json"),
2527
- projectLocalConfig: path2.join(projectDir, "config.local.json"),
2528
- inProjectConfig: path2.join(opts.projectRoot, ".wrongstack", "config.json"),
2529
- inProjectAgentsFile: path2.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
2530
- inProjectSkills: path2.join(opts.projectRoot, ".wrongstack", "skills"),
2531
- inProjectWorktrees: path2.join(opts.projectRoot, ".wrongstack", "worktrees"),
2532
- projectHash: hash,
2533
- projectSlug: slug,
2534
- projectGoal: path2.join(projectDir, "goal.json"),
2535
- projectSpecs: path2.join(projectDir, "specs"),
2536
- projectTaskGraphs: path2.join(projectDir, "task-graphs"),
2537
- projectSddSession: path2.join(projectDir, "sdd-session.json"),
2538
- projectPlan: path2.join(projectDir, "plan.json"),
2539
- projectAutophase: path2.join(projectDir, "autophase"),
2540
- syncConfig: path2.join(globalRoot, "sync.json")
2541
- };
2542
- }
2543
-
2544
- // src/storage/goal-store.ts
2545
2640
  var MAX_JOURNAL_ENTRIES = 500;
2546
2641
  function goalFilePath(projectRoot) {
2547
2642
  return resolveWstackPaths({ projectRoot }).projectGoal;
@@ -2569,7 +2664,7 @@ async function loadGoal(filePath, events) {
2569
2664
  store: "goal",
2570
2665
  filePath,
2571
2666
  operation: "load",
2572
- error: err instanceof Error ? err.message : String(err),
2667
+ error: toErrorMessage(err),
2573
2668
  recoverable: false
2574
2669
  });
2575
2670
  throw err;
@@ -2642,11 +2737,11 @@ async function saveGoal(filePath, goal, events) {
2642
2737
  store: "goal",
2643
2738
  filePath,
2644
2739
  operation: "save",
2645
- error: err instanceof Error ? err.message : String(err),
2740
+ error: toErrorMessage(err),
2646
2741
  recoverable: false
2647
2742
  });
2648
2743
  throw new FsError({
2649
- message: err instanceof Error ? err.message : String(err),
2744
+ message: toErrorMessage(err),
2650
2745
  code: ERROR_CODES.FS_ATOMIC_WRITE_FAILED,
2651
2746
  path: filePath,
2652
2747
  cause: err
@@ -2700,11 +2795,6 @@ function computeTrend(history) {
2700
2795
  return "steady";
2701
2796
  }
2702
2797
 
2703
- // src/utils/sleep.ts
2704
- function sleep(ms) {
2705
- return new Promise((resolve2) => setTimeout(resolve2, ms));
2706
- }
2707
-
2708
2798
  // src/execution/autonomy-brain.ts
2709
2799
  var RISK_LEVELS = {
2710
2800
  low: 0,
@@ -2941,7 +3031,7 @@ var EternalAutonomyEngine = class {
2941
3031
  console.error(JSON.stringify({
2942
3032
  level: "error",
2943
3033
  event: "engine.persist_state_failed",
2944
- message: err instanceof Error ? err.message : String(err),
3034
+ message: toErrorMessage(err),
2945
3035
  context: { expectedState: "stopped" },
2946
3036
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
2947
3037
  }));
@@ -2974,7 +3064,7 @@ var EternalAutonomyEngine = class {
2974
3064
  } catch (err) {
2975
3065
  this.consecutiveFailures++;
2976
3066
  this.opts.onError?.(err instanceof Error ? err : new Error(String(err)), this.consecutiveFailures);
2977
- await this.appendFailure("engine error", err instanceof Error ? err.message : String(err));
3067
+ await this.appendFailure("engine error", toErrorMessage(err));
2978
3068
  }
2979
3069
  if (iterationOk) {
2980
3070
  this.consecutiveFailures = 0;
@@ -3075,7 +3165,7 @@ var EternalAutonomyEngine = class {
3075
3165
  } catch (err) {
3076
3166
  const isAbort = err instanceof Error && (err.name === "AbortError" || err.message.includes("abort"));
3077
3167
  status = isAbort ? "aborted" : "failure";
3078
- note = err instanceof Error ? err.message : String(err);
3168
+ note = toErrorMessage(err);
3079
3169
  if (!isAbort && typeof err?.recoverable === "boolean") {
3080
3170
  isTransientFailure = err.recoverable;
3081
3171
  }
@@ -3565,6 +3655,7 @@ ${recentJournal}` : ""
3565
3655
 
3566
3656
  // src/coordination/subagent-budget.ts
3567
3657
  var TIMEOUT_PREEMPT_FRACTION = 0.85;
3658
+ var DECISION_TIMEOUT_MS = 6e4;
3568
3659
  var BudgetExceededError = class extends Error {
3569
3660
  kind;
3570
3661
  limit;
@@ -3594,6 +3685,31 @@ var BudgetThresholdSignal = class extends Error {
3594
3685
  };
3595
3686
  var SubagentBudget = class _SubagentBudget {
3596
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
+ }
3597
3713
  iterations = 0;
3598
3714
  toolCalls = 0;
3599
3715
  tokenInput = 0;
@@ -3614,12 +3730,44 @@ var SubagentBudget = class _SubagentBudget {
3614
3730
  * or hung listener (Director not built / event filter detached mid-run)
3615
3731
  * leaves the budget over-limit and never enforces anything.
3616
3732
  */
3617
- static DECISION_TIMEOUT_MS = 6e4;
3733
+ static DECISION_TIMEOUT_MS = DECISION_TIMEOUT_MS;
3618
3734
  /**
3619
3735
  * Injected by the runner when wiring the budget to its EventBus.
3620
3736
  * Used to emit `budget.threshold_reached` events in `'auto'` mode.
3621
3737
  */
3622
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
+ }
3623
3771
  /**
3624
3772
  * Negotiation mode — controls whether a threshold hit tries to emit
3625
3773
  * `budget.threshold_reached` and wait for a coordinator decision, or
@@ -3720,7 +3868,8 @@ var SubagentBudget = class _SubagentBudget {
3720
3868
  if (this.limits.idleTimeoutMs !== void 0 && idle > this.limits.idleTimeoutMs) {
3721
3869
  exceeded.push({ kind: "idle_timeout", used: idle, limit: this.limits.idleTimeoutMs });
3722
3870
  }
3723
- 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) {
3724
3873
  exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
3725
3874
  }
3726
3875
  }
@@ -3734,19 +3883,99 @@ var SubagentBudget = class _SubagentBudget {
3734
3883
  throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
3735
3884
  }
3736
3885
  const bus = this._events;
3737
- if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
3886
+ if (!bus) {
3738
3887
  const first2 = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
3739
3888
  throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
3740
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;
3741
3901
  for (const entry of exceeded) {
3742
3902
  if (this._pendingNegotiations.has(entry.kind)) continue;
3743
- const decision2 = this._negotiateExtension(entry.kind, exceeded);
3744
- 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);
3745
3908
  }
3746
- const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
3747
- const decision = this._pendingNegotiations.get(first.kind);
3748
- if (!decision) throw new Error(`No pending negotiation for ${first.kind}`);
3749
- 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
+ });
3750
3979
  }
3751
3980
  /**
3752
3981
  * Per-kind in-flight negotiation Promises. Each budget kind can have its
@@ -3766,77 +3995,33 @@ var SubagentBudget = class _SubagentBudget {
3766
3995
  * `{ extend: {} }` — keep going without patching; next overrun fires
3767
3996
  * a fresh signal.
3768
3997
  */
3769
- async _negotiateExtension(kind, exceeded) {
3998
+ async _negotiateExtension(entry) {
3770
3999
  if (!this._onThreshold) {
3771
4000
  return "stop";
3772
4001
  }
3773
4002
  try {
3774
- const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
3775
4003
  const result = this._onThreshold({
3776
- kind: first.kind,
3777
- used: first.used,
3778
- limit: first.limit,
3779
- requestDecision: () => {
3780
- const bus = this._events;
3781
- if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
3782
- return Promise.resolve("stop");
3783
- }
3784
- return new Promise((resolve2) => {
3785
- let resolved = false;
3786
- const respond = (d) => {
3787
- if (resolved) return;
3788
- resolved = true;
3789
- resolve2(d);
3790
- };
3791
- const fallback = setTimeout(
3792
- () => respond("stop"),
3793
- _SubagentBudget.DECISION_TIMEOUT_MS
3794
- );
3795
- for (const { kind: kind2, used, limit } of exceeded) {
3796
- bus.emit("budget.threshold_reached", {
3797
- kind: kind2,
3798
- used,
3799
- limit,
3800
- timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
3801
- extend: (extra) => {
3802
- clearTimeout(fallback);
3803
- respond({ extend: extra });
3804
- },
3805
- deny: () => {
3806
- clearTimeout(fallback);
3807
- respond("stop");
3808
- }
3809
- });
3810
- }
3811
- });
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: () => {
3812
4015
  }
3813
4016
  });
3814
4017
  if (result === "throw") return "stop";
3815
4018
  if (result === "continue") return { extend: {} };
3816
4019
  const decision = await result;
3817
4020
  if (decision === "stop") return "stop";
3818
- const ext = decision.extend;
3819
- if (ext.maxIterations !== void 0) {
3820
- this.limits.maxIterations = ext.maxIterations;
3821
- }
3822
- if (ext.maxToolCalls !== void 0) {
3823
- this.limits.maxToolCalls = ext.maxToolCalls;
3824
- }
3825
- if (ext.maxTokens !== void 0) {
3826
- this.limits.maxTokens = ext.maxTokens;
3827
- }
3828
- if (ext.maxCostUsd !== void 0) {
3829
- this.limits.maxCostUsd = ext.maxCostUsd;
3830
- }
3831
- if (ext.timeoutMs !== void 0) {
3832
- this.limits.timeoutMs = ext.timeoutMs;
3833
- }
3834
- if (ext.idleTimeoutMs !== void 0) {
3835
- this.limits.idleTimeoutMs = ext.idleTimeoutMs;
3836
- }
4021
+ this.patchLimits(decision.extend);
3837
4022
  return decision;
3838
4023
  } finally {
3839
- this._pendingNegotiations.delete(kind);
4024
+ this._pendingNegotiations.delete(entry.kind);
3840
4025
  }
3841
4026
  }
3842
4027
  recordIteration() {
@@ -3879,7 +4064,8 @@ var SubagentBudget = class _SubagentBudget {
3879
4064
  const { timeoutMs, idleTimeoutMs } = this.limits;
3880
4065
  if (timeoutMs === void 0 && idleTimeoutMs === void 0) return;
3881
4066
  const elapsed = Date.now() - this.startTime;
3882
- 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;
3883
4069
  const idleTripped = idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs;
3884
4070
  if (!wallTripped && !idleTripped) return;
3885
4071
  void this.checkLimits(elapsed);
@@ -6819,11 +7005,6 @@ async function dispatchAgent(task, opts = {}) {
6819
7005
  };
6820
7006
  }
6821
7007
 
6822
- // src/utils/string.ts
6823
- function truncate(s, max) {
6824
- return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
6825
- }
6826
-
6827
7008
  // src/types/provider.ts
6828
7009
  var ProviderError = class extends WrongStackError {
6829
7010
  status;
@@ -6903,7 +7084,7 @@ function classifySubagentError(err, hints = {}) {
6903
7084
  const baseMessage2 = err.describe();
6904
7085
  return providerErrorToSubagentError(err, baseMessage2, cause);
6905
7086
  }
6906
- const baseMessage = err instanceof Error ? err.message : String(err);
7087
+ const baseMessage = toErrorMessage(err);
6907
7088
  if (err instanceof BudgetExceededError) {
6908
7089
  const map = {
6909
7090
  iterations: "budget_iterations",
@@ -7203,6 +7384,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7203
7384
  terminating = /* @__PURE__ */ new Set();
7204
7385
  constructor(config, options = {}) {
7205
7386
  super();
7387
+ this.setMaxListeners(0);
7206
7388
  this.coordinatorId = config.coordinatorId;
7207
7389
  this.config = config;
7208
7390
  this.runner = options.runner;
@@ -7597,7 +7779,13 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7597
7779
  let result;
7598
7780
  budget.start();
7599
7781
  try {
7600
- 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
+ );
7601
7789
  result = {
7602
7790
  subagentId,
7603
7791
  taskId: task.id,
@@ -7624,7 +7812,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7624
7812
  }
7625
7813
  this.recordCompletion(result);
7626
7814
  }
7627
- async executeWithTimeout(runner, task, ctx, budget) {
7815
+ async executeWithTimeout(runner, task, ctx, budget, preemptFraction = TIMEOUT_PREEMPT_FRACTION) {
7628
7816
  const initialTimeoutMs = budget.limits.timeoutMs;
7629
7817
  const idleLimitMs = budget.limits.idleTimeoutMs;
7630
7818
  if (initialTimeoutMs === void 0 && idleLimitMs === void 0) {
@@ -7632,8 +7820,21 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7632
7820
  }
7633
7821
  const start = Date.now();
7634
7822
  let timer = null;
7635
- 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;
7636
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
+ };
7637
7838
  const armFor = (ms) => {
7638
7839
  if (timer) clearTimeout(timer);
7639
7840
  timer = setTimeout(onTick, Math.max(0, ms));
@@ -7642,7 +7843,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7642
7843
  const wallLimit = budget.limits.timeoutMs ?? initialTimeoutMs;
7643
7844
  const wallRemaining = initialTimeoutMs === void 0 ? Number.POSITIVE_INFINITY : wallLimit - (Date.now() - start);
7644
7845
  const idleRemaining = idleLimitMs === void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
7645
- 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);
7646
7847
  armFor(Math.max(25, Math.min(wallRemaining, idleRemaining, preemptRemaining)));
7647
7848
  };
7648
7849
  const negotiateTimeout = async (used, limit) => {
@@ -7652,16 +7853,42 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7652
7853
  kind: "timeout",
7653
7854
  used,
7654
7855
  limit,
7655
- requestDecision: () => new Promise((resolveDecision) => {
7656
- budget._events?.emit("budget.threshold_reached", {
7657
- kind: "timeout",
7658
- used,
7659
- limit,
7660
- timeoutMs: 6e4,
7661
- extend: (extra) => resolveDecision({ extend: extra }),
7662
- 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
+ });
7663
7890
  });
7664
- })
7891
+ }
7665
7892
  });
7666
7893
  return typeof result === "string" ? result : await result;
7667
7894
  };
@@ -7672,21 +7899,45 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7672
7899
  const wallExceeded = wallLimit !== void 0 && elapsed >= wallLimit;
7673
7900
  const idleExceeded = idleLimit !== void 0 && budget.idleMs() >= idleLimit;
7674
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
+ });
7675
7912
  this.subagents.get(ctx.subagentId)?.abortController.abort();
7676
- reject(new BudgetExceededError("timeout", idleLimit ?? 0, budget.idleMs()));
7913
+ reject(new BudgetExceededError("idle_timeout", idleLimit ?? 0, budget.idleMs()));
7677
7914
  return;
7678
7915
  }
7679
- 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);
7680
7925
  try {
7681
7926
  const decision = await negotiateTimeout(elapsed, wallLimit);
7682
7927
  if (typeof decision !== "string" && decision.extend.timeoutMs !== void 0) {
7683
- budget.limits.timeoutMs = decision.extend.timeoutMs;
7684
- preemptedForLimit = null;
7928
+ budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
7929
+ lastGrantActivityTs = Date.now() - budget.idleMs();
7930
+ preemptState = "active" /* ACTIVE */;
7931
+ preemptedCeiling = null;
7685
7932
  } else {
7686
- preemptedForLimit = wallLimit;
7933
+ preemptState = "locked" /* LOCKED */;
7934
+ preemptedCeiling = wallLimit;
7687
7935
  }
7688
7936
  } catch {
7689
- preemptedForLimit = wallLimit;
7937
+ preemptState = "locked" /* LOCKED */;
7938
+ preemptedCeiling = wallLimit;
7939
+ } finally {
7940
+ budget.clearWatchdogNegotiation();
7690
7941
  }
7691
7942
  scheduleNext();
7692
7943
  return;
@@ -7701,26 +7952,41 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7701
7952
  reject(new BudgetExceededError("timeout", limit, elapsed));
7702
7953
  return;
7703
7954
  }
7955
+ budget.setWatchdogNegotiation(limit);
7704
7956
  try {
7705
7957
  const decision = await negotiateTimeout(elapsed, limit);
7706
- if (decision === "continue" || decision === "throw" || decision === "stop") {
7707
- 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;
7708
7965
  armFor(Math.max(1e3, limit));
7709
7966
  return;
7710
7967
  }
7968
+ if (decision === "stop") {
7969
+ terminate("timeout", limit, elapsed);
7970
+ return;
7971
+ }
7711
7972
  if (decision.extend.timeoutMs !== void 0) {
7712
- budget.limits.timeoutMs = decision.extend.timeoutMs;
7713
- preemptedForLimit = null;
7973
+ budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
7974
+ lastGrantActivityTs = Date.now() - budget.idleMs();
7975
+ preemptState = "active" /* ACTIVE */;
7976
+ preemptedCeiling = null;
7714
7977
  scheduleNext();
7715
7978
  return;
7716
7979
  }
7717
- this.subagents.get(ctx.subagentId)?.abortController.abort();
7718
- reject(new BudgetExceededError("timeout", limit, elapsed));
7980
+ terminate("timeout", limit, elapsed);
7981
+ return;
7719
7982
  } catch (err) {
7720
7983
  this.subagents.get(ctx.subagentId)?.abortController.abort();
7721
7984
  reject(
7722
7985
  err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
7723
7986
  );
7987
+ return;
7988
+ } finally {
7989
+ budget.clearWatchdogNegotiation();
7724
7990
  }
7725
7991
  };
7726
7992
  scheduleNext();
@@ -7883,7 +8149,7 @@ var ParallelEternalEngine = class {
7883
8149
  console.error(JSON.stringify({
7884
8150
  level: "error",
7885
8151
  event: "engine.persist_state_failed",
7886
- message: err instanceof Error ? err.message : String(err),
8152
+ message: toErrorMessage(err),
7887
8153
  context: { expectedState: "stopped" },
7888
8154
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
7889
8155
  }));
@@ -7919,7 +8185,7 @@ var ParallelEternalEngine = class {
7919
8185
  );
7920
8186
  await this.appendFailure(
7921
8187
  "engine error",
7922
- err instanceof Error ? err.message : String(err)
8188
+ toErrorMessage(err)
7923
8189
  );
7924
8190
  }
7925
8191
  if (this.stopRequested) break;
@@ -8598,12 +8864,12 @@ var DefaultSkillLoader = class {
8598
8864
  }
8599
8865
  async find(name) {
8600
8866
  const all = await this.list();
8601
- return all.find((s) => s.name === name);
8867
+ const lower = name.toLowerCase();
8868
+ return all.find((s) => s.name.toLowerCase() === lower);
8602
8869
  }
8603
8870
  async manifestText() {
8604
- const skills = await this.list();
8605
- if (skills.length === 0) return "";
8606
8871
  const entries = await this.listEntries();
8872
+ if (entries.length === 0) return "";
8607
8873
  const lines = ["## Available skills"];
8608
8874
  for (const e of entries) {
8609
8875
  const scopeTag = e.scope.length > 0 ? ` \u2014 ${e.scope.slice(0, 3).join(", ")}` : "";
@@ -8617,12 +8883,8 @@ var DefaultSkillLoader = class {
8617
8883
  const skills = await this.list();
8618
8884
  const entries = [];
8619
8885
  for (const s of skills) {
8620
- try {
8621
- const raw = await fs.readFile(s.path, "utf8");
8622
- const { trigger, scope } = parseDescription(raw);
8623
- entries.push({ name: s.name, trigger, scope, source: s.source, path: s.path });
8624
- } catch {
8625
- }
8886
+ const { trigger, scope } = parseDescriptionFromText(s.description ?? "");
8887
+ entries.push({ name: s.name, trigger, scope, source: s.source, path: s.path });
8626
8888
  }
8627
8889
  this.entriesCache = entries;
8628
8890
  return entries;
@@ -8633,16 +8895,17 @@ var DefaultSkillLoader = class {
8633
8895
  this.bodyCache.clear();
8634
8896
  }
8635
8897
  async readBody(name) {
8636
- const cached = this.bodyCache.get(name);
8898
+ const key = name.toLowerCase();
8899
+ const cached = this.bodyCache.get(key);
8637
8900
  if (cached !== void 0) return cached;
8638
8901
  const m = await this.find(name);
8639
8902
  if (!m) throw new Error(`Skill "${name}" not found`);
8640
8903
  const body = await fs.readFile(m.path, "utf8");
8641
- this.bodyCache.set(name, body);
8904
+ this.bodyCache.set(key, body);
8642
8905
  return body;
8643
8906
  }
8644
8907
  async readSaveBody(name) {
8645
- const key = `save:${name}`;
8908
+ const key = `save:${name.toLowerCase()}`;
8646
8909
  const cached = this.bodyCache.get(key);
8647
8910
  if (cached !== void 0) return cached;
8648
8911
  const m = await this.find(name);
@@ -8703,15 +8966,13 @@ function parseFrontmatter(raw) {
8703
8966
  flush();
8704
8967
  return out;
8705
8968
  }
8706
- function parseDescription(raw) {
8707
- const fm = parseFrontmatter(raw);
8708
- const desc = fm.description ?? "";
8969
+ function parseDescriptionFromText(desc) {
8709
8970
  const firstSentenceEnd = desc.indexOf(". ");
8710
8971
  const trigger = firstSentenceEnd !== -1 ? desc.slice(0, firstSentenceEnd + 1).trim() : desc.trim().split("\n")[0] ?? "";
8711
8972
  const scope = [];
8712
8973
  const coversMatch = /(?:covers|for|including)\s+([^.]+)/i.exec(desc);
8713
8974
  if (coversMatch) {
8714
- const items = coversMatch[1] ?? "".replace(/[·•]/g, ",").split(",").map((s) => s.trim()).filter(Boolean);
8975
+ const items = (coversMatch[1] ?? "").replace(/[·•]/g, ",").split(",").map((s) => s.trim()).filter(Boolean);
8715
8976
  scope.push(...items);
8716
8977
  }
8717
8978
  return { trigger, scope };