@wrongstack/core 0.306.3 → 0.307.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 (114) hide show
  1. package/dist/chronicle/index.js +957 -953
  2. package/dist/chronicle/metrics-ingest.d.ts +55 -0
  3. package/dist/chronicle/metrics-schema.d.ts +86 -0
  4. package/dist/chronicle/metrics-store.d.ts +5 -120
  5. package/dist/chronicle/project-server.js +1083 -1084
  6. package/dist/chronicle/sqlite-journal-quota.d.ts +39 -0
  7. package/dist/chronicle/sqlite-journal-schema.d.ts +25 -0
  8. package/dist/chronicle/sqlite-journal.d.ts +12 -177
  9. package/dist/coordination/agents/index.js +1738 -1690
  10. package/dist/coordination/agents/project-agent-capture.d.ts +48 -0
  11. package/dist/coordination/agents/project-agent-identity.d.ts +3 -129
  12. package/dist/coordination/agents/project-agent-roster.d.ts +17 -0
  13. package/dist/coordination/index.js +6103 -5890
  14. package/dist/coordination/mailbox-http-actor-query.d.ts +16 -0
  15. package/dist/coordination/mailbox-http-router.d.ts +0 -48
  16. package/dist/coordination/mailbox-http-sse.d.ts +12 -0
  17. package/dist/coordination/mailbox-message-types.d.ts +71 -0
  18. package/dist/coordination/mailbox-predicates.d.ts +13 -0
  19. package/dist/coordination/mailbox-project-server.js +1 -1
  20. package/dist/coordination/mailbox-session-sync.d.ts +8 -0
  21. package/dist/coordination/mailbox-types.d.ts +6 -793
  22. package/dist/core/agent-loop-context.d.ts +28 -0
  23. package/dist/core/agent-loop-detector.d.ts +25 -0
  24. package/dist/core/conversation-state.d.ts +1 -2
  25. package/dist/core/fallback-doctor.d.ts +72 -0
  26. package/dist/core/fallback-model.d.ts +19 -1
  27. package/dist/core/fallback-profile-manager.d.ts +21 -3
  28. package/dist/core/index.d.ts +2 -1
  29. package/dist/core/index.js +1143 -766
  30. package/dist/defaults/index.js +3833 -3595
  31. package/dist/execution/auto-compaction-middleware.d.ts +64 -0
  32. package/dist/execution/autonomy-brain-llm.d.ts +55 -0
  33. package/dist/execution/autonomy-brain.d.ts +3 -156
  34. package/dist/execution/compaction-budget.d.ts +38 -0
  35. package/dist/execution/compaction-core.d.ts +3 -166
  36. package/dist/execution/compaction-elision.d.ts +58 -0
  37. package/dist/execution/council-orchestrator-helpers.d.ts +39 -0
  38. package/dist/execution/council-orchestrator.d.ts +3 -42
  39. package/dist/execution/council-response-parser.d.ts +56 -0
  40. package/dist/execution/index.d.ts +1 -1
  41. package/dist/execution/index.js +10016 -9903
  42. package/dist/execution/prompt-enhancer.js +11 -3
  43. package/dist/execution/retry-policy.d.ts +27 -0
  44. package/dist/execution/tool-executor-guard.d.ts +18 -0
  45. package/dist/execution/tool-executor-runner.d.ts +12 -0
  46. package/dist/execution/tool-executor.d.ts +0 -75
  47. package/dist/goal/index.js +19 -6
  48. package/dist/hq/index.js +58 -17
  49. package/dist/index.d.ts +2 -1
  50. package/dist/index.js +8731 -7756
  51. package/dist/infrastructure/index.js +24 -1
  52. package/dist/kernel/events/agent-events.d.ts +2 -0
  53. package/dist/kernel/events/provider-events.d.ts +13 -0
  54. package/dist/plugin/index.js +2123 -1918
  55. package/dist/plugins/auto-review-config.d.ts +53 -0
  56. package/dist/plugins/auto-review-git.d.ts +19 -0
  57. package/dist/plugins/auto-review-plugin.d.ts +2 -140
  58. package/dist/plugins/review-finding-verification.d.ts +7 -0
  59. package/dist/prompts/index.d.ts +1 -0
  60. package/dist/prompts/prompt-journal.d.ts +94 -0
  61. package/dist/security/index.js +677 -667
  62. package/dist/security/permission-explain.d.ts +19 -0
  63. package/dist/security/permission-policy.d.ts +0 -101
  64. package/dist/security/yolo-risk.d.ts +5 -4
  65. package/dist/session-catalog/index.js +270 -227
  66. package/dist/session-catalog/project-server.js +277 -234
  67. package/dist/session-catalog/store-rebuild.d.ts +9 -0
  68. package/dist/session-catalog/store-schema.d.ts +48 -0
  69. package/dist/session-catalog/store.d.ts +0 -17
  70. package/dist/storage/cloud-config-sync/sanitize.d.ts +18 -0
  71. package/dist/storage/cloud-config-sync.d.ts +1 -1
  72. package/dist/storage/director-state.d.ts +6 -0
  73. package/dist/storage/file-session-writer.d.ts +2 -58
  74. package/dist/storage/index.d.ts +2 -1
  75. package/dist/storage/index.js +2121 -1736
  76. package/dist/storage/orphan-lock-cleaner.d.ts +15 -0
  77. package/dist/storage/provider-config-watcher.d.ts +9 -0
  78. package/dist/storage/session-recovery.d.ts +9 -0
  79. package/dist/storage/session-store/rename-session.d.ts +16 -0
  80. package/dist/storage/session-store/resume-session.d.ts +26 -0
  81. package/dist/storage/session-store/session-store-clear.d.ts +11 -0
  82. package/dist/storage/session-store/session-store-index.d.ts +13 -0
  83. package/dist/storage/session-store/strict-empty-check.d.ts +7 -0
  84. package/dist/storage/session-store.d.ts +2 -129
  85. package/dist/storage/session-summary-tracker.d.ts +39 -0
  86. package/dist/storage/session-write-buffer.d.ts +43 -0
  87. package/dist/storage/session-writer/session-writer-flush.d.ts +4 -0
  88. package/dist/storage/session-writer/session-writer-summary-tracker.d.ts +28 -0
  89. package/dist/tools/fallback-agent-model-assign-tool.d.ts +18 -0
  90. package/dist/tools/fallback-leader-model-set-tool.d.ts +18 -0
  91. package/dist/tools/fallback-manage-tools.d.ts +8 -45
  92. package/dist/tools/fallback-profile-manage-tool.d.ts +16 -0
  93. package/dist/tools/fallback-provider-key-set-tool.d.ts +17 -0
  94. package/dist/tools/fallback-provider-manage-tool.d.ts +26 -0
  95. package/dist/tools/index.d.ts +1 -1
  96. package/dist/tools/index.js +5150 -5090
  97. package/dist/types/config/root.d.ts +21 -2
  98. package/dist/types/default-config.d.ts +1 -1
  99. package/dist/types/index.d.ts +54 -55
  100. package/dist/types/index.js +953 -932
  101. package/dist/types/runtime-capability-manifest.d.ts +5 -5
  102. package/dist/types/session-markers.d.ts +12 -0
  103. package/dist/types/session-markers.js +19 -0
  104. package/dist/types/session.d.ts +7 -0
  105. package/dist/worktree/worktree-manager.d.ts +1 -1
  106. package/instructions/coordination/subagent-baseline.md +2 -2
  107. package/instructions/llm/prompt-enhancer.md +2 -1
  108. package/instructions/sections/tool/common-patterns.md +21 -0
  109. package/instructions/sections/tool/delegation-compact.md +3 -1
  110. package/instructions/sections/tool/delegation-full.md +5 -1
  111. package/instructions/system-lite.md +28 -3
  112. package/instructions/system-pro.md +37 -8
  113. package/instructions/system.md +4 -2
  114. package/package.json +5 -5
@@ -1158,7 +1158,10 @@ var MAILBOX_TYPE_PROPERTIES = {
1158
1158
  }
1159
1159
  };
1160
1160
 
1161
- // src/coordination/mailbox-types.ts
1161
+ // src/coordination/mailbox-predicates.ts
1162
+ var SESSION_RECIPIENT_PREFIX = "@session:";
1163
+
1164
+ // src/coordination/mailbox-session-sync.ts
1162
1165
  function isAffectedBySessionAffinity(message) {
1163
1166
  return message.sessionAffinity !== void 0;
1164
1167
  }
@@ -1189,7 +1192,6 @@ async function acceptMailboxMessageForSession(message, currentSessionId, ctx) {
1189
1192
  if (ctx?.allowUnscoped === true) return true;
1190
1193
  return false;
1191
1194
  }
1192
- var SESSION_RECIPIENT_PREFIX = "@session:";
1193
1195
 
1194
1196
  // src/coordination/sqlite-mailbox.ts
1195
1197
  var SQLITE_MAILBOX_FILE = "_mailbox.sqlite";
@@ -1722,6 +1724,24 @@ import { randomUUID } from "node:crypto";
1722
1724
  import * as v8 from "node:v8";
1723
1725
 
1724
1726
  // src/security/secret-scrubber.ts
1727
+ var JSON_CREDENTIAL_KEY_ANCHORS = [
1728
+ 'Key"',
1729
+ 'key"',
1730
+ 'KEY"',
1731
+ 'token"',
1732
+ 'Token"',
1733
+ 'TOKEN"',
1734
+ 'secret"',
1735
+ 'Secret"',
1736
+ 'SECRET"',
1737
+ 'password"',
1738
+ 'Password"',
1739
+ 'PASSWORD"',
1740
+ 'authorization"',
1741
+ 'Authorization"',
1742
+ 'bearer"',
1743
+ 'Bearer"'
1744
+ ];
1725
1745
  var PATTERNS = [
1726
1746
  // Anchored at the start where possible so partial matches inside larger
1727
1747
  // strings don't trigger false positives.
@@ -1824,6 +1844,30 @@ var PATTERNS = [
1824
1844
  regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
1825
1845
  anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
1826
1846
  },
1847
+ {
1848
+ type: "json_credential_key",
1849
+ // The JSON counterpart to `high_entropy_env`, and the pattern that the
1850
+ // now-deleted `JSON_KEY_ANCHORS` list was written for. Without it those
1851
+ // anchors only widened the cheap pre-scan — `hasCredentialAnchors` said
1852
+ // "this text may hold a secret", every pattern then declined to match, and
1853
+ // the value went out verbatim. `high_entropy_env` cannot cover these: it
1854
+ // requires an UPPERCASE unquoted key (`API_KEY=…`), so `{"apiKey":"…"}`
1855
+ // never matched.
1856
+ //
1857
+ // Tool results are routinely serialised as JSON, and a credential with no
1858
+ // recognisable prefix (Azure, self-hosted gateways, Anthropic/Codex OAuth)
1859
+ // has no other pattern that can catch it — this is the only thing standing
1860
+ // between such a value and the session JSONL, chronicle, HQ broadcast and
1861
+ // the model's own context.
1862
+ //
1863
+ // The key may carry a prefix (`"anthropicApiKey"`), but the credential word
1864
+ // must END the key: `"tokenCount"` and `"maxTokens"` do not match, because
1865
+ // the closing quote has to follow the word immediately.
1866
+ // Value floor of 8 chars keeps enum-ish values (`"authorization":"none"`)
1867
+ // out. Capture groups: 1=key + punctuation, 2=value, 3=closing quote.
1868
+ regex: /("[A-Za-z0-9_]*(?:apiKey|api_key|token|secret|password|authorization|bearer|private_key|access_token|refresh_token|client_secret)"\s*:\s*")([^"\\]{8,512})(")/gi,
1869
+ anchor: JSON_CREDENTIAL_KEY_ANCHORS
1870
+ },
1827
1871
  // ── Ported from packages/plugins credential-patterns.ts (WS-034) ─────────
1828
1872
  // The plugin runtime carried 37 patterns while this scrubber — the one that
1829
1873
  // guards session JSONL, chronicle, HQ broadcast, WebUI events and the auth
@@ -1906,9 +1950,12 @@ var PATTERNS = [
1906
1950
  anchor: "GOCSPX-"
1907
1951
  }
1908
1952
  ];
1909
- var SIMPLE_PATTERNS = PATTERNS.filter((p) => p.type !== "high_entropy_env");
1953
+ var SIMPLE_PATTERNS = PATTERNS.filter(
1954
+ (p) => p.type !== "high_entropy_env" && p.type !== "json_credential_key"
1955
+ );
1910
1956
  var COMBINED_REGEX = new RegExp(SIMPLE_PATTERNS.map((p) => `(${p.regex.source})`).join("|"), "g");
1911
1957
  var HIGH_ENTROPY_REGEX = PATTERNS.find((p) => p.type === "high_entropy_env").regex;
1958
+ var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key").regex;
1912
1959
  var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
1913
1960
  var SCRUB_CHUNK_BYTES = 64 * 1024;
1914
1961
  var SCRUB_OVERLAP_BYTES = 1024;
@@ -1919,20 +1966,7 @@ var PATTERN_ANCHORS = [
1919
1966
  )
1920
1967
  )
1921
1968
  ];
1922
- var JSON_KEY_ANCHORS = [
1923
- '"apiKey"',
1924
- '"api_key"',
1925
- '"token"',
1926
- '"secret"',
1927
- '"password"',
1928
- '"authorization"',
1929
- '"bearer"',
1930
- '"private_key"',
1931
- '"access_token"',
1932
- '"refresh_token"',
1933
- '"client_secret"'
1934
- ];
1935
- var ALL_ANCHORS = [...PATTERN_ANCHORS, ...JSON_KEY_ANCHORS];
1969
+ var ALL_ANCHORS = PATTERN_ANCHORS;
1936
1970
  function hasCredentialAnchors(text) {
1937
1971
  for (const anchor of ALL_ANCHORS) {
1938
1972
  if (text.includes(anchor)) return true;
@@ -1981,6 +2015,9 @@ var DefaultSecretScrubber = class {
1981
2015
  out = out.replace(HIGH_ENTROPY_REGEX, (_match, lead, key, _value) => {
1982
2016
  return `${lead}${key}=[REDACTED:high_entropy_env]`;
1983
2017
  });
2018
+ out = out.replace(JSON_CREDENTIAL_REGEX, (_match, keyPrefix, _value, closingQuote) => {
2019
+ return `${keyPrefix}[REDACTED:json_credential_key]${closingQuote}`;
2020
+ });
1984
2021
  return out;
1985
2022
  }
1986
2023
  /**
@@ -4669,6 +4706,7 @@ function getCalibrationState(calibrationKey = CALIBRATION_GLOBAL_KEY) {
4669
4706
  }
4670
4707
 
4671
4708
  // src/core/context.ts
4709
+ import { realpathSync } from "node:fs";
4672
4710
  import * as path10 from "node:path";
4673
4711
 
4674
4712
  // src/core/conversation-state.ts
@@ -4744,12 +4782,15 @@ var ConversationState = class {
4744
4782
  * cap determines the starting index for the sum but does not gate it.
4745
4783
  */
4746
4784
  overflowCount(arr) {
4747
- let drop = Context.MAX_MESSAGES > 0 ? Math.max(0, arr.length - Context.MAX_MESSAGES) : 0;
4748
- if (Context.MAX_MESSAGE_TOKENS <= 0) return this.protocolSafeDropCount(arr, drop);
4785
+ const contextClass = this.ctx.constructor;
4786
+ const maxMessages = contextClass.MAX_MESSAGES;
4787
+ const maxMessageTokens = contextClass.MAX_MESSAGE_TOKENS;
4788
+ let drop = maxMessages > 0 ? Math.max(0, arr.length - maxMessages) : 0;
4789
+ if (maxMessageTokens <= 0) return this.protocolSafeDropCount(arr, drop);
4749
4790
  let total = 0;
4750
4791
  for (let i = drop; i < arr.length; i++) total += arr[i]?._estTokens ?? 0;
4751
- if (total <= Context.MAX_MESSAGE_TOKENS) return this.protocolSafeDropCount(arr, drop);
4752
- while (drop < arr.length - 1 && total > Context.MAX_MESSAGE_TOKENS) {
4792
+ if (total <= maxMessageTokens) return this.protocolSafeDropCount(arr, drop);
4793
+ while (drop < arr.length - 1 && total > maxMessageTokens) {
4753
4794
  total -= arr[drop]?._estTokens ?? 0;
4754
4795
  drop++;
4755
4796
  }
@@ -5575,6 +5616,19 @@ var Context = class _Context {
5575
5616
  if (rel.startsWith("..") || path10.isAbsolute(rel)) {
5576
5617
  throw new Error(`Working directory "${resolved}" is outside project root "${root}"`);
5577
5618
  }
5619
+ let realTarget = resolved;
5620
+ let realRoot = root;
5621
+ try {
5622
+ realTarget = realpathSync.native(resolved);
5623
+ realRoot = realpathSync.native(root);
5624
+ } catch {
5625
+ }
5626
+ const realRel = path10.relative(realRoot, realTarget);
5627
+ if (realRel.startsWith("..") || path10.isAbsolute(realRel)) {
5628
+ throw new Error(
5629
+ `Working directory "${resolved}" resolves to "${realTarget}", outside project root "${realRoot}"`
5630
+ );
5631
+ }
5578
5632
  }
5579
5633
  const old = this.workingDir;
5580
5634
  this.workingDir = resolved;
@@ -5609,237 +5663,651 @@ function resolveEventSessionId(ctx) {
5609
5663
  return session?.id ?? "";
5610
5664
  }
5611
5665
 
5612
- // src/core/continue-to-next-iteration.ts
5613
- function parseContinueDirective(text) {
5614
- const LINE_MARKERS = /^\s*\[(continue|next step|proceed|done)\]\s*$/gim;
5615
- const tail = text.length <= DIRECTIVE_SCAN_WINDOW ? text : text.slice(text.length - DIRECTIVE_SCAN_WINDOW);
5616
- let match;
5617
- let lastDirective = "none";
5618
- while ((match = LINE_MARKERS.exec(tail)) !== null) {
5619
- const value = (match[1] ?? "").toLowerCase();
5620
- if (value === "continue" || value === "next step" || value === "proceed") {
5621
- lastDirective = "continue";
5622
- } else if (value === "done") {
5623
- lastDirective = "stop";
5624
- }
5666
+ // src/core/agent-loop-context.ts
5667
+ function createAgentLoopContextManager(a, handlers) {
5668
+ let _lastEmittedMsgCount = -1;
5669
+ let _lastEmittedToolCount = -1;
5670
+ let _lastEmittedRevision = -1;
5671
+ let _lastEmittedMaxContext = -1;
5672
+ let _lastPreFlightMsgCount = -1;
5673
+ let _lastPreFlightToolCount = -1;
5674
+ let _lastPreFlightRevision = -1;
5675
+ let _lastCompactionMsgCount = -1;
5676
+ let _lastCompactionRevision = -1;
5677
+ let _lastCompactionToolsRef = null;
5678
+ let _lastCompactionSystemRef = null;
5679
+ let _lastCompactionRequestTokens;
5680
+ let _lastCompactionMaxContext = -1;
5681
+ let _lastCompactionWasNoop = false;
5682
+ let _cachedSysRef = null;
5683
+ let _cachedToolsRef = null;
5684
+ let _cachedOverheadTokens = 0;
5685
+ const calibrationKey = (model = a.ctx.model) => `${a.ctx.provider?.id ?? "unknown"}/${model}`;
5686
+ function currentMaxContext() {
5687
+ const metaLimit = a.ctx.meta?.["effectiveMaxContext"];
5688
+ const providerMax = a.ctx.provider.capabilities.maxContext;
5689
+ return typeof metaLimit === "number" && metaLimit > 0 ? metaLimit : typeof providerMax === "number" && providerMax > 0 ? providerMax : 2e5;
5625
5690
  }
5626
- return lastDirective;
5627
- }
5628
- var DIRECTIVE_SCAN_WINDOW = 2048;
5629
- var META_KEY2 = "_autonomousContinue";
5630
- function consumeAutonomousContinue(ctx) {
5631
- const val = ctx.meta[META_KEY2] === true;
5632
- delete ctx.meta[META_KEY2];
5633
- return val;
5634
- }
5635
-
5636
- // src/core/iteration-limit.ts
5637
- function requestLimitExtension(opts) {
5638
- const { events, sessionId, currentIterations, currentLimit, autoExtend, timeoutMs = 3e4 } = opts;
5639
- return new Promise((resolve10) => {
5640
- let resolved = false;
5641
- const timerFired = () => {
5642
- if (!resolved) {
5643
- resolved = true;
5644
- resolve10(0);
5645
- }
5646
- };
5647
- const timer = setTimeout(timerFired, timeoutMs);
5648
- timer.unref();
5649
- const deny = () => {
5650
- if (!resolved) {
5651
- resolved = true;
5652
- clearTimeout(timer);
5653
- resolve10(0);
5654
- }
5691
+ async function refreshProviderContextLimit(provider, model, opts = {}) {
5692
+ a.ctx.meta ??= {};
5693
+ const routeKey = `${provider.id}/${model}`;
5694
+ const previousRouteKey = a.ctx.meta["contextLimitRouteKey"];
5695
+ const routeChanged = previousRouteKey !== void 0 && previousRouteKey !== routeKey;
5696
+ const previousMaxContext = currentMaxContext();
5697
+ if (routeChanged) {
5698
+ delete a.ctx.meta["providerOverflowMaxContext"];
5699
+ delete a.ctx.meta["contextLimitBaseline"];
5700
+ delete a.ctx.meta["providerLimitEffectiveMaxContext"];
5701
+ delete a.ctx.meta["effectiveMaxContext"];
5702
+ delete a.ctx.meta["effectiveMaxContextSource"];
5703
+ delete a.ctx.meta["contextLimitLastEmittedMaxContext"];
5704
+ delete a.ctx.meta["contextLimitLastEmittedSource"];
5705
+ }
5706
+ a.ctx.meta["contextLimitRouteKey"] = routeKey;
5707
+ const routeMeta = a.ctx.meta["effectiveMaxContext"];
5708
+ const routeProviderCap = provider.capabilities.maxContext;
5709
+ const currentEffective = typeof routeMeta === "number" && routeMeta > 0 ? routeMeta : typeof routeProviderCap === "number" && routeProviderCap > 0 ? routeProviderCap : 2e5;
5710
+ const emitEffectiveLimit = (effective2, source) => {
5711
+ const lastEmitted = a.ctx.meta["contextLimitLastEmittedMaxContext"];
5712
+ const lastEmittedSource = a.ctx.meta["contextLimitLastEmittedSource"];
5713
+ if (lastEmitted === effective2 && lastEmittedSource === source) return;
5714
+ const eventPrevious = typeof lastEmitted === "number" && Number.isFinite(lastEmitted) && lastEmitted > 0 ? Math.floor(lastEmitted) : previousMaxContext;
5715
+ a.ctx.meta["contextLimitLastEmittedMaxContext"] = effective2;
5716
+ a.ctx.meta["contextLimitLastEmittedSource"] = source;
5717
+ a.events.emit("ctx.max_context", {
5718
+ sessionId: resolveEventSessionId(a.ctx),
5719
+ providerId: provider.id,
5720
+ modelId: model,
5721
+ maxContext: effective2,
5722
+ ...eventPrevious > 0 ? { previousMaxContext: eventPrevious } : {},
5723
+ source,
5724
+ decreased: eventPrevious > 0 && effective2 < eventPrevious
5725
+ });
5655
5726
  };
5656
- const grant = (extra) => {
5657
- if (!resolved) {
5658
- resolved = true;
5659
- clearTimeout(timer);
5660
- resolve10(Math.max(0, extra));
5727
+ const emitNativeAfterRouteChange = () => {
5728
+ if (routeChanged && currentEffective > 0) {
5729
+ a.ctx.meta["effectiveMaxContext"] = currentEffective;
5730
+ a.ctx.meta["effectiveMaxContextSource"] = "configured";
5731
+ emitEffectiveLimit(currentEffective, "configured");
5661
5732
  }
5662
5733
  };
5663
- events.emit("iteration.limit_reached", {
5664
- sessionId,
5665
- currentIterations,
5666
- currentLimit,
5667
- grant,
5668
- deny
5669
- });
5670
- if (autoExtend) {
5671
- setImmediate(() => {
5672
- if (!resolved) {
5673
- resolved = true;
5674
- clearTimeout(timer);
5675
- resolve10(100);
5676
- }
5677
- });
5734
+ const refresh = provider.refreshContextLimit;
5735
+ if (opts.probe === false || !refresh) {
5736
+ emitNativeAfterRouteChange();
5737
+ return;
5678
5738
  }
5679
- });
5680
- }
5681
-
5682
- // src/core/next-steps-slot.ts
5683
- var SLOT_KEY = "nextsteps.pending";
5684
- var MAX_PENDING_NEXT_STEPS = 4;
5685
- var NEXT_STEPS_TAG_RE = /<nextsteps\b[^>]*>/i;
5686
- function writePendingNextSteps(ctx, steps) {
5687
- const cleaned = [];
5688
- for (const step of steps) {
5689
- const text = typeof step?.text === "string" ? step.text.replace(/\s+/g, " ").trim() : "";
5690
- if (!text) continue;
5691
- cleaned.push(step.auto === true && cleaned.length === 0 ? { text, auto: true } : { text });
5692
- if (cleaned.length >= MAX_PENDING_NEXT_STEPS) break;
5693
- }
5694
- if (cleaned.length === 0) {
5695
- clearPendingNextSteps(ctx);
5696
- return;
5697
- }
5698
- ctx.meta[SLOT_KEY] = cleaned;
5699
- }
5700
- function readPendingNextSteps(ctx) {
5701
- const raw = ctx.meta[SLOT_KEY];
5702
- if (!Array.isArray(raw) || raw.length === 0) return void 0;
5703
- const steps = raw.filter(
5704
- (s) => typeof s?.text === "string" && s.text.length > 0
5705
- );
5706
- return steps.length > 0 ? steps : void 0;
5707
- }
5708
- function clearPendingNextSteps(ctx) {
5709
- delete ctx.meta[SLOT_KEY];
5710
- }
5711
- function hasNextStepsTag(text) {
5712
- return NEXT_STEPS_TAG_RE.test(text);
5713
- }
5714
- function renderNextStepsBlock(steps) {
5715
- const lines = steps.map((step, i) => {
5716
- const auto = i === 0 && step.auto === true ? ' auto="true"' : "";
5717
- return `${i + 1}. ${step.text}${auto}`;
5718
- });
5719
- return `<nextsteps>
5720
- ${lines.join("\n")}
5721
- </nextsteps>`;
5722
- }
5723
- function endsTurn(stopReason) {
5724
- return stopReason !== "tool_use" && stopReason !== "tool_call";
5725
- }
5726
- function maybeAppendPendingNextSteps(ctx, res) {
5727
- if (ctx.agentId !== "leader") return res;
5728
- if (!endsTurn(res.stopReason)) return res;
5729
- if (ctx.signal.aborted) return res;
5730
- const steps = readPendingNextSteps(ctx);
5731
- if (!steps) return res;
5732
- if (hasOpenTodos(ctx.todos)) {
5733
- clearPendingNextSteps(ctx);
5734
- return res;
5739
+ let discovered;
5740
+ try {
5741
+ discovered = await refresh.call(provider, model, { signal: a.ctx.signal });
5742
+ } catch {
5743
+ emitNativeAfterRouteChange();
5744
+ return;
5745
+ }
5746
+ const providerLimit = discovered?.maxContext;
5747
+ if (typeof providerLimit !== "number" || !Number.isFinite(providerLimit) || providerLimit <= 0) {
5748
+ emitNativeAfterRouteChange();
5749
+ return;
5750
+ }
5751
+ const providerLimitFloored = Math.floor(providerLimit);
5752
+ const existingBaseline = a.ctx.meta["contextLimitBaseline"];
5753
+ const lastApplied = a.ctx.meta["providerLimitEffectiveMaxContext"];
5754
+ const existingOverflow = a.ctx.meta["providerOverflowMaxContext"];
5755
+ const effectiveChangedOutsideProbe = typeof lastApplied === "number" && Number.isFinite(lastApplied) && currentEffective !== Math.floor(lastApplied) && currentEffective !== existingOverflow;
5756
+ const baseline = effectiveChangedOutsideProbe || typeof existingBaseline !== "number" || !Number.isFinite(existingBaseline) || existingBaseline <= 0 ? currentEffective : Math.floor(existingBaseline);
5757
+ a.ctx.meta["contextLimitBaseline"] = baseline;
5758
+ const overflowLimit = a.ctx.meta["providerOverflowMaxContext"];
5759
+ const overflowFloored = typeof overflowLimit === "number" && Number.isFinite(overflowLimit) && overflowLimit > 0 ? Math.floor(overflowLimit) : void 0;
5760
+ const ceilings = [baseline, providerLimitFloored];
5761
+ if (overflowFloored !== void 0) ceilings.push(overflowFloored);
5762
+ const effective = Math.min(...ceilings.filter((limit) => limit > 0));
5763
+ let effectiveSource;
5764
+ if (overflowFloored !== void 0 && effective === overflowFloored) {
5765
+ effectiveSource = "provider_overflow";
5766
+ } else if (effective === providerLimitFloored) {
5767
+ effectiveSource = "provider";
5768
+ } else {
5769
+ effectiveSource = "configured";
5770
+ }
5771
+ a.ctx.meta["effectiveMaxContext"] = effective;
5772
+ a.ctx.meta["providerLimitEffectiveMaxContext"] = effective;
5773
+ a.ctx.meta["effectiveMaxContextSource"] = effectiveSource;
5774
+ emitEffectiveLimit(effective, effectiveSource);
5735
5775
  }
5736
- const lastTextIndex = res.content.findLastIndex(isTextBlock);
5737
- if (lastTextIndex >= 0) {
5738
- const block = res.content[lastTextIndex];
5739
- if (block && isTextBlock(block) && hasNextStepsTag(block.text)) {
5740
- clearPendingNextSteps(ctx);
5741
- return res;
5776
+ function systemAndToolsOverhead() {
5777
+ const sysRef = a.ctx.systemPrompt;
5778
+ const toolsRef = a.ctx.tools;
5779
+ if (sysRef === _cachedSysRef && toolsRef === _cachedToolsRef && _cachedOverheadTokens > 0) {
5780
+ return _cachedOverheadTokens;
5742
5781
  }
5782
+ const breakdown = estimateRequestTokens([], sysRef, toolsRef ?? [], calibrationKey());
5783
+ _cachedSysRef = sysRef;
5784
+ _cachedToolsRef = toolsRef;
5785
+ _cachedOverheadTokens = breakdown.systemPrompt + breakdown.tools;
5786
+ return _cachedOverheadTokens;
5743
5787
  }
5744
- const rendered = renderNextStepsBlock(steps);
5745
- const content = [...res.content];
5746
- const target = lastTextIndex >= 0 ? content[lastTextIndex] : void 0;
5747
- if (target && isTextBlock(target)) {
5748
- const separator = target.text.trim() ? "\n\n" : "";
5749
- content[lastTextIndex] = { ...target, text: `${target.text}${separator}${rendered}` };
5750
- } else {
5751
- content.push({ type: "text", text: rendered });
5788
+ function stashRequestTokens(req) {
5789
+ const preFlight = estimateRequestTokens(
5790
+ req.messages,
5791
+ req.system,
5792
+ req.tools ?? [],
5793
+ calibrationKey(req.model)
5794
+ );
5795
+ a.ctx.lastRequestTokens = preFlight.total;
5796
+ _lastPreFlightMsgCount = req.messages.length;
5797
+ _lastPreFlightToolCount = (req.tools ?? []).length;
5798
+ _lastPreFlightRevision = a.ctx.state.revision;
5799
+ _cachedSysRef = req.system;
5800
+ _cachedToolsRef = req.tools ?? [];
5801
+ _cachedOverheadTokens = preFlight.systemPrompt + preFlight.tools;
5802
+ a.ctx.meta["lastRequestTokensAt"] = {
5803
+ msgCount: req.messages.length,
5804
+ toolCount: (req.tools ?? []).length,
5805
+ revision: a.ctx.state.revision
5806
+ };
5807
+ return preFlight;
5752
5808
  }
5753
- clearPendingNextSteps(ctx);
5754
- return { ...res, content };
5809
+ function refreshContextRequestTokenStash(opts = {}) {
5810
+ const msgCount = a.ctx.messages.length;
5811
+ const toolCount = (a.ctx.tools ?? []).length;
5812
+ const revision = a.ctx.state.revision;
5813
+ const stashed = a.ctx.lastRequestTokens;
5814
+ const stashedAt = a.ctx.meta?.["lastRequestTokensAt"];
5815
+ if (!opts.force && typeof stashed === "number" && stashed > 0 && typeof stashedAt === "object" && stashedAt !== null) {
5816
+ const meta = stashedAt;
5817
+ if (meta.msgCount === msgCount && meta.toolCount === toolCount && meta.revision === revision && _lastPreFlightRevision === revision) {
5818
+ return stashed;
5819
+ }
5820
+ }
5821
+ const refreshed = estimateMessageTokens(a.ctx.messages) + systemAndToolsOverhead();
5822
+ a.ctx.lastRequestTokens = refreshed;
5823
+ _lastPreFlightMsgCount = msgCount;
5824
+ _lastPreFlightToolCount = toolCount;
5825
+ _lastPreFlightRevision = revision;
5826
+ a.ctx.meta["lastRequestTokensAt"] = { msgCount, toolCount, revision };
5827
+ return refreshed;
5828
+ }
5829
+ async function compactContextIfNeeded() {
5830
+ const msgCount = a.ctx.messages.length;
5831
+ const maxContext = currentMaxContext();
5832
+ const revision = a.ctx.state.revision;
5833
+ const toolsRef = a.ctx.tools;
5834
+ const systemRef = a.ctx.systemPrompt;
5835
+ const requestTokens = a.ctx.lastRequestTokens;
5836
+ if (_lastCompactionMsgCount === msgCount && _lastCompactionRevision === revision && _lastCompactionToolsRef === toolsRef && _lastCompactionSystemRef === systemRef && _lastCompactionRequestTokens === requestTokens && _lastCompactionWasNoop && _lastCompactionMaxContext === maxContext && maxContext > 0) {
5837
+ return false;
5838
+ }
5839
+ const beforeMessages = a.ctx.messages;
5840
+ const beforeMsgCount = a.ctx.messages.length;
5841
+ const beforeRevision = a.ctx.state.revision;
5842
+ await a.pipelines.contextWindow.run(a.ctx);
5843
+ _lastCompactionMsgCount = a.ctx.messages.length;
5844
+ _lastCompactionRevision = a.ctx.state.revision;
5845
+ _lastCompactionToolsRef = a.ctx.tools;
5846
+ _lastCompactionSystemRef = a.ctx.systemPrompt;
5847
+ _lastCompactionMaxContext = maxContext;
5848
+ const changed = a.ctx.state.revision !== beforeRevision || a.ctx.messages !== beforeMessages || a.ctx.messages.length !== beforeMsgCount;
5849
+ const tokens = refreshContextRequestTokenStash({ force: changed });
5850
+ _lastCompactionRequestTokens = tokens;
5851
+ const load = maxContext > 0 ? tokens / maxContext : 0;
5852
+ _lastCompactionWasNoop = tokens > 0 && load < 0.5;
5853
+ if (changed) {
5854
+ _lastEmittedMsgCount = -1;
5855
+ _lastEmittedToolCount = -1;
5856
+ _lastEmittedMaxContext = -1;
5857
+ }
5858
+ return changed;
5859
+ }
5860
+ async function buildRequestWithPreflightCompaction(opts) {
5861
+ let prepared = await handlers.response.buildAndRunRequestPipeline(opts);
5862
+ let req = prepared.request;
5863
+ let preFlight = stashRequestTokens(req);
5864
+ let probedRouteKey = `${prepared.provider.id}/${req.model}`;
5865
+ await refreshProviderContextLimit(prepared.provider, req.model);
5866
+ if (await compactContextIfNeeded()) {
5867
+ prepared = await handlers.response.buildAndRunRequestPipeline(opts);
5868
+ req = prepared.request;
5869
+ preFlight = stashRequestTokens(req);
5870
+ const rebuiltRouteKey = `${prepared.provider.id}/${req.model}`;
5871
+ if (rebuiltRouteKey !== probedRouteKey) {
5872
+ probedRouteKey = rebuiltRouteKey;
5873
+ await refreshProviderContextLimit(prepared.provider, req.model);
5874
+ if (await compactContextIfNeeded()) {
5875
+ prepared = await handlers.response.buildAndRunRequestPipeline(opts);
5876
+ req = prepared.request;
5877
+ preFlight = stashRequestTokens(req);
5878
+ }
5879
+ }
5880
+ }
5881
+ return { req, provider: prepared.provider, preFlight };
5882
+ }
5883
+ function emitContextPct() {
5884
+ const msgCount = a.ctx.messages.length;
5885
+ const toolCount = (a.ctx.tools ?? []).length;
5886
+ const maxContext = currentMaxContext();
5887
+ const revision = a.ctx.state.revision;
5888
+ if (msgCount === _lastEmittedMsgCount && toolCount === _lastEmittedToolCount && revision === _lastEmittedRevision && maxContext === _lastEmittedMaxContext && maxContext > 0) {
5889
+ return;
5890
+ }
5891
+ _lastEmittedMsgCount = msgCount;
5892
+ _lastEmittedToolCount = toolCount;
5893
+ _lastEmittedRevision = revision;
5894
+ _lastEmittedMaxContext = maxContext;
5895
+ if (msgCount !== _lastPreFlightMsgCount || toolCount !== _lastPreFlightToolCount || revision !== _lastPreFlightRevision) {
5896
+ refreshContextRequestTokenStash({ force: true });
5897
+ }
5898
+ let total;
5899
+ const anchored = realAnchoredInputTokens(
5900
+ a.ctx.messages,
5901
+ a.ctx.lastRealInputTokens,
5902
+ typeof a.ctx.meta?.["realAnchorMsgCount"] === "number" ? a.ctx.meta["realAnchorMsgCount"] : void 0
5903
+ );
5904
+ const stashed = a.ctx.lastRequestTokens;
5905
+ if (anchored !== null) {
5906
+ total = anchored;
5907
+ } else if (typeof stashed === "number" && stashed > 0) {
5908
+ const cal = getCalibrationState(calibrationKey());
5909
+ total = cal.calibrated ? Math.round(stashed * Math.min(1.5, Math.max(0.5, cal.ratio))) : stashed;
5910
+ } else {
5911
+ const raw = refreshContextRequestTokenStash({ force: true });
5912
+ const cal = getCalibrationState(calibrationKey());
5913
+ total = cal.calibrated ? Math.round(raw * Math.min(1.5, Math.max(0.5, cal.ratio))) : raw;
5914
+ }
5915
+ const rawLoad = maxContext > 0 ? total / maxContext : 0;
5916
+ const load = Math.max(0, Math.min(1, rawLoad));
5917
+ a.events.emit("ctx.pct", {
5918
+ sessionId: resolveEventSessionId(a.ctx),
5919
+ load,
5920
+ rawLoad,
5921
+ tokens: total,
5922
+ maxContext
5923
+ });
5924
+ }
5925
+ return {
5926
+ refreshProviderContextLimit,
5927
+ compactContextIfNeeded,
5928
+ stashRequestTokens,
5929
+ refreshContextRequestTokenStash,
5930
+ buildRequestWithPreflightCompaction,
5931
+ emitContextPct,
5932
+ currentMaxContext,
5933
+ calibrationKey,
5934
+ get lastPreFlightMsgCount() {
5935
+ return _lastPreFlightMsgCount;
5936
+ }
5937
+ };
5755
5938
  }
5756
5939
 
5757
- // src/core/streaming-response-builder.ts
5758
- import { randomUUID as randomUUID4 } from "node:crypto";
5759
-
5760
- // src/utils/json-repair.ts
5761
- function completePartialObject(s) {
5762
- if (!s.trim().startsWith("{")) return s;
5763
- if (tryParse(s).ok) return s;
5764
- return repairTruncated(s);
5940
+ // src/core/agent-loop-detector.ts
5941
+ function iterationFingerprint(blocks) {
5942
+ const toolUses = blocks.filter(isToolUseBlock);
5943
+ const texts = blocks.filter(isTextBlock);
5944
+ const toolNameSet = Array.from(new Set(toolUses.map((u) => u.name))).sort();
5945
+ const firstInputHash = toolUses[0] ? hashSmall(stableStringify(toolUses[0].input ?? {})) : "";
5946
+ const textBlob = texts.map((t2) => t2.text).join("").slice(0, 512);
5947
+ const hasContent = toolNameSet.length > 0 || textBlob.length > 0;
5948
+ if (!hasContent) return "__empty__";
5949
+ return [
5950
+ `tools=${toolNameSet.join("+") || "-"}`,
5951
+ `in0=${firstInputHash}`,
5952
+ `txt=${textBlob}`
5953
+ ].join("\n");
5765
5954
  }
5766
- function repairTruncated(s) {
5767
- const stack = [];
5768
- let inString = false;
5769
- let escaped = false;
5770
- let sawKey = false;
5771
- let prevSig = "";
5772
- let contentEnd = 0;
5773
- let stringBraceDepth = 0;
5955
+ function stableStringify(value) {
5956
+ return JSON.stringify(canonicalize(value));
5957
+ }
5958
+ function canonicalize(value) {
5959
+ if (Array.isArray(value)) return value.map(canonicalize);
5960
+ if (value && typeof value === "object") {
5961
+ const src = value;
5962
+ const out = {};
5963
+ for (const k of Object.keys(src).sort()) out[k] = canonicalize(src[k]);
5964
+ return out;
5965
+ }
5966
+ return value;
5967
+ }
5968
+ function hashSmall(s) {
5969
+ let h = 2166136261;
5774
5970
  for (let i = 0; i < s.length; i++) {
5775
- const ch = expectDefined(s[i]);
5776
- if (inString) {
5777
- contentEnd = i + 1;
5778
- if (escaped) {
5779
- escaped = false;
5780
- continue;
5971
+ h ^= s.charCodeAt(i);
5972
+ h = h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0;
5973
+ }
5974
+ return h.toString(36);
5975
+ }
5976
+ var AgentLoopDetector = class {
5977
+ constructor(a) {
5978
+ this.a = a;
5979
+ }
5980
+ a;
5981
+ lastToolSignature = "";
5982
+ toolLoopCount = 0;
5983
+ iterationSteerDone = false;
5984
+ recentCallKeys = [];
5985
+ steeredCallKeys = /* @__PURE__ */ new Set();
5986
+ checkIteration(iterationIndex, content, toolUses, queueSteer) {
5987
+ const loopCfg = this.a.loopDetection;
5988
+ if (loopCfg.mode === "off") return { cut: false };
5989
+ const sig = iterationFingerprint(content);
5990
+ if (sig !== "__empty__") {
5991
+ if (sig === this.lastToolSignature) {
5992
+ this.toolLoopCount++;
5993
+ } else {
5994
+ this.lastToolSignature = sig;
5995
+ this.toolLoopCount = 1;
5996
+ this.iterationSteerDone = false;
5781
5997
  }
5782
- if (ch === "\\") {
5783
- escaped = true;
5784
- continue;
5998
+ const names = toolUses.map((t2) => t2.name).join(", ");
5999
+ const hasText2 = content.some(isTextBlock);
6000
+ const kind = toolUses.length > 0 && hasText2 ? "mixed" : toolUses.length > 0 ? "tool" : "message";
6001
+ const observationRepeat = loopCfg.mode === "steer-then-cut" && toolUses.length > 0 && toolUses.every((use) => {
6002
+ const tool = this.a.tools.get(use.name);
6003
+ return tool?.mutating === false && (tool.riskTier === "safe" || (tool.capabilities?.length ?? 0) > 0);
6004
+ });
6005
+ const repeatMultiplier = observationRepeat ? 2 : 1;
6006
+ const detail = kind === "tool" ? `"${names}" called with effectively identical inputs ${this.toolLoopCount} times in a row` : kind === "mixed" ? `"${names}" + same text repeated ${this.toolLoopCount} times in a row` : `same assistant text repeated ${this.toolLoopCount} times in a row`;
6007
+ const cutAt = (loopCfg.mode === "cut" ? loopCfg.steerThreshold : loopCfg.cutThreshold) * repeatMultiplier;
6008
+ if (this.toolLoopCount >= cutAt) {
6009
+ this.a.logger.warn(`Loop detected: ${detail} \u2014 stopping to prevent infinite loop.`);
6010
+ this.a.events.emit("tool.loop_detected", {
6011
+ sessionId: resolveEventSessionId(this.a.ctx),
6012
+ ctx: this.a.ctx,
6013
+ tools: names,
6014
+ repeatCount: this.toolLoopCount,
6015
+ iteration: iterationIndex,
6016
+ kind,
6017
+ action: "cut",
6018
+ scope: "iteration"
6019
+ });
6020
+ const summary = kind === "message" ? `[Loop detected: same assistant message repeated ${this.toolLoopCount}\xD7 \u2014 stopping to prevent infinite repetition.]` : `[Loop detected: ${detail} \u2014 stopping to prevent infinite repetition.]`;
6021
+ return { cut: true, cutSummary: summary };
5785
6022
  }
5786
- if (ch === '"') {
5787
- inString = false;
5788
- prevSig = '"';
5789
- stringBraceDepth = 0;
5790
- continue;
6023
+ if (loopCfg.mode === "steer-then-cut" && this.toolLoopCount >= loopCfg.steerThreshold * repeatMultiplier && !this.iterationSteerDone) {
6024
+ this.iterationSteerDone = true;
6025
+ this.a.logger.warn(`Loop detected: ${detail} \u2014 steering the model to change approach.`);
6026
+ this.a.events.emit("tool.loop_detected", {
6027
+ sessionId: resolveEventSessionId(this.a.ctx),
6028
+ ctx: this.a.ctx,
6029
+ tools: names,
6030
+ repeatCount: this.toolLoopCount,
6031
+ iteration: iterationIndex,
6032
+ kind,
6033
+ action: "steer",
6034
+ scope: "iteration"
6035
+ });
6036
+ queueSteer(
6037
+ `[loop-detector] Your last ${this.toolLoopCount} responses were effectively identical (${detail}). This approach is not working. Change strategy: use a different tool, different arguments, or a different plan \u2014 or explain what is blocking you and stop. Repeating the same response ${cutAt - this.toolLoopCount} more time(s) will terminate the turn.`
6038
+ );
5791
6039
  }
5792
- if (ch === "{") stringBraceDepth++;
5793
- else if (ch === "}" && stringBraceDepth > 0) stringBraceDepth--;
5794
- continue;
5795
- }
5796
- if (ch === " " || ch === " " || ch === "\n" || ch === "\r") continue;
5797
- contentEnd = i + 1;
5798
- if (ch === '"') {
5799
- inString = true;
5800
- sawKey = true;
5801
- stringBraceDepth = 0;
5802
- prevSig = '"';
5803
- } else if (ch === "{" || ch === "[") {
5804
- stack.push(ch);
5805
- prevSig = ch;
5806
- } else if (ch === "}" || ch === "]") {
5807
- stack.pop();
5808
- prevSig = ch;
5809
6040
  } else {
5810
- prevSig = ch;
6041
+ this.lastToolSignature = "";
6042
+ this.toolLoopCount = 0;
6043
+ this.iterationSteerDone = false;
6044
+ }
6045
+ if (loopCfg.mode === "steer-then-cut") {
6046
+ for (const u of toolUses) {
6047
+ const key = `${u.name}:${hashSmall(stableStringify(u.input ?? {}))}`;
6048
+ this.recentCallKeys.push(key);
6049
+ if (this.recentCallKeys.length > loopCfg.windowSize) this.recentCallKeys.shift();
6050
+ if (this.steeredCallKeys.has(key)) continue;
6051
+ let count = 0;
6052
+ for (const k of this.recentCallKeys) if (k === key) count++;
6053
+ const tool = this.a.tools.get(u.name);
6054
+ const observationThreshold = tool?.mutating === false && (tool.riskTier === "safe" || (tool.capabilities?.length ?? 0) > 0) ? loopCfg.callRepeatThreshold * 2 : loopCfg.callRepeatThreshold;
6055
+ if (count < observationThreshold) continue;
6056
+ this.steeredCallKeys.add(key);
6057
+ const preview = JSON.stringify(u.input ?? {}).slice(0, 160);
6058
+ this.a.logger.warn(
6059
+ `Loop detected: "${u.name}" called with identical arguments ${count}\xD7 within the last ${loopCfg.windowSize} tool calls \u2014 steering the model to change approach.`
6060
+ );
6061
+ this.a.events.emit("tool.loop_detected", {
6062
+ sessionId: resolveEventSessionId(this.a.ctx),
6063
+ ctx: this.a.ctx,
6064
+ tools: u.name,
6065
+ repeatCount: count,
6066
+ iteration: iterationIndex,
6067
+ kind: "tool",
6068
+ action: "steer",
6069
+ scope: "call"
6070
+ });
6071
+ queueSteer(
6072
+ `[loop-detector] You have called ${u.name}(${preview}) ${count} times with identical arguments within the last ${loopCfg.windowSize} tool calls. The result will not change. Do not repeat this call \u2014 use what you already know, try a different approach, or explain the blocker.`
6073
+ );
6074
+ }
5811
6075
  }
6076
+ return { cut: false };
5812
6077
  }
5813
- if (!sawKey && !inString) return s;
5814
- let result = s.slice(0, contentEnd);
5815
- if (inString) {
5816
- if (escaped) {
5817
- result = result.slice(0, -1);
5818
- } else if (endsWithInvalidEscape(result)) {
5819
- result = result.slice(0, -2);
6078
+ };
6079
+
6080
+ // src/core/continue-to-next-iteration.ts
6081
+ function parseContinueDirective(text) {
6082
+ const LINE_MARKERS = /^\s*\[(continue|next step|proceed|done)\]\s*$/gim;
6083
+ const tail = text.length <= DIRECTIVE_SCAN_WINDOW ? text : text.slice(text.length - DIRECTIVE_SCAN_WINDOW);
6084
+ let match;
6085
+ let lastDirective = "none";
6086
+ while ((match = LINE_MARKERS.exec(tail)) !== null) {
6087
+ const value = (match[1] ?? "").toLowerCase();
6088
+ if (value === "continue" || value === "next step" || value === "proceed") {
6089
+ lastDirective = "continue";
6090
+ } else if (value === "done") {
6091
+ lastDirective = "stop";
5820
6092
  }
5821
- if (stringBraceDepth > 0) result += "}".repeat(stringBraceDepth);
5822
- result += '"';
5823
- } else if (prevSig === ":") {
5824
- result += "null";
5825
- }
5826
- for (let k = stack.length - 1; k >= 0; k--) {
5827
- result += stack[k] === "{" ? "}" : "]";
5828
- }
5829
- if (!tryParse(result).ok) {
5830
- const patched = result.replace(/:(\s*)([}\]])/g, ":null$2");
5831
- if (tryParse(patched).ok) result = patched;
5832
6093
  }
5833
- return result;
6094
+ return lastDirective;
5834
6095
  }
5835
- var VALID_ESCAPE = /* @__PURE__ */ new Set(['"', "\\", "/", "b", "f", "n", "r", "t", "u"]);
5836
- function endsWithInvalidEscape(str) {
5837
- const last = str[str.length - 1];
5838
- if (str[str.length - 2] !== "\\" || last === void 0) return false;
5839
- if (VALID_ESCAPE.has(last)) return false;
5840
- let backslashes = 0;
5841
- for (let k = str.length - 2; k >= 0 && str[k] === "\\"; k--) backslashes++;
5842
- return backslashes % 2 === 1;
6096
+ var DIRECTIVE_SCAN_WINDOW = 2048;
6097
+ var META_KEY2 = "_autonomousContinue";
6098
+ function consumeAutonomousContinue(ctx) {
6099
+ const val = ctx.meta[META_KEY2] === true;
6100
+ delete ctx.meta[META_KEY2];
6101
+ return val;
6102
+ }
6103
+
6104
+ // src/core/iteration-limit.ts
6105
+ function requestLimitExtension(opts) {
6106
+ const { events, sessionId, currentIterations, currentLimit, autoExtend, timeoutMs = 3e4 } = opts;
6107
+ return new Promise((resolve10) => {
6108
+ let resolved = false;
6109
+ const timerFired = () => {
6110
+ if (!resolved) {
6111
+ resolved = true;
6112
+ resolve10(0);
6113
+ }
6114
+ };
6115
+ const timer = setTimeout(timerFired, timeoutMs);
6116
+ timer.unref();
6117
+ const deny = () => {
6118
+ if (!resolved) {
6119
+ resolved = true;
6120
+ clearTimeout(timer);
6121
+ resolve10(0);
6122
+ }
6123
+ };
6124
+ const grant = (extra) => {
6125
+ if (!resolved) {
6126
+ resolved = true;
6127
+ clearTimeout(timer);
6128
+ resolve10(Math.max(0, extra));
6129
+ }
6130
+ };
6131
+ events.emit("iteration.limit_reached", {
6132
+ sessionId,
6133
+ currentIterations,
6134
+ currentLimit,
6135
+ grant,
6136
+ deny
6137
+ });
6138
+ if (autoExtend) {
6139
+ setImmediate(() => {
6140
+ if (!resolved) {
6141
+ resolved = true;
6142
+ clearTimeout(timer);
6143
+ resolve10(100);
6144
+ }
6145
+ });
6146
+ }
6147
+ });
6148
+ }
6149
+
6150
+ // src/core/next-steps-slot.ts
6151
+ var SLOT_KEY = "nextsteps.pending";
6152
+ var MAX_PENDING_NEXT_STEPS = 4;
6153
+ var NEXT_STEPS_TAG_RE = /<nextsteps\b[^>]*>/i;
6154
+ function writePendingNextSteps(ctx, steps) {
6155
+ const cleaned = [];
6156
+ for (const step of steps) {
6157
+ const text = typeof step?.text === "string" ? step.text.replace(/\s+/g, " ").trim() : "";
6158
+ if (!text) continue;
6159
+ cleaned.push(step.auto === true && cleaned.length === 0 ? { text, auto: true } : { text });
6160
+ if (cleaned.length >= MAX_PENDING_NEXT_STEPS) break;
6161
+ }
6162
+ if (cleaned.length === 0) {
6163
+ clearPendingNextSteps(ctx);
6164
+ return;
6165
+ }
6166
+ ctx.meta[SLOT_KEY] = cleaned;
6167
+ }
6168
+ function readPendingNextSteps(ctx) {
6169
+ const raw = ctx.meta[SLOT_KEY];
6170
+ if (!Array.isArray(raw) || raw.length === 0) return void 0;
6171
+ const steps = raw.filter(
6172
+ (s) => typeof s?.text === "string" && s.text.length > 0
6173
+ );
6174
+ return steps.length > 0 ? steps : void 0;
6175
+ }
6176
+ function clearPendingNextSteps(ctx) {
6177
+ delete ctx.meta[SLOT_KEY];
6178
+ }
6179
+ function hasNextStepsTag(text) {
6180
+ return NEXT_STEPS_TAG_RE.test(text);
6181
+ }
6182
+ function renderNextStepsBlock(steps) {
6183
+ const lines = steps.map((step, i) => {
6184
+ const auto = i === 0 && step.auto === true ? ' auto="true"' : "";
6185
+ return `${i + 1}. ${step.text}${auto}`;
6186
+ });
6187
+ return `<nextsteps>
6188
+ ${lines.join("\n")}
6189
+ </nextsteps>`;
6190
+ }
6191
+ function endsTurn(stopReason) {
6192
+ return stopReason !== "tool_use" && stopReason !== "tool_call";
6193
+ }
6194
+ function maybeAppendPendingNextSteps(ctx, res) {
6195
+ if (ctx.agentId !== "leader") return res;
6196
+ if (!endsTurn(res.stopReason)) return res;
6197
+ if (ctx.signal.aborted) return res;
6198
+ const steps = readPendingNextSteps(ctx);
6199
+ if (!steps) return res;
6200
+ if (hasOpenTodos(ctx.todos)) {
6201
+ clearPendingNextSteps(ctx);
6202
+ return res;
6203
+ }
6204
+ const lastTextIndex = res.content.findLastIndex(isTextBlock);
6205
+ if (lastTextIndex >= 0) {
6206
+ const block = res.content[lastTextIndex];
6207
+ if (block && isTextBlock(block) && hasNextStepsTag(block.text)) {
6208
+ clearPendingNextSteps(ctx);
6209
+ return res;
6210
+ }
6211
+ }
6212
+ const rendered = renderNextStepsBlock(steps);
6213
+ const content = [...res.content];
6214
+ const target = lastTextIndex >= 0 ? content[lastTextIndex] : void 0;
6215
+ if (target && isTextBlock(target)) {
6216
+ const separator = target.text.trim() ? "\n\n" : "";
6217
+ content[lastTextIndex] = { ...target, text: `${target.text}${separator}${rendered}` };
6218
+ } else {
6219
+ content.push({ type: "text", text: rendered });
6220
+ }
6221
+ clearPendingNextSteps(ctx);
6222
+ return { ...res, content };
6223
+ }
6224
+
6225
+ // src/core/streaming-response-builder.ts
6226
+ import { randomUUID as randomUUID4 } from "node:crypto";
6227
+
6228
+ // src/utils/json-repair.ts
6229
+ function completePartialObject(s) {
6230
+ if (!s.trim().startsWith("{")) return s;
6231
+ if (tryParse(s).ok) return s;
6232
+ return repairTruncated(s);
6233
+ }
6234
+ function repairTruncated(s) {
6235
+ const stack = [];
6236
+ let inString = false;
6237
+ let escaped = false;
6238
+ let sawKey = false;
6239
+ let prevSig = "";
6240
+ let contentEnd = 0;
6241
+ let stringBraceDepth = 0;
6242
+ for (let i = 0; i < s.length; i++) {
6243
+ const ch = expectDefined(s[i]);
6244
+ if (inString) {
6245
+ contentEnd = i + 1;
6246
+ if (escaped) {
6247
+ escaped = false;
6248
+ continue;
6249
+ }
6250
+ if (ch === "\\") {
6251
+ escaped = true;
6252
+ continue;
6253
+ }
6254
+ if (ch === '"') {
6255
+ inString = false;
6256
+ prevSig = '"';
6257
+ stringBraceDepth = 0;
6258
+ continue;
6259
+ }
6260
+ if (ch === "{") stringBraceDepth++;
6261
+ else if (ch === "}" && stringBraceDepth > 0) stringBraceDepth--;
6262
+ continue;
6263
+ }
6264
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") continue;
6265
+ contentEnd = i + 1;
6266
+ if (ch === '"') {
6267
+ inString = true;
6268
+ sawKey = true;
6269
+ stringBraceDepth = 0;
6270
+ prevSig = '"';
6271
+ } else if (ch === "{" || ch === "[") {
6272
+ stack.push(ch);
6273
+ prevSig = ch;
6274
+ } else if (ch === "}" || ch === "]") {
6275
+ stack.pop();
6276
+ prevSig = ch;
6277
+ } else {
6278
+ prevSig = ch;
6279
+ }
6280
+ }
6281
+ if (!sawKey && !inString) return s;
6282
+ let result = s.slice(0, contentEnd);
6283
+ if (inString) {
6284
+ if (escaped) {
6285
+ result = result.slice(0, -1);
6286
+ } else if (endsWithInvalidEscape(result)) {
6287
+ result = result.slice(0, -2);
6288
+ }
6289
+ if (stringBraceDepth > 0) result += "}".repeat(stringBraceDepth);
6290
+ result += '"';
6291
+ } else if (prevSig === ":") {
6292
+ result += "null";
6293
+ }
6294
+ for (let k = stack.length - 1; k >= 0; k--) {
6295
+ result += stack[k] === "{" ? "}" : "]";
6296
+ }
6297
+ if (!tryParse(result).ok) {
6298
+ const patched = result.replace(/:(\s*)([}\]])/g, ":null$2");
6299
+ if (tryParse(patched).ok) result = patched;
6300
+ }
6301
+ return result;
6302
+ }
6303
+ var VALID_ESCAPE = /* @__PURE__ */ new Set(['"', "\\", "/", "b", "f", "n", "r", "t", "u"]);
6304
+ function endsWithInvalidEscape(str) {
6305
+ const last = str[str.length - 1];
6306
+ if (str[str.length - 2] !== "\\" || last === void 0) return false;
6307
+ if (VALID_ESCAPE.has(last)) return false;
6308
+ let backslashes = 0;
6309
+ for (let k = str.length - 2; k >= 0 && str[k] === "\\"; k--) backslashes++;
6310
+ return backslashes % 2 === 1;
5843
6311
  }
5844
6312
  function tryParse(s) {
5845
6313
  try {
@@ -6493,397 +6961,106 @@ async function runProviderWithRetry(opts) {
6493
6961
  return;
6494
6962
  }
6495
6963
  signal.addEventListener("abort", onAbort);
6496
- });
6497
- attempt++;
6498
- }
6499
- }
6500
- }
6501
- function elapsedMs(startedNs) {
6502
- return Number(process.hrtime.bigint() - startedNs) / 1e6;
6503
- }
6504
-
6505
- // src/core/queued-messages.ts
6506
- var META_KEY3 = "_queuedMessagesAwareness";
6507
- var MAX_ITEMS = 20;
6508
- var MAX_PREVIEW_CHARS = 500;
6509
- function read(ctx) {
6510
- const raw = ctx.meta[META_KEY3];
6511
- if (!raw || typeof raw !== "object") return void 0;
6512
- const s = raw;
6513
- return Array.isArray(s.items) && Array.isArray(s.seen) ? s : void 0;
6514
- }
6515
- function sameList(a, b) {
6516
- return a.length === b.length && a.every((v, i) => v === b[i]);
6517
- }
6518
- function toPreview(text) {
6519
- const trimmed = text.trim();
6520
- if (trimmed.length <= MAX_PREVIEW_CHARS) return trimmed;
6521
- return `${trimmed.slice(0, MAX_PREVIEW_CHARS - 1)}\u2026`;
6522
- }
6523
- function setQueuedMessagesSnapshot(ctx, texts) {
6524
- const items = texts.map(toPreview).filter((t2) => t2.length > 0).slice(0, MAX_ITEMS);
6525
- const prev = read(ctx);
6526
- if (items.length === 0 && (!prev || prev.seen.length === 0)) {
6527
- if (prev) delete ctx.meta[META_KEY3];
6528
- return;
6529
- }
6530
- ctx.meta[META_KEY3] = { items, seen: prev?.seen ?? [] };
6531
- }
6532
- function consumeQueuedMessagesUpdate(ctx) {
6533
- const s = read(ctx);
6534
- if (!s) return null;
6535
- if (sameList(s.items, s.seen)) return null;
6536
- if (s.items.length === 0) {
6537
- delete ctx.meta[META_KEY3];
6538
- return null;
6539
- }
6540
- ctx.meta[META_KEY3] = { items: s.items, seen: s.items };
6541
- return s.items;
6542
- }
6543
- function buildQueuedMessagesBlock(items) {
6544
- const body = items.map((t2, i) => `${i + 1}. ${t2}`).join("\n");
6545
- return [
6546
- "[QUEUED MESSAGES \u2014 the user typed these while you were working. They are",
6547
- "waiting in the input queue and each will be delivered as its own turn",
6548
- "after you finish, so do NOT answer or act on them as new tasks now. Read",
6549
- "them as a heads-up: if one changes what you should be doing right now",
6550
- "(new constraint, invalidated approach), adapt your current work; if one",
6551
- "is just an FYI, fold it in. The queue may still change before delivery.",
6552
- "This notice replaces any earlier one \u2014 the queue now holds exactly:",
6553
- "",
6554
- body,
6555
- "]"
6556
- ].join("\n");
6557
- }
6558
-
6559
- // src/core/request-provider-binding.ts
6560
- var requestProviders = /* @__PURE__ */ new WeakMap();
6561
- function bindRequestProvider(request, provider) {
6562
- requestProviders.set(request, provider);
6563
- }
6564
- function providerBoundToRequest(request) {
6565
- return requestProviders.get(request);
6566
- }
6567
-
6568
- // src/core/agent-loop.ts
6569
- function toError(err) {
6570
- return err instanceof Error ? err : new Error(String(err));
6571
- }
6572
- function signalAbortReason(signal) {
6573
- const r = signal.reason;
6574
- if (r instanceof Error) return r.message || r.name;
6575
- if (typeof r === "string" && r.length > 0) return r;
6576
- return "aborted";
6577
- }
6578
- function createAgentLoopHandler(a, handlers) {
6579
- const checkMailbox = attachMailboxChecker(a);
6580
- const fleetPulseCfg = (() => {
6581
- try {
6582
- return typeof a.container?.has === "function" && a.container.has(TOKENS.ConfigStore) ? a.container.resolve(TOKENS.ConfigStore).get().fleet?.pulse : void 0;
6583
- } catch {
6584
- return void 0;
6585
- }
6586
- })();
6587
- const getFleetPulse = attachFleetPulse(a, fleetPulseCfg);
6588
- const pulseEveryN = Math.max(1, fleetPulseCfg?.everyNIterations ?? 5);
6589
- const backgroundCoordination = () => a.ctx.meta["coordinationContextMode"] === "background";
6590
- async function refreshProviderContextLimit(provider, model, opts = {}) {
6591
- a.ctx.meta ??= {};
6592
- const routeKey = `${provider.id}/${model}`;
6593
- const previousRouteKey = a.ctx.meta["contextLimitRouteKey"];
6594
- const routeChanged = previousRouteKey !== void 0 && previousRouteKey !== routeKey;
6595
- const previousMaxContext = currentMaxContext();
6596
- if (routeChanged) {
6597
- delete a.ctx.meta["providerOverflowMaxContext"];
6598
- delete a.ctx.meta["contextLimitBaseline"];
6599
- delete a.ctx.meta["providerLimitEffectiveMaxContext"];
6600
- delete a.ctx.meta["effectiveMaxContext"];
6601
- delete a.ctx.meta["effectiveMaxContextSource"];
6602
- delete a.ctx.meta["contextLimitLastEmittedMaxContext"];
6603
- delete a.ctx.meta["contextLimitLastEmittedSource"];
6604
- }
6605
- a.ctx.meta["contextLimitRouteKey"] = routeKey;
6606
- const routeMeta = a.ctx.meta["effectiveMaxContext"];
6607
- const routeProviderCap = provider.capabilities.maxContext;
6608
- const currentEffective = typeof routeMeta === "number" && routeMeta > 0 ? routeMeta : typeof routeProviderCap === "number" && routeProviderCap > 0 ? routeProviderCap : 2e5;
6609
- const emitEffectiveLimit = (effective2, source) => {
6610
- const lastEmitted = a.ctx.meta["contextLimitLastEmittedMaxContext"];
6611
- const lastEmittedSource = a.ctx.meta["contextLimitLastEmittedSource"];
6612
- if (lastEmitted === effective2 && lastEmittedSource === source) return;
6613
- const eventPrevious = typeof lastEmitted === "number" && Number.isFinite(lastEmitted) && lastEmitted > 0 ? Math.floor(lastEmitted) : previousMaxContext;
6614
- a.ctx.meta["contextLimitLastEmittedMaxContext"] = effective2;
6615
- a.ctx.meta["contextLimitLastEmittedSource"] = source;
6616
- a.events.emit("ctx.max_context", {
6617
- sessionId: resolveEventSessionId(a.ctx),
6618
- providerId: provider.id,
6619
- modelId: model,
6620
- maxContext: effective2,
6621
- ...eventPrevious > 0 ? { previousMaxContext: eventPrevious } : {},
6622
- source,
6623
- decreased: eventPrevious > 0 && effective2 < eventPrevious
6624
- });
6625
- };
6626
- const emitNativeAfterRouteChange = () => {
6627
- if (routeChanged && currentEffective > 0) {
6628
- a.ctx.meta["effectiveMaxContext"] = currentEffective;
6629
- a.ctx.meta["effectiveMaxContextSource"] = "configured";
6630
- emitEffectiveLimit(currentEffective, "configured");
6631
- }
6632
- };
6633
- const refresh = provider.refreshContextLimit;
6634
- if (opts.probe === false || !refresh) {
6635
- emitNativeAfterRouteChange();
6636
- return;
6637
- }
6638
- let discovered;
6639
- try {
6640
- discovered = await refresh.call(provider, model, { signal: a.ctx.signal });
6641
- } catch {
6642
- emitNativeAfterRouteChange();
6643
- return;
6644
- }
6645
- const providerLimit = discovered?.maxContext;
6646
- if (typeof providerLimit !== "number" || !Number.isFinite(providerLimit) || providerLimit <= 0) {
6647
- emitNativeAfterRouteChange();
6648
- return;
6649
- }
6650
- const providerLimitFloored = Math.floor(providerLimit);
6651
- const existingBaseline = a.ctx.meta["contextLimitBaseline"];
6652
- const lastApplied = a.ctx.meta["providerLimitEffectiveMaxContext"];
6653
- const existingOverflow = a.ctx.meta["providerOverflowMaxContext"];
6654
- const effectiveChangedOutsideProbe = typeof lastApplied === "number" && Number.isFinite(lastApplied) && currentEffective !== Math.floor(lastApplied) && currentEffective !== existingOverflow;
6655
- const baseline = effectiveChangedOutsideProbe || typeof existingBaseline !== "number" || !Number.isFinite(existingBaseline) || existingBaseline <= 0 ? currentEffective : Math.floor(existingBaseline);
6656
- a.ctx.meta["contextLimitBaseline"] = baseline;
6657
- const overflowLimit = a.ctx.meta["providerOverflowMaxContext"];
6658
- const overflowFloored = typeof overflowLimit === "number" && Number.isFinite(overflowLimit) && overflowLimit > 0 ? Math.floor(overflowLimit) : void 0;
6659
- const ceilings = [baseline, providerLimitFloored];
6660
- if (overflowFloored !== void 0) ceilings.push(overflowFloored);
6661
- const effective = Math.min(...ceilings.filter((limit) => limit > 0));
6662
- let effectiveSource;
6663
- if (overflowFloored !== void 0 && effective === overflowFloored) {
6664
- effectiveSource = "provider_overflow";
6665
- } else if (effective === providerLimitFloored) {
6666
- effectiveSource = "provider";
6667
- } else {
6668
- effectiveSource = "configured";
6669
- }
6670
- a.ctx.meta["effectiveMaxContext"] = effective;
6671
- a.ctx.meta["providerLimitEffectiveMaxContext"] = effective;
6672
- a.ctx.meta["effectiveMaxContextSource"] = effectiveSource;
6673
- emitEffectiveLimit(effective, effectiveSource);
6674
- }
6675
- async function compactContextIfNeeded() {
6676
- const msgCount = a.ctx.messages.length;
6677
- const maxContext = currentMaxContext();
6678
- const revision = a.ctx.state.revision;
6679
- const toolsRef = a.ctx.tools;
6680
- const systemRef = a.ctx.systemPrompt;
6681
- const requestTokens = a.ctx.lastRequestTokens;
6682
- if (_lastCompactionMsgCount === msgCount && _lastCompactionRevision === revision && _lastCompactionToolsRef === toolsRef && _lastCompactionSystemRef === systemRef && _lastCompactionRequestTokens === requestTokens && _lastCompactionWasNoop && _lastCompactionMaxContext === maxContext && maxContext > 0) {
6683
- return false;
6684
- }
6685
- const beforeMessages = a.ctx.messages;
6686
- const beforeMsgCount = a.ctx.messages.length;
6687
- const beforeRevision = a.ctx.state.revision;
6688
- await a.pipelines.contextWindow.run(a.ctx);
6689
- _lastCompactionMsgCount = a.ctx.messages.length;
6690
- _lastCompactionRevision = a.ctx.state.revision;
6691
- _lastCompactionToolsRef = a.ctx.tools;
6692
- _lastCompactionSystemRef = a.ctx.systemPrompt;
6693
- _lastCompactionMaxContext = maxContext;
6694
- const changed = a.ctx.state.revision !== beforeRevision || a.ctx.messages !== beforeMessages || a.ctx.messages.length !== beforeMsgCount;
6695
- const tokens = refreshContextRequestTokenStash({ force: changed });
6696
- _lastCompactionRequestTokens = tokens;
6697
- const load = maxContext > 0 ? tokens / maxContext : 0;
6698
- _lastCompactionWasNoop = tokens > 0 && load < 0.5;
6699
- if (changed) {
6700
- _lastEmittedMsgCount = -1;
6701
- _lastEmittedToolCount = -1;
6702
- _lastEmittedMaxContext = -1;
6703
- }
6704
- return changed;
6705
- }
6706
- const calibrationKey = (model = a.ctx.model) => `${a.ctx.provider?.id ?? "unknown"}/${model}`;
6707
- let _cachedSysRef = null;
6708
- let _cachedToolsRef = null;
6709
- let _cachedOverheadTokens = 0;
6710
- function systemAndToolsOverhead() {
6711
- const sysRef = a.ctx.systemPrompt;
6712
- const toolsRef = a.ctx.tools;
6713
- if (sysRef === _cachedSysRef && toolsRef === _cachedToolsRef && _cachedOverheadTokens > 0) {
6714
- return _cachedOverheadTokens;
6715
- }
6716
- const breakdown = estimateRequestTokens([], sysRef, toolsRef ?? [], calibrationKey());
6717
- _cachedSysRef = sysRef;
6718
- _cachedToolsRef = toolsRef;
6719
- _cachedOverheadTokens = breakdown.systemPrompt + breakdown.tools;
6720
- return _cachedOverheadTokens;
6721
- }
6722
- function stashRequestTokens(req) {
6723
- const preFlight = estimateRequestTokens(
6724
- req.messages,
6725
- req.system,
6726
- req.tools ?? [],
6727
- calibrationKey(req.model)
6728
- );
6729
- a.ctx.lastRequestTokens = preFlight.total;
6730
- _lastPreFlightMsgCount = req.messages.length;
6731
- _lastPreFlightToolCount = (req.tools ?? []).length;
6732
- _lastPreFlightRevision = a.ctx.state.revision;
6733
- _cachedSysRef = req.system;
6734
- _cachedToolsRef = req.tools ?? [];
6735
- _cachedOverheadTokens = preFlight.systemPrompt + preFlight.tools;
6736
- a.ctx.meta["lastRequestTokensAt"] = {
6737
- msgCount: req.messages.length,
6738
- toolCount: (req.tools ?? []).length,
6739
- revision: a.ctx.state.revision
6740
- };
6741
- return preFlight;
6742
- }
6743
- function refreshContextRequestTokenStash(opts = {}) {
6744
- const msgCount = a.ctx.messages.length;
6745
- const toolCount = (a.ctx.tools ?? []).length;
6746
- const revision = a.ctx.state.revision;
6747
- const stashed = a.ctx.lastRequestTokens;
6748
- const stashedAt = a.ctx.meta?.["lastRequestTokensAt"];
6749
- if (!opts.force && typeof stashed === "number" && stashed > 0 && typeof stashedAt === "object" && stashedAt !== null) {
6750
- const meta = stashedAt;
6751
- if (meta.msgCount === msgCount && meta.toolCount === toolCount && meta.revision === revision && _lastPreFlightRevision === revision) {
6752
- return stashed;
6753
- }
6754
- }
6755
- const refreshed = estimateMessageTokens(a.ctx.messages) + systemAndToolsOverhead();
6756
- a.ctx.lastRequestTokens = refreshed;
6757
- _lastPreFlightMsgCount = msgCount;
6758
- _lastPreFlightToolCount = toolCount;
6759
- _lastPreFlightRevision = revision;
6760
- a.ctx.meta["lastRequestTokensAt"] = { msgCount, toolCount, revision };
6761
- return refreshed;
6762
- }
6763
- async function buildRequestWithPreflightCompaction(opts) {
6764
- let prepared = await handlers.response.buildAndRunRequestPipeline(opts);
6765
- let req = prepared.request;
6766
- let preFlight = stashRequestTokens(req);
6767
- let probedRouteKey = `${prepared.provider.id}/${req.model}`;
6768
- await refreshProviderContextLimit(prepared.provider, req.model);
6769
- if (await compactContextIfNeeded()) {
6770
- prepared = await handlers.response.buildAndRunRequestPipeline(opts);
6771
- req = prepared.request;
6772
- preFlight = stashRequestTokens(req);
6773
- const rebuiltRouteKey = `${prepared.provider.id}/${req.model}`;
6774
- if (rebuiltRouteKey !== probedRouteKey) {
6775
- probedRouteKey = rebuiltRouteKey;
6776
- await refreshProviderContextLimit(prepared.provider, req.model);
6777
- if (await compactContextIfNeeded()) {
6778
- prepared = await handlers.response.buildAndRunRequestPipeline(opts);
6779
- req = prepared.request;
6780
- preFlight = stashRequestTokens(req);
6781
- }
6782
- }
6964
+ });
6965
+ attempt++;
6783
6966
  }
6784
- return { req, provider: prepared.provider, preFlight };
6785
6967
  }
6786
- function emitContextPct() {
6787
- const msgCount = a.ctx.messages.length;
6788
- const toolCount = (a.ctx.tools ?? []).length;
6789
- const maxContext = currentMaxContext();
6790
- const revision = a.ctx.state.revision;
6791
- if (msgCount === _lastEmittedMsgCount && toolCount === _lastEmittedToolCount && revision === _lastEmittedRevision && maxContext === _lastEmittedMaxContext && maxContext > 0) {
6792
- return;
6793
- }
6794
- _lastEmittedMsgCount = msgCount;
6795
- _lastEmittedToolCount = toolCount;
6796
- _lastEmittedRevision = revision;
6797
- _lastEmittedMaxContext = maxContext;
6798
- if (msgCount !== _lastPreFlightMsgCount || toolCount !== _lastPreFlightToolCount || revision !== _lastPreFlightRevision) {
6799
- refreshContextRequestTokenStash({ force: true });
6800
- }
6801
- let total;
6802
- const anchored = realAnchoredInputTokens(
6803
- a.ctx.messages,
6804
- a.ctx.lastRealInputTokens,
6805
- typeof a.ctx.meta?.["realAnchorMsgCount"] === "number" ? a.ctx.meta["realAnchorMsgCount"] : void 0
6806
- );
6807
- const stashed = a.ctx.lastRequestTokens;
6808
- if (anchored !== null) {
6809
- total = anchored;
6810
- } else if (typeof stashed === "number" && stashed > 0) {
6811
- const cal = getCalibrationState(calibrationKey());
6812
- total = cal.calibrated ? Math.round(stashed * Math.min(1.5, Math.max(0.5, cal.ratio))) : stashed;
6813
- } else {
6814
- const raw = refreshContextRequestTokenStash({ force: true });
6815
- const cal = getCalibrationState(calibrationKey());
6816
- total = cal.calibrated ? Math.round(raw * Math.min(1.5, Math.max(0.5, cal.ratio))) : raw;
6817
- }
6818
- const rawLoad = maxContext > 0 ? total / maxContext : 0;
6819
- const load = Math.max(0, Math.min(1, rawLoad));
6820
- a.events.emit("ctx.pct", {
6821
- sessionId: resolveEventSessionId(a.ctx),
6822
- load,
6823
- rawLoad,
6824
- tokens: total,
6825
- maxContext
6826
- });
6968
+ }
6969
+ function elapsedMs(startedNs) {
6970
+ return Number(process.hrtime.bigint() - startedNs) / 1e6;
6971
+ }
6972
+
6973
+ // src/core/queued-messages.ts
6974
+ var META_KEY3 = "_queuedMessagesAwareness";
6975
+ var MAX_ITEMS = 20;
6976
+ var MAX_PREVIEW_CHARS = 500;
6977
+ function read(ctx) {
6978
+ const raw = ctx.meta[META_KEY3];
6979
+ if (!raw || typeof raw !== "object") return void 0;
6980
+ const s = raw;
6981
+ return Array.isArray(s.items) && Array.isArray(s.seen) ? s : void 0;
6982
+ }
6983
+ function sameList(a, b) {
6984
+ return a.length === b.length && a.every((v, i) => v === b[i]);
6985
+ }
6986
+ function toPreview(text) {
6987
+ const trimmed = text.trim();
6988
+ if (trimmed.length <= MAX_PREVIEW_CHARS) return trimmed;
6989
+ return `${trimmed.slice(0, MAX_PREVIEW_CHARS - 1)}\u2026`;
6990
+ }
6991
+ function setQueuedMessagesSnapshot(ctx, texts) {
6992
+ const items = texts.map(toPreview).filter((t2) => t2.length > 0).slice(0, MAX_ITEMS);
6993
+ const prev = read(ctx);
6994
+ if (items.length === 0 && (!prev || prev.seen.length === 0)) {
6995
+ if (prev) delete ctx.meta[META_KEY3];
6996
+ return;
6827
6997
  }
6828
- function currentMaxContext() {
6829
- const metaLimit = a.ctx.meta?.["effectiveMaxContext"];
6830
- const providerMax = a.ctx.provider.capabilities.maxContext;
6831
- return typeof metaLimit === "number" && metaLimit > 0 ? metaLimit : typeof providerMax === "number" && providerMax > 0 ? providerMax : 2e5;
6998
+ ctx.meta[META_KEY3] = { items, seen: prev?.seen ?? [] };
6999
+ }
7000
+ function consumeQueuedMessagesUpdate(ctx) {
7001
+ const s = read(ctx);
7002
+ if (!s) return null;
7003
+ if (sameList(s.items, s.seen)) return null;
7004
+ if (s.items.length === 0) {
7005
+ delete ctx.meta[META_KEY3];
7006
+ return null;
6832
7007
  }
6833
- let _lastEmittedMsgCount = -1;
6834
- let _lastEmittedToolCount = -1;
6835
- let _lastEmittedRevision = -1;
6836
- let _lastEmittedMaxContext = -1;
6837
- let _lastPreFlightMsgCount = -1;
6838
- let _lastPreFlightToolCount = -1;
6839
- let _lastPreFlightRevision = -1;
6840
- let _lastCompactionMsgCount = -1;
6841
- let _lastCompactionRevision = -1;
6842
- let _lastCompactionToolsRef = null;
6843
- let _lastCompactionSystemRef = null;
6844
- let _lastCompactionRequestTokens;
6845
- let _lastCompactionMaxContext = -1;
6846
- let _lastCompactionWasNoop = false;
7008
+ ctx.meta[META_KEY3] = { items: s.items, seen: s.items };
7009
+ return s.items;
7010
+ }
7011
+ function buildQueuedMessagesBlock(items) {
7012
+ const body = items.map((t2, i) => `${i + 1}. ${t2}`).join("\n");
7013
+ return [
7014
+ "[QUEUED MESSAGES \u2014 the user typed these while you were working. They are",
7015
+ "waiting in the input queue and each will be delivered as its own turn",
7016
+ "after you finish, so do NOT answer or act on them as new tasks now. Read",
7017
+ "them as a heads-up: if one changes what you should be doing right now",
7018
+ "(new constraint, invalidated approach), adapt your current work; if one",
7019
+ "is just an FYI, fold it in. The queue may still change before delivery.",
7020
+ "This notice replaces any earlier one \u2014 the queue now holds exactly:",
7021
+ "",
7022
+ body,
7023
+ "]"
7024
+ ].join("\n");
7025
+ }
7026
+
7027
+ // src/core/request-provider-binding.ts
7028
+ var requestProviders = /* @__PURE__ */ new WeakMap();
7029
+ function bindRequestProvider(request, provider) {
7030
+ requestProviders.set(request, provider);
7031
+ }
7032
+ function providerBoundToRequest(request) {
7033
+ return requestProviders.get(request);
7034
+ }
7035
+
7036
+ // src/core/agent-loop.ts
7037
+ function toError(err) {
7038
+ return err instanceof Error ? err : new Error(String(err));
7039
+ }
7040
+ function signalAbortReason(signal) {
7041
+ const r = signal.reason;
7042
+ if (r instanceof Error) return r.message || r.name;
7043
+ if (typeof r === "string" && r.length > 0) return r;
7044
+ return "aborted";
7045
+ }
7046
+ function createAgentLoopHandler(a, handlers) {
7047
+ const checkMailbox = attachMailboxChecker(a);
7048
+ const fleetPulseCfg = (() => {
7049
+ try {
7050
+ return typeof a.container?.has === "function" && a.container.has(TOKENS.ConfigStore) ? a.container.resolve(TOKENS.ConfigStore).get().fleet?.pulse : void 0;
7051
+ } catch {
7052
+ return void 0;
7053
+ }
7054
+ })();
7055
+ const getFleetPulse = attachFleetPulse(a, fleetPulseCfg);
7056
+ const pulseEveryN = Math.max(1, fleetPulseCfg?.everyNIterations ?? 5);
7057
+ const backgroundCoordination = () => a.ctx.meta["coordinationContextMode"] === "background";
7058
+ const loopContext = createAgentLoopContextManager(a, handlers);
6847
7059
  function foldBlockIntoConversation(block) {
6848
7060
  if (!a.ctx.state.appendBlockToLastUserMessage(block)) {
6849
7061
  a.ctx.state.appendMessage({ role: "user", content: [block], origin: "runtime" });
6850
7062
  }
6851
7063
  }
6852
- function iterationFingerprint(blocks) {
6853
- const toolUses = blocks.filter(isToolUseBlock);
6854
- const texts = blocks.filter(isTextBlock);
6855
- const toolNameSet = Array.from(new Set(toolUses.map((u) => u.name))).sort();
6856
- const firstInputHash = toolUses[0] ? hashSmall(stableStringify(toolUses[0].input ?? {})) : "";
6857
- const textBlob = texts.map((t2) => t2.text).join("").slice(0, 512);
6858
- const hasContent = toolNameSet.length > 0 || textBlob.length > 0;
6859
- if (!hasContent) return "__empty__";
6860
- return [
6861
- `tools=${toolNameSet.join("+") || "-"}`,
6862
- `in0=${firstInputHash}`,
6863
- `txt=${textBlob}`
6864
- ].join("\n");
6865
- }
6866
- function stableStringify(value) {
6867
- return JSON.stringify(canonicalize(value));
6868
- }
6869
- function canonicalize(value) {
6870
- if (Array.isArray(value)) return value.map(canonicalize);
6871
- if (value && typeof value === "object") {
6872
- const src = value;
6873
- const out = {};
6874
- for (const k of Object.keys(src).sort()) out[k] = canonicalize(src[k]);
6875
- return out;
6876
- }
6877
- return value;
6878
- }
6879
- function hashSmall(s) {
6880
- let h = 2166136261;
6881
- for (let i = 0; i < s.length; i++) {
6882
- h ^= s.charCodeAt(i);
6883
- h = h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0;
6884
- }
6885
- return h.toString(36);
6886
- }
6887
7064
  function injectPendingBtwNotes(onMailboxBlock) {
6888
7065
  const notes = consumeBtwNotes(a.ctx);
6889
7066
  if (notes.length === 0) return;
@@ -6963,15 +7140,10 @@ function createAgentLoopHandler(a, handlers) {
6963
7140
  a.ctx.state.replaceMessages(cleaned.messages);
6964
7141
  a.ctx.lastRealInputTokens = void 0;
6965
7142
  delete a.ctx.meta["realAnchorMsgCount"];
6966
- refreshContextRequestTokenStash({ force: true });
7143
+ loopContext.refreshContextRequestTokenStash({ force: true });
6967
7144
  }
6968
7145
  }
6969
- const loopCfg = a.loopDetection;
6970
- let lastToolSignature = "";
6971
- let toolLoopCount = 0;
6972
- let iterationSteerDone = false;
6973
- const recentCallKeys = [];
6974
- const steeredCallKeys = /* @__PURE__ */ new Set();
7146
+ const loopDetector = new AgentLoopDetector(a);
6975
7147
  let pendingLoopSteer = null;
6976
7148
  let todoReconcileSteers = 0;
6977
7149
  function queueLoopSteer(text) {
@@ -7078,7 +7250,7 @@ ${text}` : text;
7078
7250
  req,
7079
7251
  provider: requestProvider,
7080
7252
  preFlight
7081
- } = await buildRequestWithPreflightCompaction(opts);
7253
+ } = await loopContext.buildRequestWithPreflightCompaction(opts);
7082
7254
  await a.ctx.session.append({
7083
7255
  type: "llm_request",
7084
7256
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -7091,7 +7263,7 @@ ${text}` : text;
7091
7263
  let res;
7092
7264
  try {
7093
7265
  res = await customRunner(a.ctx, req);
7094
- const key = calibrationKey(req.model);
7266
+ const key = loopContext.calibrationKey(req.model);
7095
7267
  const cal = getCalibrationState(key);
7096
7268
  const calibratedTotal = cal.calibrated ? Math.round(preFlight.total * Math.min(1.5, Math.max(0.5, cal.ratio))) : preFlight.total;
7097
7269
  const realInputTokens = effectiveInputTokens(res.usage);
@@ -7099,10 +7271,10 @@ ${text}` : text;
7099
7271
  const previousRealInput = a.ctx.lastRealInputTokens;
7100
7272
  const previousAnchorMsgCount = typeof a.ctx.meta?.["realAnchorMsgCount"] === "number" ? a.ctx.meta["realAnchorMsgCount"] : void 0;
7101
7273
  const anchorIsPlausible = realInputTokens >= calibratedTotal * 0.5;
7102
- const anchorAdvanced = previousRealInput === void 0 || realInputTokens > previousRealInput || previousAnchorMsgCount !== void 0 && _lastPreFlightMsgCount < previousAnchorMsgCount;
7274
+ const anchorAdvanced = previousRealInput === void 0 || realInputTokens > previousRealInput || previousAnchorMsgCount !== void 0 && loopContext.lastPreFlightMsgCount < previousAnchorMsgCount;
7103
7275
  if (realInputTokens > 0 && anchorIsPlausible && anchorAdvanced) {
7104
7276
  a.ctx.lastRealInputTokens = realInputTokens;
7105
- a.ctx.meta["realAnchorMsgCount"] = _lastPreFlightMsgCount;
7277
+ a.ctx.meta["realAnchorMsgCount"] = loopContext.lastPreFlightMsgCount;
7106
7278
  }
7107
7279
  recoveryRetries = 0;
7108
7280
  } catch (err) {
@@ -7192,7 +7364,9 @@ ${text}` : text;
7192
7364
  clearEvaluatedMailboxBlocks();
7193
7365
  const responseProvider = providerBoundToRequest(req) ?? requestProvider;
7194
7366
  const responseResult = await handlers.response.processResponse(res, req, responseProvider);
7195
- await refreshProviderContextLimit(responseProvider, req.model, { probe: false });
7367
+ await loopContext.refreshProviderContextLimit(responseProvider, req.model, {
7368
+ probe: false
7369
+ });
7196
7370
  if (responseResult.finalText) {
7197
7371
  a.ctx.meta["lastAgentOutput"] = responseResult.finalText;
7198
7372
  }
@@ -7215,103 +7389,18 @@ ${text}` : text;
7215
7389
  }
7216
7390
  finalText = responseResult.finalText;
7217
7391
  const toolUses = res.content.filter(isToolUseBlock);
7218
- if (loopCfg.mode !== "off") {
7219
- const sig = iterationFingerprint(res.content);
7220
- if (sig !== "__empty__") {
7221
- if (sig === lastToolSignature) {
7222
- toolLoopCount++;
7223
- } else {
7224
- lastToolSignature = sig;
7225
- toolLoopCount = 1;
7226
- iterationSteerDone = false;
7227
- }
7228
- const names = toolUses.map((t2) => t2.name).join(", ");
7229
- const hasText2 = res.content.some(isTextBlock);
7230
- const kind = toolUses.length > 0 && hasText2 ? "mixed" : toolUses.length > 0 ? "tool" : "message";
7231
- const observationRepeat = loopCfg.mode === "steer-then-cut" && toolUses.length > 0 && toolUses.every((use) => {
7232
- const tool = a.tools.get(use.name);
7233
- return tool?.mutating === false && (tool.riskTier === "safe" || (tool.capabilities?.length ?? 0) > 0);
7234
- });
7235
- const repeatMultiplier = observationRepeat ? 2 : 1;
7236
- const detail = kind === "tool" ? `"${names}" called with effectively identical inputs ${toolLoopCount} times in a row` : kind === "mixed" ? `"${names}" + same text repeated ${toolLoopCount} times in a row` : `same assistant text repeated ${toolLoopCount} times in a row`;
7237
- const cutAt = (loopCfg.mode === "cut" ? loopCfg.steerThreshold : loopCfg.cutThreshold) * repeatMultiplier;
7238
- if (toolLoopCount >= cutAt) {
7239
- a.logger.warn(`Loop detected: ${detail} \u2014 stopping to prevent infinite loop.`);
7240
- a.events.emit("tool.loop_detected", {
7241
- sessionId: resolveEventSessionId(a.ctx),
7242
- ctx: a.ctx,
7243
- tools: names,
7244
- repeatCount: toolLoopCount,
7245
- iteration: i,
7246
- kind,
7247
- action: "cut",
7248
- scope: "iteration"
7249
- });
7250
- const summary = kind === "message" ? `[Loop detected: same assistant message repeated ${toolLoopCount}\xD7 \u2014 stopping to prevent infinite repetition.]` : `[Loop detected: ${detail} \u2014 stopping to prevent infinite repetition.]`;
7251
- return {
7252
- status: "max_iterations",
7253
- iterations,
7254
- finalText: finalText || summary,
7255
- delegateSummaries
7256
- };
7257
- }
7258
- if (loopCfg.mode === "steer-then-cut" && toolLoopCount >= loopCfg.steerThreshold * repeatMultiplier && !iterationSteerDone) {
7259
- iterationSteerDone = true;
7260
- a.logger.warn(`Loop detected: ${detail} \u2014 steering the model to change approach.`);
7261
- a.events.emit("tool.loop_detected", {
7262
- sessionId: resolveEventSessionId(a.ctx),
7263
- ctx: a.ctx,
7264
- tools: names,
7265
- repeatCount: toolLoopCount,
7266
- iteration: i,
7267
- kind,
7268
- action: "steer",
7269
- scope: "iteration"
7270
- });
7271
- queueLoopSteer(
7272
- `[loop-detector] Your last ${toolLoopCount} responses were effectively identical (${detail}). This approach is not working. Change strategy: use a different tool, different arguments, or a different plan \u2014 or explain what is blocking you and stop. Repeating the same response ${cutAt - toolLoopCount} more time(s) will terminate the turn.`
7273
- );
7274
- }
7275
- } else {
7276
- lastToolSignature = "";
7277
- toolLoopCount = 0;
7278
- iterationSteerDone = false;
7279
- }
7280
- if (loopCfg.mode === "steer-then-cut") {
7281
- for (const u of toolUses) {
7282
- const key = `${u.name}:${hashSmall(stableStringify(u.input ?? {}))}`;
7283
- recentCallKeys.push(key);
7284
- if (recentCallKeys.length > loopCfg.windowSize) recentCallKeys.shift();
7285
- if (steeredCallKeys.has(key)) continue;
7286
- let count = 0;
7287
- for (const k of recentCallKeys) if (k === key) count++;
7288
- const tool = a.tools.get(u.name);
7289
- const observationThreshold = tool?.mutating === false && (tool.riskTier === "safe" || (tool.capabilities?.length ?? 0) > 0) ? loopCfg.callRepeatThreshold * 2 : loopCfg.callRepeatThreshold;
7290
- if (count < observationThreshold) continue;
7291
- steeredCallKeys.add(key);
7292
- const preview2 = JSON.stringify(u.input ?? {}).slice(0, 160);
7293
- a.logger.warn(
7294
- `Loop detected: "${u.name}" called with identical arguments ${count}\xD7 within the last ${loopCfg.windowSize} tool calls \u2014 steering the model to change approach.`
7295
- );
7296
- a.events.emit("tool.loop_detected", {
7297
- sessionId: resolveEventSessionId(a.ctx),
7298
- ctx: a.ctx,
7299
- tools: u.name,
7300
- repeatCount: count,
7301
- iteration: i,
7302
- kind: "tool",
7303
- action: "steer",
7304
- scope: "call"
7305
- });
7306
- queueLoopSteer(
7307
- `[loop-detector] You have called ${u.name}(${preview2}) ${count} times with identical arguments within the last ${loopCfg.windowSize} tool calls. The result will not change. Do not repeat this call \u2014 use what you already know, try a different approach, or explain the blocker.`
7308
- );
7309
- }
7310
- }
7392
+ const loopCheck = loopDetector.checkIteration(i, res.content, toolUses, queueLoopSteer);
7393
+ if (loopCheck.cut) {
7394
+ return {
7395
+ status: "max_iterations",
7396
+ iterations,
7397
+ finalText: finalText || loopCheck.cutSummary || "",
7398
+ delegateSummaries
7399
+ };
7311
7400
  }
7312
7401
  if (toolUses.length === 0) {
7313
- await compactContextIfNeeded();
7314
- emitContextPct();
7402
+ await loopContext.compactContextIfNeeded();
7403
+ loopContext.emitContextPct();
7315
7404
  a.events.emit("iteration.completed", {
7316
7405
  sessionId: resolveEventSessionId(a.ctx),
7317
7406
  ctx: a.ctx,
@@ -7349,8 +7438,8 @@ ${text}` : text;
7349
7438
  throw toolErr;
7350
7439
  }
7351
7440
  if (autonomousContinue && consumeAutonomousContinue(a.ctx)) {
7352
- await compactContextIfNeeded();
7353
- emitContextPct();
7441
+ await loopContext.compactContextIfNeeded();
7442
+ loopContext.emitContextPct();
7354
7443
  a.events.emit("iteration.completed", {
7355
7444
  sessionId: resolveEventSessionId(a.ctx),
7356
7445
  ctx: a.ctx,
@@ -7359,8 +7448,8 @@ ${text}` : text;
7359
7448
  await a.extensions.runAfterIteration(a.ctx, i);
7360
7449
  continue;
7361
7450
  }
7362
- await compactContextIfNeeded();
7363
- emitContextPct();
7451
+ await loopContext.compactContextIfNeeded();
7452
+ loopContext.emitContextPct();
7364
7453
  a.events.emit("iteration.completed", {
7365
7454
  sessionId: resolveEventSessionId(a.ctx),
7366
7455
  ctx: a.ctx,
@@ -7559,6 +7648,9 @@ var RUNTIME_CAPABILITY_MANIFEST = [
7559
7648
  "codebase-incoming-calls",
7560
7649
  "codebase-outgoing-calls",
7561
7650
  "codebase-index",
7651
+ "codebase-skeleton",
7652
+ "codebase-repo-map",
7653
+ "codebase-impact-analysis",
7562
7654
  "dead-code-scan",
7563
7655
  "diff",
7564
7656
  "json",
@@ -7569,7 +7661,7 @@ var RUNTIME_CAPABILITY_MANIFEST = [
7569
7661
  id: "filesystem.write",
7570
7662
  pack: "development",
7571
7663
  exposure: "direct",
7572
- tools: ["write", "edit", "replace", "patch"]
7664
+ tools: ["write", "edit", "replace", "patch", "codebase-ast-replace"]
7573
7665
  },
7574
7666
  {
7575
7667
  id: "execution.shell",
@@ -7581,7 +7673,7 @@ var RUNTIME_CAPABILITY_MANIFEST = [
7581
7673
  id: "verification.run",
7582
7674
  pack: "development",
7583
7675
  exposure: "direct",
7584
- tools: ["lint", "format", "typecheck", "test", "e2e_plan"]
7676
+ tools: ["lint", "format", "typecheck", "test", "e2e_plan", "codebase-targeted-test"]
7585
7677
  },
7586
7678
  {
7587
7679
  id: "version-control.manage",
@@ -7718,7 +7810,13 @@ var RUNTIME_CAPABILITY_MANIFEST = [
7718
7810
  id: "quality.analyze",
7719
7811
  pack: "development",
7720
7812
  exposure: "on-demand",
7721
- tools: ["dead_code_scan", "detect_duplicate_code", "secret_scanner_test", "error_lens_history"]
7813
+ tools: [
7814
+ "dead_code_scan",
7815
+ "detect_duplicate_code",
7816
+ "secret_scanner_test",
7817
+ "error_lens_history",
7818
+ "security-ast-scan"
7819
+ ]
7722
7820
  },
7723
7821
  {
7724
7822
  id: "release.manage",
@@ -7736,7 +7834,7 @@ var RUNTIME_CAPABILITY_MANIFEST = [
7736
7834
  id: "tools.discover",
7737
7835
  pack: "admin",
7738
7836
  exposure: "direct",
7739
- tools: ["tool_search", "tool_use", "tool_help", "batch_tool_use"]
7837
+ tools: ["tool_search", "tool_use", "tool_help", "batch_tool_use", "clarify"]
7740
7838
  },
7741
7839
  {
7742
7840
  id: "runtime.admin",
@@ -8119,26 +8217,41 @@ function stripNextStepsFromMessage(msg) {
8119
8217
  strippedNextStepsCache.set(msg, clone);
8120
8218
  return clone;
8121
8219
  }
8122
- function composeRequestMessages(history, tail) {
8220
+ function markCacheBoundary(msg) {
8221
+ if (typeof msg.content === "string") return void 0;
8222
+ const blocks = msg.content.slice();
8223
+ const boundary = blocks[blocks.length - 1];
8224
+ if (!boundary || boundary.type !== "text" && boundary.type !== "tool_result") return void 0;
8225
+ blocks[blocks.length - 1] = { ...boundary, cache_control: { type: "ephemeral" } };
8226
+ return { ...msg, content: blocks };
8227
+ }
8228
+ function composeRequestMessages(history, tail, previous) {
8123
8229
  if (history.length === 0) return null;
8124
8230
  const out = history.slice();
8125
8231
  const lastIdx = out.length - 1;
8126
8232
  const last = out[lastIdx];
8233
+ if (previous && previous.index < lastIdx && history[previous.index] === previous.message) {
8234
+ const marked2 = markCacheBoundary(previous.message);
8235
+ if (marked2) out[previous.index] = marked2;
8236
+ }
8127
8237
  const blocks = typeof last.content === "string" ? [{ type: "text", text: last.content }] : last.content.slice();
8128
- const boundary = blocks[blocks.length - 1];
8129
- if (boundary && (boundary.type === "text" || boundary.type === "tool_result")) {
8130
- blocks[blocks.length - 1] = { ...boundary, cache_control: { type: "ephemeral" } };
8238
+ const tailBlock = blocks[blocks.length - 1];
8239
+ const marked = tailBlock && (tailBlock.type === "text" || tailBlock.type === "tool_result");
8240
+ if (marked) {
8241
+ blocks[blocks.length - 1] = { ...tailBlock, cache_control: { type: "ephemeral" } };
8131
8242
  }
8243
+ const boundary = marked ? { message: last, index: lastIdx } : void 0;
8132
8244
  if (tail.length === 0 || last.role !== "user") {
8133
8245
  out[lastIdx] = { ...last, content: blocks };
8134
8246
  if (tail.length > 0) out.push({ role: "user", content: [LIVE_CONTEXT_HEADER, ...tail] });
8135
- return out;
8247
+ return { messages: out, boundary };
8136
8248
  }
8137
8249
  out[lastIdx] = { ...last, content: [...blocks, LIVE_CONTEXT_HEADER, ...tail] };
8138
- return out;
8250
+ return { messages: out, boundary };
8139
8251
  }
8140
8252
  function createAgentResponseHandler(a) {
8141
8253
  const stabilizedPromptEpochs = /* @__PURE__ */ new WeakSet();
8254
+ let previousBoundary;
8142
8255
  function stabilizePromptEpoch() {
8143
8256
  const prompt = a.ctx.systemPrompt;
8144
8257
  if (stabilizedPromptEpochs.has(prompt)) return;
@@ -8161,7 +8274,7 @@ function createAgentResponseHandler(a) {
8161
8274
  ...repaired.report
8162
8275
  });
8163
8276
  a.logger.warn(
8164
- `Repaired context tool adjacency: removed ${repaired.report.removedToolUses.length} tool_use block(s), ${repaired.report.removedToolResults.length} tool_result block(s), ${repaired.report.removedMessages} empty message(s)`
8277
+ `Repaired context tool adjacency: removed ${repaired.report.removedToolUses.length} tool_use block(s), ${repaired.report.removedToolResults.length} tool_result block(s), ${repaired.report.removedMessages} empty message(s)` + formatAdjacencyRepairIds(repaired.report.removedToolUses, repaired.report.removedToolResults)
8165
8278
  );
8166
8279
  }
8167
8280
  }
@@ -8179,7 +8292,9 @@ function createAgentResponseHandler(a) {
8179
8292
  ...memoryEvidence
8180
8293
  ].filter((block) => block !== void 0);
8181
8294
  const requestHistory = stripDeliveredNextSteps(a.ctx.messages);
8182
- const composedMessages = composeRequestMessages(requestHistory, liveContextTail);
8295
+ const composed = composeRequestMessages(requestHistory, liveContextTail, previousBoundary);
8296
+ if (composed) previousBoundary = composed.boundary;
8297
+ const composedMessages = composed?.messages ?? null;
8183
8298
  const system = composedMessages ? stableSystem : liveContextTail.length > 0 ? [...stableSystem, ...liveContextTail] : stableSystem;
8184
8299
  await a.ctx.waitForModelTransition();
8185
8300
  const provider = a.ctx.provider;
@@ -8279,6 +8394,10 @@ function createAgentResponseHandler(a) {
8279
8394
  }
8280
8395
  return { buildAndRunRequestPipeline, processResponse };
8281
8396
  }
8397
+ function formatAdjacencyRepairIds(toolUses, toolResults) {
8398
+ const ids = [.../* @__PURE__ */ new Set([...toolUses, ...toolResults])].slice(0, 8);
8399
+ return ids.length > 0 ? `; affected tool ids: ${ids.join(", ")}${toolUses.length + toolResults.length > ids.length ? ", \u2026" : ""}` : "";
8400
+ }
8282
8401
 
8283
8402
  // src/utils/sage-output-block.ts
8284
8403
  var SAGE_INJECTOR_HEADINGS = /* @__PURE__ */ new Set([
@@ -8850,7 +8969,7 @@ var Agent = class {
8850
8969
  this.maxIterations = init.maxIterations ?? DEFAULT_MAX_ITERATIONS;
8851
8970
  this.executionStrategy = init.executionStrategy ?? "smart";
8852
8971
  this.perIterationOutputCapBytes = init.perIterationOutputCapBytes ?? 1e5;
8853
- this.autoExtendLimit = init.autoExtendLimit ?? true;
8972
+ this.autoExtendLimit = init.autoExtendLimit ?? false;
8854
8973
  this.loopDetection = resolveLoopDetection(init.loopDetection);
8855
8974
  this.autonomousContinue = init.autonomousContinue ?? false;
8856
8975
  this.refreshSystemPrompt = init.refreshSystemPrompt ?? false;
@@ -9217,9 +9336,6 @@ function resolveContinuation(input) {
9217
9336
  return { source: "open", text, label: "\u25B6 Continue \u2192 (no pending task \u2014 choosing next step)" };
9218
9337
  }
9219
9338
 
9220
- // src/core/fallback-model.ts
9221
- import { randomUUID as randomUUID6 } from "node:crypto";
9222
-
9223
9339
  // src/core/model-availability-calendar.ts
9224
9340
  function logicalCalendarTarget(providerId, model) {
9225
9341
  if (providerId !== "omniroute") return { providerId, model };
@@ -9326,6 +9442,12 @@ function normalizeModelRef(ref, defaultProvider) {
9326
9442
  function hasText(value) {
9327
9443
  return typeof value === "string" && value.trim().length > 0;
9328
9444
  }
9445
+ function asRefList(value) {
9446
+ return Array.isArray(value) ? value : void 0;
9447
+ }
9448
+ function asProfileName(value) {
9449
+ return hasText(value) ? value : void 0;
9450
+ }
9329
9451
  function providerHasKey(entry) {
9330
9452
  if (!entry) return false;
9331
9453
  if (hasText(entry.apiKey)) return true;
@@ -9336,7 +9458,7 @@ function providerHasKey(entry) {
9336
9458
  }
9337
9459
  function visibleProviderModels(config, providerId, providerModels) {
9338
9460
  const entry = config.providers?.[providerId];
9339
- return entry?.models !== void 0 ? [...entry.models] : providerModels;
9461
+ return Array.isArray(entry?.models) ? [...entry.models] : providerModels;
9340
9462
  }
9341
9463
  function buildProfiles(config) {
9342
9464
  const entries = /* @__PURE__ */ new Map();
@@ -9373,13 +9495,34 @@ var FallbackProfileManager = class {
9373
9495
  listProfiles() {
9374
9496
  return Object.freeze([...this.profiles.keys()]);
9375
9497
  }
9498
+ /**
9499
+ * The profile the session has selected (`config.fallbackProfile`, set by
9500
+ * `/fallback profile use <name>`), or undefined when none is selected or the
9501
+ * name no longer resolves to a defined profile.
9502
+ *
9503
+ * Consulted by every resolution entry point when the caller does not name a
9504
+ * profile itself. Without this the leader — which passes no profile — could
9505
+ * never use a named profile at all: `config.fallbackProfiles` was reachable
9506
+ * only by copying a chain into `fallbackModels`.
9507
+ */
9508
+ activeProfileName() {
9509
+ const name = asProfileName(this.config.fallbackProfile);
9510
+ return name && this.profiles.has(name) ? name : void 0;
9511
+ }
9376
9512
  // ── Resolution ─────────────────────────────────────────────────────────
9377
9513
  /**
9378
9514
  * Resolve a named fallback profile to a validated, provider-filtered chain.
9379
9515
  *
9380
- * Returns an empty chain when:
9381
- * - The profile doesn't exist.
9382
- * - Every entry's provider is missing, has no key, or has no matching model.
9516
+ * Returns an empty chain when the profile doesn't exist, or when every entry
9517
+ * is excluded, quarantined, or blacked out.
9518
+ *
9519
+ * Filtering is intentionally identical to {@link resolveRefs} (the explicit
9520
+ * `fallbackModels` path): the self-exclusion, the runtime status tracker,
9521
+ * and the availability calendar — nothing else. Anything a named profile
9522
+ * drops, an explicit chain drops too, and vice versa. Profiles used to apply
9523
+ * two extra filters (provider "usability" and the `providers[].models`
9524
+ * snapshot) that the explicit path did not, which silently rerouted roles
9525
+ * pinned to a profile onto a different model than the one configured.
9383
9526
  *
9384
9527
  * @param name - Profile name from config.fallbackProfiles.
9385
9528
  * @param defaultProvider - Used when an entry has no explicit provider.
@@ -9400,13 +9543,9 @@ var FallbackProfileManager = class {
9400
9543
  if (seen.has(key)) continue;
9401
9544
  seen.add(key);
9402
9545
  if (excludeKey && key === excludeKey) continue;
9403
- const health = this.checkProvider(providerId);
9404
- if (!health.usable) continue;
9405
9546
  if (this.statusTracker && !this.statusTracker.isAvailable(providerId, parsed.model)) continue;
9406
9547
  if (!evaluateModelCalendar(this.config.modelAvailabilitySchedule, providerId, parsed.model).allowed)
9407
9548
  continue;
9408
- const allowedModels = this.config.providers?.[providerId]?.models;
9409
- if (allowedModels && !allowedModels.includes(parsed.model)) continue;
9410
9549
  resolved.push({
9411
9550
  providerId,
9412
9551
  model: parsed.model,
@@ -9424,12 +9563,14 @@ var FallbackProfileManager = class {
9424
9563
  resolveEffective(opts = {}) {
9425
9564
  const bridge = this.resolveBridge(opts.exclude);
9426
9565
  let selected = FREEZER_EMPTY;
9427
- if (opts.fallbackModels && opts.fallbackModels.length > 0) {
9428
- const resolved = this.resolveRefs(opts.fallbackModels, opts.exclude);
9566
+ const explicitRefs = asRefList(opts.fallbackModels);
9567
+ const profileName = asProfileName(opts.fallbackProfile) ?? this.activeProfileName();
9568
+ if (explicitRefs && explicitRefs.length > 0) {
9569
+ const resolved = this.resolveRefs(explicitRefs, opts.exclude);
9429
9570
  if (resolved.length > 0) selected = resolved;
9430
9571
  }
9431
- if (selected.length === 0 && opts.fallbackProfile) {
9432
- const resolved = this.resolve(opts.fallbackProfile, { exclude: opts.exclude });
9572
+ if (selected.length === 0 && profileName) {
9573
+ const resolved = this.resolve(profileName, { exclude: opts.exclude });
9433
9574
  if (resolved.length > 0) selected = resolved;
9434
9575
  }
9435
9576
  if (selected.length === 0 && opts.fallbackAuto !== false) {
@@ -9505,13 +9646,14 @@ var FallbackProfileManager = class {
9505
9646
  };
9506
9647
  const configFallbackAuto = this.config.fallbackAuto;
9507
9648
  const effectiveFallbackAuto = configFallbackAuto !== void 0 && configFallbackAuto !== null ? configFallbackAuto : !opts.closedWorld;
9508
- const explicitRefs = opts.fallbackModels ?? this.config.fallbackModels;
9649
+ const explicitRefs = asRefList(opts.fallbackModels) ?? asRefList(this.config.fallbackModels);
9650
+ const profileName = asProfileName(opts.fallbackProfile) ?? this.activeProfileName();
9509
9651
  const explicitUsable = explicitRefs !== void 0 && explicitRefs.length > 0 && this.resolveRefs(explicitRefs, current).length > 0;
9510
- const profileUsable = opts.fallbackProfile !== void 0 && this.hasProfile(opts.fallbackProfile) && this.resolve(opts.fallbackProfile, { exclude: current }).length > 0;
9652
+ const profileUsable = profileName !== void 0 && this.hasProfile(profileName) && this.resolve(profileName, { exclude: current }).length > 0;
9511
9653
  const fromExplicitSource = explicitUsable || profileUsable;
9512
- const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) : opts.fallbackProfile ? this.resolve(opts.fallbackProfile, { exclude: current }) : FREEZER_EMPTY : this.resolveEffective({
9654
+ const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) : profileName ? this.resolve(profileName, { exclude: current }) : FREEZER_EMPTY : this.resolveEffective({
9513
9655
  fallbackModels: explicitRefs,
9514
- fallbackProfile: opts.fallbackProfile,
9656
+ fallbackProfile: profileName,
9515
9657
  fallbackAuto: effectiveFallbackAuto,
9516
9658
  exclude: current
9517
9659
  });
@@ -9528,7 +9670,7 @@ var FallbackProfileManager = class {
9528
9670
  });
9529
9671
  }
9530
9672
  candidates.push(...selectedChain);
9531
- if (!fromExplicitSource && effectiveFallbackAuto && opts.fallbackProfile !== "default") {
9673
+ if (!fromExplicitSource && effectiveFallbackAuto && profileName !== "default") {
9532
9674
  candidates.push(...this.resolve("default", { exclude: current }));
9533
9675
  }
9534
9676
  if (!fromExplicitSource && effectiveFallbackAuto) {
@@ -9606,7 +9748,7 @@ var FallbackProfileManager = class {
9606
9748
  const leaderModel = this.config.model;
9607
9749
  const providers = this.config.providers ?? {};
9608
9750
  const favoriteSet = new Set(
9609
- (this.config.favoriteModels ?? []).map((ref) => {
9751
+ (asRefList(this.config.favoriteModels) ?? []).map((ref) => {
9610
9752
  const p = parseModelRef(ref);
9611
9753
  return `${p.provider ?? leaderProvider}/${p.model}`;
9612
9754
  })
@@ -9681,7 +9823,206 @@ function dedupeChain(entries, current) {
9681
9823
  );
9682
9824
  }
9683
9825
 
9826
+ // src/core/fallback-doctor.ts
9827
+ function diagnoseFallbackConfig(config, tracker, opts) {
9828
+ const mgr = new FallbackProfileManager(config, { statusTracker: tracker });
9829
+ const primary = { providerId: config.provider, model: config.model };
9830
+ const activeProfile = mgr.activeProfileName();
9831
+ const explicitChain = Object.freeze([...config.fallbackModels ?? []]);
9832
+ const effectiveCandidates = mgr.resolveCandidates(primary, {
9833
+ fallbackProfile: activeProfile
9834
+ });
9835
+ const effectiveOrder = Object.freeze(
9836
+ effectiveCandidates.map((c) => `${c.providerId}/${c.model}`)
9837
+ );
9838
+ const autoEnabled = config.fallbackAuto !== false;
9839
+ const warnings = [];
9840
+ const providersInChain = /* @__PURE__ */ new Set();
9841
+ for (const entry of effectiveCandidates) {
9842
+ providersInChain.add(entry.providerId);
9843
+ }
9844
+ if (effectiveCandidates.length === 0) {
9845
+ warnings.push({
9846
+ code: "EMPTY_CHAIN",
9847
+ severity: "critical",
9848
+ message: "The effective fallback chain is empty. Any rate limit or outage on the primary model will immediately fail without recovery.",
9849
+ recommendation: "Add fallback models using /fallback add <provider/model> or enable smart defaults with /fallback auto on."
9850
+ });
9851
+ }
9852
+ if (effectiveCandidates.length > 0) {
9853
+ const onlyPrimaryProvider = providersInChain.size === 1 && providersInChain.has(primary.providerId);
9854
+ if (onlyPrimaryProvider) {
9855
+ warnings.push({
9856
+ code: "SIBLING_QUARANTINE_RISK",
9857
+ severity: "warning",
9858
+ message: `All fallback models belong to the same provider ("${primary.providerId}"). An account-level quota or token exhaustion will quarantine all sibling models at once.`,
9859
+ recommendation: "Add at least one cross-provider model (e.g. OpenAI, Anthropic, or Google) to your fallback profile."
9860
+ });
9861
+ }
9862
+ }
9863
+ for (const entry of effectiveCandidates) {
9864
+ const health = mgr.checkProvider(entry.providerId);
9865
+ if (!health.usable) {
9866
+ warnings.push({
9867
+ code: "PROVIDER_UNUSABLE",
9868
+ severity: "critical",
9869
+ target: `${entry.providerId}/${entry.model}`,
9870
+ message: `Provider "${entry.providerId}" is missing an API key or baseUrl in configuration.`,
9871
+ recommendation: `Configure API credentials for "${entry.providerId}" or remove it from the fallback chain.`
9872
+ });
9873
+ }
9874
+ if (tracker && !tracker.isAvailable(entry.providerId, entry.model)) {
9875
+ const status2 = tracker.getStatus(entry.providerId, entry.model);
9876
+ const remainingMs = status2?.stateExpiresAt ? Math.max(0, status2.stateExpiresAt - Date.now()) : 0;
9877
+ const remainingSec = Math.round(remainingMs / 1e3);
9878
+ warnings.push({
9879
+ code: "MODEL_QUARANTINED",
9880
+ severity: "warning",
9881
+ target: `${entry.providerId}/${entry.model}`,
9882
+ message: `Model is currently in quarantine (waiting room) due to ${status2?.lastErrorKind ?? "failures"}.${remainingSec > 0 ? ` Resets in ${remainingSec}s.` : ""}`,
9883
+ recommendation: "Model will be automatically re-admitted once cooldown/reset expires."
9884
+ });
9885
+ }
9886
+ const cal = evaluateModelCalendar(
9887
+ config.modelAvailabilitySchedule,
9888
+ entry.providerId,
9889
+ entry.model
9890
+ );
9891
+ if (!cal.allowed) {
9892
+ warnings.push({
9893
+ code: "CALENDAR_BLOCKED",
9894
+ severity: "warning",
9895
+ target: `${entry.providerId}/${entry.model}`,
9896
+ message: `Model is blocked by the model availability calendar (${cal.rule?.label ?? "schedule restriction"}).`
9897
+ });
9898
+ }
9899
+ if (opts?.modelContextLimitMap) {
9900
+ const primaryLimit = opts.modelContextLimitMap[`${primary.providerId}/${primary.model}`] ?? opts.modelContextLimitMap[primary.model];
9901
+ const entryLimit = opts.modelContextLimitMap[`${entry.providerId}/${entry.model}`] ?? opts.modelContextLimitMap[entry.model];
9902
+ if (typeof primaryLimit === "number" && typeof entryLimit === "number" && entryLimit > 0 && primaryLimit > 0 && entryLimit < primaryLimit) {
9903
+ warnings.push({
9904
+ code: "CONTEXT_WINDOW_WARNING",
9905
+ severity: "warning",
9906
+ target: `${entry.providerId}/${entry.model}`,
9907
+ message: `Context window downgrade: primary model has ${primaryLimit.toLocaleString()} tokens capacity, but fallback candidate has only ${entryLimit.toLocaleString()} tokens. Long sessions will fail failover or require aggressive compaction.`,
9908
+ recommendation: `Ensure fallback models have equal or greater context capacity (>= ${primaryLimit.toLocaleString()} tokens) or keep sessions below ${entryLimit.toLocaleString()} tokens.`
9909
+ });
9910
+ }
9911
+ }
9912
+ }
9913
+ const uniqueProviders = Object.freeze([...providersInChain]);
9914
+ const crossProviderCount = uniqueProviders.filter((p) => p !== primary.providerId).length;
9915
+ const hasCritical = warnings.some((w) => w.severity === "critical");
9916
+ const hasWarning = warnings.some((w) => w.severity === "warning");
9917
+ const status = hasCritical ? "critical" : hasWarning ? "warning" : "healthy";
9918
+ return Object.freeze({
9919
+ primary: Object.freeze(primary),
9920
+ activeProfile,
9921
+ explicitChain,
9922
+ effectiveOrder,
9923
+ autoEnabled,
9924
+ status,
9925
+ warnings: Object.freeze(warnings),
9926
+ crossProviderCount,
9927
+ uniqueProviders
9928
+ });
9929
+ }
9930
+ function simulateFallbackFailover(config, opts = {}) {
9931
+ const errorKind = opts.errorKind ?? "rate_limit";
9932
+ const isEligible = isFallbackWorthy(errorKind);
9933
+ const primary = { providerId: config.provider, model: config.model };
9934
+ if (!isEligible) {
9935
+ return Object.freeze({
9936
+ triggeringError: errorKind,
9937
+ isFallbackEligible: false,
9938
+ primary: Object.freeze(primary),
9939
+ steps: Object.freeze([]),
9940
+ finalTarget: null,
9941
+ summary: `Error kind "${errorKind}" is not fallback-worthy (request-shaped error, e.g. context_overflow/auth). The agent loop will not attempt fallback.`
9942
+ });
9943
+ }
9944
+ const mgr = new FallbackProfileManager(config, { statusTracker: opts.statusTracker });
9945
+ const candidates = mgr.resolveCandidates(primary);
9946
+ const steps = [];
9947
+ let finalTarget = null;
9948
+ for (let i = 0; i < candidates.length; i++) {
9949
+ const entry = candidates[i];
9950
+ const key = `${entry.providerId}/${entry.model}`;
9951
+ const providerSwitched = entry.providerId !== primary.providerId;
9952
+ const cal = evaluateModelCalendar(
9953
+ config.modelAvailabilitySchedule,
9954
+ entry.providerId,
9955
+ entry.model
9956
+ );
9957
+ if (!cal.allowed) {
9958
+ steps.push({
9959
+ index: i + 1,
9960
+ providerId: entry.providerId,
9961
+ model: entry.model,
9962
+ skipped: true,
9963
+ reason: `Blocked by model availability calendar (${cal.rule?.label ?? "schedule"})`,
9964
+ providerSwitched
9965
+ });
9966
+ continue;
9967
+ }
9968
+ if (opts.statusTracker && !opts.statusTracker.isAvailable(entry.providerId, entry.model)) {
9969
+ steps.push({
9970
+ index: i + 1,
9971
+ providerId: entry.providerId,
9972
+ model: entry.model,
9973
+ skipped: true,
9974
+ reason: "Quarantined in status tracker waiting room",
9975
+ providerSwitched
9976
+ });
9977
+ continue;
9978
+ }
9979
+ const contextLimit = opts.modelContextLimitMap?.[key];
9980
+ if (typeof opts.currentTokens === "number" && opts.currentTokens > 0 && typeof contextLimit === "number" && contextLimit > 0 && opts.currentTokens > contextLimit) {
9981
+ steps.push({
9982
+ index: i + 1,
9983
+ providerId: entry.providerId,
9984
+ model: entry.model,
9985
+ skipped: true,
9986
+ reason: `Context pre-filter: request tokens (${opts.currentTokens}) exceeds model window (${contextLimit})`,
9987
+ providerSwitched
9988
+ });
9989
+ continue;
9990
+ }
9991
+ const health = mgr.checkProvider(entry.providerId);
9992
+ if (!health.usable) {
9993
+ steps.push({
9994
+ index: i + 1,
9995
+ providerId: entry.providerId,
9996
+ model: entry.model,
9997
+ skipped: true,
9998
+ reason: `Cannot construct provider "${entry.providerId}" (missing API key/endpoint)`,
9999
+ providerSwitched
10000
+ });
10001
+ continue;
10002
+ }
10003
+ steps.push({
10004
+ index: i + 1,
10005
+ providerId: entry.providerId,
10006
+ model: entry.model,
10007
+ skipped: false,
10008
+ providerSwitched
10009
+ });
10010
+ finalTarget = { providerId: entry.providerId, model: entry.model };
10011
+ break;
10012
+ }
10013
+ const summary = finalTarget ? `Failover succeeded: will rotate from ${primary.providerId}/${primary.model} to ${finalTarget.providerId}/${finalTarget.model}.` : "Failover failed: no usable fallback candidate could be reached.";
10014
+ return Object.freeze({
10015
+ triggeringError: errorKind,
10016
+ isFallbackEligible: true,
10017
+ primary: Object.freeze(primary),
10018
+ steps: Object.freeze(steps),
10019
+ finalTarget: finalTarget ? Object.freeze(finalTarget) : null,
10020
+ summary
10021
+ });
10022
+ }
10023
+
9684
10024
  // src/core/fallback-model.ts
10025
+ import { randomUUID as randomUUID6 } from "node:crypto";
9685
10026
  function fallbackProfileChain(config, profileName) {
9686
10027
  if (!profileName) return [];
9687
10028
  const mgr = new FallbackProfileManager(config);
@@ -9717,9 +10058,15 @@ function effectiveFallbackChain(config) {
9717
10058
  const mgr = new FallbackProfileManager(config);
9718
10059
  return mgr.resolveEffective({
9719
10060
  fallbackModels: config.fallbackModels,
10061
+ fallbackProfile: config.fallbackProfile,
9720
10062
  fallbackAuto: config.fallbackAuto
9721
10063
  }).map((e) => `${e.providerId}/${e.model}`);
9722
10064
  }
10065
+ function runtimeFallbackChain(config) {
10066
+ const mgr = new FallbackProfileManager(config);
10067
+ const current = primaryTarget(config);
10068
+ return mgr.resolveCandidates(current, {}).map((e) => `${e.providerId}/${e.model}`);
10069
+ }
9723
10070
  var DEFAULT_PRIMARY_COOLDOWN_MS = 6e4;
9724
10071
  var DEFAULT_PRIMARY_COOLDOWN_MAX_MS = 10 * 6e4;
9725
10072
  var DEFAULT_PRIMARY_RECOVERY_SUCCESSES = 2;
@@ -9759,7 +10106,11 @@ function createFallbackModelExtension(deps) {
9759
10106
  let blockedPrimary;
9760
10107
  let primaryBlockedUntil = 0;
9761
10108
  const now = () => deps.now?.() ?? Date.now();
9762
- const cooldownBase = () => Math.max(0, deps.primaryCooldownMs ?? DEFAULT_PRIMARY_COOLDOWN_MS);
10109
+ const liveStickiness = () => deps.getConfig().fallbackStickiness;
10110
+ const cooldownBase = () => Math.max(
10111
+ 0,
10112
+ deps.primaryCooldownMs ?? liveStickiness()?.primaryProbeInterval ?? DEFAULT_PRIMARY_COOLDOWN_MS
10113
+ );
9763
10114
  const cooldownMax = () => Math.max(cooldownBase(), deps.primaryCooldownMaxMs ?? DEFAULT_PRIMARY_COOLDOWN_MAX_MS);
9764
10115
  const selectedPrimary = (cfg) => deps.getPrimaryTarget?.() ?? primaryTarget(cfg);
9765
10116
  const primaryInCooldown = (cfg) => sameTarget(blockedPrimary, selectedPrimary(cfg)) && now() < primaryBlockedUntil;
@@ -9778,7 +10129,7 @@ function createFallbackModelExtension(deps) {
9778
10129
  primaryBlockedUntil = now() + Math.min(cooldownMax(), base * multiplier);
9779
10130
  };
9780
10131
  const recoveryTarget = () => Math.max(1, deps.primaryRecoverySuccesses ?? DEFAULT_PRIMARY_RECOVERY_SUCCESSES);
9781
- const stickyTarget = () => Math.max(0, deps.stickyFallbackTurns ?? 0);
10132
+ const stickyTarget = () => Math.max(0, deps.stickyFallbackTurns ?? liveStickiness()?.stickyFallbackTurns ?? 0);
9782
10133
  const inStickyWindow = () => stickyTurnsElapsed < stickyTarget();
9783
10134
  const onPrimarySuccess = (cfg) => {
9784
10135
  if (!sameTarget(blockedPrimary, selectedPrimary(cfg))) return;
@@ -9806,7 +10157,25 @@ function createFallbackModelExtension(deps) {
9806
10157
  if (!evaluateModelCalendar(cfg.modelAvailabilitySchedule, primary.providerId, primary.model).allowed || deps.statusTracker && !deps.statusTracker.isAvailable(primary.providerId, primary.model))
9807
10158
  return;
9808
10159
  try {
9809
- ctx.provider = await deps.buildProvider(primary.providerId, primary.model);
10160
+ const primaryProvider = await deps.buildProvider(primary.providerId, primary.model);
10161
+ const currentTokens = ctx.lastRequestTokens;
10162
+ const maxContext = maxContextOf(primaryProvider);
10163
+ if (maxContext > 0 && typeof currentTokens === "number" && currentTokens > maxContext) {
10164
+ deps.events.emit("provider.primary_probe_context_blocked", {
10165
+ sessionId: resolveEventSessionId(ctx),
10166
+ providerId: primary.providerId,
10167
+ model: primary.model,
10168
+ currentTokens,
10169
+ maxContext,
10170
+ timestamp: now()
10171
+ });
10172
+ deps.logger?.warn(
10173
+ `fallback-model: deferring primary probe "${primary.providerId}/${primary.model}" \u2014 context (${currentTokens}) exceeds target window (${maxContext})`
10174
+ );
10175
+ markPrimaryFailure(cfg);
10176
+ return;
10177
+ }
10178
+ ctx.provider = primaryProvider;
9810
10179
  ctx.model = primary.model;
9811
10180
  await deps.onModelSwitch?.(primary.providerId, primary.model);
9812
10181
  primaryBlockedUntil = 0;
@@ -9920,9 +10289,10 @@ function createFallbackModelExtension(deps) {
9920
10289
  const status = shouldFallback(firstErr_);
9921
10290
  if (status === null) throw firstErr_;
9922
10291
  let gateRequestId;
9923
- if (deps.fallbackGate && usableChain.length > 0) {
10292
+ const configuredGateSeconds = cfg.fallbackGateSeconds ?? deps.fallbackGateSeconds;
10293
+ if (configuredGateSeconds !== 0 && deps.fallbackGate && usableChain.length > 0) {
9924
10294
  gateRequestId = randomUUID6();
9925
- const autoSwitchSeconds = Math.max(1, deps.fallbackGateSeconds ?? 7);
10295
+ const autoSwitchSeconds = Math.max(1, configuredGateSeconds ?? 7);
9926
10296
  const gateCandidates = usableChain.map((e) => ({
9927
10297
  providerId: e.providerId,
9928
10298
  model: e.model
@@ -10749,6 +11119,7 @@ function shellGuidanceBlock(shell, detail) {
10749
11119
  var MAX_ENVIRONMENT_CACHE_ENTRIES = 16;
10750
11120
  async function buildEnvironment(ctx, env) {
10751
11121
  const modelCapabilities = env.modelCapabilities;
11122
+ const today = env.todayIso ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
10752
11123
  const cacheKey = [
10753
11124
  ctx.projectRoot,
10754
11125
  ctx.provider ?? "",
@@ -10757,7 +11128,9 @@ async function buildEnvironment(ctx, env) {
10757
11128
  modelCapabilities?.supportsTools ? 1 : 0,
10758
11129
  modelCapabilities?.supportsVision ? 1 : 0,
10759
11130
  modelCapabilities?.supportsReasoning ? 1 : 0,
10760
- env.skillCache ?? ""
11131
+ env.skillCache ?? "",
11132
+ today,
11133
+ env.modeId ?? ""
10761
11134
  ].join("\0");
10762
11135
  const cached = env.envCacheByRoot.get(cacheKey);
10763
11136
  if (cached) {
@@ -10765,7 +11138,6 @@ async function buildEnvironment(ctx, env) {
10765
11138
  env.envCacheByRoot.set(cacheKey, cached);
10766
11139
  return cached;
10767
11140
  }
10768
- const today = env.todayIso ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
10769
11141
  const platform2 = `${os3.platform()} ${os3.release()}`;
10770
11142
  const effShell = effectiveShell(os3.platform(), process.env["WRONGSTACK_SHELL"]);
10771
11143
  const shell = effShell === "posix" ? process.env.SHELL ?? process.env.ComSpec ?? "unknown" : SHELL_DISPLAY[effShell];
@@ -11611,7 +11983,9 @@ ${peers}
11611
11983
  const tpl = tplCtx ?? this.templateContext(ctx);
11612
11984
  const section = (key, vars = {}) => instructionSection(instructions, key, vars, tpl);
11613
11985
  const tier = this.tier;
11614
- if (this._toolsUsageCache?.toolsRef === tools && this._toolsUsageCache?.tier === tier) {
11986
+ const subagent = ctx.subagent === true;
11987
+ const maxContextTokens = this.modelCapabilities()?.maxContextTokens ?? 0;
11988
+ if (this._toolsUsageCache?.toolsRef === tools && this._toolsUsageCache?.tier === tier && this._toolsUsageCache?.subagent === subagent && this._toolsUsageCache?.maxContextTokens === maxContextTokens) {
11615
11989
  return this._toolsUsageCache.text;
11616
11990
  }
11617
11991
  const byCat = /* @__PURE__ */ new Map();
@@ -11738,7 +12112,7 @@ ${hint.trim()}`);
11738
12112
  }
11739
12113
  }
11740
12114
  const text = lines.join("\n");
11741
- this._toolsUsageCache = { toolsRef: tools, tier, text };
12115
+ this._toolsUsageCache = { toolsRef: tools, tier, subagent, maxContextTokens, text };
11742
12116
  return text;
11743
12117
  }
11744
12118
  renderOnlineAgents(agents) {
@@ -11801,6 +12175,7 @@ export {
11801
12175
  createDefaultPipelines,
11802
12176
  createFallbackModelExtension,
11803
12177
  detectContinueIntent,
12178
+ diagnoseFallbackConfig,
11804
12179
  effectiveFallbackChain,
11805
12180
  fallbackProfileChain,
11806
12181
  formatModelRef,
@@ -11816,8 +12191,10 @@ export {
11816
12191
  renderNextStepsBlock,
11817
12192
  resolveContinuation,
11818
12193
  runProviderWithRetry,
12194
+ runtimeFallbackChain,
11819
12195
  setBtwNote,
11820
12196
  setQueuedMessagesSnapshot,
12197
+ simulateFallbackFailover,
11821
12198
  smartDefaultFallbackChain,
11822
12199
  wrapAsState,
11823
12200
  writePendingNextSteps