@wrongstack/core 0.264.0 → 0.267.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/{agent-bridge-D8sa1vtv.d.ts → agent-bridge-STJ3JwwK.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-c9DLkaas.d.ts → agent-subagent-runner-CzPGP3jA.d.ts} +131 -11
  3. package/dist/{brain-O1IdKPaK.d.ts → brain-Cdg77tVN.d.ts} +103 -2
  4. package/dist/{compactor-BBy0rCtB.d.ts → compactor-iMZ84CXq.d.ts} +19 -1
  5. package/dist/{config-Dz2F3H2K.d.ts → config-Du3pYYln.d.ts} +132 -13
  6. package/dist/{context-BGSpZNSE.d.ts → context-dT5Ueund.d.ts} +90 -12
  7. package/dist/coordination/index.d.ts +78 -22
  8. package/dist/coordination/index.js +695 -273
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/{default-config-CXsDvOmP.d.ts → default-config-B0cj-Hry.d.ts} +11 -1
  11. package/dist/defaults/index.d.ts +28 -28
  12. package/dist/defaults/index.js +2327 -965
  13. package/dist/defaults/index.js.map +1 -1
  14. package/dist/execution/index.d.ts +16 -16
  15. package/dist/execution/index.js +1500 -371
  16. package/dist/execution/index.js.map +1 -1
  17. package/dist/execution/prompt-enhancer.d.ts +2 -2
  18. package/dist/execution/prompt-enhancer.js +1 -1
  19. package/dist/execution/prompt-enhancer.js.map +1 -1
  20. package/dist/extension/index.d.ts +6 -6
  21. package/dist/{goal-preamble-DzjFuN3p.d.ts → goal-preamble-SulMTowG.d.ts} +33 -12
  22. package/dist/{goal-store-CxWmCGbH.d.ts → goal-store-CABDwdFE.d.ts} +1 -1
  23. package/dist/{index-CbLSI66_.d.ts → index-Bms0m4oy.d.ts} +5 -5
  24. package/dist/{index-CYIQrXVF.d.ts → index-DtCVWel4.d.ts} +8 -8
  25. package/dist/index-IEuxQd-E.d.ts +82 -0
  26. package/dist/index.d.ts +261 -57
  27. package/dist/index.js +4799 -2212
  28. package/dist/index.js.map +1 -1
  29. package/dist/infrastructure/index.d.ts +6 -6
  30. package/dist/infrastructure/index.js +84 -9
  31. package/dist/infrastructure/index.js.map +1 -1
  32. package/dist/kernel/index.d.ts +9 -9
  33. package/dist/kernel/index.js +1 -1
  34. package/dist/kernel/index.js.map +1 -1
  35. package/dist/{mcp-servers-DC4QRPUI.d.ts → mcp-servers-C2cBTxUR.d.ts} +3 -3
  36. package/dist/models/index.d.ts +5 -5
  37. package/dist/models/index.js +104 -31
  38. package/dist/models/index.js.map +1 -1
  39. package/dist/{models-registry-B_siPxqN.d.ts → models-registry-BqGZNJQ-.d.ts} +1 -1
  40. package/dist/{multi-agent-coordinator-CK5Jdj9K.d.ts → multi-agent-coordinator-B8R43uPz.d.ts} +1 -1
  41. package/dist/{null-fleet-bus-DgvD4SCO.d.ts → null-fleet-bus-CnXa5oTH.d.ts} +14 -9
  42. package/dist/observability/index.d.ts +2 -2
  43. package/dist/{parallel-eternal-engine-bK0JQBR_.d.ts → parallel-eternal-engine-DdNnw9BQ.d.ts} +11 -9
  44. package/dist/{path-resolver-BPEDlN38.d.ts → path-resolver-COIMLCQL.d.ts} +3 -3
  45. package/dist/{permission-4yvGmMRB.d.ts → permission-B75JAi3-.d.ts} +1 -1
  46. package/dist/{permission-policy-C6XpsBOy.d.ts → permission-policy-DlR9eJAM.d.ts} +2 -2
  47. package/dist/{pipeline-CXCeMz8J.d.ts → pipeline-BfD2k1rT.d.ts} +3 -3
  48. package/dist/{plan-templates-BvzRBkJc.d.ts → plan-templates-DSIKCXZN.d.ts} +32 -8
  49. package/dist/provider-model-resolve-BNRsNuJx.d.ts +107 -0
  50. package/dist/{provider-runner-C5aQpDWE.d.ts → provider-runner-CX7iIvox.d.ts} +3 -3
  51. package/dist/{retry-policy-CFhdtRzz.d.ts → retry-policy-BilV1ujH.d.ts} +1 -1
  52. package/dist/sdd/index.d.ts +8 -8
  53. package/dist/sdd/index.js +286 -105
  54. package/dist/sdd/index.js.map +1 -1
  55. package/dist/secret-vault-BAKpgFw_.d.ts +57 -0
  56. package/dist/{secret-vault-CxiVLbt1.d.ts → secret-vault-gkvEZZfE.d.ts} +43 -4
  57. package/dist/security/index.d.ts +6 -68
  58. package/dist/security/index.js +296 -95
  59. package/dist/security/index.js.map +1 -1
  60. package/dist/{selector-gIuhRTkN.d.ts → selector-Bc7eWtT3.d.ts} +1 -1
  61. package/dist/{session-event-bridge-DkvvrpDt.d.ts → session-event-bridge-D-araDEz.d.ts} +1 -1
  62. package/dist/{session-reader-KdfVwkKP.d.ts → session-reader-D7Dapswh.d.ts} +1 -1
  63. package/dist/storage/index.d.ts +112 -15
  64. package/dist/storage/index.js +491 -156
  65. package/dist/storage/index.js.map +1 -1
  66. package/dist/tools/index.d.ts +4 -2
  67. package/dist/tools/index.js.map +1 -1
  68. package/dist/types/index.d.ts +21 -21
  69. package/dist/types/index.js +1523 -450
  70. package/dist/types/index.js.map +1 -1
  71. package/dist/utils/index.d.ts +455 -407
  72. package/dist/utils/index.js +2191 -1203
  73. package/dist/utils/index.js.map +1 -1
  74. package/dist/{wstack-paths-CJjEwPXn.d.ts → wstack-paths-hOpNLmvf.d.ts} +2 -0
  75. package/package.json +1 -1
  76. package/skills/api-design/SKILL.md +1 -1
  77. package/skills/audit-log/SKILL.md +6 -6
  78. package/skills/bug-hunter/SKILL.md +5 -5
  79. package/skills/chimera/SKILL.md +4 -4
  80. package/skills/docker-deploy/SKILL.md +1 -1
  81. package/skills/git-flow/SKILL.md +3 -3
  82. package/skills/multi-agent/SKILL.md +3 -3
  83. package/skills/node-modern/SKILL.md +1 -0
  84. package/skills/observability/SKILL.md +2 -2
  85. package/skills/output-standards/SKILL.md +51 -28
  86. package/skills/refactor-planner/SKILL.md +3 -3
  87. package/skills/security-scanner/SKILL.md +4 -3
  88. package/skills/tech-stack/SKILL.md +1 -2
  89. package/dist/llm-selector-DzxuZnNz.d.ts +0 -58
  90. package/dist/secret-vault-BJDY28ev.d.ts +0 -25
@@ -1,11 +1,81 @@
1
+ import * as path3 from 'path';
1
2
  import { randomUUID, randomBytes, createHash } from 'crypto';
2
3
  import * as fs from 'fs/promises';
3
- import * as path2 from 'path';
4
4
  import * as os from 'os';
5
5
  import { execFile } from 'child_process';
6
6
  import { promisify } from 'util';
7
7
  import { EventEmitter } from 'events';
8
8
 
9
+ // src/utils/tool-wire-compact.ts
10
+ var TOOL_DESCRIPTION_MAX_CHARS = 640;
11
+ var SCHEMA_DESCRIPTION_MAX_CHARS = 180;
12
+ var compactCache = /* @__PURE__ */ new WeakMap();
13
+ function compactToolDefinitionForWire(tool, opts = {}) {
14
+ const useDefaultOptions = opts.descriptionMaxChars === void 0 && opts.schemaDescriptionMaxChars === void 0;
15
+ if (useDefaultOptions && typeof tool === "object" && tool !== null) {
16
+ const cached = compactCache.get(tool);
17
+ if (cached) return cached;
18
+ }
19
+ const compact = {
20
+ name: tool.name,
21
+ description: compactDescription(
22
+ tool.description ?? "",
23
+ opts.descriptionMaxChars ?? TOOL_DESCRIPTION_MAX_CHARS
24
+ ),
25
+ inputSchema: compactSchemaDescriptions(
26
+ tool.inputSchema,
27
+ opts.schemaDescriptionMaxChars ?? SCHEMA_DESCRIPTION_MAX_CHARS
28
+ )
29
+ };
30
+ if (useDefaultOptions && typeof tool === "object" && tool !== null) {
31
+ compactCache.set(tool, compact);
32
+ }
33
+ return compact;
34
+ }
35
+ function compactSchemaDescriptions(schema, maxDescriptionChars = SCHEMA_DESCRIPTION_MAX_CHARS) {
36
+ const compact = compactSchemaNode(schema, maxDescriptionChars);
37
+ return isRecord(compact) ? compact : { type: "object", properties: {} };
38
+ }
39
+ function compactSchemaNode(node, maxDescriptionChars) {
40
+ if (Array.isArray(node)) {
41
+ return node.map((item) => compactSchemaNode(item, maxDescriptionChars));
42
+ }
43
+ if (!isRecord(node)) return node;
44
+ const out = {};
45
+ for (const [key, value] of Object.entries(node)) {
46
+ if (key === "description" && typeof value === "string") {
47
+ out[key] = compactDescription(value, maxDescriptionChars);
48
+ } else {
49
+ out[key] = compactSchemaNode(value, maxDescriptionChars);
50
+ }
51
+ }
52
+ return out;
53
+ }
54
+ function compactDescription(text, maxChars) {
55
+ const normalized = text.replace(/\s+/g, " ").trim();
56
+ if (normalized.length <= maxChars) return normalized;
57
+ if (maxChars <= 20) return normalized.slice(0, maxChars);
58
+ const hardLimit = maxChars - 12;
59
+ const boundary = findSemanticBoundary(normalized, hardLimit);
60
+ const head = normalized.slice(0, boundary > 0 ? boundary : hardLimit).trimEnd();
61
+ return `${head} ...`;
62
+ }
63
+ function findSemanticBoundary(text, limit) {
64
+ const punctuation = Math.max(
65
+ text.lastIndexOf(". ", limit),
66
+ text.lastIndexOf("; ", limit),
67
+ text.lastIndexOf(": ", limit)
68
+ );
69
+ if (punctuation >= Math.floor(limit * 0.45)) return punctuation + 1;
70
+ const comma = text.lastIndexOf(", ", limit);
71
+ if (comma >= Math.floor(limit * 0.6)) return comma + 1;
72
+ const space = text.lastIndexOf(" ", limit);
73
+ return space >= Math.floor(limit * 0.6) ? space : limit;
74
+ }
75
+ function isRecord(value) {
76
+ return !!value && typeof value === "object" && !Array.isArray(value);
77
+ }
78
+
9
79
  // src/utils/token-estimate.ts
10
80
  var RoughTokenEstimate = (text, charsPerToken = 3.5) => Math.max(1, Math.ceil(text.length / charsPerToken));
11
81
  var CALIBRATION_GLOBAL_KEY = "__global__";
@@ -25,12 +95,9 @@ function getCachedEstimate(key, compute) {
25
95
  const existing = ESTIMATE_CACHE.get(key);
26
96
  if (existing !== void 0) return existing;
27
97
  if (ESTIMATE_CACHE.size >= ESTIMATE_CACHE_MAX_SIZE) {
28
- let evicted = 0;
29
- const maxEvict = Math.floor(ESTIMATE_CACHE_MAX_SIZE / 4);
30
98
  for (const k of ESTIMATE_CACHE.keys()) {
31
- if (evicted >= maxEvict) break;
99
+ if (ESTIMATE_CACHE.size <= Math.floor(ESTIMATE_CACHE_MAX_SIZE / 2)) break;
32
100
  ESTIMATE_CACHE.delete(k);
33
- evicted++;
34
101
  }
35
102
  }
36
103
  const estimate = compute(key);
@@ -76,7 +143,8 @@ function estimateMessageTokens(messages) {
76
143
  function estimateToolDefTokens(tool) {
77
144
  const cached = tool._estDefTokens;
78
145
  if (typeof cached === "number" && cached > 0) return cached;
79
- return RoughTokenEstimate(tool.name) + RoughTokenEstimate(tool.description ?? "") + RoughTokenEstimate(JSON.stringify(tool.inputSchema));
146
+ const compact = compactToolDefinitionForWire(tool);
147
+ return RoughTokenEstimate(tool.name) + RoughTokenEstimate(compact.description) + RoughTokenEstimate(JSON.stringify(compact.inputSchema));
80
148
  }
81
149
  function estimateRequestTokens(messages, systemPrompt, tools, calibrationKey = CALIBRATION_GLOBAL_KEY) {
82
150
  let messagesTokens = 0;
@@ -254,6 +322,79 @@ function isEmptyMessage(msg) {
254
322
  if (typeof msg.content === "string") return msg.content.trim().length === 0;
255
323
  return msg.content.length === 0;
256
324
  }
325
+ var MAX_DIGEST_CHARS = 4e3;
326
+ function createContextEvidenceState() {
327
+ return {
328
+ sessionGoals: [],
329
+ implicitFacts: [],
330
+ activeErrors: [],
331
+ toolCalls: [],
332
+ fileGraph: {},
333
+ repeatedReads: [],
334
+ updatedAt: Date.now()
335
+ };
336
+ }
337
+ function buildContextEvidenceDigest(ctx) {
338
+ const state = ensureEvidence(ctx);
339
+ const lines = [];
340
+ if (state.currentIntent?.text) {
341
+ lines.push(`intent: ${state.currentIntent.text}`);
342
+ }
343
+ const goals = state.sessionGoals.slice(-3);
344
+ if (goals.length > 0) {
345
+ lines.push("session_goals:");
346
+ for (const goal of goals) lines.push(`- ${goal}`);
347
+ }
348
+ const activeErrors = state.activeErrors.slice(-5);
349
+ if (activeErrors.length > 0) {
350
+ lines.push("active_errors:");
351
+ for (const err of activeErrors) lines.push(`- ${err}`);
352
+ }
353
+ const files = Object.values(state.fileGraph).sort((a, b) => b.writes - a.writes || b.reads - a.reads || a.path.localeCompare(b.path)).slice(0, 12);
354
+ if (files.length > 0) {
355
+ lines.push("dependency_graph:");
356
+ for (const file of files) {
357
+ const actions = [
358
+ file.reads > 0 ? `read ${file.reads}x` : "",
359
+ file.writes > 0 ? `write ${file.writes}x` : ""
360
+ ].filter(Boolean).join(", ");
361
+ const refs = file.referenced ? "; referenced by assistant" : "";
362
+ const via = file.lastToolUseId ? `; last via ${file.lastToolUseId}` : "";
363
+ lines.push(`- ${file.path} (${actions || "seen"}${refs}${via})`);
364
+ }
365
+ }
366
+ const referenced = state.toolCalls.filter((tool) => tool.status === "referenced").slice(-10);
367
+ const recentSeen = state.toolCalls.filter((tool) => tool.status === "seen").slice(-5);
368
+ const trail = [...referenced, ...recentSeen];
369
+ if (trail.length > 0) {
370
+ lines.push("tool_trail:");
371
+ for (const tool of trail) {
372
+ const size = tool.outputTokens ? `; ~${tool.outputTokens} tokens` : "";
373
+ const filesText = tool.files.length > 0 ? `; files=${tool.files.slice(0, 4).join(", ")}` : "";
374
+ const symbolsText = tool.symbols.length > 0 ? `; symbols=${tool.symbols.slice(0, 4).join(", ")}` : "";
375
+ lines.push(
376
+ `- ${tool.toolUseId} ${tool.toolName} ${tool.status}: ${tool.summary}${filesText}${symbolsText}${size}`
377
+ );
378
+ }
379
+ }
380
+ const facts = state.implicitFacts.slice(-8);
381
+ if (facts.length > 0) {
382
+ lines.push("implicit_facts:");
383
+ for (const fact of facts) lines.push(`- ${fact}`);
384
+ }
385
+ const digest = lines.join("\n");
386
+ if (digest.length <= MAX_DIGEST_CHARS) return digest;
387
+ return `${digest.slice(0, MAX_DIGEST_CHARS)}... [+${digest.length - MAX_DIGEST_CHARS} chars]`;
388
+ }
389
+ function repeatedReadPressure(ctx) {
390
+ return ensureEvidence(ctx).repeatedReads.reduce((max, item) => Math.max(max, item.count), 0);
391
+ }
392
+ function ensureEvidence(ctx) {
393
+ if (!ctx.contextEvidence) {
394
+ ctx.contextEvidence = createContextEvidenceState();
395
+ }
396
+ return ctx.contextEvidence;
397
+ }
257
398
 
258
399
  // src/types/blocks.ts
259
400
  function isTextBlock(b) {
@@ -261,7 +402,11 @@ function isTextBlock(b) {
261
402
  }
262
403
 
263
404
  // src/execution/compaction-core.ts
405
+ function compactionDebugEnabled() {
406
+ return process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1";
407
+ }
264
408
  function emitCompactionMetrics(event, metrics) {
409
+ if (!compactionDebugEnabled()) return;
265
410
  console.log(
266
411
  JSON.stringify({
267
412
  level: "debug",
@@ -296,38 +441,41 @@ function findPreserveStart(messages, preserveK) {
296
441
  preserveStart = i;
297
442
  }
298
443
  }
299
- let forwardWalkIterations = 0;
300
- let forwardWalkInnerIterations = 0;
301
- for (let i = preserveStart; i < messages.length; i++) {
302
- forwardWalkIterations++;
303
- const m = messages[i];
304
- if (!m || typeof m.content === "string" || !Array.isArray(m.content)) continue;
305
- const hasToolUse3 = m.content.some((b) => {
306
- forwardWalkInnerIterations++;
307
- return b.type === "tool_use";
444
+ let pairRepairIterations = 0;
445
+ let pairRepairInnerIterations = 0;
446
+ while (preserveStart > 0) {
447
+ pairRepairIterations++;
448
+ const first = messages[preserveStart];
449
+ const prev = messages[preserveStart - 1];
450
+ if (!first || !prev || first.role !== "user" || prev.role !== "assistant") break;
451
+ if (typeof first.content === "string" || typeof prev.content === "string") break;
452
+ const resultIds = /* @__PURE__ */ new Set();
453
+ for (const block of first.content) {
454
+ pairRepairInnerIterations++;
455
+ if (block.type === "tool_result") resultIds.add(block.tool_use_id);
456
+ }
457
+ if (resultIds.size === 0) break;
458
+ const hasMatchingUse = prev.content.some((block) => {
459
+ pairRepairInnerIterations++;
460
+ return block.type === "tool_use" && resultIds.has(block.id);
308
461
  });
309
- if (hasToolUse3 && i + 1 < messages.length) {
310
- const next = messages[i + 1];
311
- if (next && next.role === "user" && typeof next.content !== "string" && Array.isArray(next.content) && next.content.some((b) => {
312
- forwardWalkInnerIterations++;
313
- return b.type === "tool_result";
314
- })) {
315
- preserveStart = i + 1;
316
- }
317
- }
462
+ if (!hasMatchingUse) break;
463
+ preserveStart--;
464
+ }
465
+ if (compactionDebugEnabled()) {
466
+ console.log(
467
+ JSON.stringify({
468
+ level: "debug",
469
+ event: "compaction.find_preserve_start.ended",
470
+ messageCount: messages.length,
471
+ preserveK,
472
+ preserveStart,
473
+ pairRepairIterations,
474
+ pairRepairInnerIterations,
475
+ pairRepairInnerPerOuter: pairRepairIterations > 0 ? pairRepairInnerIterations / pairRepairIterations : 0
476
+ })
477
+ );
318
478
  }
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
- );
331
479
  return preserveStart;
332
480
  }
333
481
  function eliseOldToolResults(messages, opts) {
@@ -341,7 +489,8 @@ function eliseOldToolResults(messages, opts) {
341
489
  if (!msg || !Array.isArray(msg.content)) continue;
342
490
  for (const b of msg.content) {
343
491
  fastPathInnerIterations++;
344
- if (b.type === "tool_result" && estimateToolResultTokens(b.content) >= opts.eliseThreshold) {
492
+ const oversized = b.type === "tool_result" && estimateToolResultTokens(b.content) >= opts.eliseThreshold || b.type === "tool_use" && estimateToolInputTokens(b.input) >= opts.eliseThreshold;
493
+ if (oversized) {
345
494
  hasOversized = true;
346
495
  break;
347
496
  }
@@ -375,6 +524,13 @@ function eliseOldToolResults(messages, opts) {
375
524
  }
376
525
  const original = msg.content;
377
526
  const newContent = original.map((b) => {
527
+ if (b.type === "tool_use") {
528
+ const tokens2 = estimateToolInputTokens(b.input);
529
+ if (tokens2 < opts.eliseThreshold) return b;
530
+ const elidedInput = summarizeToolUseInputElision(b, tokens2);
531
+ saved += Math.max(0, tokens2 - estimateToolInputTokens(elidedInput));
532
+ return { ...b, input: elidedInput };
533
+ }
378
534
  if (b.type !== "tool_result") return b;
379
535
  const tokens = estimateToolResultTokens(b.content);
380
536
  if (tokens < opts.eliseThreshold) return b;
@@ -382,7 +538,7 @@ function eliseOldToolResults(messages, opts) {
382
538
  const elided = {
383
539
  type: "tool_result",
384
540
  tool_use_id: b.tool_use_id,
385
- content: `[elided: ~${tokens} tokens]`,
541
+ content: summarizeToolResultElision(b, tokens),
386
542
  is_error: b.is_error
387
543
  };
388
544
  return elided;
@@ -394,7 +550,7 @@ function eliseOldToolResults(messages, opts) {
394
550
  changed = true;
395
551
  }
396
552
  fullPassInnerIterations += original.length;
397
- if (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1") {
553
+ if (compactionDebugEnabled()) {
398
554
  const ratio = fullPassInnerIterations / fullPassIterations;
399
555
  if (ratio > 10) {
400
556
  console.error(
@@ -422,6 +578,65 @@ function eliseOldToolResults(messages, opts) {
422
578
  });
423
579
  return { messages: changed ? next : messages, saved, changed };
424
580
  }
581
+ function summarizeToolUseInputElision(block, tokens) {
582
+ const fields = {};
583
+ for (const [key, value] of Object.entries(block.input ?? {})) {
584
+ fields[key] = summarizeToolUseInputValue(value);
585
+ }
586
+ return {
587
+ __elided_tool_input: `~${tokens} tokens; original arguments are in the session log`,
588
+ tool: block.name,
589
+ fields
590
+ };
591
+ }
592
+ function summarizeToolUseInputValue(value) {
593
+ if (value === null || value === void 0) return value;
594
+ if (typeof value === "number" || typeof value === "boolean") return value;
595
+ if (typeof value === "string") {
596
+ const oneLine = value.replace(/\s+/g, " ").trim();
597
+ return oneLine.length <= 160 ? oneLine : `${oneLine.slice(0, 120)}...(${oneLine.length} chars)`;
598
+ }
599
+ if (Array.isArray(value)) {
600
+ return `[array:${value.length}]`;
601
+ }
602
+ if (typeof value === "object") {
603
+ const keys = Object.keys(value);
604
+ return `[object:${keys.slice(0, 8).join(",")}${keys.length > 8 ? ",..." : ""}]`;
605
+ }
606
+ return String(value);
607
+ }
608
+ function summarizeToolResultElision(block, tokens) {
609
+ const parts = [`elided: ~${tokens} tokens`];
610
+ if (block.name) parts.push(`tool=${block.name}`);
611
+ const files = extractPathHints(block.content).slice(0, 5);
612
+ if (files.length > 0) parts.push(`files=${files.join(", ")}`);
613
+ const error = firstErrorLine(block.content);
614
+ if (error) parts.push(`error=${error}`);
615
+ return `[${parts.join("; ")}]`;
616
+ }
617
+ function extractPathHints(content) {
618
+ const text = typeof content === "string" ? content : JSON.stringify(content);
619
+ const out = /* @__PURE__ */ new Set();
620
+ const re = /(?:(?:[A-Za-z]:)?[./\\]?[\w@.-]+(?:[\\/][\w@(). -]+)+\.[A-Za-z0-9]{1,12})/g;
621
+ for (const match of text.matchAll(re)) {
622
+ const clean = match[0]?.replace(/\\/g, "/").replace(/^["'`]+|["'`),;:]+$/g, "");
623
+ if (clean && clean.length <= 220) out.add(clean);
624
+ if (out.size >= 5) break;
625
+ }
626
+ return [...out];
627
+ }
628
+ function firstErrorLine(content) {
629
+ const text = typeof content === "string" ? content : JSON.stringify(content);
630
+ for (const line of text.split(/\r?\n/)) {
631
+ if (!/\b(error|exception|failed|failure|fatal|panic|timeout|denied|enoent|eacces|eperm)\b/i.test(
632
+ line
633
+ ))
634
+ continue;
635
+ const trimmed = line.replace(/\s+/g, " ").trim();
636
+ if (trimmed) return trimmed.slice(0, 180);
637
+ }
638
+ return void 0;
639
+ }
425
640
  function buildLosslessDigest(messages) {
426
641
  const lines = [];
427
642
  for (const m of messages) {
@@ -532,15 +747,15 @@ function buildSmartDigest(messages) {
532
747
  lines.push(`[${m.role}]: ${display}${marker}`);
533
748
  }
534
749
  if (noiseCount > 0) {
535
- lines.push(`[system]: ${noiseCount} low-importance turn(s) collapsed (repeated failures / pure tool I/O)`);
750
+ lines.push(
751
+ `[system]: ${noiseCount} low-importance turn(s) collapsed (repeated failures / pure tool I/O)`
752
+ );
536
753
  }
537
754
  return lines.join("\n");
538
755
  }
539
756
  function countToolBlocks(m) {
540
757
  if (typeof m.content === "string") return 0;
541
- return m.content.filter(
542
- (b) => b.type === "tool_use" || b.type === "tool_result"
543
- ).length;
758
+ return m.content.filter((b) => b.type === "tool_use" || b.type === "tool_result").length;
544
759
  }
545
760
  function firstSentence(text) {
546
761
  const trimmed = text.trim();
@@ -609,10 +824,12 @@ var HybridCompactor = class {
609
824
  if (elide.changed) ctx.state.replaceMessages(elide.messages);
610
825
  if (elide.saved > 0) reductions.push({ phase: "elision", saved: elide.saved });
611
826
  let collapsedDigest;
827
+ let evidenceDigest;
612
828
  if (opts.aggressive) {
613
829
  const phase2 = this.collapseAncientTurns(ctx, preserveK);
614
830
  if (phase2.saved > 0) reductions.push({ phase: "summary", saved: phase2.saved });
615
831
  collapsedDigest = phase2.digest;
832
+ evidenceDigest = phase2.evidenceDigest;
616
833
  }
617
834
  const repaired = repairToolUseAdjacency(ctx.messages);
618
835
  if (repaired.report.changed) {
@@ -620,6 +837,11 @@ var HybridCompactor = class {
620
837
  }
621
838
  const afterTokens = estimateMessages(ctx.messages);
622
839
  const afterFull = this.estimateFullRequest(ctx);
840
+ const quality = checkCompactionQuality(ctx, {
841
+ collapsedDigest,
842
+ evidenceDigest,
843
+ reduced: beforeTokens > afterTokens || beforeFull > afterFull
844
+ });
623
845
  return {
624
846
  before: beforeTokens,
625
847
  after: afterTokens,
@@ -627,6 +849,8 @@ var HybridCompactor = class {
627
849
  fullRequestTokensAfter: afterFull,
628
850
  reductions,
629
851
  collapsedDigest,
852
+ evidenceDigest,
853
+ quality,
630
854
  repaired: repaired.report.changed ? {
631
855
  removedToolUses: repaired.report.removedToolUses,
632
856
  removedToolResults: repaired.report.removedToolResults,
@@ -668,7 +892,13 @@ var HybridCompactor = class {
668
892
  if (boundary <= 0) return { saved: 0 };
669
893
  const removed = messages.slice(0, boundary);
670
894
  const removedTokens = estimateMessages(removed);
671
- const digest = this.smart ? buildSmartDigest(removed) || `${removed.length} earlier turns (no textual content; tool I/O omitted \u2014 see session log)` : buildLosslessDigest(removed) || `${removed.length} earlier turns (no textual content; tool I/O omitted \u2014 see session log)`;
895
+ const historyDigest = this.smart ? buildSmartDigest(removed) || `${removed.length} earlier turns (no textual content; tool I/O omitted; see session log)` : buildLosslessDigest(removed) || `${removed.length} earlier turns (no textual content; tool I/O omitted; see session log)`;
896
+ const evidenceDigest = buildContextEvidenceDigest(ctx);
897
+ const digest = evidenceDigest ? `[context_state]
898
+ ${evidenceDigest}
899
+
900
+ [prior_history]
901
+ ${historyDigest}` : historyDigest;
672
902
  const summaryMsg = {
673
903
  role: "system",
674
904
  content: `[prior_turns_digest: ${digest}]`
@@ -677,10 +907,29 @@ var HybridCompactor = class {
677
907
  ctx.state.replaceMessages([summaryMsg, ...tail]);
678
908
  return {
679
909
  saved: Math.max(0, removedTokens - estimateMessages([summaryMsg])),
680
- digest
910
+ digest,
911
+ evidenceDigest: evidenceDigest || void 0
681
912
  };
682
913
  }
683
914
  };
915
+ function checkCompactionQuality(ctx, opts) {
916
+ const evidence = ctx.contextEvidence;
917
+ const digest = `${opts.collapsedDigest ?? ""}
918
+ ${opts.evidenceDigest ?? ""}`;
919
+ const hasIntent = Boolean(evidence?.currentIntent?.text || /\b(intent|goal|session_goals)\b/i.test(digest));
920
+ const hasPathTrail = Boolean(
921
+ Object.keys(evidence?.fileGraph ?? {}).length > 0 || (evidence?.toolCalls.length ?? 0) > 0 || /\b(dependency_graph|tool_trail|files=)\b/i.test(digest)
922
+ );
923
+ const issues = [];
924
+ if (opts.reduced && !hasIntent) issues.push("missing intent anchor");
925
+ if (opts.reduced && !hasPathTrail) issues.push("missing tool/path trail");
926
+ return {
927
+ ok: issues.length === 0,
928
+ hasIntent,
929
+ hasPathTrail,
930
+ issues
931
+ };
932
+ }
684
933
  function readContextWindowPolicy(ctx) {
685
934
  const policy = ctx.meta?.["contextWindowPolicy"];
686
935
  if (!policy || typeof policy !== "object") return null;
@@ -801,7 +1050,12 @@ var IntelligentCompactor = class {
801
1050
  };
802
1051
  const ac = ctx.signal ? void 0 : new AbortController();
803
1052
  const signal = ctx.signal ?? ac?.signal;
804
- const res = await this.provider.complete(req, { signal });
1053
+ let res;
1054
+ try {
1055
+ res = await this.provider.complete(req, { signal });
1056
+ } finally {
1057
+ ac?.abort();
1058
+ }
805
1059
  const textBlocks = res.content.filter(isTextBlock);
806
1060
  return textBlocks.map((b) => b.text).join("\n").trim() || "(empty summary)";
807
1061
  }
@@ -845,9 +1099,9 @@ Rules:
845
1099
  - If unsure, keep rather than collapse (errors are more costly than waste)
846
1100
 
847
1101
  Return ONLY the JSON object, no markdown, no explanation outside the JSON.`;
848
- function formatMessages(messages, maxChars = 8e3) {
1102
+ function formatMessages(messages, maxTokens = 2048) {
849
1103
  const lines = [];
850
- let used = 0;
1104
+ let usedTokens = 0;
851
1105
  for (let i = 0; i < messages.length; i++) {
852
1106
  const m = expectDefined(messages[i]);
853
1107
  const role = m.role.padEnd(10, " ");
@@ -859,13 +1113,14 @@ function formatMessages(messages, maxChars = 8e3) {
859
1113
  text = content.filter(isTextBlock).map((b) => b.text).join(" ");
860
1114
  const toolUses = content.filter((b) => b.type === "tool_use");
861
1115
  if (toolUses.length > 0) {
862
- text += ` [tools: ${toolUses.map((b) => b.name).join(", ")}]`;
1116
+ text += ` [tools: ${toolUses.map((b) => b.name).filter(Boolean).join(", ")}]`;
863
1117
  }
864
1118
  }
865
1119
  const line = `[${i}][${role}]: ${text}`;
866
- if (used + line.length > maxChars) break;
1120
+ const lineTokens = estimateTextTokens(line);
1121
+ if (usedTokens + lineTokens > maxTokens) break;
867
1122
  lines.push(line);
868
- used += line.length;
1123
+ usedTokens += lineTokens;
869
1124
  }
870
1125
  return lines.join("\n");
871
1126
  }
@@ -874,20 +1129,29 @@ var LLMSelector = class {
874
1129
  model;
875
1130
  maxContextTokens;
876
1131
  systemPrompt;
1132
+ maxOutputTokens;
877
1133
  constructor(opts) {
878
1134
  this.provider = opts.provider;
879
1135
  this.model = opts.model ?? "unknown";
1136
+ if (this.model === "unknown" && (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1")) {
1137
+ console.warn(
1138
+ "[LLMSelector] model not set \u2014 selector will use the provider default. Set `model` explicitly in LLMSelectorOptions to silence this warning."
1139
+ );
1140
+ }
880
1141
  this.maxContextTokens = opts.maxContextTokens ?? 4e4;
881
1142
  this.systemPrompt = opts.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
1143
+ this.maxOutputTokens = opts.maxOutputTokens ?? 1024;
882
1144
  }
883
1145
  async select(messages, maxToKeep) {
884
1146
  const effectiveBudget = Math.min(maxToKeep, this.maxContextTokens);
885
- const historyText = formatMessages(messages);
886
1147
  const totalTokens = estimateMessageTokens(messages);
887
1148
  const systemText = `${this.systemPrompt}
888
1149
 
889
1150
  Conversation (${messages.length} messages, ~${totalTokens} tokens, budget: ${effectiveBudget}):
890
1151
  `;
1152
+ const systemTokens = estimateTextTokens(systemText);
1153
+ const historyBudget = Math.max(512, effectiveBudget - systemTokens - this.maxOutputTokens);
1154
+ const historyText = formatMessages(messages, historyBudget);
891
1155
  const budgetInstruction = totalTokens > effectiveBudget ? `
892
1156
 
893
1157
  IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiveBudget}). You MUST collapse enough to fit. Prefer collapsing older/lower-importance ranges.` : "";
@@ -895,18 +1159,26 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
895
1159
  model: this.model,
896
1160
  system: [{ type: "text", text: systemText + budgetInstruction }],
897
1161
  messages: [{ role: "user", content: historyText }],
898
- maxTokens: 1024
1162
+ maxTokens: this.maxOutputTokens
899
1163
  };
900
1164
  let raw;
1165
+ const ac = new AbortController();
901
1166
  try {
902
- const ac = new AbortController();
903
- const res = await this.provider.complete(req, { signal: ac.signal });
1167
+ const timeoutSignal = AbortSignal.timeout(3e4);
1168
+ const res = await this.provider.complete(req, {
1169
+ signal: AbortSignal.any([ac.signal, timeoutSignal])
1170
+ });
904
1171
  const textBlocks = res.content.filter(isTextBlock);
905
1172
  raw = textBlocks.map((b) => b.text).join("\n").trim();
906
- } catch (_err) {
1173
+ } catch (err) {
1174
+ if (err instanceof Error) {
1175
+ console.warn("[LLMSelector] selector call failed, using recency fallback:", err.message);
1176
+ }
907
1177
  return this.fallbackSelect(messages, effectiveBudget);
1178
+ } finally {
1179
+ ac.abort();
908
1180
  }
909
- return this.parseSelectorOutput(raw, messages.length);
1181
+ return this.parseSelectorOutput(raw, messages);
910
1182
  }
911
1183
  fallbackSelect(messages, budget) {
912
1184
  const toKeep = [];
@@ -933,34 +1205,63 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
933
1205
  reasoning: `Fallback: kept last ${messages.length - startIdx} messages within ${budget} token budget`
934
1206
  };
935
1207
  }
936
- parseSelectorOutput(raw, messageCount) {
1208
+ /**
1209
+ * Parse and validate the raw LLM output into a SelectorResult.
1210
+ * Falls back to recency-based selection if the LLM output is malformed,
1211
+ * out-of-bounds, or internally inconsistent.
1212
+ */
1213
+ parseSelectorOutput(raw, messages) {
1214
+ const messageCount = messages.length;
1215
+ if (messageCount === 0) {
1216
+ return { kept: [], collapsed: [], reasoning: "empty session" };
1217
+ }
937
1218
  const jsonStart = raw.indexOf("{");
938
1219
  const jsonEnd = raw.lastIndexOf("}");
939
1220
  if (jsonStart === -1 || jsonEnd === -1) {
940
- return this.fallbackSelect(
941
- Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
942
- this.maxContextTokens
943
- );
1221
+ return this.fallbackSelect(messages, this.maxContextTokens);
944
1222
  }
945
1223
  let parsed;
946
1224
  try {
947
1225
  parsed = JSON.parse(raw.slice(jsonStart, jsonEnd + 1));
948
1226
  } catch {
949
- return this.fallbackSelect(
950
- Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
951
- this.maxContextTokens
952
- );
1227
+ return this.fallbackSelect(messages, this.maxContextTokens);
953
1228
  }
954
1229
  const obj = parsed;
955
- const kept = obj.kept ?? [];
956
- const collapsed = obj.collapsed ?? [];
957
- return {
958
- kept: kept.map((k) => ({
1230
+ const keptRaw = obj.kept ?? [];
1231
+ const collapsedRaw = obj.collapsed ?? [];
1232
+ const kept = [];
1233
+ for (const k of keptRaw) {
1234
+ if (typeof k.from !== "number" || typeof k.to !== "number" || k.from < 0 || k.to >= messageCount || k.from > k.to) {
1235
+ return this.fallbackSelect(messages, this.maxContextTokens);
1236
+ }
1237
+ kept.push({
959
1238
  from: k.from,
960
1239
  to: k.to,
961
1240
  importance: k.importance ?? "medium"
962
- })),
963
- collapsed: collapsed.map((c) => ({ from: c.from, to: c.to, summary: c.summary })),
1241
+ });
1242
+ }
1243
+ const collapsed = [];
1244
+ for (const c of collapsedRaw) {
1245
+ if (typeof c.from !== "number" || typeof c.to !== "number" || c.from < 0 || c.to >= messageCount || c.from > c.to) {
1246
+ return this.fallbackSelect(messages, this.maxContextTokens);
1247
+ }
1248
+ collapsed.push({ from: c.from, to: c.to, summary: c.summary });
1249
+ }
1250
+ const allRanges = [...kept, ...collapsed];
1251
+ for (let i = 0; i < allRanges.length; i++) {
1252
+ const a = allRanges[i];
1253
+ if (!a) continue;
1254
+ for (let j = i + 1; j < allRanges.length; j++) {
1255
+ const b = allRanges[j];
1256
+ if (!b) continue;
1257
+ if (a.from <= b.to && a.to >= b.from) {
1258
+ return this.fallbackSelect(messages, this.maxContextTokens);
1259
+ }
1260
+ }
1261
+ }
1262
+ return {
1263
+ kept,
1264
+ collapsed,
964
1265
  reasoning: typeof obj.reasoning === "string" ? obj.reasoning : ""
965
1266
  };
966
1267
  }
@@ -980,14 +1281,19 @@ var SelectiveCompactor = class {
980
1281
  summarizerPrompt;
981
1282
  constructor(opts) {
982
1283
  this.provider = opts.provider;
983
- this.selector = opts.selector ?? new LLMSelector({ provider: opts.provider, model: opts.selectorModel });
1284
+ this.selector = opts.selector ?? new LLMSelector({ provider: opts.provider, model: opts.selectorModel, maxOutputTokens: opts.selectorMaxOutputTokens });
984
1285
  this.warnThreshold = opts.warnThreshold ?? 0.6;
985
1286
  this.softThreshold = opts.softThreshold ?? 0.75;
986
1287
  this.hardThreshold = opts.hardThreshold ?? 0.9;
987
1288
  this.maxContext = opts.maxContext ?? 128e3;
988
1289
  this.preserveK = opts.preserveK ?? 4;
989
1290
  this.eliseThreshold = opts.eliseThreshold ?? 500;
990
- this.summarizerModel = opts.summarizerModel ?? opts.selectorModel ?? "unknown";
1291
+ this.summarizerModel = opts.summarizerModel ?? opts.selectorModel;
1292
+ if (this.summarizerModel === void 0 && (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1")) {
1293
+ console.warn(
1294
+ "[SelectiveCompactor] summarizerModel not set \u2014 will fall back to ctx.model at summarize time. Set `summarizerModel` explicitly to silence this warning."
1295
+ );
1296
+ }
991
1297
  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
1298
  }
993
1299
  async compact(ctx, opts = {}) {
@@ -1093,14 +1399,15 @@ var SelectiveCompactor = class {
1093
1399
  Summarize the following message range:`;
1094
1400
  const body = messages.map((m, i) => `[${i}] ${m.role}: ${this.messagePreview(m)}`).join("\n");
1095
1401
  const req = {
1096
- model: this.summarizerModel,
1402
+ model: this.summarizerModel ?? ctx.model,
1097
1403
  system: [{ type: "text", text: systemText }],
1098
1404
  messages: [{ role: "user", content: body }],
1099
1405
  maxTokens: 512
1100
1406
  };
1101
1407
  try {
1408
+ const timeoutSignal = AbortSignal.timeout(3e4);
1102
1409
  const res = await this.provider.complete(req, {
1103
- signal: ctx.signal ?? new AbortController().signal
1410
+ signal: AbortSignal.any([ctx.signal, timeoutSignal])
1104
1411
  });
1105
1412
  return res.content.filter(isTextBlock).map((b) => b.text).join("\n").trim() || "(empty)";
1106
1413
  } catch {
@@ -1159,27 +1466,16 @@ Summarize the following message range:`;
1159
1466
  if (typeof m.content === "string") return m.content.trim().length > 0;
1160
1467
  return m.content.some((b) => b.type === "text" && b.text.trim().length > 0);
1161
1468
  }
1469
+ /**
1470
+ * Estimate message-array tokens via the shared `estimateMessages` primitive
1471
+ * so SelectiveCompactor's before/after/load figures agree with the
1472
+ * middleware threshold math and the other compactors. Previously this used a
1473
+ * private `ceil(len/3.5)` walk that diverged from the calibrated shared
1474
+ * estimator, causing the selective `load`/`targetBudget` comparison to mix
1475
+ * two incompatible token scales.
1476
+ */
1162
1477
  estimateTokens(messages) {
1163
- let total = 0;
1164
- for (const m of messages) {
1165
- if (typeof m.content === "string") {
1166
- total += this.roughTokenEstimate(m.content);
1167
- } else {
1168
- for (const b of m.content) {
1169
- if (b.type === "text") total += this.roughTokenEstimate(b.text);
1170
- else if (b.type === "tool_use") total += this.roughTokenEstimate(JSON.stringify(b.input));
1171
- else if (b.type === "tool_result") {
1172
- total += this.roughTokenEstimate(
1173
- typeof b.content === "string" ? b.content : JSON.stringify(b.content)
1174
- );
1175
- }
1176
- }
1177
- }
1178
- }
1179
- return total;
1180
- }
1181
- roughTokenEstimate(text) {
1182
- return Math.max(1, Math.ceil(text.length / 3.5));
1478
+ return estimateMessages(messages);
1183
1479
  }
1184
1480
  };
1185
1481
 
@@ -1234,6 +1530,7 @@ var ProviderBackedCompactor = class {
1234
1530
  return new SelectiveCompactor({
1235
1531
  ...common,
1236
1532
  selectorModel: this.opts.summarizerModel,
1533
+ selectorMaxOutputTokens: this.opts.selectorMaxOutputTokens,
1237
1534
  summarizerModel: this.opts.summarizerModel
1238
1535
  });
1239
1536
  }
@@ -1250,10 +1547,19 @@ function readPolicy(ctx) {
1250
1547
  if (typeof candidate.preserveK !== "number" || !candidate.thresholds) return null;
1251
1548
  return candidate;
1252
1549
  }
1550
+
1551
+ // src/utils/assert-never.ts
1552
+ function assertNever(x, message) {
1553
+ const err = new Error(
1554
+ `Unhandled case: ${JSON.stringify(x)}`
1555
+ );
1556
+ err.name = "AssertNeverError";
1557
+ throw err;
1558
+ }
1253
1559
  async function atomicWrite(targetPath, content, opts = {}) {
1254
- const dir = path2.dirname(targetPath);
1560
+ const dir = path3.dirname(targetPath);
1255
1561
  await fs.mkdir(dir, { recursive: true });
1256
- const tmp = path2.join(dir, `.${path2.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
1562
+ const tmp = path3.join(dir, `.${path3.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
1257
1563
  try {
1258
1564
  if (typeof content === "string") {
1259
1565
  await fs.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
@@ -1306,7 +1612,7 @@ async function renameWithRetry(from, to) {
1306
1612
  if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
1307
1613
  throw err;
1308
1614
  }
1309
- await new Promise((resolve2) => setTimeout(resolve2, delays[i]));
1615
+ await new Promise((resolve3) => setTimeout(resolve3, delays[i]));
1310
1616
  }
1311
1617
  }
1312
1618
  throw lastErr;
@@ -1317,136 +1623,17 @@ function toErrorMessage(err) {
1317
1623
  return err instanceof Error ? err.message : String(err);
1318
1624
  }
1319
1625
 
1320
- // src/utils/string.ts
1321
- function truncate(s, max) {
1322
- return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
1323
- }
1324
- function projectHash(absRoot) {
1325
- return createHash("sha256").update(path2.resolve(absRoot)).digest("hex").slice(0, 12);
1326
- }
1327
- function projectSlug(absRoot) {
1328
- const base = slugify(path2.basename(absRoot));
1329
- const hash = createHash("sha256").update(path2.resolve(absRoot)).digest("hex").slice(0, 6);
1330
- return `${base}-${hash}`;
1331
- }
1332
- function slugify(name) {
1333
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
1334
- }
1335
- function wstackGlobalRoot() {
1336
- const fromEnv = process.env["WRONGSTACK_HOME"];
1337
- if (fromEnv && fromEnv.trim().length > 0) return path2.resolve(fromEnv);
1338
- return path2.join(os.homedir(), ".wrongstack");
1339
- }
1340
- function resolveWstackPaths(opts) {
1341
- const globalRoot = opts.globalRoot ?? (opts.userHome ? path2.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
1342
- const hash = projectHash(opts.projectRoot);
1343
- const slug = projectSlug(opts.projectRoot);
1344
- const projectDir = path2.join(globalRoot, "projects", slug);
1345
- return {
1346
- globalRoot,
1347
- configDir: globalRoot,
1348
- globalConfig: path2.join(globalRoot, "config.json"),
1349
- secretsKey: path2.join(globalRoot, ".key"),
1350
- globalMemory: path2.join(globalRoot, "memory.md"),
1351
- globalSkills: path2.join(globalRoot, "skills"),
1352
- globalPrompts: path2.join(globalRoot, "prompts"),
1353
- cacheDir: path2.join(globalRoot, "cache"),
1354
- modelsCache: path2.join(globalRoot, "cache", "models.dev.json"),
1355
- modelsOverlayCache: path2.join(globalRoot, "cache", "models-overlay.json"),
1356
- historyFile: path2.join(globalRoot, "history"),
1357
- logFile: path2.join(globalRoot, "logs", "wrongstack.log"),
1358
- projectDir,
1359
- projectCodebaseIndex: path2.join(projectDir, "codebase-index"),
1360
- projectMemory: path2.join(projectDir, "memory.md"),
1361
- projectSessions: path2.join(projectDir, "sessions"),
1362
- projectTrust: path2.join(projectDir, "trust.json"),
1363
- projectMeta: path2.join(projectDir, "meta.json"),
1364
- projectLocalConfig: path2.join(projectDir, "config.local.json"),
1365
- inProjectConfig: path2.join(opts.projectRoot, ".wrongstack", "config.json"),
1366
- inProjectAgentsFile: path2.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
1367
- inProjectSkills: path2.join(opts.projectRoot, ".wrongstack", "skills"),
1368
- inProjectWorktrees: path2.join(opts.projectRoot, ".wrongstack", "worktrees"),
1369
- projectHash: hash,
1370
- projectSlug: slug,
1371
- projectGoal: path2.join(projectDir, "goal.json"),
1372
- projectSpecs: path2.join(projectDir, "specs"),
1373
- projectTaskGraphs: path2.join(projectDir, "task-graphs"),
1374
- projectSddSession: path2.join(projectDir, "sdd-session.json"),
1375
- projectPlan: path2.join(projectDir, "plan.json"),
1376
- projectAutophase: path2.join(projectDir, "autophase"),
1377
- syncConfig: path2.join(globalRoot, "sync.json")
1378
- };
1379
- }
1380
-
1381
- // src/utils/sleep.ts
1382
- function sleep(ms) {
1383
- return new Promise((resolve2) => setTimeout(resolve2, ms));
1384
- }
1385
-
1386
- // src/utils/assert-never.ts
1387
- function assertNever(x, message) {
1388
- const err = new Error(
1389
- `Unhandled case: ${JSON.stringify(x)}`
1390
- );
1391
- err.name = "AssertNeverError";
1392
- throw err;
1393
- }
1394
-
1395
- // src/utils/tool-output-serializer.ts
1396
- function createToolOutputSerializer(opts = {}) {
1397
- const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
1398
- function serialize(value) {
1399
- if (typeof value === "string") return value;
1400
- if (value === null || value === void 0) return "";
1401
- if (typeof value === "object") {
1402
- if (Array.isArray(value)) return value.map(serialize).join("\n");
1403
- if ("text" in value) {
1404
- const t = value.text;
1405
- return typeof t === "string" ? t : JSON.stringify(value, null, 2);
1406
- }
1407
- try {
1408
- return JSON.stringify(value, null, 2);
1409
- } catch {
1410
- return String(value);
1411
- }
1412
- }
1413
- return String(value);
1414
- }
1415
- function enforceCap(text, remainingBudget) {
1416
- if (remainingBudget <= 0) {
1417
- return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
1418
- }
1419
- const textBytes = Buffer.byteLength(text, "utf8");
1420
- if (textBytes <= remainingBudget) {
1421
- return { text, newBudget: remainingBudget - textBytes };
1422
- }
1423
- const marker = `
1424
- \u2026[truncated ${textBytes - remainingBudget} bytes]\u2026
1425
- `;
1426
- const markerBytes = Buffer.byteLength(marker, "utf8");
1427
- const available = remainingBudget - markerBytes;
1428
- if (available <= 0) {
1429
- return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
1430
- }
1431
- const half = Math.floor(available / 2);
1432
- const first = text.slice(0, half);
1433
- const second = text.slice(text.length - half);
1434
- return { text: `${first}${marker}${second}`, newBudget: 0 };
1435
- }
1436
- return { serialize, enforceCap, capBytes };
1437
- }
1438
-
1439
1626
  // src/utils/json-schema-validate.ts
1440
1627
  function validateAgainstSchema(value, schema) {
1441
1628
  const errors = [];
1442
1629
  walk(value, schema, "", errors);
1443
1630
  return { ok: errors.length === 0, errors };
1444
1631
  }
1445
- function walk(value, schema, path4, errors) {
1632
+ function walk(value, schema, path6, errors) {
1446
1633
  if (schema.enum !== void 0) {
1447
1634
  if (!schema.enum.some((e) => deepEqual(e, value))) {
1448
1635
  errors.push({
1449
- path: path4 || "<root>",
1636
+ path: path6 || "<root>",
1450
1637
  message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
1451
1638
  });
1452
1639
  return;
@@ -1455,7 +1642,7 @@ function walk(value, schema, path4, errors) {
1455
1642
  if (typeof schema.type === "string") {
1456
1643
  if (!checkType(value, schema.type)) {
1457
1644
  errors.push({
1458
- path: path4 || "<root>",
1645
+ path: path6 || "<root>",
1459
1646
  message: `expected ${schema.type}, got ${describeType(value)}`
1460
1647
  });
1461
1648
  return;
@@ -1465,20 +1652,20 @@ function walk(value, schema, path4, errors) {
1465
1652
  const obj = value;
1466
1653
  for (const req of schema.required ?? []) {
1467
1654
  if (!(req in obj)) {
1468
- errors.push({ path: joinPath(path4, req), message: "required property missing" });
1655
+ errors.push({ path: joinPath(path6, req), message: "required property missing" });
1469
1656
  }
1470
1657
  }
1471
1658
  if (schema.properties) {
1472
1659
  for (const [key, subSchema] of Object.entries(schema.properties)) {
1473
1660
  if (key in obj) {
1474
- walk(obj[key], subSchema, joinPath(path4, key), errors);
1661
+ walk(obj[key], subSchema, joinPath(path6, key), errors);
1475
1662
  }
1476
1663
  }
1477
1664
  }
1478
1665
  }
1479
1666
  if (schema.type === "array" && Array.isArray(value) && schema.items) {
1480
1667
  for (let i = 0; i < value.length; i++) {
1481
- walk(value[i], schema.items, `${path4}[${i}]`, errors);
1668
+ walk(value[i], schema.items, `${path6}[${i}]`, errors);
1482
1669
  }
1483
1670
  }
1484
1671
  }
@@ -1568,6 +1755,714 @@ function compileUserRegex(pattern, flags) {
1568
1755
  }
1569
1756
  }
1570
1757
 
1758
+ // src/utils/sleep.ts
1759
+ function sleep(ms) {
1760
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
1761
+ }
1762
+
1763
+ // src/utils/string.ts
1764
+ function truncate(s, max) {
1765
+ return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
1766
+ }
1767
+
1768
+ // src/utils/tool-output-serializer.ts
1769
+ var DEFAULT_LIST_LIMIT = 500;
1770
+ var LOG_ENTRY_LIMIT = 200;
1771
+ var INLINE_LIMIT = 240;
1772
+ var GREP_FILE_LIMIT = 80;
1773
+ var GREP_MATCHES_PER_FILE = 3;
1774
+ var DIFF_INLINE_LINE_LIMIT = 260;
1775
+ var DIFF_HUNK_LIMIT = 8;
1776
+ var DIFF_HUNK_CONTEXT = 14;
1777
+ function createToolOutputSerializer(opts = {}) {
1778
+ const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
1779
+ function serialize(value, context = {}) {
1780
+ if (typeof value === "string") return value;
1781
+ if (value === null || value === void 0) return "";
1782
+ if (typeof value === "object") {
1783
+ if (Array.isArray(value)) return value.map((item) => serialize(item)).join("\n");
1784
+ if (context.toolName) {
1785
+ const compact = renderToolObject(context.toolName, value, context.input);
1786
+ if (compact !== void 0) return compact;
1787
+ return renderGenericToolObject(context.toolName, value);
1788
+ }
1789
+ if ("text" in value) {
1790
+ const t = value.text;
1791
+ return typeof t === "string" ? t : JSON.stringify(value, null, 2);
1792
+ }
1793
+ try {
1794
+ return JSON.stringify(value, null, 2);
1795
+ } catch {
1796
+ return String(value);
1797
+ }
1798
+ }
1799
+ return String(value);
1800
+ }
1801
+ function enforceCap(text, remainingBudget) {
1802
+ if (remainingBudget <= 0) {
1803
+ return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
1804
+ }
1805
+ const textBytes = Buffer.byteLength(text, "utf8");
1806
+ if (textBytes <= remainingBudget) {
1807
+ return { text, newBudget: remainingBudget - textBytes };
1808
+ }
1809
+ const marker = `
1810
+ \u2026[truncated ${textBytes - remainingBudget} bytes]\u2026
1811
+ `;
1812
+ const markerBytes = Buffer.byteLength(marker, "utf8");
1813
+ const available = remainingBudget - markerBytes;
1814
+ if (available <= 0) {
1815
+ return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
1816
+ }
1817
+ const half = Math.floor(available / 2);
1818
+ const first = text.slice(0, half);
1819
+ const second = text.slice(text.length - half);
1820
+ return { text: `${first}${marker}${second}`, newBudget: 0 };
1821
+ }
1822
+ return { serialize, enforceCap, capBytes };
1823
+ }
1824
+ function renderToolObject(toolName, obj, input) {
1825
+ if (toolName === "read" && typeof obj["text"] === "string") {
1826
+ return joinSections([
1827
+ renderHeader(
1828
+ `read: ${stringFromInput(input, "path") ?? stringField(obj, "path") ?? "<unknown>"}`,
1829
+ {
1830
+ offset: numberFromInput(input, "offset"),
1831
+ limit: numberFromInput(input, "limit"),
1832
+ total_lines: obj["total_lines"],
1833
+ encoding: obj["encoding"],
1834
+ truncated: obj["truncated"],
1835
+ cached: obj["cached"],
1836
+ note: obj["note"]
1837
+ }
1838
+ ),
1839
+ obj["text"]
1840
+ ]);
1841
+ }
1842
+ if (toolName === "grep" && Array.isArray(obj["matches"])) {
1843
+ const matches = stringArrayField(obj, "matches");
1844
+ return joinSections([
1845
+ renderHeader(`grep: ${stringFromInput(input, "pattern") ?? "<pattern>"}`, {
1846
+ path: stringFromInput(input, "path"),
1847
+ glob: stringFromInput(input, "glob"),
1848
+ mode: stringFromInput(input, "output_mode"),
1849
+ count: obj["count"],
1850
+ shown: matches.length,
1851
+ truncated: obj["truncated"],
1852
+ used: obj["used"]
1853
+ }),
1854
+ renderGrepMatches(matches, stringFromInput(input, "output_mode"))
1855
+ ]);
1856
+ }
1857
+ if (toolName === "patch" && Array.isArray(obj["files"])) {
1858
+ const files = stringArrayField(obj, "files");
1859
+ return joinSections([
1860
+ renderHeader("patch", {
1861
+ applied: obj["applied"],
1862
+ rejected: obj["rejected"],
1863
+ files: files.length,
1864
+ dry_run: obj["dry_run"]
1865
+ }),
1866
+ typeof obj["message"] === "string" ? `message:
1867
+ ${obj["message"]}` : void 0,
1868
+ files.length > 0 ? `files:
1869
+ ${renderStringList(files)}` : void 0
1870
+ ]);
1871
+ }
1872
+ if (toolName === "glob" && Array.isArray(obj["files"])) {
1873
+ const files = stringArrayField(obj, "files");
1874
+ return joinSections([
1875
+ renderHeader(
1876
+ `${toolName}: ${stringFromInput(input, "pattern") ?? stringFromInput(input, "files") ?? stringFromInput(input, "path") ?? ""}`.trim(),
1877
+ {
1878
+ path: stringFromInput(input, "path"),
1879
+ files: files.length,
1880
+ truncated: obj["truncated"]
1881
+ }
1882
+ ),
1883
+ renderStringList(files, "(no files)")
1884
+ ]);
1885
+ }
1886
+ if (toolName === "tree" && typeof obj["tree"] === "string") {
1887
+ return joinSections([
1888
+ renderHeader(
1889
+ `tree: ${stringField(obj, "path") ?? stringFromInput(input, "path") ?? "<cwd>"}`,
1890
+ {
1891
+ total_files: obj["total_files"],
1892
+ total_dirs: obj["total_dirs"],
1893
+ truncated: obj["truncated"]
1894
+ }
1895
+ ),
1896
+ obj["tree"]
1897
+ ]);
1898
+ }
1899
+ if (toolName === "fetch" && typeof obj["content"] === "string") {
1900
+ return joinSections([
1901
+ renderHeader(
1902
+ `fetch: ${stringField(obj, "url") ?? stringFromInput(input, "url") ?? "<url>"}`,
1903
+ {
1904
+ status: obj["status"],
1905
+ content_type: obj["content_type"]
1906
+ }
1907
+ ),
1908
+ obj["content"]
1909
+ ]);
1910
+ }
1911
+ if (toolName === "replace" && Array.isArray(obj["results"])) {
1912
+ const results = obj["results"].filter(isRecord2);
1913
+ const sections = [
1914
+ renderHeader("replace", {
1915
+ files_modified: obj["files_modified"],
1916
+ total_replacements: obj["total_replacements"],
1917
+ dry_run: obj["dry_run"]
1918
+ })
1919
+ ];
1920
+ for (const r of results.slice(0, DEFAULT_LIST_LIMIT)) {
1921
+ sections.push(
1922
+ joinSections([
1923
+ renderHeader(`file: ${stringField(r, "path") ?? "<unknown>"}`, {
1924
+ replacements: r["replacements"]
1925
+ }),
1926
+ typeof r["diff"] === "string" ? r["diff"] : void 0
1927
+ ])
1928
+ );
1929
+ }
1930
+ if (results.length > DEFAULT_LIST_LIMIT) {
1931
+ sections.push(`[serializer omitted ${results.length - DEFAULT_LIST_LIMIT} result item(s)]`);
1932
+ }
1933
+ return joinSections(sections);
1934
+ }
1935
+ if (typeof obj["diff"] === "string") {
1936
+ const diff = obj["diff"];
1937
+ return joinSections([
1938
+ renderHeader(toolName, {
1939
+ path: obj["path"],
1940
+ replacements: obj["replacements"],
1941
+ bytes_written: obj["bytes_written"],
1942
+ created: obj["created"],
1943
+ note: obj["note"],
1944
+ files: Array.isArray(obj["files"]) ? obj["files"].length : void 0,
1945
+ truncated: obj["truncated"],
1946
+ mode: obj["mode"]
1947
+ }),
1948
+ compactDiff(diff)
1949
+ ]);
1950
+ }
1951
+ if (toolName === "test" && typeof obj["output"] === "string") {
1952
+ return renderTestOutput(obj, input);
1953
+ }
1954
+ if ((toolName === "typecheck" || toolName === "lint" || toolName === "format") && typeof obj["output"] === "string") {
1955
+ return renderVerifierOutput(toolName, obj, input);
1956
+ }
1957
+ if (hasCommandOutputShape(obj)) {
1958
+ return renderCommandOutput(toolName, obj, input);
1959
+ }
1960
+ if (toolName === "json" && typeof obj["formatted"] === "string") {
1961
+ return joinSections([
1962
+ renderHeader("json", {
1963
+ type: obj["type"],
1964
+ keys: Array.isArray(obj["keys"]) ? obj["keys"].length : void 0,
1965
+ query: stringFromInput(input, "query"),
1966
+ error: obj["error"]
1967
+ }),
1968
+ obj["formatted"]
1969
+ ]);
1970
+ }
1971
+ if (toolName === "logs" && Array.isArray(obj["entries"])) {
1972
+ const entries = obj["entries"].filter(isRecord2);
1973
+ const lines = entries.slice(0, LOG_ENTRY_LIMIT).map((entry) => {
1974
+ const ts = stringField(entry, "timestamp") ?? "";
1975
+ const level = stringField(entry, "level") ?? "info";
1976
+ const message = stringField(entry, "message") ?? "";
1977
+ const source = stringField(entry, "source");
1978
+ return [ts, level, source, message].filter(Boolean).join(" ");
1979
+ });
1980
+ if (entries.length > LOG_ENTRY_LIMIT) {
1981
+ lines.push(`[serializer omitted ${entries.length - LOG_ENTRY_LIMIT} log entry item(s)]`);
1982
+ }
1983
+ return joinSections([
1984
+ renderHeader(`logs: ${stringField(obj, "source") ?? "<source>"}`, {
1985
+ total: obj["total"],
1986
+ shown: Math.min(entries.length, LOG_ENTRY_LIMIT),
1987
+ truncated: obj["truncated"],
1988
+ stream_mode: obj["stream_mode"]
1989
+ }),
1990
+ lines.length > 0 ? lines.join("\n") : "(no log entries)"
1991
+ ]);
1992
+ }
1993
+ if (toolName === "audit" && Array.isArray(obj["vulnerabilities"])) {
1994
+ const vulns = obj["vulnerabilities"].filter(isRecord2);
1995
+ const lines = vulns.slice(0, DEFAULT_LIST_LIMIT).map((v) => {
1996
+ const severity = stringField(v, "severity") ?? "unknown";
1997
+ const pkg = stringField(v, "package") ?? "<package>";
1998
+ const title = stringField(v, "title") ?? "";
1999
+ const url = stringField(v, "url");
2000
+ return [severity, pkg, title, url].filter(Boolean).join(" | ");
2001
+ });
2002
+ if (vulns.length > DEFAULT_LIST_LIMIT) {
2003
+ lines.push(`[serializer omitted ${vulns.length - DEFAULT_LIST_LIMIT} vulnerability item(s)]`);
2004
+ }
2005
+ return joinSections([
2006
+ renderHeader("audit", {
2007
+ exit_code: obj["exit_code"],
2008
+ total: obj["total"],
2009
+ summary: obj["summary"],
2010
+ truncated: obj["truncated"]
2011
+ }),
2012
+ lines.length > 0 ? lines.join("\n") : stringField(obj, "output")
2013
+ ]);
2014
+ }
2015
+ if (toolName === "outdated" && Array.isArray(obj["packages"])) {
2016
+ const packages = obj["packages"].filter(isRecord2);
2017
+ const lines = packages.slice(0, DEFAULT_LIST_LIMIT).map(
2018
+ (p) => [
2019
+ stringField(p, "name") ?? "<package>",
2020
+ `current=${stringField(p, "current") ?? "unknown"}`,
2021
+ `wanted=${stringField(p, "wanted") ?? "unknown"}`,
2022
+ `latest=${stringField(p, "latest") ?? "unknown"}`,
2023
+ stringField(p, "type")
2024
+ ].filter(Boolean).join(" | ")
2025
+ );
2026
+ if (packages.length > DEFAULT_LIST_LIMIT) {
2027
+ lines.push(`[serializer omitted ${packages.length - DEFAULT_LIST_LIMIT} package item(s)]`);
2028
+ }
2029
+ return joinSections([
2030
+ renderHeader("outdated", {
2031
+ exit_code: obj["exit_code"],
2032
+ total: obj["total"],
2033
+ truncated: obj["truncated"]
2034
+ }),
2035
+ lines.length > 0 ? lines.join("\n") : stringField(obj, "output")
2036
+ ]);
2037
+ }
2038
+ return void 0;
2039
+ }
2040
+ function renderTestOutput(obj, input) {
2041
+ const exitCode = numberField(obj, "exit_code") ?? 0;
2042
+ const failed = numberField(obj, "failed") ?? 0;
2043
+ const output = stringField(obj, "output") ?? "";
2044
+ const header = renderHeader(`test: ${stringField(obj, "runner") ?? "runner"}`, {
2045
+ exit_code: obj["exit_code"],
2046
+ tests_run: obj["tests_run"],
2047
+ passed: obj["passed"],
2048
+ failed: obj["failed"],
2049
+ duration_ms: obj["duration_ms"],
2050
+ truncated: obj["truncated"],
2051
+ files: inputListSummary(input, "files"),
2052
+ grep: stringFromInput(input, "grep")
2053
+ });
2054
+ if (exitCode === 0 && failed === 0) {
2055
+ return joinSections([
2056
+ header,
2057
+ joinSections([
2058
+ "report:",
2059
+ `status=passed`,
2060
+ `tests_run=${obj["tests_run"] ?? 0}`,
2061
+ `passed=${obj["passed"] ?? 0}`,
2062
+ `failed=${obj["failed"] ?? 0}`,
2063
+ `duration_ms=${obj["duration_ms"] ?? 0}`,
2064
+ extractSpoolNote(output)
2065
+ ])
2066
+ ]);
2067
+ }
2068
+ return joinSections([
2069
+ header,
2070
+ `error_context:
2071
+ ${compactFailureOutput(output || "(no runner output)")}`
2072
+ ]);
2073
+ }
2074
+ function renderVerifierOutput(toolName, obj, input) {
2075
+ const exitCode = numberField(obj, "exit_code") ?? 0;
2076
+ const errors = numberField(obj, "errors") ?? 0;
2077
+ const warnings = numberField(obj, "warnings") ?? 0;
2078
+ const output = stringField(obj, "output") ?? "";
2079
+ const changed = numberField(obj, "files_changed") ?? 0;
2080
+ const header = renderHeader(toolName, {
2081
+ exit_code: obj["exit_code"],
2082
+ errors: obj["errors"],
2083
+ warnings: obj["warnings"],
2084
+ files_checked: obj["files_checked"],
2085
+ files_changed: obj["files_changed"],
2086
+ fix_applied: obj["fix_applied"],
2087
+ fixer: obj["fixer"],
2088
+ linter: obj["linter"],
2089
+ project: obj["project"],
2090
+ truncated: obj["truncated"],
2091
+ files: inputListSummary(input, "files"),
2092
+ cwd: stringFromInput(input, "cwd")
2093
+ });
2094
+ if (exitCode === 0 && errors === 0 && (toolName !== "format" || changed === 0)) {
2095
+ return joinSections([
2096
+ header,
2097
+ joinSections([
2098
+ "report:",
2099
+ "status=passed",
2100
+ `errors=${errors}`,
2101
+ `warnings=${warnings}`,
2102
+ toolName === "format" ? `files_changed=${changed}` : void 0,
2103
+ extractSpoolNote(output)
2104
+ ])
2105
+ ]);
2106
+ }
2107
+ if (exitCode === 0 && toolName === "format") {
2108
+ return joinSections([
2109
+ header,
2110
+ joinSections([
2111
+ "report:",
2112
+ "status=changed",
2113
+ `files_changed=${changed}`,
2114
+ extractSpoolNote(output)
2115
+ ])
2116
+ ]);
2117
+ }
2118
+ return joinSections([
2119
+ header,
2120
+ `error_context:
2121
+ ${compactFailureOutput(output || "(no verifier output)")}`
2122
+ ]);
2123
+ }
2124
+ function renderGrepMatches(matches, mode) {
2125
+ if (matches.length === 0) return "(no matches)";
2126
+ if (mode === "files_with_matches") return renderStringList(matches, "(no files)");
2127
+ if (mode === "count") return renderStringList(matches, "(no counts)");
2128
+ const groups = /* @__PURE__ */ new Map();
2129
+ const passthrough = [];
2130
+ for (const match of matches) {
2131
+ const parsed = parseGrepContentLine(match);
2132
+ if (!parsed) {
2133
+ passthrough.push(match);
2134
+ continue;
2135
+ }
2136
+ const list = groups.get(parsed.file) ?? [];
2137
+ list.push(`${parsed.line}:${parsed.text}`);
2138
+ groups.set(parsed.file, list);
2139
+ }
2140
+ if (groups.size === 0) return renderStringList(matches, "(no matches)");
2141
+ const sections = [];
2142
+ let fileIndex = 0;
2143
+ for (const [file, lines] of groups) {
2144
+ fileIndex++;
2145
+ if (fileIndex > GREP_FILE_LIMIT) break;
2146
+ const shown = lines.slice(0, GREP_MATCHES_PER_FILE);
2147
+ sections.push(
2148
+ `${file} (${lines.length} match(es), showing ${shown.length})
2149
+ ${shown.join("\n")}`
2150
+ );
2151
+ }
2152
+ if (groups.size > GREP_FILE_LIMIT) {
2153
+ sections.push(`[serializer omitted ${groups.size - GREP_FILE_LIMIT} file group(s)]`);
2154
+ }
2155
+ if (passthrough.length > 0) {
2156
+ sections.push(`ungrouped:
2157
+ ${renderStringList(passthrough, "", 50)}`);
2158
+ }
2159
+ return sections.join("\n");
2160
+ }
2161
+ function parseGrepContentLine(line) {
2162
+ const match = /^(.+?):(\d+):(.*)$/.exec(line);
2163
+ if (!match?.[1] || !match[2]) return void 0;
2164
+ return { file: match[1], line: match[2], text: match[3] ?? "" };
2165
+ }
2166
+ function compactDiff(diff) {
2167
+ const lines = diff.split(/\r?\n/);
2168
+ if (lines.length <= DIFF_INLINE_LINE_LIMIT) return diff;
2169
+ const fileCount = Math.max(
2170
+ new Set(
2171
+ lines.map(
2172
+ (line) => /^diff --git\s+a\/(.+?)\s+b\//.exec(line)?.[1] ?? /^---\s+(.+)/.exec(line)?.[1]
2173
+ ).filter(Boolean)
2174
+ ).size,
2175
+ 0
2176
+ );
2177
+ const hunks = lines.filter((line) => line.startsWith("@@")).length;
2178
+ const added = lines.filter((line) => line.startsWith("+") && !line.startsWith("+++")).length;
2179
+ const removed = lines.filter((line) => line.startsWith("-") && !line.startsWith("---")).length;
2180
+ const selected = /* @__PURE__ */ new Set();
2181
+ let hunkCount = 0;
2182
+ for (let i = 0; i < lines.length; i++) {
2183
+ const line = lines[i] ?? "";
2184
+ if (line.startsWith("diff --git") || line.startsWith("--- ") || line.startsWith("+++ ")) {
2185
+ selected.add(i);
2186
+ continue;
2187
+ }
2188
+ if (!line.startsWith("@@")) continue;
2189
+ if (hunkCount >= DIFF_HUNK_LIMIT) continue;
2190
+ hunkCount++;
2191
+ for (let j = i; j <= Math.min(lines.length - 1, i + DIFF_HUNK_CONTEXT); j++) {
2192
+ selected.add(j);
2193
+ }
2194
+ }
2195
+ if (selected.size === 0) {
2196
+ return joinSections([
2197
+ renderHeader("diff_summary", {
2198
+ files: fileCount,
2199
+ hunks,
2200
+ added,
2201
+ removed,
2202
+ lines: lines.length
2203
+ }),
2204
+ lines.slice(0, DIFF_INLINE_LINE_LIMIT).join("\n"),
2205
+ `[serializer omitted ${Math.max(0, lines.length - DIFF_INLINE_LINE_LIMIT)} diff line(s)]`
2206
+ ]);
2207
+ }
2208
+ const excerpt = [];
2209
+ let previous = -1;
2210
+ for (const index of [...selected].sort((a, b) => a - b)) {
2211
+ if (index > previous + 1) {
2212
+ const omitted = previous === -1 ? index : index - previous - 1;
2213
+ excerpt.push(`[serializer omitted ${omitted} diff line(s)]`);
2214
+ }
2215
+ excerpt.push(lines[index] ?? "");
2216
+ previous = index;
2217
+ }
2218
+ const trailing = lines.length - previous - 1;
2219
+ if (trailing > 0) excerpt.push(`[serializer omitted ${trailing} trailing diff line(s)]`);
2220
+ return joinSections([
2221
+ renderHeader("diff_summary", {
2222
+ files: fileCount,
2223
+ hunks,
2224
+ shown_hunks: Math.min(hunks, DIFF_HUNK_LIMIT),
2225
+ added,
2226
+ removed,
2227
+ lines: lines.length
2228
+ }),
2229
+ excerpt.join("\n")
2230
+ ]);
2231
+ }
2232
+ function compactFailureOutput(output) {
2233
+ const lines = output.split(/\r?\n/);
2234
+ if (lines.length <= 260) return output.trimEnd();
2235
+ const selected = /* @__PURE__ */ new Set();
2236
+ const marker = /\b(fail|failed|failure|error|exception|assertionerror|expected|received|actual|timeout|stack)\b/i;
2237
+ let markerHits = 0;
2238
+ for (let i = 0; i < lines.length; i++) {
2239
+ if (!marker.test(lines[i] ?? "")) continue;
2240
+ markerHits++;
2241
+ for (let j = Math.max(0, i - 4); j <= Math.min(lines.length - 1, i + 10); j++) {
2242
+ selected.add(j);
2243
+ }
2244
+ }
2245
+ if (markerHits === 0) {
2246
+ return lines.slice(-220).join("\n").trimEnd();
2247
+ }
2248
+ const ordered = [...selected].sort((a, b) => a - b);
2249
+ const out = [];
2250
+ let previous = -1;
2251
+ for (const index of ordered) {
2252
+ if (index > previous + 1) {
2253
+ const omitted = previous === -1 ? index : index - previous - 1;
2254
+ out.push(`[serializer omitted ${omitted} line(s)]`);
2255
+ }
2256
+ out.push(lines[index] ?? "");
2257
+ previous = index;
2258
+ }
2259
+ return out.join("\n").trimEnd();
2260
+ }
2261
+ function extractSpoolNote(output) {
2262
+ return output.split(/\r?\n/).find((line) => line.startsWith("[output truncated") && line.includes("full"));
2263
+ }
2264
+ function hasCommandOutputShape(obj) {
2265
+ return typeof obj["stdout"] === "string" || typeof obj["stderr"] === "string" || typeof obj["output"] === "string" || typeof obj["exitCode"] === "number" || typeof obj["exit_code"] === "number";
2266
+ }
2267
+ function renderCommandOutput(toolName, obj, input) {
2268
+ const command = stringField(obj, "command") ?? stringFromInput(input, "command");
2269
+ const args = stringArrayField(obj, "args");
2270
+ const commandLine = command ? [command, ...args].join(" ") : void 0;
2271
+ const output = stringField(obj, "output");
2272
+ const stdout = stringField(obj, "stdout");
2273
+ const stderr = stringField(obj, "stderr");
2274
+ return joinSections([
2275
+ renderHeader(commandLine ? `${toolName}: ${commandLine}` : toolName, {
2276
+ exit_code: obj["exit_code"] ?? obj["exitCode"],
2277
+ timed_out: obj["timed_out"],
2278
+ pid: obj["pid"],
2279
+ allowed: obj["allowed"],
2280
+ truncated: obj["truncated"],
2281
+ runner: obj["runner"],
2282
+ linter: obj["linter"],
2283
+ fixer: obj["fixer"],
2284
+ project: obj["project"],
2285
+ tests_run: obj["tests_run"],
2286
+ passed: obj["passed"],
2287
+ failed: obj["failed"],
2288
+ duration_ms: obj["duration_ms"],
2289
+ errors: obj["errors"],
2290
+ warnings: obj["warnings"],
2291
+ files_checked: obj["files_checked"],
2292
+ files_changed: obj["files_changed"],
2293
+ fix_applied: obj["fix_applied"]
2294
+ }),
2295
+ stringField(obj, "error") ? `error:
2296
+ ${stringField(obj, "error")}` : void 0,
2297
+ output ? `output:
2298
+ ${output}` : void 0,
2299
+ stdout ? `stdout:
2300
+ ${stdout}` : void 0,
2301
+ stderr ? `stderr:
2302
+ ${stderr}` : void 0
2303
+ ]);
2304
+ }
2305
+ function renderGenericToolObject(toolName, obj) {
2306
+ const scalars = {};
2307
+ const blocks = [];
2308
+ for (const [key, value] of Object.entries(obj)) {
2309
+ if (value === void 0) continue;
2310
+ if (isScalar(value)) {
2311
+ const inline = String(value);
2312
+ if (inline.length <= INLINE_LIMIT && !inline.includes("\n")) {
2313
+ scalars[key] = value;
2314
+ } else {
2315
+ blocks.push(`${key}:
2316
+ ${inline}`);
2317
+ }
2318
+ continue;
2319
+ }
2320
+ if (Array.isArray(value)) {
2321
+ if (value.every((item) => typeof item === "string")) {
2322
+ blocks.push(`${key}:
2323
+ ${renderStringList(value)}`);
2324
+ } else {
2325
+ blocks.push(`${key}:
2326
+ ${renderUnknownList(value)}`);
2327
+ }
2328
+ continue;
2329
+ }
2330
+ blocks.push(`${key}: ${clipInline(oneLineJson(value))}`);
2331
+ }
2332
+ return joinSections([renderHeader(toolName, scalars), ...blocks]);
2333
+ }
2334
+ function renderHeader(label, fields) {
2335
+ const parts = Object.entries(fields).filter(([, value]) => value !== void 0 && value !== null && value !== "").map(([key, value]) => `${key}=${clipInline(formatInlineValue(value))}`);
2336
+ return parts.length > 0 ? `${label} (${parts.join(" ")})` : label;
2337
+ }
2338
+ function renderStringList(items, empty = "", limit = DEFAULT_LIST_LIMIT) {
2339
+ if (items.length === 0) return empty;
2340
+ const shown = items.slice(0, limit);
2341
+ const omitted = items.length - shown.length;
2342
+ return [
2343
+ ...shown,
2344
+ ...omitted > 0 ? [`[serializer omitted ${omitted} item(s); narrow the request for more]`] : []
2345
+ ].join("\n");
2346
+ }
2347
+ function renderUnknownList(items, limit = DEFAULT_LIST_LIMIT) {
2348
+ const shown = items.slice(0, limit).map((item) => clipInline(oneLineJson(item), 1e3));
2349
+ const omitted = items.length - shown.length;
2350
+ if (omitted > 0)
2351
+ shown.push(`[serializer omitted ${omitted} item(s); narrow the request for more]`);
2352
+ return shown.join("\n");
2353
+ }
2354
+ function joinSections(sections) {
2355
+ return sections.map((section) => typeof section === "string" ? section.trimEnd() : void 0).filter((section) => !!section).join("\n");
2356
+ }
2357
+ function formatInlineValue(value) {
2358
+ if (Array.isArray(value)) return `[${value.map(formatInlineValue).join(",")}]`;
2359
+ if (isScalar(value)) return String(value);
2360
+ return oneLineJson(value);
2361
+ }
2362
+ function clipInline(value, max = INLINE_LIMIT) {
2363
+ const compact = value.replace(/\s+/g, " ").trim();
2364
+ return compact.length <= max ? compact : `${compact.slice(0, max - 15)}...(${compact.length} chars)`;
2365
+ }
2366
+ function oneLineJson(value) {
2367
+ try {
2368
+ return JSON.stringify(value);
2369
+ } catch {
2370
+ return String(value);
2371
+ }
2372
+ }
2373
+ function stringField(obj, key) {
2374
+ const value = obj[key];
2375
+ return typeof value === "string" ? value : void 0;
2376
+ }
2377
+ function numberField(obj, key) {
2378
+ const value = obj[key];
2379
+ return typeof value === "number" ? value : void 0;
2380
+ }
2381
+ function stringArrayField(obj, key) {
2382
+ const value = obj[key];
2383
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
2384
+ }
2385
+ function stringFromInput(input, key) {
2386
+ if (!isRecord2(input)) return void 0;
2387
+ const value = input[key];
2388
+ return typeof value === "string" ? value : void 0;
2389
+ }
2390
+ function numberFromInput(input, key) {
2391
+ if (!isRecord2(input)) return void 0;
2392
+ const value = input[key];
2393
+ return typeof value === "number" ? value : void 0;
2394
+ }
2395
+ function inputListSummary(input, key) {
2396
+ if (!isRecord2(input)) return void 0;
2397
+ const value = input[key];
2398
+ if (typeof value === "string") return value;
2399
+ if (Array.isArray(value)) return value.filter((item) => typeof item === "string").join(",");
2400
+ return void 0;
2401
+ }
2402
+ function isRecord2(value) {
2403
+ return !!value && typeof value === "object" && !Array.isArray(value);
2404
+ }
2405
+ function isScalar(value) {
2406
+ return value === null || ["string", "number", "boolean"].includes(typeof value);
2407
+ }
2408
+ function projectHash(absRoot) {
2409
+ return createHash("sha256").update(path3.resolve(absRoot)).digest("hex").slice(0, 12);
2410
+ }
2411
+ function projectSlug(absRoot) {
2412
+ const base = slugify(path3.basename(absRoot));
2413
+ const hash = createHash("sha256").update(path3.resolve(absRoot)).digest("hex").slice(0, 6);
2414
+ return `${base}-${hash}`;
2415
+ }
2416
+ function slugify(name) {
2417
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
2418
+ }
2419
+ function wstackGlobalRoot() {
2420
+ const fromEnv = process.env["WRONGSTACK_HOME"];
2421
+ if (fromEnv && fromEnv.trim().length > 0) return path3.resolve(fromEnv);
2422
+ return path3.join(os.homedir(), ".wrongstack");
2423
+ }
2424
+ function resolveWstackPaths(opts) {
2425
+ const globalRoot = opts.globalRoot ?? (opts.userHome ? path3.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
2426
+ const hash = projectHash(opts.projectRoot);
2427
+ const slug = projectSlug(opts.projectRoot);
2428
+ const projectDir = path3.join(globalRoot, "projects", slug);
2429
+ return {
2430
+ globalRoot,
2431
+ configDir: globalRoot,
2432
+ globalConfig: path3.join(globalRoot, "config.json"),
2433
+ secretsKey: path3.join(globalRoot, ".key"),
2434
+ globalMemory: path3.join(globalRoot, "memory.md"),
2435
+ globalSkills: path3.join(globalRoot, "skills"),
2436
+ globalPrompts: path3.join(globalRoot, "prompts"),
2437
+ cacheDir: path3.join(globalRoot, "cache"),
2438
+ modelsCache: path3.join(globalRoot, "cache", "models.dev.json"),
2439
+ modelsOverlayCache: path3.join(globalRoot, "cache", "models-overlay.json"),
2440
+ historyFile: path3.join(globalRoot, "history"),
2441
+ logFile: path3.join(globalRoot, "logs", "wrongstack.log"),
2442
+ projectDir,
2443
+ projectCodebaseIndex: path3.join(projectDir, "codebase-index"),
2444
+ projectMemory: path3.join(projectDir, "memory.md"),
2445
+ projectSessions: path3.join(projectDir, "sessions"),
2446
+ projectTrust: path3.join(projectDir, "trust.json"),
2447
+ projectMeta: path3.join(projectDir, "meta.json"),
2448
+ projectLocalConfig: path3.join(projectDir, "config.local.json"),
2449
+ inProjectConfig: path3.join(opts.projectRoot, ".wrongstack", "config.json"),
2450
+ inProjectAgentsFile: path3.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
2451
+ inProjectSkills: path3.join(opts.projectRoot, ".wrongstack", "skills"),
2452
+ inProjectWorktrees: path3.join(opts.projectRoot, ".wrongstack", "worktrees"),
2453
+ projectHash: hash,
2454
+ projectSlug: slug,
2455
+ projectGoal: path3.join(projectDir, "goal.json"),
2456
+ projectSpecs: path3.join(projectDir, "specs"),
2457
+ projectTaskGraphs: path3.join(projectDir, "task-graphs"),
2458
+ projectSddSession: path3.join(projectDir, "sdd-session.json"),
2459
+ projectPlan: path3.join(projectDir, "plan.json"),
2460
+ projectAutophase: path3.join(projectDir, "autophase"),
2461
+ syncConfig: path3.join(globalRoot, "sync.json"),
2462
+ projectStatus: (projectHash2) => path3.join(globalRoot, "projects", projectHash2, "status.json")
2463
+ };
2464
+ }
2465
+
1571
2466
  // src/types/errors.ts
1572
2467
  var ERROR_CODES = {
1573
2468
  // Provider
@@ -1771,15 +2666,20 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
1771
2666
  this._cachedMsgCount = msgCount;
1772
2667
  this._cachedToolCount = toolCount;
1773
2668
  }
1774
- const load = tokens / this._maxContext;
2669
+ const budget = computeContextWindowBudget(ctx, tokens, this._maxContext);
2670
+ const load = budget.load;
1775
2671
  const policy = this.policyProvider?.(ctx);
1776
2672
  const thresholds = policy?.thresholds ?? {
1777
2673
  warn: this.warnThreshold,
1778
2674
  soft: this.softThreshold,
1779
2675
  hard: this.hardThreshold
1780
2676
  };
2677
+ const repetition = repeatedReadPressure(ctx);
2678
+ const adaptiveThresholds = adaptThresholdsForSignals(thresholds, {
2679
+ repeatedReadCount: repetition
2680
+ });
1781
2681
  const aggressiveOn = policy?.aggressiveOn ?? this.aggressiveOn;
1782
- const level = load >= thresholds.hard ? "hard" : load >= thresholds.soft ? "soft" : load >= thresholds.warn ? "warn" : null;
2682
+ const level = load >= adaptiveThresholds.hard ? "hard" : load >= adaptiveThresholds.soft ? "soft" : load >= adaptiveThresholds.warn ? "warn" : null;
1783
2683
  if (!level) {
1784
2684
  this.lastNoopAttempt = null;
1785
2685
  return next(ctx);
@@ -1788,7 +2688,13 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
1788
2688
  return next(ctx);
1789
2689
  }
1790
2690
  const aggressive = level === "hard" ? true : level === "soft" ? aggressiveOn !== "hard" : aggressiveOn === "warn";
1791
- await this.compact(ctx, aggressive, { level, tokens, load });
2691
+ await this.compact(ctx, aggressive, {
2692
+ level,
2693
+ tokens,
2694
+ load,
2695
+ budget,
2696
+ signals: { repeatedReadCount: repetition }
2697
+ });
1792
2698
  return next(ctx);
1793
2699
  };
1794
2700
  }
@@ -1841,6 +2747,8 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
1841
2747
  tokens: pressure.tokens,
1842
2748
  load: pressure.load,
1843
2749
  maxContext: this._maxContext,
2750
+ budget: pressure.budget,
2751
+ signals: pressure.signals,
1844
2752
  report,
1845
2753
  aggressive
1846
2754
  });
@@ -1852,6 +2760,8 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
1852
2760
  level: pressure.level,
1853
2761
  aggressive,
1854
2762
  reductions: report.reductions?.map((r) => ({ phase: r.phase, saved: r.saved })),
2763
+ budget: pressure.budget,
2764
+ signals: pressure.signals,
1855
2765
  // Record what was collapsed so the audit trail shows the preserved
1856
2766
  // content, not just token counts. Bounded to keep the log line small;
1857
2767
  // the full original turns are already in the session JSONL.
@@ -1867,6 +2777,8 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
1867
2777
  level: pressure.level,
1868
2778
  tokens: pressure.tokens,
1869
2779
  maxContext: this._maxContext,
2780
+ budget: pressure.budget,
2781
+ signals: pressure.signals,
1870
2782
  load: pressure.load,
1871
2783
  fatal
1872
2784
  });
@@ -1886,6 +2798,37 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
1886
2798
  }
1887
2799
  }
1888
2800
  };
2801
+ function computeContextWindowBudget(ctx, inputTokens, maxContext) {
2802
+ const reservedOutputTokens = readPositiveMetaNumber(ctx, "contextOutputReserveTokens") ?? Math.floor(Math.min(8192, maxContext * 0.08));
2803
+ const reservedSafetyTokens = readPositiveMetaNumber(ctx, "contextSafetyBufferTokens") ?? Math.floor(Math.min(4096, maxContext * 0.02));
2804
+ const availableInputTokens = Math.max(
2805
+ 1,
2806
+ maxContext - reservedOutputTokens - reservedSafetyTokens
2807
+ );
2808
+ const remainingInputTokens = availableInputTokens - inputTokens;
2809
+ return {
2810
+ maxContext,
2811
+ inputTokens,
2812
+ availableInputTokens,
2813
+ remainingInputTokens,
2814
+ reservedOutputTokens,
2815
+ reservedSafetyTokens,
2816
+ load: inputTokens / availableInputTokens,
2817
+ overflowTokens: Math.max(0, -remainingInputTokens)
2818
+ };
2819
+ }
2820
+ function readPositiveMetaNumber(ctx, key) {
2821
+ const value = ctx.meta?.[key];
2822
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : void 0;
2823
+ }
2824
+ function adaptThresholdsForSignals(thresholds, signals) {
2825
+ if (signals.repeatedReadCount < 3) return thresholds;
2826
+ return {
2827
+ warn: Math.max(0.25, thresholds.warn - 0.08),
2828
+ soft: Math.max(0.35, thresholds.soft - 0.04),
2829
+ hard: thresholds.hard
2830
+ };
2831
+ }
1889
2832
 
1890
2833
  // src/security/capabilities.ts
1891
2834
  var ToolCapabilities = {
@@ -2028,7 +2971,8 @@ ${errorDetails}`,
2028
2971
  let effectivePermission = decision.permission;
2029
2972
  const policy = this.opts.permissionPolicy;
2030
2973
  const yolo = policy.getYolo?.() === true || policy.getYoloDestructive?.() === true;
2031
- if (toolDangerousCaps.length > 0 && effectivePermission === "auto" && !yolo) {
2974
+ const authoritativeAuto = decision.source === "yolo";
2975
+ if (toolDangerousCaps.length > 0 && effectivePermission === "auto" && !yolo && !authoritativeAuto) {
2032
2976
  effectivePermission = "confirm";
2033
2977
  }
2034
2978
  if (effectivePermission === "deny") {
@@ -2170,9 +3114,10 @@ ${post.additionalContext}`;
2170
3114
  });
2171
3115
  this.opts.renderer?.writeToolCall(tool.name, use.input);
2172
3116
  const output = await this.runWithTimeout(tool, use.input, ctx.signal, ctx, use.id);
2173
- const text = this.serializer.serialize(output);
3117
+ const text = this.serializer.serialize(output, { toolName: tool.name, input: use.input });
2174
3118
  const scrubbed = this.opts.secretScrubber.scrub(text);
2175
- const { text: capped, newBudget } = this.serializer.enforceCap(scrubbed, budget);
3119
+ const withArtifact = await maybePersistLargeToolOutput(tool.name, scrubbed, budget);
3120
+ const { text: capped, newBudget } = this.serializer.enforceCap(withArtifact, budget);
2176
3121
  this.opts.renderer?.writeToolResult(tool.name, capped, false);
2177
3122
  return {
2178
3123
  block: {
@@ -2197,38 +3142,27 @@ ${post.additionalContext}`;
2197
3142
  tool.timeoutMs ?? this.iterationTimeoutMs,
2198
3143
  this.maxToolTimeoutMs
2199
3144
  );
2200
- const ctrl = new AbortController();
2201
- const timer = setTimeout(() => ctrl.abort(new Error("tool timeout")), timeoutMs);
2202
- const combined = AbortSignal.any([parentSignal, ctrl.signal]);
2203
- let cleanupCalled = false;
2204
- let caught = false;
3145
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
3146
+ const combined = AbortSignal.any([parentSignal, timeoutSignal]);
3147
+ let output;
2205
3148
  try {
2206
- if (typeof tool.executeStream === "function") {
2207
- return await this.runStreamedTool(tool, input, ctx, combined, toolUseId);
2208
- }
2209
- return await tool.execute(input, ctx, { signal: combined });
3149
+ output = typeof tool.executeStream === "function" ? await this.runStreamedTool(tool, input, ctx, combined, toolUseId) : await tool.execute(input, ctx, { signal: combined });
2210
3150
  } catch (err) {
2211
- caught = true;
2212
- if (combined.aborted && typeof tool.cleanup === "function") {
2213
- cleanupCalled = true;
2214
- try {
2215
- await tool.cleanup(input, ctx);
2216
- } catch {
2217
- }
2218
- }
3151
+ if (combined.aborted) await this.runToolCleanup(tool, input, ctx);
2219
3152
  throw err;
2220
- } finally {
2221
- clearTimeout(timer);
2222
- if (combined.aborted && !caught) {
2223
- if (!cleanupCalled && typeof tool.cleanup === "function") {
2224
- try {
2225
- await tool.cleanup(input, ctx);
2226
- } catch {
2227
- }
2228
- }
2229
- const reason = combined.reason instanceof Error ? combined.reason : new Error(typeof combined.reason === "string" ? combined.reason : "aborted");
2230
- throw reason;
2231
- }
3153
+ }
3154
+ if (combined.aborted) {
3155
+ await this.runToolCleanup(tool, input, ctx);
3156
+ throw combined.reason instanceof Error ? combined.reason : new Error(typeof combined.reason === "string" ? combined.reason : "tool timeout");
3157
+ }
3158
+ return output;
3159
+ }
3160
+ /** Best-effort tool cleanup; never let it mask the original error. */
3161
+ async runToolCleanup(tool, input, ctx) {
3162
+ if (typeof tool.cleanup !== "function") return;
3163
+ try {
3164
+ await tool.cleanup(input, ctx);
3165
+ } catch {
2232
3166
  }
2233
3167
  }
2234
3168
  async runStreamedTool(tool, input, ctx, signal, toolUseId) {
@@ -2397,6 +3331,25 @@ function extractMalformedRaw(input) {
2397
3331
  return String(value);
2398
3332
  }
2399
3333
  }
3334
+ var TOOL_OUTPUT_ARTIFACT_THRESHOLD_BYTES = 64 * 1024;
3335
+ async function maybePersistLargeToolOutput(toolName, content, budget) {
3336
+ const bytes = Buffer.byteLength(content, "utf8");
3337
+ if (bytes <= Math.min(TOOL_OUTPUT_ARTIFACT_THRESHOLD_BYTES, Math.max(0, budget))) {
3338
+ return content;
3339
+ }
3340
+ try {
3341
+ const dir = path3.join(wstackGlobalRoot(), "tool-output");
3342
+ await fs.mkdir(dir, { recursive: true });
3343
+ const safeTool = toolName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 40) || "tool";
3344
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3345
+ const filePath = path3.join(dir, `${stamp}-${safeTool}-${randomUUID()}.log`);
3346
+ await fs.writeFile(filePath, content, "utf8");
3347
+ return content + `
3348
+ [full tool output: ${bytes} bytes at ${filePath}; read/grep that file selectively instead of re-running or requesting more output]`;
3349
+ } catch {
3350
+ return content;
3351
+ }
3352
+ }
2400
3353
 
2401
3354
  // src/execution/autonomous-runner.ts
2402
3355
  var DoneConditionChecker = class {
@@ -3592,6 +4545,7 @@ ${recentJournal}` : ""
3592
4545
 
3593
4546
  // src/coordination/subagent-budget.ts
3594
4547
  var TIMEOUT_PREEMPT_FRACTION = 0.85;
4548
+ var DECISION_TIMEOUT_MS = 6e4;
3595
4549
  var BudgetExceededError = class extends Error {
3596
4550
  kind;
3597
4551
  limit;
@@ -3621,6 +4575,31 @@ var BudgetThresholdSignal = class extends Error {
3621
4575
  };
3622
4576
  var SubagentBudget = class _SubagentBudget {
3623
4577
  limits;
4578
+ /** Patch one or more budget limits in-place after construction.
4579
+ * Used by the coordinator watchdog when granting an extension.
4580
+ * All fields are optional — only provided fields are updated.
4581
+ * This is the single write path for limit mutations so that future
4582
+ * validation or side-effects live in one place (M1). */
4583
+ patchLimits(ext) {
4584
+ if (ext.maxIterations !== void 0) {
4585
+ this.limits.maxIterations = ext.maxIterations;
4586
+ }
4587
+ if (ext.maxToolCalls !== void 0) {
4588
+ this.limits.maxToolCalls = ext.maxToolCalls;
4589
+ }
4590
+ if (ext.maxTokens !== void 0) {
4591
+ this.limits.maxTokens = ext.maxTokens;
4592
+ }
4593
+ if (ext.maxCostUsd !== void 0) {
4594
+ this.limits.maxCostUsd = ext.maxCostUsd;
4595
+ }
4596
+ if (ext.timeoutMs !== void 0) {
4597
+ this.limits.timeoutMs = ext.timeoutMs;
4598
+ }
4599
+ if (ext.idleTimeoutMs !== void 0) {
4600
+ this.limits.idleTimeoutMs = ext.idleTimeoutMs;
4601
+ }
4602
+ }
3624
4603
  iterations = 0;
3625
4604
  toolCalls = 0;
3626
4605
  tokenInput = 0;
@@ -3641,12 +4620,44 @@ var SubagentBudget = class _SubagentBudget {
3641
4620
  * or hung listener (Director not built / event filter detached mid-run)
3642
4621
  * leaves the budget over-limit and never enforces anything.
3643
4622
  */
3644
- static DECISION_TIMEOUT_MS = 6e4;
4623
+ static DECISION_TIMEOUT_MS = DECISION_TIMEOUT_MS;
3645
4624
  /**
3646
4625
  * Injected by the runner when wiring the budget to its EventBus.
3647
4626
  * Used to emit `budget.threshold_reached` events in `'auto'` mode.
3648
4627
  */
3649
4628
  _events;
4629
+ /**
4630
+ * Guard against dual-path races between the coordinator watchdog
4631
+ * (`executeWithTimeout`) and the budget's own `checkTimeout()`.
4632
+ * Both paths detect `elapsed >= timeoutMs` and can emit
4633
+ * `budget.threshold_reached` for kind `'timeout'` simultaneously.
4634
+ * Set to the current `timeoutMs` ceiling by the coordinator BEFORE
4635
+ * calling `onThreshold`, and cleared after the negotiation resolves.
4636
+ * `checkTimeout()` skips its wall-clock check while this is set so
4637
+ * the coordinator's watchdog is the sole source of wall-clock timeout
4638
+ * events — `checkTimeout()` focuses exclusively on `idle_timeout`.
4639
+ */
4640
+ _watchdogActive;
4641
+ /** Returns the timeout ceiling currently being negotiated by the watchdog,
4642
+ * or `undefined` when no wall-clock negotiation is in flight.
4643
+ * Used by `executeWithTimeout` to detect a stale lock (M3). */
4644
+ get watchdogActive() {
4645
+ return this._watchdogActive;
4646
+ }
4647
+ /** Called by the coordinator watchdog BEFORE calling `onThreshold` so that
4648
+ * `checkTimeout()` skips its wall-clock check for this ceiling. Prevents
4649
+ * the budget's own `checkTimeout()` from emitting a second
4650
+ * `budget.threshold_reached` event while the watchdog is already
4651
+ * negotiating the same wall-clock deadline (C1). */
4652
+ setWatchdogNegotiation(timeoutMs) {
4653
+ this._watchdogActive = timeoutMs;
4654
+ }
4655
+ /** Clears the watchdog guard after negotiation resolves. Called in the
4656
+ * `finally` block of both the pre-empt and deadline branches so it fires
4657
+ * on every exit path: grant, deny, throw, or error. */
4658
+ clearWatchdogNegotiation() {
4659
+ this._watchdogActive = void 0;
4660
+ }
3650
4661
  /**
3651
4662
  * Negotiation mode — controls whether a threshold hit tries to emit
3652
4663
  * `budget.threshold_reached` and wait for a coordinator decision, or
@@ -3747,7 +4758,8 @@ var SubagentBudget = class _SubagentBudget {
3747
4758
  if (this.limits.idleTimeoutMs !== void 0 && idle > this.limits.idleTimeoutMs) {
3748
4759
  exceeded.push({ kind: "idle_timeout", used: idle, limit: this.limits.idleTimeoutMs });
3749
4760
  }
3750
- if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs) {
4761
+ const wallOwnedByWatchdog = this._onThreshold !== void 0 && this._watchdogActive === this.limits.timeoutMs;
4762
+ if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs && !wallOwnedByWatchdog) {
3751
4763
  exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
3752
4764
  }
3753
4765
  }
@@ -3761,19 +4773,99 @@ var SubagentBudget = class _SubagentBudget {
3761
4773
  throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
3762
4774
  }
3763
4775
  const bus = this._events;
3764
- if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
4776
+ if (!bus) {
3765
4777
  const first2 = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
3766
4778
  throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
3767
4779
  }
4780
+ const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
4781
+ if (bus.hasListenerFor("budget.threshold_reached")) {
4782
+ for (const entry of exceeded) {
4783
+ if (this._pendingNegotiations.has(entry.kind)) continue;
4784
+ this._pendingNegotiations.set(entry.kind, this._negotiateExtension(entry));
4785
+ }
4786
+ const decision = this._pendingNegotiations.get(first.kind);
4787
+ if (!decision) throw new Error(`No pending negotiation for ${first.kind}`);
4788
+ throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
4789
+ }
4790
+ let hardStop = null;
3768
4791
  for (const entry of exceeded) {
3769
4792
  if (this._pendingNegotiations.has(entry.kind)) continue;
3770
- const decision2 = this._negotiateExtension(entry.kind, exceeded);
3771
- this._pendingNegotiations.set(entry.kind, decision2);
4793
+ const marker = Promise.resolve("stop");
4794
+ this._pendingNegotiations.set(entry.kind, marker);
4795
+ void marker.finally(() => this._pendingNegotiations.delete(entry.kind));
4796
+ const sync = this._invokeHandlerSync(entry);
4797
+ if (!sync) hardStop ??= new BudgetExceededError(entry.kind, entry.limit, entry.used);
3772
4798
  }
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);
4799
+ if (hardStop) throw hardStop;
4800
+ return exceeded;
4801
+ }
4802
+ /**
4803
+ * Invoke `onThreshold` once for `entry` on the NO-LISTENER path and report
4804
+ * whether it decided synchronously. Returns `true` when the handler returned
4805
+ * a synchronous decision (already honored — an `extend` patched the limits),
4806
+ * or `false` when it returned a Promise (async; the caller hard-stops, since
4807
+ * there is no listener to resolve the negotiation). The handler is given the
4808
+ * full info shape (`requestDecision` plus direct `extend`/`deny`) so both
4809
+ * recording handlers and policy handlers work without a wired listener.
4810
+ */
4811
+ _invokeHandlerSync(entry) {
4812
+ const handler = this._onThreshold;
4813
+ if (!handler) return false;
4814
+ let extendArg;
4815
+ const result = handler({
4816
+ kind: entry.kind,
4817
+ used: entry.used,
4818
+ limit: entry.limit,
4819
+ requestDecision: () => this._busRequestDecision(entry),
4820
+ // Direct hooks for synchronous policy/recording handlers.
4821
+ extend: (extra) => {
4822
+ extendArg = extra;
4823
+ },
4824
+ deny: () => {
4825
+ }
4826
+ });
4827
+ if (result && typeof result.then === "function") return false;
4828
+ if (result === "throw") return false;
4829
+ if (result && typeof result === "object" && "extend" in result) {
4830
+ extendArg = result.extend;
4831
+ }
4832
+ if (extendArg) this.patchLimits(extendArg);
4833
+ return true;
4834
+ }
4835
+ /**
4836
+ * Emit `budget.threshold_reached` and resolve to the listener's verdict.
4837
+ * Resolves to `'stop'` immediately when there is no listener (or no bus) so
4838
+ * no negotiation can hang and no fallback timer leaks. Mirrors the
4839
+ * coordinator watchdog's own request path so both agree on the no-listener
4840
+ * default.
4841
+ */
4842
+ _busRequestDecision(entry) {
4843
+ const bus = this._events;
4844
+ if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
4845
+ return Promise.resolve("stop");
4846
+ }
4847
+ return new Promise((resolve3) => {
4848
+ let resolved = false;
4849
+ const respond = (d) => {
4850
+ if (resolved) return;
4851
+ resolved = true;
4852
+ clearTimeout(fallback);
4853
+ resolve3(d);
4854
+ };
4855
+ const fallback = setTimeout(() => respond("stop"), _SubagentBudget.DECISION_TIMEOUT_MS);
4856
+ bus.emit("budget.threshold_reached", {
4857
+ kind: entry.kind,
4858
+ used: entry.used,
4859
+ limit: entry.limit,
4860
+ timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
4861
+ // deny() wins over a same-dispatch extend(): a listener that both grants
4862
+ // and denies (or two listeners disagreeing) is resolved as a stop. The
4863
+ // grant is deferred a microtask so a synchronous deny in the same emit
4864
+ // pre-empts it; async grants still resolve normally.
4865
+ extend: (extra) => queueMicrotask(() => respond({ extend: extra })),
4866
+ deny: () => respond("stop")
4867
+ });
4868
+ });
3777
4869
  }
3778
4870
  /**
3779
4871
  * Per-kind in-flight negotiation Promises. Each budget kind can have its
@@ -3793,77 +4885,33 @@ var SubagentBudget = class _SubagentBudget {
3793
4885
  * `{ extend: {} }` — keep going without patching; next overrun fires
3794
4886
  * a fresh signal.
3795
4887
  */
3796
- async _negotiateExtension(kind, exceeded) {
4888
+ async _negotiateExtension(entry) {
3797
4889
  if (!this._onThreshold) {
3798
4890
  return "stop";
3799
4891
  }
3800
4892
  try {
3801
- const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
3802
4893
  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
- });
4894
+ kind: entry.kind,
4895
+ used: entry.used,
4896
+ limit: entry.limit,
4897
+ // One event for THIS kind only — each exceeded kind has its own
4898
+ // negotiation (and its own resolve), so there is no cross-kind
4899
+ // first-wins drop and no O(N^2) re-emission.
4900
+ requestDecision: () => this._busRequestDecision(entry),
4901
+ extend: (extra) => {
4902
+ this.patchLimits(extra);
4903
+ },
4904
+ deny: () => {
3839
4905
  }
3840
4906
  });
3841
4907
  if (result === "throw") return "stop";
3842
4908
  if (result === "continue") return { extend: {} };
3843
4909
  const decision = await result;
3844
4910
  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
- }
4911
+ this.patchLimits(decision.extend);
3864
4912
  return decision;
3865
4913
  } finally {
3866
- this._pendingNegotiations.delete(kind);
4914
+ this._pendingNegotiations.delete(entry.kind);
3867
4915
  }
3868
4916
  }
3869
4917
  recordIteration() {
@@ -3906,7 +4954,8 @@ var SubagentBudget = class _SubagentBudget {
3906
4954
  const { timeoutMs, idleTimeoutMs } = this.limits;
3907
4955
  if (timeoutMs === void 0 && idleTimeoutMs === void 0) return;
3908
4956
  const elapsed = Date.now() - this.startTime;
3909
- const wallTripped = timeoutMs !== void 0 && elapsed > timeoutMs;
4957
+ const wallSkipped = this._onThreshold !== void 0 && this._watchdogActive !== void 0 && timeoutMs !== void 0 && this._watchdogActive === timeoutMs;
4958
+ const wallTripped = wallSkipped ? false : timeoutMs !== void 0 && elapsed > timeoutMs;
3910
4959
  const idleTripped = idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs;
3911
4960
  if (!wallTripped && !idleTripped) return;
3912
4961
  void this.checkLimits(elapsed);
@@ -7225,6 +8274,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7225
8274
  terminating = /* @__PURE__ */ new Set();
7226
8275
  constructor(config, options = {}) {
7227
8276
  super();
8277
+ this.setMaxListeners(0);
7228
8278
  this.coordinatorId = config.coordinatorId;
7229
8279
  this.config = config;
7230
8280
  this.runner = options.runner;
@@ -7422,7 +8472,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7422
8472
  taskIds.map((id) => {
7423
8473
  const cached = this.completedResults.find((r) => r.taskId === id);
7424
8474
  if (cached) return cached;
7425
- return new Promise((resolve2, reject) => {
8475
+ return new Promise((resolve3, reject) => {
7426
8476
  const timeout = setTimeout(() => {
7427
8477
  this.off("task.completed", handler);
7428
8478
  reject(new Error(`awaitTasks timed out waiting for task "${id}"`));
@@ -7431,7 +8481,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7431
8481
  if (result.taskId === id) {
7432
8482
  clearTimeout(timeout);
7433
8483
  this.off("task.completed", handler);
7434
- resolve2(result);
8484
+ resolve3(result);
7435
8485
  }
7436
8486
  };
7437
8487
  this.on("task.completed", handler);
@@ -7619,7 +8669,13 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7619
8669
  let result;
7620
8670
  budget.start();
7621
8671
  try {
7622
- const outcome = await this.executeWithTimeout(this.runner, task, runCtx, budget);
8672
+ const outcome = await this.executeWithTimeout(
8673
+ this.runner,
8674
+ task,
8675
+ runCtx,
8676
+ budget,
8677
+ subagent.config.preemptFraction
8678
+ );
7623
8679
  result = {
7624
8680
  subagentId,
7625
8681
  taskId: task.id,
@@ -7646,7 +8702,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7646
8702
  }
7647
8703
  this.recordCompletion(result);
7648
8704
  }
7649
- async executeWithTimeout(runner, task, ctx, budget) {
8705
+ async executeWithTimeout(runner, task, ctx, budget, preemptFraction = TIMEOUT_PREEMPT_FRACTION) {
7650
8706
  const initialTimeoutMs = budget.limits.timeoutMs;
7651
8707
  const idleLimitMs = budget.limits.idleTimeoutMs;
7652
8708
  if (initialTimeoutMs === void 0 && idleLimitMs === void 0) {
@@ -7654,8 +8710,21 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7654
8710
  }
7655
8711
  const start = Date.now();
7656
8712
  let timer = null;
7657
- let preemptedForLimit = null;
8713
+ let PreemptState;
8714
+ ((PreemptState2) => {
8715
+ PreemptState2["ACTIVE"] = "active";
8716
+ PreemptState2["LOCKED"] = "locked";
8717
+ })(PreemptState || (PreemptState = {}));
8718
+ let preemptedCeiling = null;
8719
+ let preemptState = "active" /* ACTIVE */;
8720
+ let lastGrantActivityTs = -1;
7658
8721
  const timeoutPromise = new Promise((_, reject) => {
8722
+ const terminate = (kind, limit, used) => {
8723
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
8724
+ reject(
8725
+ budget._events?.hasListenerFor("budget.threshold_reached") ? new Error(`subagent stopped: budget ${kind} (limit=${limit}, used=${used})`) : new BudgetExceededError(kind, limit, used)
8726
+ );
8727
+ };
7659
8728
  const armFor = (ms) => {
7660
8729
  if (timer) clearTimeout(timer);
7661
8730
  timer = setTimeout(onTick, Math.max(0, ms));
@@ -7664,7 +8733,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7664
8733
  const wallLimit = budget.limits.timeoutMs ?? initialTimeoutMs;
7665
8734
  const wallRemaining = initialTimeoutMs === void 0 ? Number.POSITIVE_INFINITY : wallLimit - (Date.now() - start);
7666
8735
  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);
8736
+ const preemptRemaining = initialTimeoutMs === void 0 || preemptedCeiling === wallLimit ? Number.POSITIVE_INFINITY : wallLimit * preemptFraction - (Date.now() - start);
7668
8737
  armFor(Math.max(25, Math.min(wallRemaining, idleRemaining, preemptRemaining)));
7669
8738
  };
7670
8739
  const negotiateTimeout = async (used, limit) => {
@@ -7674,16 +8743,42 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7674
8743
  kind: "timeout",
7675
8744
  used,
7676
8745
  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")
8746
+ requestDecision: () => {
8747
+ if (!budget._events?.hasListenerFor("budget.threshold_reached")) {
8748
+ return Promise.resolve("stop");
8749
+ }
8750
+ return new Promise((resolveDecision) => {
8751
+ let settled = false;
8752
+ const resolve3 = (d) => {
8753
+ if (settled) return;
8754
+ settled = true;
8755
+ resolveDecision(d);
8756
+ };
8757
+ const fallback = setTimeout(() => resolve3("stop"), DECISION_TIMEOUT_MS);
8758
+ budget._events?.emit("budget.threshold_reached", {
8759
+ kind: "timeout",
8760
+ used,
8761
+ limit,
8762
+ // Informational: the budget's own decision deadline. Listeners may use
8763
+ // this to display a countdown. The coordinator does NOT enforce it —
8764
+ // it is the budget's own `setTimeout(fallback)` that races against
8765
+ // the listener's `extend()`/`deny()` call to guarantee progress.
8766
+ timeoutMs: DECISION_TIMEOUT_MS,
8767
+ // deny() wins over a same-dispatch extend(): defer the grant a
8768
+ // microtask so a synchronous deny in the same emit pre-empts it
8769
+ // (a listener that both grants and denies, or two listeners
8770
+ // disagreeing, resolves as a stop). Async grants still resolve.
8771
+ extend: (extra) => {
8772
+ clearTimeout(fallback);
8773
+ queueMicrotask(() => resolve3({ extend: extra }));
8774
+ },
8775
+ deny: () => {
8776
+ clearTimeout(fallback);
8777
+ resolve3("stop");
8778
+ }
8779
+ });
7685
8780
  });
7686
- })
8781
+ }
7687
8782
  });
7688
8783
  return typeof result === "string" ? result : await result;
7689
8784
  };
@@ -7694,21 +8789,45 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7694
8789
  const wallExceeded = wallLimit !== void 0 && elapsed >= wallLimit;
7695
8790
  const idleExceeded = idleLimit !== void 0 && budget.idleMs() >= idleLimit;
7696
8791
  if (idleExceeded && !wallExceeded) {
8792
+ budget._events?.emit("budget.threshold_reached", {
8793
+ kind: "idle_timeout",
8794
+ used: budget.idleMs(),
8795
+ limit: idleLimit ?? 0,
8796
+ timeoutMs: DECISION_TIMEOUT_MS,
8797
+ extend: () => {
8798
+ },
8799
+ deny: () => {
8800
+ }
8801
+ });
7697
8802
  this.subagents.get(ctx.subagentId)?.abortController.abort();
7698
- reject(new BudgetExceededError("timeout", idleLimit ?? 0, budget.idleMs()));
8803
+ reject(new BudgetExceededError("idle_timeout", idleLimit ?? 0, budget.idleMs()));
7699
8804
  return;
7700
8805
  }
7701
- if (wallLimit !== void 0 && !wallExceeded && budget.onThreshold && preemptedForLimit !== wallLimit && elapsed >= wallLimit * TIMEOUT_PREEMPT_FRACTION) {
8806
+ if (wallLimit !== void 0 && !wallExceeded && budget.onThreshold && preemptState === "active" /* ACTIVE */ && elapsed >= wallLimit * preemptFraction) {
8807
+ const activityTs = Date.now() - budget.idleMs();
8808
+ if (activityTs <= lastGrantActivityTs) {
8809
+ preemptState = "locked" /* LOCKED */;
8810
+ preemptedCeiling = wallLimit;
8811
+ scheduleNext();
8812
+ return;
8813
+ }
8814
+ budget.setWatchdogNegotiation(wallLimit);
7702
8815
  try {
7703
8816
  const decision = await negotiateTimeout(elapsed, wallLimit);
7704
8817
  if (typeof decision !== "string" && decision.extend.timeoutMs !== void 0) {
7705
- budget.limits.timeoutMs = decision.extend.timeoutMs;
7706
- preemptedForLimit = null;
8818
+ budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
8819
+ lastGrantActivityTs = Date.now() - budget.idleMs();
8820
+ preemptState = "active" /* ACTIVE */;
8821
+ preemptedCeiling = null;
7707
8822
  } else {
7708
- preemptedForLimit = wallLimit;
8823
+ preemptState = "locked" /* LOCKED */;
8824
+ preemptedCeiling = wallLimit;
7709
8825
  }
7710
8826
  } catch {
7711
- preemptedForLimit = wallLimit;
8827
+ preemptState = "locked" /* LOCKED */;
8828
+ preemptedCeiling = wallLimit;
8829
+ } finally {
8830
+ budget.clearWatchdogNegotiation();
7712
8831
  }
7713
8832
  scheduleNext();
7714
8833
  return;
@@ -7723,26 +8842,41 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
7723
8842
  reject(new BudgetExceededError("timeout", limit, elapsed));
7724
8843
  return;
7725
8844
  }
8845
+ budget.setWatchdogNegotiation(limit);
7726
8846
  try {
7727
8847
  const decision = await negotiateTimeout(elapsed, limit);
7728
- if (decision === "continue" || decision === "throw" || decision === "stop") {
7729
- preemptedForLimit = null;
8848
+ if (decision === "throw") {
8849
+ terminate("timeout", limit, elapsed);
8850
+ return;
8851
+ }
8852
+ if (decision === "continue") {
8853
+ preemptState = "locked" /* LOCKED */;
8854
+ preemptedCeiling = wallLimit;
7730
8855
  armFor(Math.max(1e3, limit));
7731
8856
  return;
7732
8857
  }
8858
+ if (decision === "stop") {
8859
+ terminate("timeout", limit, elapsed);
8860
+ return;
8861
+ }
7733
8862
  if (decision.extend.timeoutMs !== void 0) {
7734
- budget.limits.timeoutMs = decision.extend.timeoutMs;
7735
- preemptedForLimit = null;
8863
+ budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
8864
+ lastGrantActivityTs = Date.now() - budget.idleMs();
8865
+ preemptState = "active" /* ACTIVE */;
8866
+ preemptedCeiling = null;
7736
8867
  scheduleNext();
7737
8868
  return;
7738
8869
  }
7739
- this.subagents.get(ctx.subagentId)?.abortController.abort();
7740
- reject(new BudgetExceededError("timeout", limit, elapsed));
8870
+ terminate("timeout", limit, elapsed);
8871
+ return;
7741
8872
  } catch (err) {
7742
8873
  this.subagents.get(ctx.subagentId)?.abortController.abort();
7743
8874
  reject(
7744
8875
  err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
7745
8876
  );
8877
+ return;
8878
+ } finally {
8879
+ budget.clearWatchdogNegotiation();
7746
8880
  }
7747
8881
  };
7748
8882
  scheduleNext();
@@ -8595,7 +9729,7 @@ var DefaultSkillLoader = class {
8595
9729
  const entries = await fs.readdir(dir, { withFileTypes: true });
8596
9730
  for (const e of entries) {
8597
9731
  if (!e.isDirectory()) continue;
8598
- const skillFile = path2.join(dir, e.name, "SKILL.md");
9732
+ const skillFile = path3.join(dir, e.name, "SKILL.md");
8599
9733
  try {
8600
9734
  const raw = await fs.readFile(skillFile, "utf8");
8601
9735
  const meta = parseFrontmatter(raw);
@@ -8620,12 +9754,12 @@ var DefaultSkillLoader = class {
8620
9754
  }
8621
9755
  async find(name) {
8622
9756
  const all = await this.list();
8623
- return all.find((s) => s.name === name);
9757
+ const lower = name.toLowerCase();
9758
+ return all.find((s) => s.name.toLowerCase() === lower);
8624
9759
  }
8625
9760
  async manifestText() {
8626
- const skills = await this.list();
8627
- if (skills.length === 0) return "";
8628
9761
  const entries = await this.listEntries();
9762
+ if (entries.length === 0) return "";
8629
9763
  const lines = ["## Available skills"];
8630
9764
  for (const e of entries) {
8631
9765
  const scopeTag = e.scope.length > 0 ? ` \u2014 ${e.scope.slice(0, 3).join(", ")}` : "";
@@ -8639,12 +9773,8 @@ var DefaultSkillLoader = class {
8639
9773
  const skills = await this.list();
8640
9774
  const entries = [];
8641
9775
  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
- }
9776
+ const { trigger, scope } = parseDescriptionFromText(s.description ?? "");
9777
+ entries.push({ name: s.name, trigger, scope, source: s.source, path: s.path });
8648
9778
  }
8649
9779
  this.entriesCache = entries;
8650
9780
  return entries;
@@ -8655,21 +9785,22 @@ var DefaultSkillLoader = class {
8655
9785
  this.bodyCache.clear();
8656
9786
  }
8657
9787
  async readBody(name) {
8658
- const cached = this.bodyCache.get(name);
9788
+ const key = name.toLowerCase();
9789
+ const cached = this.bodyCache.get(key);
8659
9790
  if (cached !== void 0) return cached;
8660
9791
  const m = await this.find(name);
8661
9792
  if (!m) throw new Error(`Skill "${name}" not found`);
8662
9793
  const body = await fs.readFile(m.path, "utf8");
8663
- this.bodyCache.set(name, body);
9794
+ this.bodyCache.set(key, body);
8664
9795
  return body;
8665
9796
  }
8666
9797
  async readSaveBody(name) {
8667
- const key = `save:${name}`;
9798
+ const key = `save:${name.toLowerCase()}`;
8668
9799
  const cached = this.bodyCache.get(key);
8669
9800
  if (cached !== void 0) return cached;
8670
9801
  const m = await this.find(name);
8671
9802
  if (!m) throw new Error(`Skill "${name}" not found`);
8672
- const savePath = path2.join(path2.dirname(m.path), "SKILL.save.md");
9803
+ const savePath = path3.join(path3.dirname(m.path), "SKILL.save.md");
8673
9804
  let result;
8674
9805
  try {
8675
9806
  result = await fs.readFile(savePath, "utf8");
@@ -8725,9 +9856,7 @@ function parseFrontmatter(raw) {
8725
9856
  flush();
8726
9857
  return out;
8727
9858
  }
8728
- function parseDescription(raw) {
8729
- const fm = parseFrontmatter(raw);
8730
- const desc = fm.description ?? "";
9859
+ function parseDescriptionFromText(desc) {
8731
9860
  const firstSentenceEnd = desc.indexOf(". ");
8732
9861
  const trigger = firstSentenceEnd !== -1 ? desc.slice(0, firstSentenceEnd + 1).trim() : desc.trim().split("\n")[0] ?? "";
8733
9862
  const scope = [];