open-multi-agent-kit 0.95.2 → 0.96.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +37 -4
  3. package/dist/approvals/approval-receipt-store.d.ts.map +1 -1
  4. package/dist/approvals/approval-receipt-store.js +25 -0
  5. package/dist/approvals/approval-receipt-store.js.map +1 -1
  6. package/dist/commands/doctor-provider.d.ts.map +1 -1
  7. package/dist/commands/doctor-provider.js +15 -20
  8. package/dist/commands/doctor-provider.js.map +1 -1
  9. package/dist/commands/sdk-session-cli.d.ts +24 -0
  10. package/dist/commands/sdk-session-cli.d.ts.map +1 -0
  11. package/dist/commands/sdk-session-cli.js +225 -0
  12. package/dist/commands/sdk-session-cli.js.map +1 -0
  13. package/dist/core/advisory-judge-model.d.ts +18 -0
  14. package/dist/core/advisory-judge-model.d.ts.map +1 -0
  15. package/dist/core/advisory-judge-model.js +168 -0
  16. package/dist/core/advisory-judge-model.js.map +1 -0
  17. package/dist/core/advisory-judge-response.d.ts +8 -0
  18. package/dist/core/advisory-judge-response.d.ts.map +1 -0
  19. package/dist/core/advisory-judge-response.js +61 -0
  20. package/dist/core/advisory-judge-response.js.map +1 -0
  21. package/dist/core/advisory-judge-types.d.ts +53 -0
  22. package/dist/core/advisory-judge-types.d.ts.map +1 -0
  23. package/dist/core/advisory-judge-types.js +2 -0
  24. package/dist/core/advisory-judge-types.js.map +1 -0
  25. package/dist/core/advisory-judge.d.ts +8 -0
  26. package/dist/core/advisory-judge.d.ts.map +1 -0
  27. package/dist/core/advisory-judge.js +178 -0
  28. package/dist/core/advisory-judge.js.map +1 -0
  29. package/dist/core/agent-session-services.d.ts +1 -0
  30. package/dist/core/agent-session-services.d.ts.map +1 -1
  31. package/dist/core/agent-session-services.js +1 -0
  32. package/dist/core/agent-session-services.js.map +1 -1
  33. package/dist/core/agent-session.d.ts +6 -3
  34. package/dist/core/agent-session.d.ts.map +1 -1
  35. package/dist/core/agent-session.js +141 -184
  36. package/dist/core/agent-session.js.map +1 -1
  37. package/dist/core/auth-storage.d.ts.map +1 -1
  38. package/dist/core/auth-storage.js +28 -4
  39. package/dist/core/auth-storage.js.map +1 -1
  40. package/dist/core/compaction-gate.d.ts +28 -0
  41. package/dist/core/compaction-gate.d.ts.map +1 -0
  42. package/dist/core/compaction-gate.js +34 -0
  43. package/dist/core/compaction-gate.js.map +1 -0
  44. package/dist/core/context-budget-system-prompt-items.d.ts.map +1 -1
  45. package/dist/core/context-budget-system-prompt-items.js +5 -3
  46. package/dist/core/context-budget-system-prompt-items.js.map +1 -1
  47. package/dist/core/domain-loadouts.d.ts.map +1 -1
  48. package/dist/core/domain-loadouts.js +2 -2
  49. package/dist/core/domain-loadouts.js.map +1 -1
  50. package/dist/core/durable-goal-checkpoint.d.ts +6 -0
  51. package/dist/core/durable-goal-checkpoint.d.ts.map +1 -0
  52. package/dist/core/durable-goal-checkpoint.js +195 -0
  53. package/dist/core/durable-goal-checkpoint.js.map +1 -0
  54. package/dist/core/durable-goal-parse.d.ts +3 -0
  55. package/dist/core/durable-goal-parse.d.ts.map +1 -0
  56. package/dist/core/durable-goal-parse.js +86 -0
  57. package/dist/core/durable-goal-parse.js.map +1 -0
  58. package/dist/core/durable-goal-reducer.d.ts +3 -0
  59. package/dist/core/durable-goal-reducer.d.ts.map +1 -0
  60. package/dist/core/durable-goal-reducer.js +175 -0
  61. package/dist/core/durable-goal-reducer.js.map +1 -0
  62. package/dist/core/durable-goal-store.d.ts +19 -0
  63. package/dist/core/durable-goal-store.d.ts.map +1 -0
  64. package/dist/core/durable-goal-store.js +126 -0
  65. package/dist/core/durable-goal-store.js.map +1 -0
  66. package/dist/core/durable-goal.d.ts +92 -0
  67. package/dist/core/durable-goal.d.ts.map +1 -0
  68. package/dist/core/durable-goal.js +62 -0
  69. package/dist/core/durable-goal.js.map +1 -0
  70. package/dist/core/extensions/builtin/goal-controller.d.ts +3 -0
  71. package/dist/core/extensions/builtin/goal-controller.d.ts.map +1 -0
  72. package/dist/core/extensions/builtin/goal-controller.js +125 -0
  73. package/dist/core/extensions/builtin/goal-controller.js.map +1 -0
  74. package/dist/core/extensions/builtin/identical-loop.d.ts +3 -0
  75. package/dist/core/extensions/builtin/identical-loop.d.ts.map +1 -0
  76. package/dist/core/extensions/builtin/identical-loop.js +31 -0
  77. package/dist/core/extensions/builtin/identical-loop.js.map +1 -0
  78. package/dist/core/extensions/builtin/prompt-preset.d.ts +3 -0
  79. package/dist/core/extensions/builtin/prompt-preset.d.ts.map +1 -0
  80. package/dist/core/extensions/builtin/prompt-preset.js +13 -0
  81. package/dist/core/extensions/builtin/prompt-preset.js.map +1 -0
  82. package/dist/core/extensions/builtin/tool-pair-repair.d.ts +3 -0
  83. package/dist/core/extensions/builtin/tool-pair-repair.d.ts.map +1 -0
  84. package/dist/core/extensions/builtin/tool-pair-repair.js +12 -0
  85. package/dist/core/extensions/builtin/tool-pair-repair.js.map +1 -0
  86. package/dist/core/extensions/types.d.ts +6 -2
  87. package/dist/core/extensions/types.d.ts.map +1 -1
  88. package/dist/core/extensions/types.js.map +1 -1
  89. package/dist/core/goal-continuation.d.ts +13 -0
  90. package/dist/core/goal-continuation.d.ts.map +1 -0
  91. package/dist/core/goal-continuation.js +10 -0
  92. package/dist/core/goal-continuation.js.map +1 -0
  93. package/dist/core/grok-harness.d.ts +1 -1
  94. package/dist/core/grok-harness.d.ts.map +1 -1
  95. package/dist/core/grok-harness.js +1 -1
  96. package/dist/core/grok-harness.js.map +1 -1
  97. package/dist/core/grok-playbook.d.ts +1 -1
  98. package/dist/core/grok-playbook.d.ts.map +1 -1
  99. package/dist/core/grok-playbook.js +1 -1
  100. package/dist/core/grok-playbook.js.map +1 -1
  101. package/dist/core/identical-loop.d.ts +15 -0
  102. package/dist/core/identical-loop.d.ts.map +1 -0
  103. package/dist/core/identical-loop.js +34 -0
  104. package/dist/core/identical-loop.js.map +1 -0
  105. package/dist/core/index.d.ts +1 -1
  106. package/dist/core/index.d.ts.map +1 -1
  107. package/dist/core/index.js +1 -1
  108. package/dist/core/index.js.map +1 -1
  109. package/dist/core/model-prompt-policy.d.ts +11 -0
  110. package/dist/core/model-prompt-policy.d.ts.map +1 -0
  111. package/dist/core/model-prompt-policy.js +17 -0
  112. package/dist/core/model-prompt-policy.js.map +1 -0
  113. package/dist/core/model-registry.d.ts.map +1 -1
  114. package/dist/core/model-registry.js +16 -4
  115. package/dist/core/model-registry.js.map +1 -1
  116. package/dist/core/model-resolver.d.ts.map +1 -1
  117. package/dist/core/model-resolver.js +2 -2
  118. package/dist/core/model-resolver.js.map +1 -1
  119. package/dist/core/prompt-budget.d.ts +21 -0
  120. package/dist/core/prompt-budget.d.ts.map +1 -0
  121. package/dist/core/prompt-budget.js +51 -0
  122. package/dist/core/prompt-budget.js.map +1 -0
  123. package/dist/core/prompt-cache.d.ts +14 -0
  124. package/dist/core/prompt-cache.d.ts.map +1 -0
  125. package/dist/core/prompt-cache.js +16 -0
  126. package/dist/core/prompt-cache.js.map +1 -0
  127. package/dist/core/prompt-preset.d.ts +8 -0
  128. package/dist/core/prompt-preset.d.ts.map +1 -0
  129. package/dist/core/prompt-preset.js +56 -0
  130. package/dist/core/prompt-preset.js.map +1 -0
  131. package/dist/core/provider-resilience.d.ts +23 -0
  132. package/dist/core/provider-resilience.d.ts.map +1 -1
  133. package/dist/core/provider-resilience.js +34 -1
  134. package/dist/core/provider-resilience.js.map +1 -1
  135. package/dist/core/provider-retry.d.ts +29 -0
  136. package/dist/core/provider-retry.d.ts.map +1 -0
  137. package/dist/core/provider-retry.js +49 -0
  138. package/dist/core/provider-retry.js.map +1 -0
  139. package/dist/core/provider-usage.d.ts +2 -1
  140. package/dist/core/provider-usage.d.ts.map +1 -1
  141. package/dist/core/provider-usage.js +40 -6
  142. package/dist/core/provider-usage.js.map +1 -1
  143. package/dist/core/resource-loader.d.ts.map +1 -1
  144. package/dist/core/resource-loader.js +32 -0
  145. package/dist/core/resource-loader.js.map +1 -1
  146. package/dist/core/sandbox/backend.d.ts +12 -0
  147. package/dist/core/sandbox/backend.d.ts.map +1 -1
  148. package/dist/core/sandbox/backend.js +52 -42
  149. package/dist/core/sandbox/backend.js.map +1 -1
  150. package/dist/core/sandbox/policy.d.ts +2 -0
  151. package/dist/core/sandbox/policy.d.ts.map +1 -1
  152. package/dist/core/sandbox/policy.js +2 -1
  153. package/dist/core/sandbox/policy.js.map +1 -1
  154. package/dist/core/sdk.d.ts +2 -0
  155. package/dist/core/sdk.d.ts.map +1 -1
  156. package/dist/core/sdk.js +1 -0
  157. package/dist/core/sdk.js.map +1 -1
  158. package/dist/core/session-system-prompt.d.ts +28 -0
  159. package/dist/core/session-system-prompt.d.ts.map +1 -0
  160. package/dist/core/session-system-prompt.js +37 -0
  161. package/dist/core/session-system-prompt.js.map +1 -0
  162. package/dist/core/strict-record.d.ts +2 -0
  163. package/dist/core/strict-record.d.ts.map +1 -0
  164. package/dist/core/strict-record.js +9 -0
  165. package/dist/core/strict-record.js.map +1 -0
  166. package/dist/core/subagent-orchestration.d.ts.map +1 -1
  167. package/dist/core/subagent-orchestration.js +4 -2
  168. package/dist/core/subagent-orchestration.js.map +1 -1
  169. package/dist/core/tool-pair-repair.d.ts +11 -0
  170. package/dist/core/tool-pair-repair.d.ts.map +1 -0
  171. package/dist/core/tool-pair-repair.js +38 -0
  172. package/dist/core/tool-pair-repair.js.map +1 -0
  173. package/dist/core/tools/artifact-spill.d.ts +13 -0
  174. package/dist/core/tools/artifact-spill.d.ts.map +1 -0
  175. package/dist/core/tools/artifact-spill.js +26 -0
  176. package/dist/core/tools/artifact-spill.js.map +1 -0
  177. package/dist/core/tools/bash.d.ts +7 -4
  178. package/dist/core/tools/bash.d.ts.map +1 -1
  179. package/dist/core/tools/bash.js +8 -7
  180. package/dist/core/tools/bash.js.map +1 -1
  181. package/dist/core/tools/diagnostics.d.ts.map +1 -1
  182. package/dist/core/tools/diagnostics.js +8 -2
  183. package/dist/core/tools/diagnostics.js.map +1 -1
  184. package/dist/core/tools/index.d.ts +1 -1
  185. package/dist/core/tools/index.d.ts.map +1 -1
  186. package/dist/core/tools/index.js.map +1 -1
  187. package/dist/core/tools/read.d.ts +1 -0
  188. package/dist/core/tools/read.d.ts.map +1 -1
  189. package/dist/core/tools/read.js +10 -1
  190. package/dist/core/tools/read.js.map +1 -1
  191. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
  192. package/dist/core/tools/tool-definition-wrapper.js +2 -0
  193. package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
  194. package/dist/guardrails/evidence-protocol.d.ts +10 -0
  195. package/dist/guardrails/evidence-protocol.d.ts.map +1 -0
  196. package/dist/guardrails/evidence-protocol.js +34 -0
  197. package/dist/guardrails/evidence-protocol.js.map +1 -0
  198. package/dist/guardrails/evidence-system.d.ts +3 -1
  199. package/dist/guardrails/evidence-system.d.ts.map +1 -1
  200. package/dist/guardrails/evidence-system.js +22 -14
  201. package/dist/guardrails/evidence-system.js.map +1 -1
  202. package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -1
  203. package/dist/guardrails/replay-ledger-lock-owner.js +3 -1
  204. package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -1
  205. package/dist/guardrails/replay-payload-hash.d.ts +6 -0
  206. package/dist/guardrails/replay-payload-hash.d.ts.map +1 -0
  207. package/dist/guardrails/replay-payload-hash.js +24 -0
  208. package/dist/guardrails/replay-payload-hash.js.map +1 -0
  209. package/dist/index.d.ts +20 -2
  210. package/dist/index.d.ts.map +1 -1
  211. package/dist/index.js +19 -0
  212. package/dist/index.js.map +1 -1
  213. package/dist/main.d.ts.map +1 -1
  214. package/dist/main.js +8 -0
  215. package/dist/main.js.map +1 -1
  216. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  217. package/dist/modes/interactive/theme/theme.js +14 -3
  218. package/dist/modes/interactive/theme/theme.js.map +1 -1
  219. package/dist/types/evidence.d.ts +11 -3
  220. package/dist/types/evidence.d.ts.map +1 -1
  221. package/dist/types/evidence.js.map +1 -1
  222. package/docs/adaptorch-preview-spec.md +1 -1
  223. package/docs/adaptorch-preview.md +2 -2
  224. package/docs/book-to-skill.md +79 -0
  225. package/docs/compaction.md +10 -0
  226. package/docs/containerization.md +9 -0
  227. package/docs/docs.json +18 -2
  228. package/docs/environment-variables.md +11 -4
  229. package/docs/extensions.md +14 -1
  230. package/docs/grok-harness.md +35 -12
  231. package/docs/index.md +9 -5
  232. package/docs/loadout-domains/grok-harness.md +2 -2
  233. package/docs/models.md +21 -8
  234. package/docs/provider-resilience.md +42 -55
  235. package/docs/providers.md +13 -4
  236. package/docs/run-protocol.md +104 -0
  237. package/docs/sdk.md +111 -5
  238. package/docs/security.md +22 -32
  239. package/docs/sessions.md +20 -0
  240. package/docs/settings.md +2 -0
  241. package/docs/skills.md +9 -1
  242. package/docs/usage.md +38 -16
  243. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  244. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  245. package/examples/extensions/custom-provider-gitlab-duo/index.ts +6 -1
  246. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  247. package/examples/extensions/gondolin/package-lock.json +2 -2
  248. package/examples/extensions/gondolin/package.json +1 -1
  249. package/examples/extensions/sandbox/package-lock.json +2 -2
  250. package/examples/extensions/sandbox/package.json +1 -1
  251. package/examples/extensions/with-deps/package-lock.json +2 -2
  252. package/examples/extensions/with-deps/package.json +1 -1
  253. package/npm-shrinkwrap.json +37 -15
  254. package/package.json +8 -5
  255. package/dist/core/context-budget-compressors.d.ts +0 -50
  256. package/dist/core/context-budget-compressors.d.ts.map +0 -1
  257. package/dist/core/context-budget-compressors.js +0 -166
  258. package/dist/core/context-budget-compressors.js.map +0 -1
  259. package/dist/core/extensions/loader.js.bak-sharedjiti-20260812-111246 +0 -553
@@ -16,7 +16,7 @@ import { createHash, randomUUID } from "node:crypto";
16
16
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
17
17
  import { basename, dirname, join as joinPath } from "node:path";
18
18
  import { isDeepStrictEqual } from "node:util";
19
- import { getVisionRouteModel, isVisionRouteModel } from "omk-agent-core";
19
+ import { getVisionRouteModel } from "omk-agent-core";
20
20
  import { clampThinkingLevel, cleanupSessionResources, deriveContextPromptCacheKey, getSupportedThinkingLevels, isContextOverflow, modelsAreEqual, resetApiProviders, streamSimple, } from "omk-ai";
21
21
  import { APP_NAME, VERSION } from "../config.js";
22
22
  import { theme } from "../modes/interactive/theme/theme.js";
@@ -32,6 +32,7 @@ import { getCompactionHeadroomThreshold } from "./compaction/compaction.js";
32
32
  import { createCompactionHysteresisConfig, createCompactionHysteresisState, stepCompactionHysteresis, } from "./compaction/hysteresis.js";
33
33
  import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateProjectedContextTokens, generateBranchSummary, prepareCompaction, resolveCompactionModel, } from "./compaction/index.js";
34
34
  import { compactionEmitWillRetry } from "./compaction/resume-policy.js";
35
+ import { isSessionModelOverflow, shouldSkipCompactionCheck } from "./compaction-gate.js";
35
36
  import { estimateToolResultReserve, } from "./context-budget-reserved-tokens.js";
36
37
  import { createDiskContextBudgetCacheProviderV2, DiskContextBudgetCacheProviderV2, } from "./context-budget-v2-cache-disk.js";
37
38
  import { applyContextCacheInvalidation, createContextCacheInvalidationSnapshot, } from "./context-budget-v2-cache-invalidation.js";
@@ -50,6 +51,8 @@ import { buildRuntimeProvenance } from "./runtime-provenance.js";
50
51
  import { TurnMetricsSink } from "./turn-metrics.js";
51
52
  /** Values that disable a session-level feature through its environment variable. */
52
53
  const DISABLED_ENV_VALUES = new Set(["0", "false", "off", "disable", "disabled"]);
54
+ const MAX_OVERFLOW_RECOVERY_ATTEMPTS = 2;
55
+ const OVERFLOW_RECOVERY_EMERGENCY_TOKENS = 4_096;
53
56
  function isDisabledEnvValue(value) {
54
57
  return value !== undefined && DISABLED_ENV_VALUES.has(value.trim().toLowerCase());
55
58
  }
@@ -68,9 +71,13 @@ function firstTextContent(result) {
68
71
  return undefined;
69
72
  }
70
73
  import { redactCredentialShapedContent } from "./compaction/transaction.js";
74
+ import { selectContextFilesForModel } from "./model-prompt-policy.js";
71
75
  import { findExactModelReferenceMatch } from "./model-resolver.js";
76
+ import { computePromptTokenBudget } from "./prompt-budget.js";
77
+ import { classifyPromptCacheTransition } from "./prompt-cache.js";
72
78
  import { expandPromptTemplate } from "./prompt-templates.js";
73
- import { isContentSafetyStopMessage, isStickySafetyModel, isTransientProviderErrorMessage, pickFailoverCandidate, resolveProviderResilience, stickySafetyBlockMessage, } from "./provider-resilience.js";
79
+ import { isQuotaExhaustionMessage, isStickySafetyModel, pickFailoverCandidate, resolveProviderResilience, shouldEjectStickySafetyModel, shouldHonorSafetyFailover, stickySafetyBlockMessage, } from "./provider-resilience.js";
80
+ import { computeRetryDelayMs, failoverModelKey, isFailoverTriggerError, isRetryableAssistantError, nextRetryAttempt, retryBudgetForAssistantError, } from "./provider-retry.js";
74
81
  import { getBiasStepsForCell, parseRouterBiasSnapshot } from "./reasoning-router-bias.js";
75
82
  import { classifyTaskV4, deriveRouterFeedbackFeaturesV4, resolveThinkingLevelV4WithUncertainty, TASK_CLASS_THINKING_LEVELS_V4, } from "./reasoning-router-v4.js";
76
83
  import { redactSensitiveText, redactSensitiveTextForced } from "./redaction.js";
@@ -82,6 +89,7 @@ import { SessionBashService } from "./session-bash-service.js";
82
89
  import { SessionCompactionService } from "./session-compaction-service.js";
83
90
  import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
84
91
  import { acquireSessionOwnerLeaseSync } from "./session-owner-lease.js";
92
+ import { assembleSessionSystemPrompt } from "./session-system-prompt.js";
85
93
  import { classifySessionTermination, } from "./session-termination.js";
86
94
  import { createSyntheticSourceInfo } from "./source-info.js";
87
95
  import { buildSystemPromptPlan } from "./system-prompt.js";
@@ -302,7 +310,7 @@ export class AgentSession {
302
310
  // Compaction state
303
311
  _compactionAbortController = undefined;
304
312
  _autoCompactionAbortController = undefined;
305
- _overflowRecoveryAttempted = false;
313
+ _overflowRecoveryAttempts = 0;
306
314
  _thresholdCompactionEmergency = false;
307
315
  _compactionHysteresisState = createCompactionHysteresisState();
308
316
  // Branch summarization state
@@ -313,6 +321,8 @@ export class AgentSession {
313
321
  // v10.3-Ω: models that already refused/failed this turn — failover must advance,
314
322
  // not re-pick the same candidate (claude-opus-5 → grok → grok → grok loop fix).
315
323
  _refusedModels = new Set();
324
+ /** True when `--model` / `--provider` pinned this process to one model. */
325
+ _modelPinned;
316
326
  // Bash execution state
317
327
  _bashService;
318
328
  _compactionService;
@@ -381,6 +391,7 @@ export class AgentSession {
381
391
  this.sessionManager = config.sessionManager;
382
392
  this.settingsManager = config.settingsManager;
383
393
  this._scopedModels = config.scopedModels ?? [];
394
+ this._modelPinned = config.modelPinned === true;
384
395
  this._resourceLoader = config.resourceLoader;
385
396
  this._customTools = config.customTools ?? [];
386
397
  this._cwd = config.cwd;
@@ -585,12 +596,15 @@ export class AgentSession {
585
596
  if (/stop_reason\s*=\s*(refusal|sensitive)|content\/safety stop|safety stop|provider\.refusal|kind=provider_refusal/i.test(text)) {
586
597
  return { area: "provider", code: "refusal" };
587
598
  }
599
+ // Quota/billing exhaustion is checked BEFORE the generic 401/403 auth
600
+ // patterns: "403 ... usage limit for this billing cycle" is transient per
601
+ // cycle and must fail over, not terminate the turn as an auth error.
602
+ if (isQuotaExhaustionMessage(text) || /rate.?limit|too many requests|429/i.test(text)) {
603
+ return { area: "provider", code: "rate_limit" };
604
+ }
588
605
  if (/auth|unauthori[sz]ed|forbidden|invalid.?api.?key|no api key|401|403|\/login/i.test(text)) {
589
606
  return { area: "provider", code: "auth" };
590
607
  }
591
- if (/rate.?limit|too many requests|429|quota|available balance|billing/i.test(text)) {
592
- return { area: "provider", code: "rate_limit" };
593
- }
594
608
  // Kimi/K3 + OpenAI-compat: orphan tool results after dropped error assistants.
595
609
  // Sanitize-and-retry (transform-messages drops orphans), not a hard tool fatal.
596
610
  if (/tool_call_id\s+is\s+not\s+found|tool_call_id\s+not\s+found|unknown\s+tool_call_id/i.test(text)) {
@@ -609,6 +623,9 @@ export class AgentSession {
609
623
  if (!this.model || /no model|model selected|model is required/i.test(message)) {
610
624
  return { area: "configuration", code: "invalid" };
611
625
  }
626
+ if (isQuotaExhaustionMessage(message)) {
627
+ return { area: "provider", code: "rate_limit" };
628
+ }
612
629
  if (/auth|api key|unauthori[sz]ed|forbidden|401|403|\/login/i.test(message)) {
613
630
  return { area: "provider", code: "auth" };
614
631
  }
@@ -966,7 +983,7 @@ export class AgentSession {
966
983
  // When a user message starts, check if it's from either queue and remove it BEFORE emitting
967
984
  // This ensures the UI sees the updated queue state
968
985
  if (event.type === "message_start" && event.message.role === "user") {
969
- this._overflowRecoveryAttempted = false;
986
+ this._overflowRecoveryAttempts = 0;
970
987
  const messageText = this._getUserMessageText(event.message);
971
988
  if (messageText) {
972
989
  // Check steering queue first
@@ -1017,7 +1034,7 @@ export class AgentSession {
1017
1034
  this._lastAssistantMessage = finalizedEvent.message;
1018
1035
  const assistantMsg = finalizedEvent.message;
1019
1036
  if (assistantMsg.stopReason !== "error") {
1020
- this._overflowRecoveryAttempted = false;
1037
+ this._overflowRecoveryAttempts = 0;
1021
1038
  }
1022
1039
  // Reset retry counter immediately on successful assistant response
1023
1040
  // This prevents accumulation across multiple LLM calls within a turn
@@ -1035,13 +1052,14 @@ export class AgentSession {
1035
1052
  };
1036
1053
  _willRetryAfterAgentEnd(event) {
1037
1054
  const settings = this.settingsManager.getRetrySettings();
1038
- if (!settings.enabled || this._retryAttempt >= settings.maxRetries) {
1055
+ if (!settings.enabled)
1039
1056
  return false;
1040
- }
1041
1057
  for (let i = event.messages.length - 1; i >= 0; i--) {
1042
1058
  const message = event.messages[i];
1043
1059
  if (message.role === "assistant") {
1044
- return this._isRetryableError(message);
1060
+ const assistant = message;
1061
+ const maxRetries = retryBudgetForAssistantError(assistant, settings.maxRetries);
1062
+ return this._retryAttempt < maxRetries && this._isRetryableError(assistant);
1045
1063
  }
1046
1064
  }
1047
1065
  return false;
@@ -1546,7 +1564,7 @@ export class AgentSession {
1546
1564
  }
1547
1565
  return undefined;
1548
1566
  }
1549
- _getContextBudgetOptions(queryContext = this._extractCurrentQuery()) {
1567
+ _getContextBudgetOptions(queryContext = this._extractCurrentQuery(), model = this.model) {
1550
1568
  const contextGovernorOverride = process.env.OMK_CONTEXT_GOVERNOR;
1551
1569
  if (contextGovernorOverride === "0") {
1552
1570
  return undefined;
@@ -1554,56 +1572,24 @@ export class AgentSession {
1554
1572
  if (contextGovernorOverride !== "1" && !this.settingsManager.getContextBudgetEnabled()) {
1555
1573
  return undefined;
1556
1574
  }
1557
- const contextWindow = this.model?.contextWindow ?? 0;
1558
- // --- maxPromptTokens ---
1559
- // Priority 1: explicit env override (user knows best)
1560
- const envMaxPrompt = parsePositiveIntegerEnv("OMK_CONTEXT_GOVERNOR_MAX_PROMPT_TOKENS");
1561
- let maxPromptTokens;
1562
- let responseReserveTokens;
1563
- if (envMaxPrompt !== undefined) {
1564
- // User override — use as-is for prompt, derive response reserve normally
1565
- maxPromptTokens = envMaxPrompt;
1566
- responseReserveTokens =
1567
- parsePositiveIntegerEnv("OMK_CONTEXT_GOVERNOR_RESPONSE_RESERVE_TOKENS") ??
1568
- this._computeResponseReserve(contextWindow);
1569
- }
1570
- else if (contextWindow > 0) {
1571
- // Dynamic: derive from model contextWindow
1572
- const envPromptRatio = parsePositiveFloatEnv("OMK_CONTEXT_GOVERNOR_PROMPT_RATIO");
1573
- const envResponseRatio = parsePositiveFloatEnv("OMK_CONTEXT_GOVERNOR_RESPONSE_RATIO");
1574
- responseReserveTokens =
1575
- parsePositiveIntegerEnv("OMK_CONTEXT_GOVERNOR_RESPONSE_RESERVE_TOKENS") ??
1576
- this._computeResponseReserve(contextWindow, envResponseRatio);
1577
- const safetyMargin = Math.floor(contextWindow * SAFETY_MARGIN_RATIO);
1578
- if (envPromptRatio !== undefined && envPromptRatio > 0 && envPromptRatio < 1) {
1579
- maxPromptTokens = Math.floor(contextWindow * envPromptRatio);
1580
- }
1581
- else {
1582
- // Default: contextWindow minus reserves and safety margin
1583
- maxPromptTokens = contextWindow - responseReserveTokens - safetyMargin;
1584
- }
1585
- }
1586
- else {
1587
- // No model info — legacy fallback
1588
- maxPromptTokens = LEGACY_MAX_PROMPT_TOKENS;
1589
- responseReserveTokens =
1590
- parsePositiveIntegerEnv("OMK_CONTEXT_GOVERNOR_RESPONSE_RESERVE_TOKENS") ?? LEGACY_RESPONSE_RESERVE_TOKENS;
1591
- }
1575
+ const contextWindow = model?.contextWindow ?? 0;
1576
+ const budget = computePromptTokenBudget({
1577
+ contextWindow,
1578
+ modelMaxTokens: model?.maxTokens,
1579
+ envMaxPromptTokens: parsePositiveIntegerEnv("OMK_CONTEXT_GOVERNOR_MAX_PROMPT_TOKENS"),
1580
+ envResponseReserveTokens: parsePositiveIntegerEnv("OMK_CONTEXT_GOVERNOR_RESPONSE_RESERVE_TOKENS"),
1581
+ envPromptRatio: parsePositiveFloatEnv("OMK_CONTEXT_GOVERNOR_PROMPT_RATIO"),
1582
+ envResponseRatio: parsePositiveFloatEnv("OMK_CONTEXT_GOVERNOR_RESPONSE_RATIO"),
1583
+ });
1584
+ const maxPromptTokens = budget.maxPromptTokens;
1585
+ const responseReserveTokens = budget.responseReserveTokens;
1592
1586
  const cacheProvider = this._contextBudgetCacheProvider ?? this._createContextBudgetCacheProvider();
1593
1587
  cacheProvider.setInvalidationSnapshot?.(this._contextCacheInvalidationSnapshot);
1594
1588
  this._contextBudgetCacheProvider = cacheProvider;
1595
- // Enforce floor
1596
- if (maxPromptTokens < MIN_PROMPT_TOKENS) {
1597
- maxPromptTokens = MIN_PROMPT_TOKENS;
1598
- }
1599
- // Ensure responseReserve does not exceed maxPromptTokens
1600
- if (responseReserveTokens >= maxPromptTokens) {
1601
- responseReserveTokens = Math.max(Math.floor(maxPromptTokens / 4), LEGACY_RESPONSE_RESERVE_TOKENS);
1602
- }
1603
1589
  return {
1604
1590
  maxPromptTokens,
1605
1591
  responseReserveTokens,
1606
- modelId: this.model?.id ?? "unknown",
1592
+ modelId: model?.id ?? "unknown",
1607
1593
  tokenizerMode: parseTokenizerModeEnv(process.env.OMK_CONTEXT_GOVERNOR_TOKENIZER),
1608
1594
  activeSkillNames: parseCommaSeparatedEnv(process.env.OMK_CONTEXT_GOVERNOR_ACTIVE_SKILLS),
1609
1595
  queryContext,
@@ -1614,15 +1600,6 @@ export class AgentSession {
1614
1600
  * Compute responseReserveTokens from contextWindow.
1615
1601
  * Prefers the model's own maxTokens when available, otherwise uses a ratio.
1616
1602
  */
1617
- _computeResponseReserve(contextWindow, overrideRatio) {
1618
- // Prefer model's maxTokens (actual output limit) if available and reasonable
1619
- const modelMaxTokens = this.model?.maxTokens;
1620
- if (modelMaxTokens !== undefined && modelMaxTokens > 0 && modelMaxTokens < contextWindow) {
1621
- return modelMaxTokens;
1622
- }
1623
- const ratio = overrideRatio ?? RESPONSE_RESERVE_RATIO;
1624
- return Math.max(Math.floor(contextWindow * ratio), LEGACY_RESPONSE_RESERVE_TOKENS);
1625
- }
1626
1603
  _recordPromptCachePlan(reason) {
1627
1604
  const boundary = this.agent.state.systemPromptCacheBoundary;
1628
1605
  const scope = this.model ? `${this.model.provider}/${this.model.id}` : "unknown";
@@ -1633,60 +1610,40 @@ export class AgentSession {
1633
1610
  messages: [],
1634
1611
  tools: this.agent.state.tools,
1635
1612
  }, scope);
1636
- if (!nextKey) {
1613
+ const transition = classifyPromptCacheTransition(this._promptCacheKey, nextKey);
1614
+ if (transition.kind === "bypass") {
1637
1615
  this._promptCacheBoundaryBypasses += 1;
1638
1616
  this._promptCacheStablePrefixCharacters = 0;
1639
- if (this._promptCacheKey !== undefined) {
1617
+ if (transition.recordBreak) {
1640
1618
  this._promptCacheLastBreakReason = reason;
1641
1619
  }
1642
1620
  this._promptCacheKey = undefined;
1643
1621
  return;
1644
1622
  }
1645
- if (this._promptCacheKey !== undefined && this._promptCacheKey !== nextKey) {
1623
+ if (transition.kind === "changed") {
1646
1624
  this._promptCacheKeyChanges += 1;
1647
1625
  this._promptCacheLastBreakReason = reason;
1648
1626
  }
1649
1627
  this._promptCacheKey = nextKey;
1650
1628
  this._promptCacheStablePrefixCharacters = boundary ?? 0;
1651
1629
  }
1652
- _rebuildSystemPrompt(toolNames) {
1653
- const validToolNames = toolNames.filter((name) => this._toolRegistry.has(name));
1654
- const toolSnippets = {};
1655
- const promptGuidelines = [];
1656
- for (const name of validToolNames) {
1657
- const snippet = this._toolPromptSnippets.get(name);
1658
- if (snippet) {
1659
- toolSnippets[name] = snippet;
1660
- }
1661
- const toolGuidelines = this._toolPromptGuidelines.get(name);
1662
- if (toolGuidelines) {
1663
- promptGuidelines.push(...toolGuidelines);
1664
- }
1665
- }
1666
- const loaderSystemPrompt = this._resourceLoader.getSystemPrompt();
1667
- const loaderAppendSystemPrompt = this._resourceLoader.getAppendSystemPrompt();
1668
- const grokAppend = grokPlaybookAppendForProvider(this.model?.provider);
1669
- const appendParts = [...loaderAppendSystemPrompt];
1670
- if (grokAppend) {
1671
- appendParts.push(grokAppend);
1672
- }
1673
- const appendSystemPrompt = appendParts.length > 0 ? appendParts.join("\n\n") : undefined;
1674
- const loadedSkills = this._resourceLoader.getSkills().skills;
1675
- const loadedContextFiles = this._resourceLoader.getAgentsFiles().agentsFiles;
1676
- this._baseSystemPromptOptions = {
1630
+ _rebuildSystemPrompt(toolNames, model = this.model) {
1631
+ const assembled = assembleSessionSystemPrompt({
1677
1632
  cwd: this._cwd,
1678
- skills: loadedSkills,
1679
- contextFiles: loadedContextFiles,
1680
- customPrompt: loaderSystemPrompt,
1681
- appendSystemPrompt,
1682
- selectedTools: validToolNames,
1683
- toolSnippets,
1684
- promptGuidelines,
1685
- contextBudget: this._getContextBudgetOptions(),
1686
- };
1687
- const built = buildSystemPromptPlan(this._baseSystemPromptOptions);
1688
- this._baseSystemPromptCacheBoundary = built.cacheBoundary;
1689
- return built.prompt;
1633
+ toolNames,
1634
+ hasTool: (name) => this._toolRegistry.has(name),
1635
+ toolPromptSnippets: this._toolPromptSnippets,
1636
+ toolPromptGuidelines: this._toolPromptGuidelines,
1637
+ customPrompt: this._resourceLoader.getSystemPrompt(),
1638
+ appendSystemPrompt: this._resourceLoader.getAppendSystemPrompt(),
1639
+ providerAppend: grokPlaybookAppendForProvider(model?.provider),
1640
+ skills: this._resourceLoader.getSkills().skills,
1641
+ contextFiles: selectContextFilesForModel(this._resourceLoader.getAgentsFiles().agentsFiles, model),
1642
+ contextBudget: this._getContextBudgetOptions(undefined, model),
1643
+ });
1644
+ this._baseSystemPromptOptions = assembled.options;
1645
+ this._baseSystemPromptCacheBoundary = assembled.cacheBoundary;
1646
+ return assembled.prompt;
1690
1647
  }
1691
1648
  // =========================================================================
1692
1649
  // Prompting
@@ -2226,16 +2183,25 @@ export class AgentSession {
2226
2183
  if (!this._modelRegistry.hasConfiguredAuth(model)) {
2227
2184
  throw new Error(`No API key for ${model.provider}/${model.id}`);
2228
2185
  }
2229
- // Root-level: block sticky safety models (Fable) unless explicitly allowed in settings.
2186
+ // Root-level: block sticky safety models (Fable) unless the user pinned `--model`.
2230
2187
  const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
2231
- if (resilience.blockStickySafetyModels && isStickySafetyModel(model.id, model.provider)) {
2188
+ if (shouldEjectStickySafetyModel({
2189
+ blockStickySafetyModels: resilience.blockStickySafetyModels,
2190
+ modelPinned: this._modelPinned,
2191
+ }) &&
2192
+ isStickySafetyModel(model.id, model.provider)) {
2232
2193
  throw new Error(stickySafetyBlockMessage(model.id, model.provider));
2233
2194
  }
2234
2195
  // Grok OAuth: block selecting Imagine models as the session chat model.
2235
2196
  assertTextChatModelForCompletion(model.id, model.provider);
2236
2197
  const previousModel = this.model;
2237
2198
  const thinkingLevel = this._getThinkingLevelForModelSwitch();
2199
+ const nextSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames(), model);
2238
2200
  this.agent.state.model = model;
2201
+ this._baseSystemPrompt = nextSystemPrompt;
2202
+ this.agent.state.systemPrompt = this._baseSystemPrompt;
2203
+ this.agent.state.systemPromptCacheBoundary = this._baseSystemPromptCacheBoundary;
2204
+ this.agent.state.systemPromptCacheBoundaryBypass = false;
2239
2205
  this.sessionManager.appendModelChange(model.provider, model.id);
2240
2206
  this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
2241
2207
  // Re-clamp thinking level for new model's capabilities
@@ -2949,46 +2915,41 @@ export class AgentSession {
2949
2915
  */
2950
2916
  async _checkCompaction(assistantMessage, skipAbortedCheck = true) {
2951
2917
  const settings = this.settingsManager.getCompactionSettings();
2952
- if (!settings.enabled)
2953
- return false;
2954
- // Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
2955
- if (skipAbortedCheck && assistantMessage.stopReason === "aborted")
2956
- return false;
2957
- const contextWindow = this.model?.contextWindow ?? 0;
2958
- // Skip overflow check if the message came from a different model.
2959
- // This handles the case where user switched from a smaller-context model (e.g. opus)
2960
- // to a larger-context model (e.g. codex) - the overflow error from the old model
2961
- // shouldn't trigger compaction for the new model.
2962
- const sameModel = this.model && assistantMessage.provider === this.model.provider && assistantMessage.model === this.model.id;
2963
- // Image-bearing turns are auto-routed to a vision-capable model (gpt-5.6-luna)
2964
- // while the session model stays text-only (deepseek). An overflow from that
2965
- // routed model must still count as the session's own overflow: compaction is
2966
- // the fix, and the raw context_length_exceeded error must never reach the user.
2967
- const visionRouteOverflow = this.model != null &&
2968
- !(this.model.input ?? []).includes("image") &&
2969
- isVisionRouteModel({ provider: assistantMessage.provider, id: assistantMessage.model });
2970
- // Skip compaction checks if this assistant message is older than the latest
2971
- // compaction boundary. This prevents a stale pre-compaction usage/error
2972
- // from retriggering compaction on the first prompt after compaction.
2918
+ // Stale pre-compaction usage/errors must not retrigger compaction on the
2919
+ // first prompt after one finished.
2973
2920
  const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());
2974
- const assistantIsFromBeforeCompaction = compactionEntry !== null && assistantMessage.timestamp <= new Date(compactionEntry.timestamp).getTime();
2975
- if (assistantIsFromBeforeCompaction) {
2921
+ if (shouldSkipCompactionCheck({
2922
+ enabled: settings.enabled,
2923
+ skipAbortedCheck,
2924
+ stopReason: assistantMessage.stopReason,
2925
+ messageTimestamp: assistantMessage.timestamp,
2926
+ latestCompactionTimestamp: compactionEntry ? new Date(compactionEntry.timestamp).getTime() : undefined,
2927
+ })) {
2976
2928
  return false;
2977
2929
  }
2978
- // Case 1: Overflow - LLM returned context overflow error
2979
- if ((sameModel || visionRouteOverflow) && isContextOverflow(assistantMessage, contextWindow)) {
2980
- if (this._overflowRecoveryAttempted) {
2930
+ const contextWindow = this.model?.contextWindow ?? 0;
2931
+ // Case 1: Overflow - LLM returned context overflow error. Only counts when
2932
+ // the failing message belongs to the session model — or to the auto-routed
2933
+ // vision model while the session model is text-only.
2934
+ if (isSessionModelOverflow({
2935
+ message: assistantMessage,
2936
+ contextWindow,
2937
+ sessionProvider: this.model?.provider,
2938
+ sessionModelId: this.model?.id,
2939
+ sessionInputs: this.model?.input,
2940
+ })) {
2941
+ if (this._overflowRecoveryAttempts >= MAX_OVERFLOW_RECOVERY_ATTEMPTS) {
2981
2942
  this._emit({
2982
2943
  type: "compaction_end",
2983
2944
  reason: "overflow",
2984
2945
  result: undefined,
2985
2946
  aborted: false,
2986
2947
  willRetry: false,
2987
- errorMessage: "Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.",
2948
+ errorMessage: "Context overflow recovery failed after two staged compact-and-retry attempts. Reduce the latest input or switch to a model with a larger effective context window.",
2988
2949
  });
2989
2950
  return false;
2990
2951
  }
2991
- this._overflowRecoveryAttempted = true;
2952
+ this._overflowRecoveryAttempts++;
2992
2953
  // Remove the error message from agent state (it IS saved to session for history,
2993
2954
  // but we don't want it in context for the retry)
2994
2955
  const messages = this.agent.state.messages;
@@ -3026,11 +2987,24 @@ export class AgentSession {
3026
2987
  }
3027
2988
  return false;
3028
2989
  }
2990
+ _overflowCompactionSettings(settings, attempt) {
2991
+ if (attempt < MAX_OVERFLOW_RECOVERY_ATTEMPTS)
2992
+ return settings;
2993
+ return {
2994
+ ...settings,
2995
+ reserveTokens: Math.min(settings.reserveTokens, OVERFLOW_RECOVERY_EMERGENCY_TOKENS),
2996
+ reservedOutputTokens: Math.min(settings.reservedOutputTokens ?? settings.reserveTokens, OVERFLOW_RECOVERY_EMERGENCY_TOKENS),
2997
+ keepRecentTokens: Math.min(settings.keepRecentTokens, OVERFLOW_RECOVERY_EMERGENCY_TOKENS),
2998
+ };
2999
+ }
3029
3000
  /**
3030
3001
  * Internal: Run auto-compaction with events.
3031
3002
  */
3032
3003
  async _runAutoCompaction(reason, willRetry, emergency = reason === "overflow" || this._thresholdCompactionEmergency) {
3033
- const settings = this.settingsManager.getCompactionSettings();
3004
+ const configuredSettings = this.settingsManager.getCompactionSettings();
3005
+ const settings = reason === "overflow"
3006
+ ? this._overflowCompactionSettings(configuredSettings, this._overflowRecoveryAttempts)
3007
+ : configuredSettings;
3034
3008
  this._emit({ type: "compaction_start", reason });
3035
3009
  this._autoCompactionAbortController = new AbortController();
3036
3010
  try {
@@ -3666,25 +3640,12 @@ export class AgentSession {
3666
3640
  // =========================================================================
3667
3641
  // Auto-Retry
3668
3642
  // =========================================================================
3669
- _isNonRetryableProviderLimitError(errorMessage) {
3670
- return /GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota exceeded|billing/i.test(errorMessage);
3671
- }
3672
3643
  /**
3673
3644
  * Check if an error is retryable (overloaded, rate limit, server errors).
3674
3645
  * Context overflow errors are NOT retryable (handled by compaction instead).
3675
3646
  */
3676
3647
  _isRetryableError(message) {
3677
- if (message.stopReason !== "error" || !message.errorMessage)
3678
- return false;
3679
- // Context overflow is handled by compaction, not retry
3680
- const contextWindow = this.model?.contextWindow ?? 0;
3681
- if (isContextOverflow(message, contextWindow))
3682
- return false;
3683
- const err = message.errorMessage;
3684
- if (this._isNonRetryableProviderLimitError(err))
3685
- return false;
3686
- // Shared contract with provider-resilience.ts (Fable safety stop, K3 orphan tool_call_id, terminated).
3687
- return isTransientProviderErrorMessage(err);
3648
+ return isRetryableAssistantError(message, this.model?.contextWindow ?? 0);
3688
3649
  }
3689
3650
  /**
3690
3651
  * Retry policy + callbacks shared by compaction and branch-summary summarization calls.
@@ -3721,8 +3682,12 @@ export class AgentSession {
3721
3682
  /** Eject sticky safety model at prompt boundary (session resume / leftover default). */
3722
3683
  async _ejectStickySafetyModelIfNeeded() {
3723
3684
  const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
3724
- if (!resilience.blockStickySafetyModels)
3685
+ if (!shouldEjectStickySafetyModel({
3686
+ blockStickySafetyModels: resilience.blockStickySafetyModels,
3687
+ modelPinned: this._modelPinned,
3688
+ })) {
3725
3689
  return;
3690
+ }
3726
3691
  const current = this.model;
3727
3692
  if (!current || !isStickySafetyModel(current.id, current.provider))
3728
3693
  return;
@@ -3750,19 +3715,28 @@ export class AgentSession {
3750
3715
  */
3751
3716
  async _maybeFailoverFromSafetyStop(message) {
3752
3717
  const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
3753
- if (!resilience.autoFailoverOnSafetyStop)
3718
+ if (!shouldHonorSafetyFailover({
3719
+ autoFailoverOnSafetyStop: resilience.autoFailoverOnSafetyStop,
3720
+ modelPinned: this._modelPinned,
3721
+ modelId: this.model?.id,
3722
+ provider: this.model?.provider,
3723
+ })) {
3754
3724
  return undefined;
3755
- if (!isContentSafetyStopMessage(message.errorMessage))
3725
+ }
3726
+ // Fire on safety-stop FPs AND on billing/quota exhaustion: both mean the
3727
+ // current model cannot finish this turn and same-model retry is useless.
3728
+ if (!isFailoverTriggerError(message.errorMessage)) {
3756
3729
  return undefined;
3730
+ }
3757
3731
  const current = this.model;
3758
3732
  // v10.0-Ω: do NOT gate on isStickySafetyModel(source).
3759
3733
  // claude-opus-5 emits the same stop_reason=refusal FP; same-model retry is useless.
3760
3734
  // v10.3-Ω: mark the model that just refused so we never re-pick it this turn.
3761
3735
  if (current)
3762
- this._refusedModels.add(`${current.provider}/${current.id}`);
3736
+ this._refusedModels.add(failoverModelKey(current.provider, current.id));
3763
3737
  const pick = pickFailoverCandidate(resilience.failoverCandidates, current ? { provider: current.provider, id: current.id } : undefined, (c) => {
3764
3738
  // Skip any model that already refused/failed this turn → advance the chain.
3765
- if (this._refusedModels.has(`${c.provider}/${c.id}`))
3739
+ if (this._refusedModels.has(failoverModelKey(c.provider, c.id)))
3766
3740
  return false;
3767
3741
  const next = this._modelRegistry.find(c.provider, c.id);
3768
3742
  if (!next)
@@ -3780,34 +3754,32 @@ export class AgentSession {
3780
3754
  }
3781
3755
  catch {
3782
3756
  // setModel failed (auth/guard) — blacklist so next retry advances further.
3783
- this._refusedModels.add(`${pick.provider}/${pick.id}`);
3757
+ this._refusedModels.add(failoverModelKey(pick.provider, pick.id));
3784
3758
  return undefined;
3785
3759
  }
3786
3760
  }
3787
3761
  async _prepareRetry(message) {
3788
3762
  const settings = this.settingsManager.getRetrySettings();
3789
- if (!settings.enabled) {
3790
- return false;
3791
- }
3792
- this._retryAttempt++;
3793
- if (this._retryAttempt > settings.maxRetries) {
3794
- // Preserve the completed attempt count so post-run handling can emit the final failure.
3795
- this._retryAttempt--;
3763
+ const maxRetries = retryBudgetForAssistantError(message, settings.maxRetries);
3764
+ const attempt = nextRetryAttempt({
3765
+ enabled: settings.enabled,
3766
+ completedAttempts: this._retryAttempt,
3767
+ maxRetries,
3768
+ });
3769
+ if (attempt === undefined)
3796
3770
  return false;
3797
- }
3771
+ this._retryAttempt = attempt;
3798
3772
  // Content/safety stop (Fable/Opus/Sonnet): switch model BEFORE delay so retry is not same-model refusal.
3799
3773
  const failoverTo = await this._maybeFailoverFromSafetyStop(message);
3800
3774
  // Safety stops are usually immediate false positives — short delay after failover, full backoff otherwise.
3801
- const delayMs = failoverTo
3802
- ? Math.min(400, settings.baseDelayMs)
3803
- : settings.baseDelayMs * 2 ** (this._retryAttempt - 1);
3775
+ const delayMs = computeRetryDelayMs(settings.baseDelayMs, attempt, failoverTo !== undefined);
3804
3776
  const errorMessage = failoverTo
3805
3777
  ? `${message.errorMessage || "content/safety stop"} → failover ${failoverTo}`
3806
3778
  : message.errorMessage || "Unknown error";
3807
3779
  this._emit({
3808
3780
  type: "auto_retry_start",
3809
- attempt: this._retryAttempt,
3810
- maxAttempts: settings.maxRetries,
3781
+ attempt,
3782
+ maxAttempts: maxRetries,
3811
3783
  delayMs,
3812
3784
  errorMessage,
3813
3785
  });
@@ -4319,21 +4291,6 @@ export class AgentSession {
4319
4291
  return this._extensionRunner;
4320
4292
  }
4321
4293
  }
4322
- // ---------------------------------------------------------------------------
4323
- // Context budget constants
4324
- // ---------------------------------------------------------------------------
4325
- /** Fraction of contextWindow reserved for model response generation. */
4326
- const RESPONSE_RESERVE_RATIO = 0.2;
4327
- /** Fraction of contextWindow held back as safety margin for token-count imprecision. */
4328
- const SAFETY_MARGIN_RATIO = 0.1;
4329
- // Default prompt budget = contextWindow - responseReserve - safetyMargin.
4330
- // With the defaults above (0.2 + 0.1) this yields ~0.70 of contextWindow, but it
4331
- // stays correct when responseReserve is overridden by model.maxTokens.
4332
- /** Absolute floor for maxPromptTokens — below this the budget is meaningless. */
4333
- const MIN_PROMPT_TOKENS = 4000;
4334
- /** Legacy defaults when no model contextWindow is known. */
4335
- const LEGACY_MAX_PROMPT_TOKENS = 60_000;
4336
- const LEGACY_RESPONSE_RESERVE_TOKENS = 8_192;
4337
4294
  function parsePositiveIntegerEnv(name) {
4338
4295
  const raw = process.env[name];
4339
4296
  if (raw === undefined || raw.trim() === "") {