@wrongstack/core 0.6.0 → 0.6.3

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 (41) hide show
  1. package/dist/{agent-bridge-BKNiE1VH.d.ts → agent-bridge-eb7qnNrd.d.ts} +1 -1
  2. package/dist/coordination/index.d.ts +5 -5
  3. package/dist/coordination/index.js +183 -87
  4. package/dist/coordination/index.js.map +1 -1
  5. package/dist/defaults/index.d.ts +10 -10
  6. package/dist/defaults/index.js +596 -114
  7. package/dist/defaults/index.js.map +1 -1
  8. package/dist/{events-DPQKFX7W.d.ts → events-BHuIHekD.d.ts} +27 -4
  9. package/dist/execution/index.d.ts +149 -6
  10. package/dist/execution/index.js +401 -21
  11. package/dist/execution/index.js.map +1 -1
  12. package/dist/extension/index.d.ts +2 -2
  13. package/dist/{goal-store-BQ3YX1h1.d.ts → goal-store-DVCfj7Ff.d.ts} +20 -0
  14. package/dist/{index-B0qTujQW.d.ts → index-BOn9NK7D.d.ts} +1 -1
  15. package/dist/{index-DkdRz6yK.d.ts → index-CPcDqvZh.d.ts} +11 -4
  16. package/dist/index.d.ts +50 -16
  17. package/dist/index.js +831 -125
  18. package/dist/index.js.map +1 -1
  19. package/dist/infrastructure/index.d.ts +2 -2
  20. package/dist/infrastructure/index.js +1 -1
  21. package/dist/infrastructure/index.js.map +1 -1
  22. package/dist/kernel/index.d.ts +2 -2
  23. package/dist/kernel/index.js.map +1 -1
  24. package/dist/{multi-agent-Cpp7FXUl.d.ts → multi-agent-CxSb-9dQ.d.ts} +30 -5
  25. package/dist/observability/index.d.ts +1 -1
  26. package/dist/observability/index.js +1 -1
  27. package/dist/observability/index.js.map +1 -1
  28. package/dist/{path-resolver--g_hKJ7V.d.ts → path-resolver-CMGNadvq.d.ts} +1 -1
  29. package/dist/{plan-templates-CKJs_sYh.d.ts → plan-templates-BJflQY2i.d.ts} +1 -1
  30. package/dist/{provider-runner-hl4Il3xS.d.ts → provider-runner-BFgNXpaP.d.ts} +1 -1
  31. package/dist/sdd/index.d.ts +2 -2
  32. package/dist/storage/index.d.ts +3 -3
  33. package/dist/storage/index.js +3 -0
  34. package/dist/storage/index.js.map +1 -1
  35. package/dist/{tool-executor-B03CRwu-.d.ts → tool-executor-FoxBjULX.d.ts} +1 -1
  36. package/dist/types/index.d.ts +7 -7
  37. package/dist/types/index.js +30 -4
  38. package/dist/types/index.js.map +1 -1
  39. package/dist/utils/index.js +1 -1
  40. package/dist/utils/index.js.map +1 -1
  41. package/package.json +5 -1
@@ -3691,17 +3691,23 @@ async function streamProviderToResponse(provider, req, signal, ctx, events) {
3691
3691
  handleTextDelta(state, ev.text);
3692
3692
  events.emit("provider.text_delta", { ctx, text: ev.text });
3693
3693
  break;
3694
- case "tool_use_start":
3695
- handleToolUseStart(state, ev);
3696
- events.emit("provider.tool_use_start", { ctx, id: ev.id, name: ev.name });
3694
+ case "tool_use_start": {
3695
+ const idVal = ev.id;
3696
+ const nameVal = ev.name;
3697
+ handleToolUseStart(state, { id: idVal, name: nameVal });
3698
+ const emittedPayload = { ctx, id: idVal ?? "unknown", name: nameVal ?? "unknown" };
3699
+ events.emit("provider.tool_use_start", emittedPayload);
3697
3700
  break;
3701
+ }
3698
3702
  case "tool_use_input_delta":
3699
3703
  handleToolUseInputDelta(state, ev);
3700
3704
  break;
3701
- case "tool_use_stop":
3705
+ case "tool_use_stop": {
3706
+ const stoppedName = state.tools.get(ev.id)?.name ?? "unknown";
3702
3707
  handleToolUseStop(state, ev);
3703
- events.emit("provider.tool_use_stop", { ctx, id: ev.id });
3708
+ events.emit("provider.tool_use_stop", { ctx, id: ev.id, name: stoppedName });
3704
3709
  break;
3710
+ }
3705
3711
  case "thinking_start":
3706
3712
  handleThinkingStart(state, ev);
3707
3713
  break;
@@ -3828,7 +3834,7 @@ var DefaultProviderRunner = class {
3828
3834
  };
3829
3835
 
3830
3836
  // src/utils/token-estimate.ts
3831
- var RoughTokenEstimate = (text) => Math.max(1, Math.ceil(text.length / 4));
3837
+ var RoughTokenEstimate = (text, charsPerToken = 3.5) => Math.max(1, Math.ceil(text.length / charsPerToken));
3832
3838
  var ESTIMATE_CACHE = /* @__PURE__ */ new Map();
3833
3839
  var ESTIMATE_CACHE_MAX_SIZE = 1e4;
3834
3840
  function getCachedEstimate(key, compute) {
@@ -3915,7 +3921,7 @@ var HybridCompactor = class {
3915
3921
  eliseThreshold;
3916
3922
  estimator;
3917
3923
  constructor(opts = {}) {
3918
- this.preserveK = opts.preserveK ?? 10;
3924
+ this.preserveK = opts.preserveK ?? 5;
3919
3925
  this.eliseThreshold = opts.eliseThreshold ?? 2e3;
3920
3926
  this.estimator = opts.estimator ?? estimateTextTokens;
3921
3927
  }
@@ -3959,6 +3965,17 @@ var HybridCompactor = class {
3959
3965
  preserveStart = i;
3960
3966
  }
3961
3967
  }
3968
+ for (let i = preserveStart; i < messages.length; i++) {
3969
+ const m = messages[i];
3970
+ if (!m || typeof m.content === "string" || !Array.isArray(m.content)) continue;
3971
+ const hasToolUse2 = m.content.some((b) => b.type === "tool_use");
3972
+ if (hasToolUse2 && i + 1 < messages.length) {
3973
+ const next = messages[i + 1];
3974
+ if (next && next.role === "user" && typeof next.content !== "string" && Array.isArray(next.content) && next.content.some((b) => b.type === "tool_result")) {
3975
+ preserveStart = i + 1;
3976
+ }
3977
+ }
3978
+ }
3962
3979
  let saved = 0;
3963
3980
  let changed = false;
3964
3981
  const nextMessages = new Array(messages.length);
@@ -3980,7 +3997,7 @@ var HybridCompactor = class {
3980
3997
  const elided = {
3981
3998
  type: "tool_result",
3982
3999
  tool_use_id: b.tool_use_id,
3983
- content: `[elided: ~${tokens} tokens removed. Call the tool again if needed.]`,
4000
+ content: `[elided: ~${tokens} tokens]`,
3984
4001
  is_error: b.is_error
3985
4002
  };
3986
4003
  return elided;
@@ -4119,7 +4136,28 @@ var IntelligentCompactor = class {
4119
4136
  try {
4120
4137
  summaryText = await this.callSummarizer(toSummarize, ctx);
4121
4138
  } catch {
4122
- summaryText = `[${toSummarize.length} earlier turns omitted \u2014 key decisions and file states preserved in context]`;
4139
+ const toolNames = /* @__PURE__ */ new Set();
4140
+ const filePaths = /* @__PURE__ */ new Set();
4141
+ let userTurns = 0, assistantTurns = 0;
4142
+ for (const m of toSummarize) {
4143
+ if (m.role === "user") userTurns++;
4144
+ else if (m.role === "assistant") {
4145
+ assistantTurns++;
4146
+ if (Array.isArray(m.content)) {
4147
+ for (const b of m.content) {
4148
+ if (b.type === "tool_use") toolNames.add(b.name ?? "unknown");
4149
+ }
4150
+ }
4151
+ }
4152
+ const text = typeof m.content === "string" ? m.content : "";
4153
+ const matches = text.matchAll(/(?:[\w.,\-/@]+\/)*[\w.,\-/@]+\.\w+/g);
4154
+ for (const m_ of matches) filePaths.add(m_[0]);
4155
+ }
4156
+ const parts = [`${toSummarize.length} turns (${userTurns} user, ${assistantTurns} assistant)`];
4157
+ if (toolNames.size > 0) parts.push(`tools: ${[...toolNames].join(", ")}`);
4158
+ if (filePaths.size > 0) parts.push(`files: ${[...filePaths].slice(0, 10).join(", ")}`);
4159
+ summaryText = parts.join(" | ");
4160
+ if (!summaryText) summaryText = `${toSummarize.length} earlier turns omitted`;
4123
4161
  }
4124
4162
  const summaryMsg = {
4125
4163
  role: "system",
@@ -4203,6 +4241,17 @@ var IntelligentCompactor = class {
4203
4241
  preserveStart = i;
4204
4242
  }
4205
4243
  }
4244
+ for (let i = preserveStart; i < messages.length; i++) {
4245
+ const m = messages[i];
4246
+ if (!m || typeof m.content === "string" || !Array.isArray(m.content)) continue;
4247
+ const hasToolUse2 = m.content.some((b) => b.type === "tool_use");
4248
+ if (hasToolUse2 && i + 1 < messages.length) {
4249
+ const next = messages[i + 1];
4250
+ if (next && next.role === "user" && typeof next.content !== "string" && Array.isArray(next.content) && next.content.some((b) => b.type === "tool_result")) {
4251
+ preserveStart = i + 1;
4252
+ }
4253
+ }
4254
+ }
4206
4255
  let saved = 0;
4207
4256
  let changed = false;
4208
4257
  const nextMessages = new Array(messages.length);
@@ -4717,7 +4766,15 @@ var AutoCompactionMiddleware = class {
4717
4766
  }
4718
4767
  async compact(ctx, aggressive, pressure) {
4719
4768
  try {
4720
- await this.compactor.compact(ctx, { aggressive });
4769
+ const report = await this.compactor.compact(ctx, { aggressive });
4770
+ this.events?.emit("compaction.fired", {
4771
+ level: pressure.level,
4772
+ tokens: pressure.tokens,
4773
+ load: pressure.load,
4774
+ maxContext: this._maxContext,
4775
+ report,
4776
+ aggressive
4777
+ });
4721
4778
  } catch (err) {
4722
4779
  const error = err instanceof Error ? err : new Error(String(err));
4723
4780
  const fatal = this.failureMode === "throw" || this.failureMode === "throw_on_hard" && pressure.level === "hard";
@@ -4971,13 +5028,17 @@ var ToolExecutor = class {
4971
5028
  const ctrl = new AbortController();
4972
5029
  const timer = setTimeout(() => ctrl.abort(new Error("tool timeout")), timeoutMs);
4973
5030
  const combined = AbortSignal.any([parentSignal, ctrl.signal]);
5031
+ let cleanupCalled = false;
5032
+ let caught = false;
4974
5033
  try {
4975
5034
  if (typeof tool.executeStream === "function") {
4976
5035
  return await this.runStreamedTool(tool, input, ctx, combined, toolUseId);
4977
5036
  }
4978
5037
  return await tool.execute(input, ctx, { signal: combined });
4979
5038
  } catch (err) {
5039
+ caught = true;
4980
5040
  if (combined.aborted && typeof tool.cleanup === "function") {
5041
+ cleanupCalled = true;
4981
5042
  try {
4982
5043
  await tool.cleanup(input, ctx);
4983
5044
  } catch {
@@ -4986,6 +5047,16 @@ var ToolExecutor = class {
4986
5047
  throw err;
4987
5048
  } finally {
4988
5049
  clearTimeout(timer);
5050
+ if (combined.aborted && !caught) {
5051
+ if (!cleanupCalled && typeof tool.cleanup === "function") {
5052
+ try {
5053
+ await tool.cleanup(input, ctx);
5054
+ } catch {
5055
+ }
5056
+ }
5057
+ const reason = combined.reason instanceof Error ? combined.reason : new Error(typeof combined.reason === "string" ? combined.reason : "aborted");
5058
+ throw reason;
5059
+ }
4989
5060
  }
4990
5061
  }
4991
5062
  async runStreamedTool(tool, input, ctx, signal, toolUseId) {
@@ -5043,7 +5114,8 @@ var ToolExecutor = class {
5043
5114
  if (subjectKey) {
5044
5115
  const v = obj[subjectKey];
5045
5116
  if (typeof v === "string") {
5046
- return subjectKey === "path" || subjectKey === "file" || subjectKey === "files" ? normalizePath(v) : escapeGlob(v);
5117
+ const isPathKey = subjectKey === "path" || subjectKey === "file" || subjectKey === "files";
5118
+ return isPathKey ? normalizePath(v) : escapeGlob(v);
5047
5119
  }
5048
5120
  }
5049
5121
  if (toolName === "bash" && typeof obj.command === "string") {
@@ -5269,6 +5341,8 @@ function appendJournal(goal, entry) {
5269
5341
 
5270
5342
  // src/execution/eternal-autonomy.ts
5271
5343
  var execFileP = promisify(execFile);
5344
+ var BRAINSTORM_DONE = /* @__PURE__ */ Symbol("brainstorm-done");
5345
+ var GOAL_COMPLETE_MARKER = /^\s*\[goal[_\s-]?complete\]\s*$/im;
5272
5346
  var EternalAutonomyEngine = class {
5273
5347
  constructor(opts) {
5274
5348
  this.opts = opts;
@@ -5278,6 +5352,14 @@ var EternalAutonomyEngine = class {
5278
5352
  state = "idle";
5279
5353
  stopRequested = false;
5280
5354
  consecutiveFailures = 0;
5355
+ consecutiveBrainstormDone = 0;
5356
+ /**
5357
+ * Count of consecutive transient (recoverable) provider failures. Drives
5358
+ * the exponential backoff between iterations. Reset on the first
5359
+ * successful iteration so a single bad afternoon doesn't permanently
5360
+ * slow the loop down.
5361
+ */
5362
+ consecutiveTransientRetries = 0;
5281
5363
  currentCtrl = null;
5282
5364
  iterationsSinceCompact = 0;
5283
5365
  goalPath;
@@ -5347,9 +5429,16 @@ var EternalAutonomyEngine = class {
5347
5429
  this.stopRequested = true;
5348
5430
  return false;
5349
5431
  }
5432
+ const missionState = goal.goalState ?? "active";
5433
+ if (missionState !== "active") {
5434
+ this.stopRequested = true;
5435
+ return false;
5436
+ }
5350
5437
  const action = await this.decide(goal);
5351
5438
  if (!action) {
5352
- await sleep(5e3);
5439
+ if (!this.stopRequested) {
5440
+ await sleep(5e3);
5441
+ }
5353
5442
  return false;
5354
5443
  }
5355
5444
  const ctrl = new AbortController();
@@ -5360,13 +5449,29 @@ var EternalAutonomyEngine = class {
5360
5449
  );
5361
5450
  let status = "success";
5362
5451
  let note;
5452
+ let finalText = "";
5453
+ let isTransientFailure = false;
5363
5454
  const tc = this.opts.agent.ctx?.tokenCounter;
5364
5455
  const beforeUsage = tc?.total?.();
5365
5456
  const beforeCost = tc?.estimateCost?.().total;
5366
5457
  try {
5367
5458
  const result = await this.opts.agent.run(
5368
5459
  [{ type: "text", text: action.directive }],
5369
- { signal: ctrl.signal }
5460
+ {
5461
+ signal: ctrl.signal,
5462
+ // Enable per-call autonomous continuation so the agent can chain
5463
+ // multiple internal tool/response cycles end-to-end on one
5464
+ // directive instead of returning to the engine after a single
5465
+ // round-trip. The model uses `[continue]` / `[done]` markers
5466
+ // (or the `continue_to_next_iteration` tool) to control the
5467
+ // inner loop. Without this flag the engine produced shallow
5468
+ // iterations and almost never let a real task finish.
5469
+ autonomousContinue: true,
5470
+ // Cap the inner loop so a runaway agent.run can't burn through
5471
+ // the iteration timeout — the engine's own outer loop is the
5472
+ // long-running thing, each tick should be bounded.
5473
+ maxIterations: this.opts.iterationMaxAgentSteps ?? 50
5474
+ }
5370
5475
  );
5371
5476
  if (result.status === "aborted") {
5372
5477
  status = "aborted";
@@ -5374,22 +5479,30 @@ var EternalAutonomyEngine = class {
5374
5479
  } else if (result.status === "failed") {
5375
5480
  status = "failure";
5376
5481
  note = result.error?.describe?.() ?? "agent run failed";
5482
+ isTransientFailure = result.error?.recoverable === true;
5377
5483
  } else if (result.status === "max_iterations") {
5378
5484
  status = "failure";
5379
5485
  note = `max iterations (${result.iterations})`;
5380
5486
  } else {
5381
5487
  status = "success";
5382
- const tail = (result.finalText ?? "").slice(0, 240).replace(/\s+/g, " ").trim();
5488
+ finalText = result.finalText ?? "";
5489
+ const tail = finalText.slice(0, 240).replace(/\s+/g, " ").trim();
5383
5490
  if (tail) note = tail;
5384
5491
  }
5385
5492
  } catch (err) {
5386
5493
  const isAbort = err instanceof Error && (err.name === "AbortError" || err.message.includes("abort"));
5387
5494
  status = isAbort ? "aborted" : "failure";
5388
5495
  note = err instanceof Error ? err.message : String(err);
5496
+ if (!isAbort && typeof err?.recoverable === "boolean") {
5497
+ isTransientFailure = err.recoverable;
5498
+ }
5389
5499
  } finally {
5390
5500
  clearTimeout(timer);
5391
5501
  this.currentCtrl = null;
5392
5502
  }
5503
+ if (action.source === "todo" && action.todoId && status !== "success") {
5504
+ await this.bumpTodoAttempt(action.todoId);
5505
+ }
5393
5506
  const afterUsage = tc?.total?.();
5394
5507
  const afterCost = tc?.estimateCost?.().total;
5395
5508
  const tokens = beforeUsage && afterUsage ? {
@@ -5422,6 +5535,14 @@ var EternalAutonomyEngine = class {
5422
5535
  costUsd
5423
5536
  });
5424
5537
  if (status === "failure") {
5538
+ if (isTransientFailure) {
5539
+ this.consecutiveTransientRetries++;
5540
+ const delay = this.computeTransientBackoffMs();
5541
+ if (delay > 0) {
5542
+ await this.sleepInterruptible(delay);
5543
+ }
5544
+ return false;
5545
+ }
5425
5546
  this.consecutiveFailures++;
5426
5547
  return false;
5427
5548
  }
@@ -5430,6 +5551,12 @@ var EternalAutonomyEngine = class {
5430
5551
  this.consecutiveFailures++;
5431
5552
  return false;
5432
5553
  }
5554
+ this.consecutiveTransientRetries = 0;
5555
+ if (GOAL_COMPLETE_MARKER.test(finalText)) {
5556
+ await this.markGoalCompleted(action, finalText);
5557
+ this.stopRequested = true;
5558
+ return true;
5559
+ }
5433
5560
  this.iterationsSinceCompact++;
5434
5561
  await this.maybeCompact().catch((err) => {
5435
5562
  this.opts.onError?.(
@@ -5493,11 +5620,12 @@ var EternalAutonomyEngine = class {
5493
5620
  async decide(goal) {
5494
5621
  const forceBrainstorm = this.consecutiveFailures >= (this.opts.failureBudget ?? 3);
5495
5622
  if (!forceBrainstorm) {
5496
- const todo = this.pickPendingTodo();
5623
+ const todo = this.pickPendingTodo(goal);
5497
5624
  if (todo) {
5498
5625
  return {
5499
5626
  source: "todo",
5500
5627
  task: todo.content,
5628
+ todoId: todo.id,
5501
5629
  directive: this.buildDirective(goal, "todo", todo.content)
5502
5630
  };
5503
5631
  }
@@ -5511,17 +5639,38 @@ var EternalAutonomyEngine = class {
5511
5639
  }
5512
5640
  }
5513
5641
  const brainstormed = await this.brainstormTask(goal);
5642
+ if (brainstormed === BRAINSTORM_DONE) {
5643
+ this.consecutiveBrainstormDone++;
5644
+ const threshold = this.opts.brainstormDoneStopThreshold ?? 3;
5645
+ if (this.consecutiveBrainstormDone >= threshold) {
5646
+ await this.markGoalCompleted(
5647
+ { source: "brainstorm", task: "no further work", directive: "" },
5648
+ `brainstorm returned DONE ${this.consecutiveBrainstormDone}x in a row`
5649
+ );
5650
+ this.stopRequested = true;
5651
+ }
5652
+ return null;
5653
+ }
5514
5654
  if (!brainstormed) return null;
5655
+ this.consecutiveBrainstormDone = 0;
5515
5656
  return {
5516
5657
  source: "brainstorm",
5517
5658
  task: brainstormed,
5518
5659
  directive: this.buildDirective(goal, "brainstorm", brainstormed)
5519
5660
  };
5520
5661
  }
5521
- pickPendingTodo() {
5662
+ pickPendingTodo(goal) {
5522
5663
  const todos = this.opts.agent.ctx.todos;
5523
5664
  if (!Array.isArray(todos)) return null;
5524
- return todos.find((t) => t.status === "pending") ?? null;
5665
+ const attempts = goal.todoAttempts ?? {};
5666
+ const ceiling = this.opts.todoMaxAttempts ?? 3;
5667
+ for (const t of todos) {
5668
+ if (t.status !== "pending") continue;
5669
+ const used = attempts[t.id] ?? 0;
5670
+ if (used >= ceiling) continue;
5671
+ return t;
5672
+ }
5673
+ return null;
5525
5674
  }
5526
5675
  async pickGitTask() {
5527
5676
  let out;
@@ -5558,7 +5707,10 @@ ${lastFew}` : "No prior iterations yet.",
5558
5707
  "- One sentence, imperative form, under 200 chars.",
5559
5708
  "- No preamble, no explanation, no markdown \u2014 just the task line.",
5560
5709
  "- If recent iterations show repeated failures on the same target, pivot.",
5561
- "- If the goal appears fully accomplished, output exactly: DONE"
5710
+ "- If the goal appears fully accomplished AND you can name a concrete",
5711
+ " artifact / test / output that proves it, output exactly: DONE",
5712
+ "- Be conservative with DONE: if the recent journal contains failures",
5713
+ " or aborted entries, the goal is almost certainly NOT done."
5562
5714
  ].join("\n");
5563
5715
  try {
5564
5716
  const ctrl = new AbortController();
@@ -5570,9 +5722,11 @@ ${lastFew}` : "No prior iterations yet.",
5570
5722
  );
5571
5723
  if (result.status !== "done") return null;
5572
5724
  const text = (result.finalText ?? "").trim();
5573
- if (!text || text === "DONE") return null;
5725
+ if (!text) return null;
5726
+ if (/^DONE\.?$/i.test(text)) return BRAINSTORM_DONE;
5574
5727
  const firstLine = text.split("\n").find((l) => l.trim().length > 0)?.trim();
5575
5728
  if (!firstLine) return null;
5729
+ if (/^DONE\.?$/i.test(firstLine)) return BRAINSTORM_DONE;
5576
5730
  return firstLine.slice(0, 240);
5577
5731
  } finally {
5578
5732
  clearTimeout(timer);
@@ -5582,19 +5736,82 @@ ${lastFew}` : "No prior iterations yet.",
5582
5736
  }
5583
5737
  }
5584
5738
  buildDirective(goal, source, task) {
5739
+ const recentJournal = goal.journal.slice(-5).map((e) => ` #${e.iteration} [${e.status}] ${e.task}${e.note ? ` \u2014 ${e.note.slice(0, 80)}` : ""}`).join("\n");
5585
5740
  return [
5586
- "[ETERNAL AUTONOMY \u2014 iteration directive]",
5741
+ "\u2550\u2550\u2550 ETERNAL AUTONOMY \u2014 iteration directive \u2550\u2550\u2550",
5587
5742
  "",
5588
- `Goal: ${goal.goal}`,
5743
+ `Mission: ${goal.goal}`,
5744
+ `Iteration: #${goal.iterations + 1}`,
5589
5745
  `Source: ${source}`,
5590
5746
  `Task: ${task}`,
5591
5747
  "",
5592
- "Execute this task end-to-end using the tools available to you. Make the",
5593
- "changes, run tests if relevant, and commit / push as appropriate. Do not",
5594
- "ask for confirmation \u2014 YOLO mode is active. When the task is done, stop;",
5595
- "the loop will pick the next action."
5748
+ recentJournal ? `Recent journal (last 5):
5749
+ ${recentJournal}` : "No prior iterations.",
5750
+ "",
5751
+ "\u2500\u2500 EXECUTION PROTOCOL \u2500\u2500",
5752
+ "You are inside a long-running autonomous loop. Each iteration you",
5753
+ "execute ONE concrete task that advances the Mission. No user is",
5754
+ "available to clarify \u2014 make defensible decisions and move forward.",
5755
+ "",
5756
+ "1. EXECUTE END-TO-END",
5757
+ " \u2022 Use multiple tool calls freely. Emit `[continue]` on its own line",
5758
+ " to chain to the next internal step without returning.",
5759
+ " \u2022 When this iteration's Task is finished (real artifact / passing",
5760
+ " test / applied diff / clean output), emit `[done]` on its own line.",
5761
+ " \u2022 Do not stop on the first obstacle \u2014 try at least 3 distinct",
5762
+ " approaches before giving up. YOLO is active; no confirmations.",
5763
+ "",
5764
+ "2. UPDATE TODO STATE (when Source is `todo`)",
5765
+ " \u2022 Mark this todo `in_progress` via the todos tool before tool work.",
5766
+ " \u2022 Mark it `completed` on success, with a one-line outcome note.",
5767
+ " \u2022 If you cannot make progress after 2 distinct attempts, mark it",
5768
+ " `cancelled` with the obstacle. The loop will skip it next time.",
5769
+ "",
5770
+ "3. MISSION-COMPLETE PROTOCOL",
5771
+ " \u2022 If \u2014 and ONLY if \u2014 the OVERALL Mission (not just this Task) is",
5772
+ " verifiably accomplished, emit on its own line:",
5773
+ " [GOAL_COMPLETE]",
5774
+ " followed by a one-paragraph verification recipe (artifact path,",
5775
+ " test command, or 10-second reproduction). This halts the loop.",
5776
+ " \u2022 NEVER emit [GOAL_COMPLETE] on optimism, partial progress, or",
5777
+ ' "looks fine". Required: a concrete artifact that proves it AND',
5778
+ " no recent journal failures contradicting completion.",
5779
+ " \u2022 If unsure, emit `[done]` instead and let the next iteration",
5780
+ " decide. The loop is patient; false completion is not.",
5781
+ "",
5782
+ "4. NO INTERACTIVITY",
5783
+ " \u2022 Do not ask questions, do not request confirmation, do not propose",
5784
+ " options. Pick the best path and execute. The user is asleep."
5596
5785
  ].join("\n");
5597
5786
  }
5787
+ /**
5788
+ * Exponential backoff for transient provider errors. `2^N * base`
5789
+ * capped at `transientBackoffMaxMs`. Zero base disables backoff.
5790
+ * Public-private to keep `runOneIteration` readable; the value is
5791
+ * recomputed each call from the current retry count, so callers
5792
+ * don't have to track state.
5793
+ */
5794
+ computeTransientBackoffMs() {
5795
+ const base = this.opts.transientBackoffBaseMs ?? 2e3;
5796
+ const cap = this.opts.transientBackoffMaxMs ?? 6e4;
5797
+ if (base <= 0) return 0;
5798
+ const exponent = Math.max(0, this.consecutiveTransientRetries - 1);
5799
+ return Math.min(cap, base * Math.pow(2, exponent));
5800
+ }
5801
+ /**
5802
+ * Sleep that wakes early if `stopRequested` flips. Polls every 250 ms
5803
+ * so SIGINT / `/autonomy stop` can land in the middle of a long
5804
+ * backoff instead of waiting up to a minute for the timer.
5805
+ */
5806
+ async sleepInterruptible(totalMs) {
5807
+ const step = 250;
5808
+ let remaining = totalMs;
5809
+ while (remaining > 0 && !this.stopRequested) {
5810
+ const chunk = Math.min(step, remaining);
5811
+ await sleep(chunk);
5812
+ remaining -= chunk;
5813
+ }
5814
+ }
5598
5815
  async appendIterationEntry(entry) {
5599
5816
  const current = await loadGoal(this.goalPath);
5600
5817
  if (!current) {
@@ -5603,6 +5820,39 @@ ${lastFew}` : "No prior iterations yet.",
5603
5820
  const updated = appendJournal(current, entry);
5604
5821
  await saveGoal(this.goalPath, updated);
5605
5822
  }
5823
+ /**
5824
+ * Persistent per-todo failure counter. Skipped silently when the goal
5825
+ * file has been removed (graceful clear). Each non-success iteration
5826
+ * against a todo source bumps the counter by 1; `pickPendingTodo` reads
5827
+ * the counter to rotate past stuck todos once they cross `todoMaxAttempts`.
5828
+ */
5829
+ async bumpTodoAttempt(todoId) {
5830
+ const current = await loadGoal(this.goalPath);
5831
+ if (!current) return;
5832
+ const attempts = { ...current.todoAttempts ?? {} };
5833
+ attempts[todoId] = (attempts[todoId] ?? 0) + 1;
5834
+ await saveGoal(this.goalPath, { ...current, todoAttempts: attempts });
5835
+ }
5836
+ /**
5837
+ * Flip the mission to `completed` and journal it. Called from two
5838
+ * paths: (a) `[GOAL_COMPLETE]` marker in a successful iteration's
5839
+ * finalText, (b) `brainstorm` returning DONE consecutively past the
5840
+ * configured threshold. Idempotent — re-entry is a no-op once the
5841
+ * goal is already `completed`.
5842
+ */
5843
+ async markGoalCompleted(action, note) {
5844
+ const current = await loadGoal(this.goalPath);
5845
+ if (!current) return;
5846
+ if (current.goalState === "completed") return;
5847
+ const withFlag = { ...current, goalState: "completed" };
5848
+ const withEntry = appendJournal(withFlag, {
5849
+ source: action.source,
5850
+ task: `MISSION COMPLETE \u2014 ${action.task}`.slice(0, 240),
5851
+ status: "success",
5852
+ note: note.slice(0, 240)
5853
+ });
5854
+ await saveGoal(this.goalPath, withEntry);
5855
+ }
5606
5856
  async appendFailure(task, note) {
5607
5857
  await this.appendIterationEntry({ source: "manual", task, status: "failure", note });
5608
5858
  }
@@ -5617,6 +5867,142 @@ function sleep(ms) {
5617
5867
  return new Promise((resolve2) => setTimeout(resolve2, ms));
5618
5868
  }
5619
5869
 
5870
+ // src/execution/autonomy-prompt-contributor.ts
5871
+ function makeAutonomyPromptContributor(opts) {
5872
+ return async (ctx) => {
5873
+ if (ctx.subagent) return [];
5874
+ if (!opts.enabled()) return [];
5875
+ let goal;
5876
+ try {
5877
+ goal = await loadGoal(opts.goalPath);
5878
+ } catch {
5879
+ return [];
5880
+ }
5881
+ if (!goal) return [];
5882
+ const missionState = goal.goalState ?? "active";
5883
+ if (missionState !== "active") return [];
5884
+ const tailSize = opts.journalTailSize ?? 5;
5885
+ const journalTail = goal.journal.slice(-tailSize).map((e) => {
5886
+ const note = e.note ? ` \u2014 ${e.note.slice(0, 80)}` : "";
5887
+ return ` #${e.iteration} [${e.status}] ${e.task}${note}`;
5888
+ });
5889
+ const text = [
5890
+ "## ETERNAL AUTONOMY \u2014 active mission",
5891
+ "",
5892
+ "You are inside a long-running autonomous loop. The user is asleep",
5893
+ "and is not available to confirm decisions. Each turn you receive a",
5894
+ "directive describing one concrete sub-task that advances the mission.",
5895
+ "",
5896
+ `Mission: ${goal.goal}`,
5897
+ `Iteration: #${goal.iterations}`,
5898
+ journalTail.length > 0 ? `Recent journal (last ${journalTail.length}):
5899
+ ${journalTail.join("\n")}` : "Recent journal: (none \u2014 this is the first iteration)",
5900
+ "",
5901
+ "### Loop control markers",
5902
+ "Emit these on their own line in your final text \u2014 case-insensitive,",
5903
+ "whitespace-tolerant, but they must occupy the entire line:",
5904
+ "- `[continue]` \u2014 chain to the next internal step without returning.",
5905
+ "- `[done]` \u2014 the current sub-task is finished; return to the engine.",
5906
+ "- `[GOAL_COMPLETE]` \u2014 emit ONLY when the OVERALL mission is",
5907
+ " verifiably done. Must be followed by a one-paragraph verification",
5908
+ " recipe (artifact path, test command, or 10-second reproduction).",
5909
+ " The engine halts on this marker \u2014 false positives waste real",
5910
+ " human time. If unsure, emit `[done]` and let the next iteration",
5911
+ " decide.",
5912
+ "",
5913
+ "### Operating principles",
5914
+ "- YOLO is active. Do NOT ask for confirmation, do NOT propose",
5915
+ " options. Pick the best path and execute it.",
5916
+ "- Use tools freely; multiple calls per turn are normal and expected.",
5917
+ "- When working on a todo, mark it `in_progress` via the todos tool",
5918
+ " before tool work and `completed` (or `cancelled` with a reason)",
5919
+ " when done. The loop reads todo state between iterations.",
5920
+ "- If an approach fails twice in a row, pivot. Don't grind on the",
5921
+ " same wall \u2014 try a different angle, file a cancel on the todo, or",
5922
+ " surface the obstacle via `[done]` and let the next iteration",
5923
+ " re-plan."
5924
+ ].join("\n");
5925
+ return [
5926
+ {
5927
+ type: "text",
5928
+ text,
5929
+ cache_control: { type: "ephemeral" }
5930
+ }
5931
+ ];
5932
+ };
5933
+ }
5934
+
5935
+ // src/execution/goal-preamble.ts
5936
+ function buildGoalPreamble(goal) {
5937
+ return [
5938
+ "[GOAL \u2014 LOCKED IN. You will work on this until it is verifiably done.",
5939
+ "The user granted you full autonomy. Read these constraints once, then act.",
5940
+ "",
5941
+ "YOUR GOAL:",
5942
+ "---",
5943
+ goal,
5944
+ "---",
5945
+ "",
5946
+ "AUTHORITY YOU HAVE:",
5947
+ "- Spawn as many subagents as the work needs (delegate / spawn_subagent).",
5948
+ " Parallel + recursive fan-out are both fine. There is no spawn budget.",
5949
+ "- Use any provider/model per subagent \u2014 pick the right tool for each",
5950
+ " piece of work. Heavy reasoning model for planning, fast model for",
5951
+ " batch work, specialist model for domain code.",
5952
+ "- Run unlimited tool calls and iterations. There is NO hidden budget.",
5953
+ " The Agent loop auto-extends every 100 iterations forever.",
5954
+ "- Retry failed tools with different inputs, alternative paths, fresh",
5955
+ " subagents. Switch providers mid-run if one is rate-limited.",
5956
+ "- Re-plan freely when an approach hits a dead end. You are not obliged",
5957
+ " to stick with the first plan you proposed.",
5958
+ "",
5959
+ 'WHAT "DONE" MEANS \u2014 non-negotiable:',
5960
+ "- You can name a concrete artifact (a passing test, a written file at",
5961
+ " a specific path, a fixed bug verified by re-running the failing case,",
5962
+ " a clean grep that previously had matches).",
5963
+ "- You can tell the user HOW to verify it themselves in 10 seconds.",
5964
+ '- You have NOT hedged. None of: "looks like it should work", "I',
5965
+ ' believe this fixes it", "the changes appear correct".',
5966
+ "",
5967
+ "WHAT IS NOT DONE \u2014 never report any of these as completion:",
5968
+ "- An error message you didn't recover from.",
5969
+ '- An empty result, a 0-line file, a "no matches found" you accepted',
5970
+ " without questioning the search.",
5971
+ '- "Should I continue?" / "Want me to also...?" / "Let me know if you',
5972
+ ' want X." Those are hedges. The user already told you to finish the',
5973
+ " goal \u2014 just do it.",
5974
+ "- Partial progress dressed up as success. Fixed 3 of 5 bugs = 60%",
5975
+ " done, not done.",
5976
+ "- A subagent's failed/timeout/stopped TaskResult that you didn't",
5977
+ " respond to with a fresh attempt (different role, different model,",
5978
+ " tighter prompt).",
5979
+ "",
5980
+ "PERSISTENCE PROTOCOL:",
5981
+ "- If blocked, try at least 3 different angles before reporting the",
5982
+ " problem to the user. Different tool inputs, different subagent",
5983
+ " roles, different providers, different decomposition of the task.",
5984
+ "- If a tool fails, read its error, alter the input, try again. Do",
5985
+ " not just report the failure back.",
5986
+ "- If a subagent returns useless output, respawn with a tighter prompt",
5987
+ ' or a different role. Do not accept "I could not determine\u2026" as the',
5988
+ " final answer.",
5989
+ "- Use `ask_subagent` for one-shot questions when you don't need a",
5990
+ " full delegated task.",
5991
+ "",
5992
+ "REPORTING:",
5993
+ "- Stream short progress notes between major actions so the user can",
5994
+ " monitor. Do not go silent for 50 tool calls then dump a wall of",
5995
+ " text \u2014 but also do not narrate every tool call.",
5996
+ "- Use the shared scratchpad (if available) to leave breadcrumbs",
5997
+ " subagents can read.",
5998
+ "- Final response must include: (a) what was accomplished, (b) how",
5999
+ " to verify, (c) any caveats (residual TODOs, things the user",
6000
+ " should know about).",
6001
+ "",
6002
+ "BEGIN.]"
6003
+ ].join("\n");
6004
+ }
6005
+
5620
6006
  // src/coordination/director.ts
5621
6007
  init_atomic_write();
5622
6008
 
@@ -5875,40 +6261,12 @@ var FleetBus = class {
5875
6261
  * subagent teardown so the listeners don't outlive the run.
5876
6262
  */
5877
6263
  attach(subagentId, bus, taskId) {
5878
- const FORWARDED_TYPES = [
5879
- "tool.started",
5880
- "tool.executed",
5881
- "tool.progress",
5882
- "tool.confirm_needed",
5883
- "iteration.started",
5884
- "iteration.completed",
5885
- "provider.text_delta",
5886
- // Subagent extended-thinking output. Forwarded so the FleetPanel /
5887
- // /fleet log can surface "the planner is thinking…" instead of a
5888
- // silent gap between iteration.started and the first text_delta.
5889
- "provider.thinking_delta",
5890
- "provider.response",
5891
- "provider.retry",
5892
- "provider.error",
5893
- "session.started",
5894
- "session.ended",
5895
- "session.damaged",
5896
- "compaction.fired",
5897
- "compaction.failed",
5898
- "token.threshold",
5899
- // Subagent hit a soft budget limit — coordinator can extend or stop.
5900
- "budget.threshold_reached"
5901
- ];
5902
- const offs = [];
5903
- for (const t of FORWARDED_TYPES) {
5904
- offs.push(
5905
- bus.on(t, (payload) => {
5906
- this.emit({ subagentId, taskId, ts: Date.now(), type: t, payload });
5907
- })
5908
- );
5909
- }
6264
+ const off = bus.onPattern("*", (type, payload) => {
6265
+ if (type.startsWith("subagent.")) return;
6266
+ this.emit({ subagentId, taskId, ts: Date.now(), type, payload });
6267
+ });
5910
6268
  return () => {
5911
- for (const off of offs) off();
6269
+ off();
5912
6270
  };
5913
6271
  }
5914
6272
  /** Subscribe to every event from one subagent. */
@@ -6073,7 +6431,7 @@ var BudgetThresholdSignal = class extends Error {
6073
6431
  this.decision = decision;
6074
6432
  }
6075
6433
  };
6076
- var SubagentBudget = class {
6434
+ var SubagentBudget = class _SubagentBudget {
6077
6435
  limits;
6078
6436
  iterations = 0;
6079
6437
  toolCalls = 0;
@@ -6082,6 +6440,26 @@ var SubagentBudget = class {
6082
6440
  costUsd = 0;
6083
6441
  startTime = null;
6084
6442
  _onThreshold;
6443
+ /**
6444
+ * Tracks which budget kinds currently have an extension request
6445
+ * in flight. While a kind is here, further `checkLimit` calls for the
6446
+ * same kind are no-ops — without this dedup, every `recordIteration`
6447
+ * after the limit is reached spawns a fresh decision Promise (until
6448
+ * the first one lands and patches limits), flooding the FleetBus
6449
+ * with redundant threshold events. Cleared in `checkLimitAsync`'s
6450
+ * `finally`.
6451
+ */
6452
+ pendingExtensions = /* @__PURE__ */ new Set();
6453
+ /**
6454
+ * Hard cap on how long `checkLimitAsync` waits for the coordinator to
6455
+ * respond before defaulting to 'stop'. Without this fallback an absent
6456
+ * or hung listener (Director not built / event filter detached mid-run)
6457
+ * leaves the budget over-limit and never enforces anything, since
6458
+ * `checkLimit` returns synchronously via `void this.checkLimitAsync`.
6459
+ * Hardcoded for now — most fleets set their own per-task timeout that
6460
+ * dwarfs this anyway. 30 s matches the existing event-emit timeoutMs.
6461
+ */
6462
+ static DECISION_TIMEOUT_MS = 3e4;
6085
6463
  /**
6086
6464
  * Injected by the runner when wiring the budget to its EventBus.
6087
6465
  * Used by `checkLimitAsync` to emit `budget.threshold_reached` events.
@@ -6124,56 +6502,92 @@ var SubagentBudget = class {
6124
6502
  if (kind === "timeout" || !this._onThreshold) {
6125
6503
  throw new BudgetExceededError(kind, limit, used);
6126
6504
  }
6127
- void this.checkLimitAsync(kind, used, limit);
6505
+ const bus = this._events;
6506
+ if (!bus || bus.listenerCount("budget.threshold_reached") === 0) {
6507
+ throw new BudgetExceededError(kind, limit, used);
6508
+ }
6509
+ if (this.pendingExtensions.has(kind)) return;
6510
+ this.pendingExtensions.add(kind);
6511
+ const decision = this.negotiateExtension(kind, used, limit);
6512
+ throw new BudgetThresholdSignal(kind, limit, used, decision);
6128
6513
  }
6129
6514
  /**
6130
- * Async threshold negotiation with the coordinator. Fire-and-forget
6131
- * any error thrown here becomes an unhandled rejection in the test environment
6132
- * because the runner's catch only handles the synchronous throw from `checkLimit`.
6515
+ * Drive the threshold handler to a decision. Resolves with `'stop'`
6516
+ * (signal the runner to abort) or `{ extend: ... }` (limits already
6517
+ * patched in-place; the runner should not abort). Always releases the
6518
+ * `pendingExtensions` slot in `finally`.
6519
+ *
6520
+ * The 'continue' return from a sync handler is treated as
6521
+ * `{ extend: {} }` — keep going without patching, next overrun will
6522
+ * fire a fresh signal.
6133
6523
  */
6134
- async checkLimitAsync(kind, used, limit) {
6135
- const result = this._onThreshold({
6136
- kind,
6137
- used,
6138
- limit,
6139
- // Inject a requestDecision helper the handler can call to emit the
6140
- // budget.threshold_reached event and wait for the coordinator's verdict.
6141
- // The runner wires this by injecting its EventBus into ctx.budget._events.
6142
- requestDecision: () => {
6143
- return new Promise((resolve2) => {
6144
- this._events?.emit("budget.threshold_reached", {
6145
- kind,
6146
- used,
6147
- limit,
6148
- timeoutMs: 3e4,
6149
- extend: (extra) => resolve2({ extend: extra }),
6150
- deny: () => resolve2("stop")
6524
+ async negotiateExtension(kind, used, limit) {
6525
+ try {
6526
+ const result = this._onThreshold({
6527
+ kind,
6528
+ used,
6529
+ limit,
6530
+ // Inject a requestDecision helper the handler can call to emit the
6531
+ // budget.threshold_reached event and wait for the coordinator's verdict.
6532
+ // A hard fallback timer guarantees the promise eventually resolves
6533
+ // even if no listener responds — without it, an absent/detached
6534
+ // Director would leave the budget permanently in "asking" state.
6535
+ requestDecision: () => {
6536
+ const bus = this._events;
6537
+ if (!bus || bus.listenerCount("budget.threshold_reached") === 0) {
6538
+ return Promise.resolve("stop");
6539
+ }
6540
+ return new Promise((resolve2) => {
6541
+ let resolved = false;
6542
+ const respond = (d) => {
6543
+ if (resolved) return;
6544
+ resolved = true;
6545
+ resolve2(d);
6546
+ };
6547
+ const fallback = setTimeout(
6548
+ () => respond("stop"),
6549
+ _SubagentBudget.DECISION_TIMEOUT_MS
6550
+ );
6551
+ bus.emit("budget.threshold_reached", {
6552
+ kind,
6553
+ used,
6554
+ limit,
6555
+ timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
6556
+ extend: (extra) => {
6557
+ clearTimeout(fallback);
6558
+ respond({ extend: extra });
6559
+ },
6560
+ deny: () => {
6561
+ clearTimeout(fallback);
6562
+ respond("stop");
6563
+ }
6564
+ });
6151
6565
  });
6152
- });
6566
+ }
6567
+ });
6568
+ if (result === "throw") return "stop";
6569
+ if (result === "continue") return { extend: {} };
6570
+ const decision = await result;
6571
+ if (decision === "stop") return "stop";
6572
+ const ext = decision.extend;
6573
+ if (ext.maxIterations !== void 0) {
6574
+ this.limits.maxIterations = ext.maxIterations;
6153
6575
  }
6154
- });
6155
- if (result === "throw") {
6156
- throw new BudgetExceededError(kind, limit, used);
6157
- }
6158
- if (result === "continue") {
6159
- return;
6160
- }
6161
- const decision = await result;
6162
- if (decision === "stop") {
6163
- throw new BudgetExceededError(kind, limit, used);
6164
- }
6165
- const ext = decision.extend;
6166
- if (ext.maxIterations !== void 0) {
6167
- this.limits.maxIterations = ext.maxIterations;
6168
- }
6169
- if (ext.maxToolCalls !== void 0) {
6170
- this.limits.maxToolCalls = ext.maxToolCalls;
6171
- }
6172
- if (ext.maxTokens !== void 0) {
6173
- this.limits.maxTokens = ext.maxTokens;
6174
- }
6175
- if (ext.maxCostUsd !== void 0) {
6176
- this.limits.maxCostUsd = ext.maxCostUsd;
6576
+ if (ext.maxToolCalls !== void 0) {
6577
+ this.limits.maxToolCalls = ext.maxToolCalls;
6578
+ }
6579
+ if (ext.maxTokens !== void 0) {
6580
+ this.limits.maxTokens = ext.maxTokens;
6581
+ }
6582
+ if (ext.maxCostUsd !== void 0) {
6583
+ this.limits.maxCostUsd = ext.maxCostUsd;
6584
+ }
6585
+ if (ext.timeoutMs !== void 0) {
6586
+ this.limits.timeoutMs = ext.timeoutMs;
6587
+ }
6588
+ return decision;
6589
+ } finally {
6590
+ this.pendingExtensions.delete(kind);
6177
6591
  }
6178
6592
  }
6179
6593
  recordIteration() {
@@ -6456,6 +6870,19 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
6456
6870
  setRunner(runner) {
6457
6871
  this.runner = runner;
6458
6872
  }
6873
+ /**
6874
+ * Change the in-flight dispatch ceiling at runtime. Lowering does NOT
6875
+ * preempt running tasks — already-dispatched subagents finish their
6876
+ * current task; only future dispatches respect the new cap. Raising
6877
+ * immediately tries to fill the freed slots from the pending queue.
6878
+ */
6879
+ setMaxConcurrent(n) {
6880
+ if (!Number.isFinite(n) || n < 1) {
6881
+ throw new Error(`maxConcurrent must be a finite integer >= 1, got ${n}`);
6882
+ }
6883
+ this.config.maxConcurrent = Math.floor(n);
6884
+ this.tryDispatchNext();
6885
+ }
6459
6886
  async spawn(subagent) {
6460
6887
  const id = subagent.id || randomUUID();
6461
6888
  if (this.subagents.has(id)) {
@@ -6715,14 +7142,65 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
6715
7142
  this.recordCompletion(result);
6716
7143
  }
6717
7144
  async executeWithTimeout(runner, task, ctx, budget) {
6718
- const timeoutMs = budget.limits.timeoutMs;
6719
- if (timeoutMs === void 0) return runner(task, ctx);
7145
+ const initialTimeoutMs = budget.limits.timeoutMs;
7146
+ if (initialTimeoutMs === void 0) return runner(task, ctx);
7147
+ const start = Date.now();
6720
7148
  let timer = null;
6721
7149
  const timeoutPromise = new Promise((_, reject) => {
6722
- timer = setTimeout(() => {
6723
- this.subagents.get(ctx.subagentId)?.abortController.abort();
6724
- reject(new BudgetExceededError("timeout", timeoutMs, Date.now()));
6725
- }, timeoutMs);
7150
+ const armFor = (ms) => {
7151
+ if (timer) clearTimeout(timer);
7152
+ timer = setTimeout(async () => {
7153
+ const elapsed = Date.now() - start;
7154
+ const limit = budget.limits.timeoutMs ?? initialTimeoutMs;
7155
+ if (!budget.onThreshold) {
7156
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
7157
+ reject(new BudgetExceededError("timeout", limit, elapsed));
7158
+ return;
7159
+ }
7160
+ try {
7161
+ const result = budget.onThreshold({
7162
+ kind: "timeout",
7163
+ used: elapsed,
7164
+ limit,
7165
+ requestDecision: () => new Promise((resolveDecision) => {
7166
+ budget._events?.emit("budget.threshold_reached", {
7167
+ kind: "timeout",
7168
+ used: elapsed,
7169
+ limit,
7170
+ timeoutMs: 3e4,
7171
+ extend: (extra) => resolveDecision({ extend: extra }),
7172
+ deny: () => resolveDecision("stop")
7173
+ });
7174
+ })
7175
+ });
7176
+ const decision = typeof result === "string" ? result : await result;
7177
+ if (decision === "continue") {
7178
+ armFor(Math.max(1e3, limit));
7179
+ return;
7180
+ }
7181
+ if (decision === "throw" || decision === "stop") {
7182
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
7183
+ reject(new BudgetExceededError("timeout", limit, elapsed));
7184
+ return;
7185
+ }
7186
+ if (decision.extend.timeoutMs !== void 0) {
7187
+ budget.limits.timeoutMs = decision.extend.timeoutMs;
7188
+ const newLimit = decision.extend.timeoutMs;
7189
+ const remaining = Math.max(1e3, newLimit - elapsed);
7190
+ armFor(remaining);
7191
+ return;
7192
+ }
7193
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
7194
+ reject(new BudgetExceededError("timeout", limit, elapsed));
7195
+ } catch (err) {
7196
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
7197
+ reject(
7198
+ err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
7199
+ );
7200
+ }
7201
+ }, ms);
7202
+ };
7203
+ armFor(initialTimeoutMs);
6726
7204
  });
6727
7205
  try {
6728
7206
  return await Promise.race([runner(task, ctx), timeoutPromise]);
@@ -7333,7 +7811,7 @@ var Director = class {
7333
7811
  return;
7334
7812
  }
7335
7813
  extendCounts.set(guardKey, prior + 1);
7336
- setTimeout(() => {
7814
+ setImmediate(() => {
7337
7815
  const extra = {};
7338
7816
  switch (payload.kind) {
7339
7817
  case "iterations":
@@ -7348,9 +7826,12 @@ var Director = class {
7348
7826
  case "cost":
7349
7827
  extra.maxCostUsd = Math.min(payload.limit * 1.5, 10);
7350
7828
  break;
7829
+ case "timeout":
7830
+ extra.timeoutMs = Math.min(Math.ceil(payload.limit * 1.5), 60 * 6e4);
7831
+ break;
7351
7832
  }
7352
7833
  payload.extend(extra);
7353
- }, Math.min(payload.timeoutMs, 3e4));
7834
+ });
7354
7835
  });
7355
7836
  }
7356
7837
  /** Best-effort session-writer append. Swallows failures — the director
@@ -8145,6 +8626,7 @@ function makeAgentSubagentRunner(opts) {
8145
8626
  const detachFleet = opts.fleetBus?.attach(ctx.subagentId, events, task.id);
8146
8627
  const aborter = new AbortController();
8147
8628
  ctx.budget._events = events;
8629
+ ctx.budget.onThreshold = ({ requestDecision }) => requestDecision();
8148
8630
  let budgetError = null;
8149
8631
  const onBudgetError = (err) => {
8150
8632
  if (err instanceof BudgetThresholdSignal) {
@@ -11458,7 +11940,7 @@ function wireMetricsToEvents(events, sink) {
11458
11940
  events.on("token.threshold", (e) => sink.gauge("agent.tokens.used", e.used)),
11459
11941
  events.on("compaction.fired", (e) => {
11460
11942
  sink.counter("compaction.fired.total");
11461
- sink.histogram("compaction.reduction_tokens", e.before - e.after);
11943
+ sink.histogram("compaction.reduction_tokens", e.report.before - e.report.after);
11462
11944
  }),
11463
11945
  events.on(
11464
11946
  "mcp.server.connected",
@@ -12231,6 +12713,6 @@ var allServers = () => ({
12231
12713
  "minimax-vision": { ...miniMaxVisionServer(), enabled: false }
12232
12714
  });
12233
12715
 
12234
- export { AISpecBuilder, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, ConfigMigrationError, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_SUBAGENT_BASELINE, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPermissionPolicy, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultTaskStore, Director, DirectorStateCheckpoint, DoneConditionChecker, EternalAutonomyEngine, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FleetBus, FleetSpawnBudgetError, FleetUsageAggregator, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, IntelligentCompactor, LLMSelector, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, SelectiveCompactor, SessionAnalyzer, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, ToolExecutor, addPlanItem, allServers, analyzeCriticalPath, applyRosterBudget, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildOtlpMetricsRequest, buildOtlpTracesRequest, classifyFamily, clearPlan, composeDirectorPrompt, composeSubagentPrompt, context7Server, contextManagerTool, createAutoExecutor, createContextManagerTool, createDelegateTool, createMessage, decryptConfigSecrets2 as decryptConfigSecrets, deriveTodosFromPlanItem, emptyPlan, encryptConfigSecrets, everArtServer, filesystemServer, formatPlan, formatPlanTemplates, getPlanTemplate, getTemplate, githubServer, googleMapsServer, listPlanTemplates, listTemplates, loadDirectorState, loadPlan, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeDirectorSessionFactory, migratePlaintextSecrets, miniMaxVisionServer, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, savePlan, saveTodosCheckpoint, sentinelServer, setPlanItemStatus, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, templateToMarkdown, wireMetricsToEvents, zaiVisionServer };
12716
+ export { AISpecBuilder, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, ConfigMigrationError, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_SUBAGENT_BASELINE, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPermissionPolicy, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultTaskStore, Director, DirectorStateCheckpoint, DoneConditionChecker, EternalAutonomyEngine, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FleetBus, FleetSpawnBudgetError, FleetUsageAggregator, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, IntelligentCompactor, LLMSelector, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, SelectiveCompactor, SessionAnalyzer, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, ToolExecutor, addPlanItem, allServers, analyzeCriticalPath, applyRosterBudget, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildGoalPreamble, buildOtlpMetricsRequest, buildOtlpTracesRequest, classifyFamily, clearPlan, composeDirectorPrompt, composeSubagentPrompt, context7Server, contextManagerTool, createAutoExecutor, createContextManagerTool, createDelegateTool, createMessage, decryptConfigSecrets2 as decryptConfigSecrets, deriveTodosFromPlanItem, emptyPlan, encryptConfigSecrets, everArtServer, filesystemServer, formatPlan, formatPlanTemplates, getPlanTemplate, getTemplate, githubServer, googleMapsServer, listPlanTemplates, listTemplates, loadDirectorState, loadPlan, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeAutonomyPromptContributor, makeDirectorSessionFactory, migratePlaintextSecrets, miniMaxVisionServer, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, savePlan, saveTodosCheckpoint, sentinelServer, setPlanItemStatus, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, templateToMarkdown, wireMetricsToEvents, zaiVisionServer };
12235
12717
  //# sourceMappingURL=index.js.map
12236
12718
  //# sourceMappingURL=index.js.map