open-multi-agent-kit 0.96.0 → 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 (224) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +24 -6
  3. package/dist/commands/doctor-provider.d.ts.map +1 -1
  4. package/dist/commands/doctor-provider.js +15 -20
  5. package/dist/commands/doctor-provider.js.map +1 -1
  6. package/dist/commands/sdk-session-cli.d.ts +24 -0
  7. package/dist/commands/sdk-session-cli.d.ts.map +1 -0
  8. package/dist/commands/sdk-session-cli.js +225 -0
  9. package/dist/commands/sdk-session-cli.js.map +1 -0
  10. package/dist/core/advisory-judge-model.d.ts +18 -0
  11. package/dist/core/advisory-judge-model.d.ts.map +1 -0
  12. package/dist/core/advisory-judge-model.js +168 -0
  13. package/dist/core/advisory-judge-model.js.map +1 -0
  14. package/dist/core/advisory-judge-response.d.ts +8 -0
  15. package/dist/core/advisory-judge-response.d.ts.map +1 -0
  16. package/dist/core/advisory-judge-response.js +61 -0
  17. package/dist/core/advisory-judge-response.js.map +1 -0
  18. package/dist/core/advisory-judge-types.d.ts +53 -0
  19. package/dist/core/advisory-judge-types.d.ts.map +1 -0
  20. package/dist/core/advisory-judge-types.js +2 -0
  21. package/dist/core/advisory-judge-types.js.map +1 -0
  22. package/dist/core/advisory-judge.d.ts +8 -0
  23. package/dist/core/advisory-judge.d.ts.map +1 -0
  24. package/dist/core/advisory-judge.js +178 -0
  25. package/dist/core/advisory-judge.js.map +1 -0
  26. package/dist/core/agent-session-services.d.ts +1 -0
  27. package/dist/core/agent-session-services.d.ts.map +1 -1
  28. package/dist/core/agent-session-services.js +1 -0
  29. package/dist/core/agent-session-services.js.map +1 -1
  30. package/dist/core/agent-session.d.ts +4 -1
  31. package/dist/core/agent-session.d.ts.map +1 -1
  32. package/dist/core/agent-session.js +107 -174
  33. package/dist/core/agent-session.js.map +1 -1
  34. package/dist/core/auth-storage.d.ts.map +1 -1
  35. package/dist/core/auth-storage.js +28 -4
  36. package/dist/core/auth-storage.js.map +1 -1
  37. package/dist/core/compaction-gate.d.ts +28 -0
  38. package/dist/core/compaction-gate.d.ts.map +1 -0
  39. package/dist/core/compaction-gate.js +34 -0
  40. package/dist/core/compaction-gate.js.map +1 -0
  41. package/dist/core/context-budget-system-prompt-items.d.ts.map +1 -1
  42. package/dist/core/context-budget-system-prompt-items.js +5 -3
  43. package/dist/core/context-budget-system-prompt-items.js.map +1 -1
  44. package/dist/core/domain-loadouts.d.ts.map +1 -1
  45. package/dist/core/domain-loadouts.js +2 -2
  46. package/dist/core/domain-loadouts.js.map +1 -1
  47. package/dist/core/durable-goal-checkpoint.d.ts +6 -0
  48. package/dist/core/durable-goal-checkpoint.d.ts.map +1 -0
  49. package/dist/core/durable-goal-checkpoint.js +195 -0
  50. package/dist/core/durable-goal-checkpoint.js.map +1 -0
  51. package/dist/core/durable-goal-parse.d.ts +3 -0
  52. package/dist/core/durable-goal-parse.d.ts.map +1 -0
  53. package/dist/core/durable-goal-parse.js +86 -0
  54. package/dist/core/durable-goal-parse.js.map +1 -0
  55. package/dist/core/durable-goal-reducer.d.ts +3 -0
  56. package/dist/core/durable-goal-reducer.d.ts.map +1 -0
  57. package/dist/core/durable-goal-reducer.js +175 -0
  58. package/dist/core/durable-goal-reducer.js.map +1 -0
  59. package/dist/core/durable-goal-store.d.ts +19 -0
  60. package/dist/core/durable-goal-store.d.ts.map +1 -0
  61. package/dist/core/durable-goal-store.js +126 -0
  62. package/dist/core/durable-goal-store.js.map +1 -0
  63. package/dist/core/durable-goal.d.ts +92 -0
  64. package/dist/core/durable-goal.d.ts.map +1 -0
  65. package/dist/core/durable-goal.js +62 -0
  66. package/dist/core/durable-goal.js.map +1 -0
  67. package/dist/core/extensions/builtin/goal-controller.d.ts +3 -0
  68. package/dist/core/extensions/builtin/goal-controller.d.ts.map +1 -0
  69. package/dist/core/extensions/builtin/goal-controller.js +125 -0
  70. package/dist/core/extensions/builtin/goal-controller.js.map +1 -0
  71. package/dist/core/extensions/builtin/identical-loop.d.ts +3 -0
  72. package/dist/core/extensions/builtin/identical-loop.d.ts.map +1 -0
  73. package/dist/core/extensions/builtin/identical-loop.js +31 -0
  74. package/dist/core/extensions/builtin/identical-loop.js.map +1 -0
  75. package/dist/core/extensions/builtin/prompt-preset.d.ts +3 -0
  76. package/dist/core/extensions/builtin/prompt-preset.d.ts.map +1 -0
  77. package/dist/core/extensions/builtin/prompt-preset.js +13 -0
  78. package/dist/core/extensions/builtin/prompt-preset.js.map +1 -0
  79. package/dist/core/extensions/builtin/tool-pair-repair.d.ts +3 -0
  80. package/dist/core/extensions/builtin/tool-pair-repair.d.ts.map +1 -0
  81. package/dist/core/extensions/builtin/tool-pair-repair.js +12 -0
  82. package/dist/core/extensions/builtin/tool-pair-repair.js.map +1 -0
  83. package/dist/core/extensions/types.d.ts +6 -2
  84. package/dist/core/extensions/types.d.ts.map +1 -1
  85. package/dist/core/extensions/types.js.map +1 -1
  86. package/dist/core/goal-continuation.d.ts +13 -0
  87. package/dist/core/goal-continuation.d.ts.map +1 -0
  88. package/dist/core/goal-continuation.js +10 -0
  89. package/dist/core/goal-continuation.js.map +1 -0
  90. package/dist/core/grok-harness.d.ts +1 -1
  91. package/dist/core/grok-harness.d.ts.map +1 -1
  92. package/dist/core/grok-harness.js +1 -1
  93. package/dist/core/grok-harness.js.map +1 -1
  94. package/dist/core/grok-playbook.d.ts +1 -1
  95. package/dist/core/grok-playbook.d.ts.map +1 -1
  96. package/dist/core/grok-playbook.js +1 -1
  97. package/dist/core/grok-playbook.js.map +1 -1
  98. package/dist/core/identical-loop.d.ts +15 -0
  99. package/dist/core/identical-loop.d.ts.map +1 -0
  100. package/dist/core/identical-loop.js +34 -0
  101. package/dist/core/identical-loop.js.map +1 -0
  102. package/dist/core/index.d.ts +1 -1
  103. package/dist/core/index.d.ts.map +1 -1
  104. package/dist/core/index.js +1 -1
  105. package/dist/core/index.js.map +1 -1
  106. package/dist/core/model-prompt-policy.d.ts +11 -0
  107. package/dist/core/model-prompt-policy.d.ts.map +1 -0
  108. package/dist/core/model-prompt-policy.js +17 -0
  109. package/dist/core/model-prompt-policy.js.map +1 -0
  110. package/dist/core/model-registry.d.ts.map +1 -1
  111. package/dist/core/model-registry.js +16 -4
  112. package/dist/core/model-registry.js.map +1 -1
  113. package/dist/core/model-resolver.d.ts.map +1 -1
  114. package/dist/core/model-resolver.js +2 -2
  115. package/dist/core/model-resolver.js.map +1 -1
  116. package/dist/core/prompt-budget.d.ts +21 -0
  117. package/dist/core/prompt-budget.d.ts.map +1 -0
  118. package/dist/core/prompt-budget.js +51 -0
  119. package/dist/core/prompt-budget.js.map +1 -0
  120. package/dist/core/prompt-cache.d.ts +14 -0
  121. package/dist/core/prompt-cache.d.ts.map +1 -0
  122. package/dist/core/prompt-cache.js +16 -0
  123. package/dist/core/prompt-cache.js.map +1 -0
  124. package/dist/core/prompt-preset.d.ts +8 -0
  125. package/dist/core/prompt-preset.d.ts.map +1 -0
  126. package/dist/core/prompt-preset.js +56 -0
  127. package/dist/core/prompt-preset.js.map +1 -0
  128. package/dist/core/provider-resilience.d.ts +16 -0
  129. package/dist/core/provider-resilience.d.ts.map +1 -1
  130. package/dist/core/provider-resilience.js +23 -1
  131. package/dist/core/provider-resilience.js.map +1 -1
  132. package/dist/core/provider-retry.d.ts +29 -0
  133. package/dist/core/provider-retry.d.ts.map +1 -0
  134. package/dist/core/provider-retry.js +49 -0
  135. package/dist/core/provider-retry.js.map +1 -0
  136. package/dist/core/provider-usage.d.ts +2 -1
  137. package/dist/core/provider-usage.d.ts.map +1 -1
  138. package/dist/core/provider-usage.js +40 -6
  139. package/dist/core/provider-usage.js.map +1 -1
  140. package/dist/core/resource-loader.d.ts.map +1 -1
  141. package/dist/core/resource-loader.js +32 -0
  142. package/dist/core/resource-loader.js.map +1 -1
  143. package/dist/core/sandbox/backend.d.ts +12 -0
  144. package/dist/core/sandbox/backend.d.ts.map +1 -1
  145. package/dist/core/sandbox/backend.js +52 -42
  146. package/dist/core/sandbox/backend.js.map +1 -1
  147. package/dist/core/sandbox/policy.d.ts +2 -0
  148. package/dist/core/sandbox/policy.d.ts.map +1 -1
  149. package/dist/core/sandbox/policy.js +2 -1
  150. package/dist/core/sandbox/policy.js.map +1 -1
  151. package/dist/core/sdk.d.ts +2 -0
  152. package/dist/core/sdk.d.ts.map +1 -1
  153. package/dist/core/sdk.js +1 -0
  154. package/dist/core/sdk.js.map +1 -1
  155. package/dist/core/session-system-prompt.d.ts +28 -0
  156. package/dist/core/session-system-prompt.d.ts.map +1 -0
  157. package/dist/core/session-system-prompt.js +37 -0
  158. package/dist/core/session-system-prompt.js.map +1 -0
  159. package/dist/core/strict-record.d.ts +2 -0
  160. package/dist/core/strict-record.d.ts.map +1 -0
  161. package/dist/core/strict-record.js +9 -0
  162. package/dist/core/strict-record.js.map +1 -0
  163. package/dist/core/tool-pair-repair.d.ts +11 -0
  164. package/dist/core/tool-pair-repair.d.ts.map +1 -0
  165. package/dist/core/tool-pair-repair.js +38 -0
  166. package/dist/core/tool-pair-repair.js.map +1 -0
  167. package/dist/core/tools/artifact-spill.d.ts +13 -0
  168. package/dist/core/tools/artifact-spill.d.ts.map +1 -0
  169. package/dist/core/tools/artifact-spill.js +26 -0
  170. package/dist/core/tools/artifact-spill.js.map +1 -0
  171. package/dist/core/tools/bash.d.ts +7 -4
  172. package/dist/core/tools/bash.d.ts.map +1 -1
  173. package/dist/core/tools/bash.js +8 -7
  174. package/dist/core/tools/bash.js.map +1 -1
  175. package/dist/core/tools/index.d.ts +1 -1
  176. package/dist/core/tools/index.d.ts.map +1 -1
  177. package/dist/core/tools/index.js.map +1 -1
  178. package/dist/core/tools/read.d.ts +1 -0
  179. package/dist/core/tools/read.d.ts.map +1 -1
  180. package/dist/core/tools/read.js +10 -1
  181. package/dist/core/tools/read.js.map +1 -1
  182. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
  183. package/dist/core/tools/tool-definition-wrapper.js +2 -0
  184. package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
  185. package/dist/index.d.ts +18 -1
  186. package/dist/index.d.ts.map +1 -1
  187. package/dist/index.js +18 -0
  188. package/dist/index.js.map +1 -1
  189. package/dist/main.d.ts.map +1 -1
  190. package/dist/main.js +8 -0
  191. package/dist/main.js.map +1 -1
  192. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  193. package/dist/modes/interactive/theme/theme.js +14 -3
  194. package/dist/modes/interactive/theme/theme.js.map +1 -1
  195. package/docs/adaptorch-preview-spec.md +1 -1
  196. package/docs/adaptorch-preview.md +2 -2
  197. package/docs/book-to-skill.md +1 -1
  198. package/docs/containerization.md +9 -0
  199. package/docs/docs.json +11 -3
  200. package/docs/environment-variables.md +11 -4
  201. package/docs/extensions.md +14 -1
  202. package/docs/grok-harness.md +35 -12
  203. package/docs/index.md +7 -6
  204. package/docs/loadout-domains/grok-harness.md +2 -2
  205. package/docs/models.md +10 -9
  206. package/docs/provider-resilience.md +9 -3
  207. package/docs/providers.md +9 -4
  208. package/docs/run-protocol.md +32 -0
  209. package/docs/sdk.md +96 -3
  210. package/docs/security.md +22 -32
  211. package/docs/sessions.md +14 -0
  212. package/docs/usage.md +37 -15
  213. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  214. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  215. package/examples/extensions/custom-provider-gitlab-duo/index.ts +6 -1
  216. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  217. package/examples/extensions/gondolin/package-lock.json +2 -2
  218. package/examples/extensions/gondolin/package.json +1 -1
  219. package/examples/extensions/sandbox/package-lock.json +2 -2
  220. package/examples/extensions/sandbox/package.json +1 -1
  221. package/examples/extensions/with-deps/package-lock.json +2 -2
  222. package/examples/extensions/with-deps/package.json +1 -1
  223. package/npm-shrinkwrap.json +18 -18
  224. package/package.json +6 -6
@@ -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";
@@ -70,9 +71,13 @@ function firstTextContent(result) {
70
71
  return undefined;
71
72
  }
72
73
  import { redactCredentialShapedContent } from "./compaction/transaction.js";
74
+ import { selectContextFilesForModel } from "./model-prompt-policy.js";
73
75
  import { findExactModelReferenceMatch } from "./model-resolver.js";
76
+ import { computePromptTokenBudget } from "./prompt-budget.js";
77
+ import { classifyPromptCacheTransition } from "./prompt-cache.js";
74
78
  import { expandPromptTemplate } from "./prompt-templates.js";
75
- import { isContentSafetyStopMessage, isQuotaExhaustionMessage, 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";
76
81
  import { getBiasStepsForCell, parseRouterBiasSnapshot } from "./reasoning-router-bias.js";
77
82
  import { classifyTaskV4, deriveRouterFeedbackFeaturesV4, resolveThinkingLevelV4WithUncertainty, TASK_CLASS_THINKING_LEVELS_V4, } from "./reasoning-router-v4.js";
78
83
  import { redactSensitiveText, redactSensitiveTextForced } from "./redaction.js";
@@ -84,6 +89,7 @@ import { SessionBashService } from "./session-bash-service.js";
84
89
  import { SessionCompactionService } from "./session-compaction-service.js";
85
90
  import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
86
91
  import { acquireSessionOwnerLeaseSync } from "./session-owner-lease.js";
92
+ import { assembleSessionSystemPrompt } from "./session-system-prompt.js";
87
93
  import { classifySessionTermination, } from "./session-termination.js";
88
94
  import { createSyntheticSourceInfo } from "./source-info.js";
89
95
  import { buildSystemPromptPlan } from "./system-prompt.js";
@@ -315,6 +321,8 @@ export class AgentSession {
315
321
  // v10.3-Ω: models that already refused/failed this turn — failover must advance,
316
322
  // not re-pick the same candidate (claude-opus-5 → grok → grok → grok loop fix).
317
323
  _refusedModels = new Set();
324
+ /** True when `--model` / `--provider` pinned this process to one model. */
325
+ _modelPinned;
318
326
  // Bash execution state
319
327
  _bashService;
320
328
  _compactionService;
@@ -383,6 +391,7 @@ export class AgentSession {
383
391
  this.sessionManager = config.sessionManager;
384
392
  this.settingsManager = config.settingsManager;
385
393
  this._scopedModels = config.scopedModels ?? [];
394
+ this._modelPinned = config.modelPinned === true;
386
395
  this._resourceLoader = config.resourceLoader;
387
396
  this._customTools = config.customTools ?? [];
388
397
  this._cwd = config.cwd;
@@ -1043,13 +1052,14 @@ export class AgentSession {
1043
1052
  };
1044
1053
  _willRetryAfterAgentEnd(event) {
1045
1054
  const settings = this.settingsManager.getRetrySettings();
1046
- if (!settings.enabled || this._retryAttempt >= settings.maxRetries) {
1055
+ if (!settings.enabled)
1047
1056
  return false;
1048
- }
1049
1057
  for (let i = event.messages.length - 1; i >= 0; i--) {
1050
1058
  const message = event.messages[i];
1051
1059
  if (message.role === "assistant") {
1052
- return this._isRetryableError(message);
1060
+ const assistant = message;
1061
+ const maxRetries = retryBudgetForAssistantError(assistant, settings.maxRetries);
1062
+ return this._retryAttempt < maxRetries && this._isRetryableError(assistant);
1053
1063
  }
1054
1064
  }
1055
1065
  return false;
@@ -1554,7 +1564,7 @@ export class AgentSession {
1554
1564
  }
1555
1565
  return undefined;
1556
1566
  }
1557
- _getContextBudgetOptions(queryContext = this._extractCurrentQuery()) {
1567
+ _getContextBudgetOptions(queryContext = this._extractCurrentQuery(), model = this.model) {
1558
1568
  const contextGovernorOverride = process.env.OMK_CONTEXT_GOVERNOR;
1559
1569
  if (contextGovernorOverride === "0") {
1560
1570
  return undefined;
@@ -1562,56 +1572,24 @@ export class AgentSession {
1562
1572
  if (contextGovernorOverride !== "1" && !this.settingsManager.getContextBudgetEnabled()) {
1563
1573
  return undefined;
1564
1574
  }
1565
- const contextWindow = this.model?.contextWindow ?? 0;
1566
- // --- maxPromptTokens ---
1567
- // Priority 1: explicit env override (user knows best)
1568
- const envMaxPrompt = parsePositiveIntegerEnv("OMK_CONTEXT_GOVERNOR_MAX_PROMPT_TOKENS");
1569
- let maxPromptTokens;
1570
- let responseReserveTokens;
1571
- if (envMaxPrompt !== undefined) {
1572
- // User override — use as-is for prompt, derive response reserve normally
1573
- maxPromptTokens = envMaxPrompt;
1574
- responseReserveTokens =
1575
- parsePositiveIntegerEnv("OMK_CONTEXT_GOVERNOR_RESPONSE_RESERVE_TOKENS") ??
1576
- this._computeResponseReserve(contextWindow);
1577
- }
1578
- else if (contextWindow > 0) {
1579
- // Dynamic: derive from model contextWindow
1580
- const envPromptRatio = parsePositiveFloatEnv("OMK_CONTEXT_GOVERNOR_PROMPT_RATIO");
1581
- const envResponseRatio = parsePositiveFloatEnv("OMK_CONTEXT_GOVERNOR_RESPONSE_RATIO");
1582
- responseReserveTokens =
1583
- parsePositiveIntegerEnv("OMK_CONTEXT_GOVERNOR_RESPONSE_RESERVE_TOKENS") ??
1584
- this._computeResponseReserve(contextWindow, envResponseRatio);
1585
- const safetyMargin = Math.floor(contextWindow * SAFETY_MARGIN_RATIO);
1586
- if (envPromptRatio !== undefined && envPromptRatio > 0 && envPromptRatio < 1) {
1587
- maxPromptTokens = Math.floor(contextWindow * envPromptRatio);
1588
- }
1589
- else {
1590
- // Default: contextWindow minus reserves and safety margin
1591
- maxPromptTokens = contextWindow - responseReserveTokens - safetyMargin;
1592
- }
1593
- }
1594
- else {
1595
- // No model info — legacy fallback
1596
- maxPromptTokens = LEGACY_MAX_PROMPT_TOKENS;
1597
- responseReserveTokens =
1598
- parsePositiveIntegerEnv("OMK_CONTEXT_GOVERNOR_RESPONSE_RESERVE_TOKENS") ?? LEGACY_RESPONSE_RESERVE_TOKENS;
1599
- }
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;
1600
1586
  const cacheProvider = this._contextBudgetCacheProvider ?? this._createContextBudgetCacheProvider();
1601
1587
  cacheProvider.setInvalidationSnapshot?.(this._contextCacheInvalidationSnapshot);
1602
1588
  this._contextBudgetCacheProvider = cacheProvider;
1603
- // Enforce floor
1604
- if (maxPromptTokens < MIN_PROMPT_TOKENS) {
1605
- maxPromptTokens = MIN_PROMPT_TOKENS;
1606
- }
1607
- // Ensure responseReserve does not exceed maxPromptTokens
1608
- if (responseReserveTokens >= maxPromptTokens) {
1609
- responseReserveTokens = Math.max(Math.floor(maxPromptTokens / 4), LEGACY_RESPONSE_RESERVE_TOKENS);
1610
- }
1611
1589
  return {
1612
1590
  maxPromptTokens,
1613
1591
  responseReserveTokens,
1614
- modelId: this.model?.id ?? "unknown",
1592
+ modelId: model?.id ?? "unknown",
1615
1593
  tokenizerMode: parseTokenizerModeEnv(process.env.OMK_CONTEXT_GOVERNOR_TOKENIZER),
1616
1594
  activeSkillNames: parseCommaSeparatedEnv(process.env.OMK_CONTEXT_GOVERNOR_ACTIVE_SKILLS),
1617
1595
  queryContext,
@@ -1622,15 +1600,6 @@ export class AgentSession {
1622
1600
  * Compute responseReserveTokens from contextWindow.
1623
1601
  * Prefers the model's own maxTokens when available, otherwise uses a ratio.
1624
1602
  */
1625
- _computeResponseReserve(contextWindow, overrideRatio) {
1626
- // Prefer model's maxTokens (actual output limit) if available and reasonable
1627
- const modelMaxTokens = this.model?.maxTokens;
1628
- if (modelMaxTokens !== undefined && modelMaxTokens > 0 && modelMaxTokens < contextWindow) {
1629
- return modelMaxTokens;
1630
- }
1631
- const ratio = overrideRatio ?? RESPONSE_RESERVE_RATIO;
1632
- return Math.max(Math.floor(contextWindow * ratio), LEGACY_RESPONSE_RESERVE_TOKENS);
1633
- }
1634
1603
  _recordPromptCachePlan(reason) {
1635
1604
  const boundary = this.agent.state.systemPromptCacheBoundary;
1636
1605
  const scope = this.model ? `${this.model.provider}/${this.model.id}` : "unknown";
@@ -1641,60 +1610,40 @@ export class AgentSession {
1641
1610
  messages: [],
1642
1611
  tools: this.agent.state.tools,
1643
1612
  }, scope);
1644
- if (!nextKey) {
1613
+ const transition = classifyPromptCacheTransition(this._promptCacheKey, nextKey);
1614
+ if (transition.kind === "bypass") {
1645
1615
  this._promptCacheBoundaryBypasses += 1;
1646
1616
  this._promptCacheStablePrefixCharacters = 0;
1647
- if (this._promptCacheKey !== undefined) {
1617
+ if (transition.recordBreak) {
1648
1618
  this._promptCacheLastBreakReason = reason;
1649
1619
  }
1650
1620
  this._promptCacheKey = undefined;
1651
1621
  return;
1652
1622
  }
1653
- if (this._promptCacheKey !== undefined && this._promptCacheKey !== nextKey) {
1623
+ if (transition.kind === "changed") {
1654
1624
  this._promptCacheKeyChanges += 1;
1655
1625
  this._promptCacheLastBreakReason = reason;
1656
1626
  }
1657
1627
  this._promptCacheKey = nextKey;
1658
1628
  this._promptCacheStablePrefixCharacters = boundary ?? 0;
1659
1629
  }
1660
- _rebuildSystemPrompt(toolNames) {
1661
- const validToolNames = toolNames.filter((name) => this._toolRegistry.has(name));
1662
- const toolSnippets = {};
1663
- const promptGuidelines = [];
1664
- for (const name of validToolNames) {
1665
- const snippet = this._toolPromptSnippets.get(name);
1666
- if (snippet) {
1667
- toolSnippets[name] = snippet;
1668
- }
1669
- const toolGuidelines = this._toolPromptGuidelines.get(name);
1670
- if (toolGuidelines) {
1671
- promptGuidelines.push(...toolGuidelines);
1672
- }
1673
- }
1674
- const loaderSystemPrompt = this._resourceLoader.getSystemPrompt();
1675
- const loaderAppendSystemPrompt = this._resourceLoader.getAppendSystemPrompt();
1676
- const grokAppend = grokPlaybookAppendForProvider(this.model?.provider);
1677
- const appendParts = [...loaderAppendSystemPrompt];
1678
- if (grokAppend) {
1679
- appendParts.push(grokAppend);
1680
- }
1681
- const appendSystemPrompt = appendParts.length > 0 ? appendParts.join("\n\n") : undefined;
1682
- const loadedSkills = this._resourceLoader.getSkills().skills;
1683
- const loadedContextFiles = this._resourceLoader.getAgentsFiles().agentsFiles;
1684
- this._baseSystemPromptOptions = {
1630
+ _rebuildSystemPrompt(toolNames, model = this.model) {
1631
+ const assembled = assembleSessionSystemPrompt({
1685
1632
  cwd: this._cwd,
1686
- skills: loadedSkills,
1687
- contextFiles: loadedContextFiles,
1688
- customPrompt: loaderSystemPrompt,
1689
- appendSystemPrompt,
1690
- selectedTools: validToolNames,
1691
- toolSnippets,
1692
- promptGuidelines,
1693
- contextBudget: this._getContextBudgetOptions(),
1694
- };
1695
- const built = buildSystemPromptPlan(this._baseSystemPromptOptions);
1696
- this._baseSystemPromptCacheBoundary = built.cacheBoundary;
1697
- 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;
1698
1647
  }
1699
1648
  // =========================================================================
1700
1649
  // Prompting
@@ -2234,16 +2183,25 @@ export class AgentSession {
2234
2183
  if (!this._modelRegistry.hasConfiguredAuth(model)) {
2235
2184
  throw new Error(`No API key for ${model.provider}/${model.id}`);
2236
2185
  }
2237
- // 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`.
2238
2187
  const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
2239
- 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)) {
2240
2193
  throw new Error(stickySafetyBlockMessage(model.id, model.provider));
2241
2194
  }
2242
2195
  // Grok OAuth: block selecting Imagine models as the session chat model.
2243
2196
  assertTextChatModelForCompletion(model.id, model.provider);
2244
2197
  const previousModel = this.model;
2245
2198
  const thinkingLevel = this._getThinkingLevelForModelSwitch();
2199
+ const nextSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames(), model);
2246
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;
2247
2205
  this.sessionManager.appendModelChange(model.provider, model.id);
2248
2206
  this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
2249
2207
  // Re-clamp thinking level for new model's capabilities
@@ -2957,34 +2915,29 @@ export class AgentSession {
2957
2915
  */
2958
2916
  async _checkCompaction(assistantMessage, skipAbortedCheck = true) {
2959
2917
  const settings = this.settingsManager.getCompactionSettings();
2960
- if (!settings.enabled)
2961
- return false;
2962
- // Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
2963
- if (skipAbortedCheck && assistantMessage.stopReason === "aborted")
2964
- return false;
2965
- const contextWindow = this.model?.contextWindow ?? 0;
2966
- // Skip overflow check if the message came from a different model.
2967
- // This handles the case where user switched from a smaller-context model (e.g. opus)
2968
- // to a larger-context model (e.g. codex) - the overflow error from the old model
2969
- // shouldn't trigger compaction for the new model.
2970
- const sameModel = this.model && assistantMessage.provider === this.model.provider && assistantMessage.model === this.model.id;
2971
- // Image-bearing turns are auto-routed to a vision-capable model (gpt-5.6-luna)
2972
- // while the session model stays text-only (deepseek). An overflow from that
2973
- // routed model must still count as the session's own overflow: compaction is
2974
- // the fix, and the raw context_length_exceeded error must never reach the user.
2975
- const visionRouteOverflow = this.model != null &&
2976
- !(this.model.input ?? []).includes("image") &&
2977
- isVisionRouteModel({ provider: assistantMessage.provider, id: assistantMessage.model });
2978
- // Skip compaction checks if this assistant message is older than the latest
2979
- // compaction boundary. This prevents a stale pre-compaction usage/error
2980
- // 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.
2981
2920
  const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());
2982
- const assistantIsFromBeforeCompaction = compactionEntry !== null && assistantMessage.timestamp <= new Date(compactionEntry.timestamp).getTime();
2983
- 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
+ })) {
2984
2928
  return false;
2985
2929
  }
2986
- // Case 1: Overflow - LLM returned context overflow error
2987
- if ((sameModel || visionRouteOverflow) && isContextOverflow(assistantMessage, contextWindow)) {
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
+ })) {
2988
2941
  if (this._overflowRecoveryAttempts >= MAX_OVERFLOW_RECOVERY_ATTEMPTS) {
2989
2942
  this._emit({
2990
2943
  type: "compaction_end",
@@ -3692,20 +3645,7 @@ export class AgentSession {
3692
3645
  * Context overflow errors are NOT retryable (handled by compaction instead).
3693
3646
  */
3694
3647
  _isRetryableError(message) {
3695
- if (message.stopReason !== "error" || !message.errorMessage)
3696
- return false;
3697
- // Context overflow is handled by compaction, not retry
3698
- const contextWindow = this.model?.contextWindow ?? 0;
3699
- if (isContextOverflow(message, contextWindow))
3700
- return false;
3701
- const err = message.errorMessage;
3702
- // Billing/quota exhaustion: useless to retry the same model until the
3703
- // cycle resets, but the turn may still be saved by failing over to the
3704
- // next candidate, so let _prepareRetry run its failover path.
3705
- if (isQuotaExhaustionMessage(err))
3706
- return true;
3707
- // Shared contract with provider-resilience.ts (Fable safety stop, K3 orphan tool_call_id, terminated).
3708
- return isTransientProviderErrorMessage(err);
3648
+ return isRetryableAssistantError(message, this.model?.contextWindow ?? 0);
3709
3649
  }
3710
3650
  /**
3711
3651
  * Retry policy + callbacks shared by compaction and branch-summary summarization calls.
@@ -3742,8 +3682,12 @@ export class AgentSession {
3742
3682
  /** Eject sticky safety model at prompt boundary (session resume / leftover default). */
3743
3683
  async _ejectStickySafetyModelIfNeeded() {
3744
3684
  const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
3745
- if (!resilience.blockStickySafetyModels)
3685
+ if (!shouldEjectStickySafetyModel({
3686
+ blockStickySafetyModels: resilience.blockStickySafetyModels,
3687
+ modelPinned: this._modelPinned,
3688
+ })) {
3746
3689
  return;
3690
+ }
3747
3691
  const current = this.model;
3748
3692
  if (!current || !isStickySafetyModel(current.id, current.provider))
3749
3693
  return;
@@ -3771,11 +3715,17 @@ export class AgentSession {
3771
3715
  */
3772
3716
  async _maybeFailoverFromSafetyStop(message) {
3773
3717
  const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
3774
- 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
+ })) {
3775
3724
  return undefined;
3725
+ }
3776
3726
  // Fire on safety-stop FPs AND on billing/quota exhaustion: both mean the
3777
3727
  // current model cannot finish this turn and same-model retry is useless.
3778
- if (!isContentSafetyStopMessage(message.errorMessage) && !isQuotaExhaustionMessage(message.errorMessage)) {
3728
+ if (!isFailoverTriggerError(message.errorMessage)) {
3779
3729
  return undefined;
3780
3730
  }
3781
3731
  const current = this.model;
@@ -3783,10 +3733,10 @@ export class AgentSession {
3783
3733
  // claude-opus-5 emits the same stop_reason=refusal FP; same-model retry is useless.
3784
3734
  // v10.3-Ω: mark the model that just refused so we never re-pick it this turn.
3785
3735
  if (current)
3786
- this._refusedModels.add(`${current.provider}/${current.id}`);
3736
+ this._refusedModels.add(failoverModelKey(current.provider, current.id));
3787
3737
  const pick = pickFailoverCandidate(resilience.failoverCandidates, current ? { provider: current.provider, id: current.id } : undefined, (c) => {
3788
3738
  // Skip any model that already refused/failed this turn → advance the chain.
3789
- if (this._refusedModels.has(`${c.provider}/${c.id}`))
3739
+ if (this._refusedModels.has(failoverModelKey(c.provider, c.id)))
3790
3740
  return false;
3791
3741
  const next = this._modelRegistry.find(c.provider, c.id);
3792
3742
  if (!next)
@@ -3804,34 +3754,32 @@ export class AgentSession {
3804
3754
  }
3805
3755
  catch {
3806
3756
  // setModel failed (auth/guard) — blacklist so next retry advances further.
3807
- this._refusedModels.add(`${pick.provider}/${pick.id}`);
3757
+ this._refusedModels.add(failoverModelKey(pick.provider, pick.id));
3808
3758
  return undefined;
3809
3759
  }
3810
3760
  }
3811
3761
  async _prepareRetry(message) {
3812
3762
  const settings = this.settingsManager.getRetrySettings();
3813
- if (!settings.enabled) {
3814
- return false;
3815
- }
3816
- this._retryAttempt++;
3817
- if (this._retryAttempt > settings.maxRetries) {
3818
- // Preserve the completed attempt count so post-run handling can emit the final failure.
3819
- 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)
3820
3770
  return false;
3821
- }
3771
+ this._retryAttempt = attempt;
3822
3772
  // Content/safety stop (Fable/Opus/Sonnet): switch model BEFORE delay so retry is not same-model refusal.
3823
3773
  const failoverTo = await this._maybeFailoverFromSafetyStop(message);
3824
3774
  // Safety stops are usually immediate false positives — short delay after failover, full backoff otherwise.
3825
- const delayMs = failoverTo
3826
- ? Math.min(400, settings.baseDelayMs)
3827
- : settings.baseDelayMs * 2 ** (this._retryAttempt - 1);
3775
+ const delayMs = computeRetryDelayMs(settings.baseDelayMs, attempt, failoverTo !== undefined);
3828
3776
  const errorMessage = failoverTo
3829
3777
  ? `${message.errorMessage || "content/safety stop"} → failover ${failoverTo}`
3830
3778
  : message.errorMessage || "Unknown error";
3831
3779
  this._emit({
3832
3780
  type: "auto_retry_start",
3833
- attempt: this._retryAttempt,
3834
- maxAttempts: settings.maxRetries,
3781
+ attempt,
3782
+ maxAttempts: maxRetries,
3835
3783
  delayMs,
3836
3784
  errorMessage,
3837
3785
  });
@@ -4343,21 +4291,6 @@ export class AgentSession {
4343
4291
  return this._extensionRunner;
4344
4292
  }
4345
4293
  }
4346
- // ---------------------------------------------------------------------------
4347
- // Context budget constants
4348
- // ---------------------------------------------------------------------------
4349
- /** Fraction of contextWindow reserved for model response generation. */
4350
- const RESPONSE_RESERVE_RATIO = 0.2;
4351
- /** Fraction of contextWindow held back as safety margin for token-count imprecision. */
4352
- const SAFETY_MARGIN_RATIO = 0.1;
4353
- // Default prompt budget = contextWindow - responseReserve - safetyMargin.
4354
- // With the defaults above (0.2 + 0.1) this yields ~0.70 of contextWindow, but it
4355
- // stays correct when responseReserve is overridden by model.maxTokens.
4356
- /** Absolute floor for maxPromptTokens — below this the budget is meaningless. */
4357
- const MIN_PROMPT_TOKENS = 4000;
4358
- /** Legacy defaults when no model contextWindow is known. */
4359
- const LEGACY_MAX_PROMPT_TOKENS = 60_000;
4360
- const LEGACY_RESPONSE_RESERVE_TOKENS = 8_192;
4361
4294
  function parsePositiveIntegerEnv(name) {
4362
4295
  const raw = process.env[name];
4363
4296
  if (raw === undefined || raw.trim() === "") {