open-multi-agent-kit 0.96.1 → 0.97.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 (267) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +14 -2
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +1 -0
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/commands/resource-doctor-cli.d.ts +22 -0
  7. package/dist/commands/resource-doctor-cli.d.ts.map +1 -0
  8. package/dist/commands/resource-doctor-cli.js +98 -0
  9. package/dist/commands/resource-doctor-cli.js.map +1 -0
  10. package/dist/core/agent-session.d.ts +46 -2
  11. package/dist/core/agent-session.d.ts.map +1 -1
  12. package/dist/core/agent-session.js +373 -12
  13. package/dist/core/agent-session.js.map +1 -1
  14. package/dist/core/attachment-store.d.ts +39 -0
  15. package/dist/core/attachment-store.d.ts.map +1 -0
  16. package/dist/core/attachment-store.js +81 -0
  17. package/dist/core/attachment-store.js.map +1 -0
  18. package/dist/core/command-safety.d.ts +8 -0
  19. package/dist/core/command-safety.d.ts.map +1 -1
  20. package/dist/core/command-safety.js +140 -14
  21. package/dist/core/command-safety.js.map +1 -1
  22. package/dist/core/compaction/compaction.d.ts +6 -1
  23. package/dist/core/compaction/compaction.d.ts.map +1 -1
  24. package/dist/core/compaction/compaction.js +35 -1
  25. package/dist/core/compaction/compaction.js.map +1 -1
  26. package/dist/core/completion-sound.d.ts +109 -0
  27. package/dist/core/completion-sound.d.ts.map +1 -0
  28. package/dist/core/completion-sound.js +165 -0
  29. package/dist/core/completion-sound.js.map +1 -0
  30. package/dist/core/context-budget-governor-v2.d.ts +1 -1
  31. package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
  32. package/dist/core/context-budget-governor-v2.js +1 -1
  33. package/dist/core/context-budget-governor-v2.js.map +1 -1
  34. package/dist/core/context-budget-system-prompt-items.d.ts.map +1 -1
  35. package/dist/core/context-budget-system-prompt-items.js +2 -2
  36. package/dist/core/context-budget-system-prompt-items.js.map +1 -1
  37. package/dist/core/context-budget-v2-cache-keys.d.ts +3 -0
  38. package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
  39. package/dist/core/context-budget-v2-cache-keys.js +2 -0
  40. package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
  41. package/dist/core/context-budget-v2-plan-cache-keys.d.ts.map +1 -1
  42. package/dist/core/context-budget-v2-plan-cache-keys.js +3 -0
  43. package/dist/core/context-budget-v2-plan-cache-keys.js.map +1 -1
  44. package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
  45. package/dist/core/context-budget-v2-planner.js +2 -1
  46. package/dist/core/context-budget-v2-planner.js.map +1 -1
  47. package/dist/core/context-budget-v2-scoring.d.ts +17 -0
  48. package/dist/core/context-budget-v2-scoring.d.ts.map +1 -1
  49. package/dist/core/context-budget-v2-scoring.js +25 -8
  50. package/dist/core/context-budget-v2-scoring.js.map +1 -1
  51. package/dist/core/context-budget-v2-selection.d.ts +2 -2
  52. package/dist/core/context-budget-v2-selection.d.ts.map +1 -1
  53. package/dist/core/context-budget-v2-selection.js +18 -1
  54. package/dist/core/context-budget-v2-selection.js.map +1 -1
  55. package/dist/core/context-budget-v2-types.d.ts +17 -0
  56. package/dist/core/context-budget-v2-types.d.ts.map +1 -1
  57. package/dist/core/context-budget-v2-types.js +15 -0
  58. package/dist/core/context-budget-v2-types.js.map +1 -1
  59. package/dist/core/extensions/builtin/command-safety-gate.d.ts +3 -1
  60. package/dist/core/extensions/builtin/command-safety-gate.d.ts.map +1 -1
  61. package/dist/core/extensions/builtin/command-safety-gate.js +7 -0
  62. package/dist/core/extensions/builtin/command-safety-gate.js.map +1 -1
  63. package/dist/core/extensions/types.d.ts +12 -1
  64. package/dist/core/extensions/types.d.ts.map +1 -1
  65. package/dist/core/extensions/types.js.map +1 -1
  66. package/dist/core/host-resource-probe.d.ts +63 -0
  67. package/dist/core/host-resource-probe.d.ts.map +1 -0
  68. package/dist/core/host-resource-probe.js +126 -0
  69. package/dist/core/host-resource-probe.js.map +1 -0
  70. package/dist/core/host-resource-snapshot.d.ts +54 -0
  71. package/dist/core/host-resource-snapshot.d.ts.map +1 -0
  72. package/dist/core/host-resource-snapshot.js +139 -0
  73. package/dist/core/host-resource-snapshot.js.map +1 -0
  74. package/dist/core/model-registry.d.ts +8 -3
  75. package/dist/core/model-registry.d.ts.map +1 -1
  76. package/dist/core/model-registry.js +71 -1
  77. package/dist/core/model-registry.js.map +1 -1
  78. package/dist/core/package-doctor-source-scan.d.ts +9 -0
  79. package/dist/core/package-doctor-source-scan.d.ts.map +1 -1
  80. package/dist/core/package-doctor-source-scan.js +77 -4
  81. package/dist/core/package-doctor-source-scan.js.map +1 -1
  82. package/dist/core/prompt-attachment.d.ts +51 -0
  83. package/dist/core/prompt-attachment.d.ts.map +1 -0
  84. package/dist/core/prompt-attachment.js +186 -0
  85. package/dist/core/prompt-attachment.js.map +1 -0
  86. package/dist/core/prompt-settlement.d.ts +81 -0
  87. package/dist/core/prompt-settlement.d.ts.map +1 -0
  88. package/dist/core/prompt-settlement.js +84 -0
  89. package/dist/core/prompt-settlement.js.map +1 -0
  90. package/dist/core/provider-resilience.d.ts +28 -0
  91. package/dist/core/provider-resilience.d.ts.map +1 -1
  92. package/dist/core/provider-resilience.js +51 -1
  93. package/dist/core/provider-resilience.js.map +1 -1
  94. package/dist/core/provider-retry.d.ts +7 -0
  95. package/dist/core/provider-retry.d.ts.map +1 -1
  96. package/dist/core/provider-retry.js +24 -0
  97. package/dist/core/provider-retry.js.map +1 -1
  98. package/dist/core/provider-usage.d.ts +15 -2
  99. package/dist/core/provider-usage.d.ts.map +1 -1
  100. package/dist/core/provider-usage.js +176 -2
  101. package/dist/core/provider-usage.js.map +1 -1
  102. package/dist/core/reasoning-router-promotion.d.ts +84 -0
  103. package/dist/core/reasoning-router-promotion.d.ts.map +1 -0
  104. package/dist/core/reasoning-router-promotion.js +92 -0
  105. package/dist/core/reasoning-router-promotion.js.map +1 -0
  106. package/dist/core/reasoning-router-resolver.d.ts.map +1 -1
  107. package/dist/core/reasoning-router-resolver.js +1 -1
  108. package/dist/core/reasoning-router-resolver.js.map +1 -1
  109. package/dist/core/resource-admission-config.d.ts +55 -0
  110. package/dist/core/resource-admission-config.d.ts.map +1 -0
  111. package/dist/core/resource-admission-config.js +97 -0
  112. package/dist/core/resource-admission-config.js.map +1 -0
  113. package/dist/core/resource-admission.d.ts +81 -0
  114. package/dist/core/resource-admission.d.ts.map +1 -0
  115. package/dist/core/resource-admission.js +128 -0
  116. package/dist/core/resource-admission.js.map +1 -0
  117. package/dist/core/resource-governor-format.d.ts +50 -0
  118. package/dist/core/resource-governor-format.d.ts.map +1 -0
  119. package/dist/core/resource-governor-format.js +113 -0
  120. package/dist/core/resource-governor-format.js.map +1 -0
  121. package/dist/core/resource-governor-settings.d.ts +56 -0
  122. package/dist/core/resource-governor-settings.d.ts.map +1 -0
  123. package/dist/core/resource-governor-settings.js +89 -0
  124. package/dist/core/resource-governor-settings.js.map +1 -0
  125. package/dist/core/resource-observation-journal.d.ts +74 -0
  126. package/dist/core/resource-observation-journal.d.ts.map +1 -0
  127. package/dist/core/resource-observation-journal.js +198 -0
  128. package/dist/core/resource-observation-journal.js.map +1 -0
  129. package/dist/core/resource-safety-gate.d.ts +54 -0
  130. package/dist/core/resource-safety-gate.d.ts.map +1 -0
  131. package/dist/core/resource-safety-gate.js +51 -0
  132. package/dist/core/resource-safety-gate.js.map +1 -0
  133. package/dist/core/run-journal.d.ts.map +1 -1
  134. package/dist/core/run-journal.js +13 -1
  135. package/dist/core/run-journal.js.map +1 -1
  136. package/dist/core/run-resource-lease.d.ts +73 -0
  137. package/dist/core/run-resource-lease.d.ts.map +1 -0
  138. package/dist/core/run-resource-lease.js +78 -0
  139. package/dist/core/run-resource-lease.js.map +1 -0
  140. package/dist/core/session-bash-service.d.ts +12 -0
  141. package/dist/core/session-bash-service.d.ts.map +1 -1
  142. package/dist/core/session-bash-service.js +18 -3
  143. package/dist/core/session-bash-service.js.map +1 -1
  144. package/dist/core/session-system-prompt.d.ts +2 -0
  145. package/dist/core/session-system-prompt.d.ts.map +1 -1
  146. package/dist/core/session-system-prompt.js +2 -0
  147. package/dist/core/session-system-prompt.js.map +1 -1
  148. package/dist/core/session-termination.d.ts +8 -3
  149. package/dist/core/session-termination.d.ts.map +1 -1
  150. package/dist/core/session-termination.js +40 -3
  151. package/dist/core/session-termination.js.map +1 -1
  152. package/dist/core/settings-manager.d.ts +12 -0
  153. package/dist/core/settings-manager.d.ts.map +1 -1
  154. package/dist/core/settings-manager.js +36 -0
  155. package/dist/core/settings-manager.js.map +1 -1
  156. package/dist/core/slash-commands.d.ts.map +1 -1
  157. package/dist/core/slash-commands.js +1 -0
  158. package/dist/core/slash-commands.js.map +1 -1
  159. package/dist/core/subagent-lane-launcher.d.ts +64 -0
  160. package/dist/core/subagent-lane-launcher.d.ts.map +1 -0
  161. package/dist/core/subagent-lane-launcher.js +107 -0
  162. package/dist/core/subagent-lane-launcher.js.map +1 -0
  163. package/dist/core/system-cpu-sampler.d.ts +45 -0
  164. package/dist/core/system-cpu-sampler.d.ts.map +1 -0
  165. package/dist/core/system-cpu-sampler.js +95 -0
  166. package/dist/core/system-cpu-sampler.js.map +1 -0
  167. package/dist/core/system-prompt.d.ts.map +1 -1
  168. package/dist/core/system-prompt.js +5 -8
  169. package/dist/core/system-prompt.js.map +1 -1
  170. package/dist/core/tools/bash.d.ts.map +1 -1
  171. package/dist/core/tools/bash.js +3 -7
  172. package/dist/core/tools/bash.js.map +1 -1
  173. package/dist/core/tools/grep.d.ts.map +1 -1
  174. package/dist/core/tools/grep.js +13 -48
  175. package/dist/core/tools/grep.js.map +1 -1
  176. package/dist/core/tools/read.d.ts.map +1 -1
  177. package/dist/core/tools/read.js +1 -34
  178. package/dist/core/tools/read.js.map +1 -1
  179. package/dist/core/workload-classifier.d.ts +32 -0
  180. package/dist/core/workload-classifier.d.ts.map +1 -0
  181. package/dist/core/workload-classifier.js +66 -0
  182. package/dist/core/workload-classifier.js.map +1 -0
  183. package/dist/core/workload-family-matcher.d.ts +3 -0
  184. package/dist/core/workload-family-matcher.d.ts.map +1 -0
  185. package/dist/core/workload-family-matcher.js +126 -0
  186. package/dist/core/workload-family-matcher.js.map +1 -0
  187. package/dist/core/workload-permit-pool.d.ts +78 -0
  188. package/dist/core/workload-permit-pool.d.ts.map +1 -0
  189. package/dist/core/workload-permit-pool.js +138 -0
  190. package/dist/core/workload-permit-pool.js.map +1 -0
  191. package/dist/core/workload-shard-executor.d.ts +68 -0
  192. package/dist/core/workload-shard-executor.d.ts.map +1 -0
  193. package/dist/core/workload-shard-executor.js +156 -0
  194. package/dist/core/workload-shard-executor.js.map +1 -0
  195. package/dist/core/workload-shard-plan.d.ts +81 -0
  196. package/dist/core/workload-shard-plan.d.ts.map +1 -0
  197. package/dist/core/workload-shard-plan.js +183 -0
  198. package/dist/core/workload-shard-plan.js.map +1 -0
  199. package/dist/core/workload-shard-runner.d.ts +11 -0
  200. package/dist/core/workload-shard-runner.d.ts.map +1 -0
  201. package/dist/core/workload-shard-runner.js +98 -0
  202. package/dist/core/workload-shard-runner.js.map +1 -0
  203. package/dist/core/workload-shard-store.d.ts +44 -0
  204. package/dist/core/workload-shard-store.d.ts.map +1 -0
  205. package/dist/core/workload-shard-store.js +189 -0
  206. package/dist/core/workload-shard-store.js.map +1 -0
  207. package/dist/core/workload-sharders.d.ts +47 -0
  208. package/dist/core/workload-sharders.d.ts.map +1 -0
  209. package/dist/core/workload-sharders.js +162 -0
  210. package/dist/core/workload-sharders.js.map +1 -0
  211. package/dist/core/workload-shell-scan.d.ts +12 -0
  212. package/dist/core/workload-shell-scan.d.ts.map +1 -0
  213. package/dist/core/workload-shell-scan.js +79 -0
  214. package/dist/core/workload-shell-scan.js.map +1 -0
  215. package/dist/index.d.ts +1 -1
  216. package/dist/index.d.ts.map +1 -1
  217. package/dist/index.js +1 -1
  218. package/dist/index.js.map +1 -1
  219. package/dist/main.d.ts.map +1 -1
  220. package/dist/main.js +6 -0
  221. package/dist/main.js.map +1 -1
  222. package/dist/modes/interactive/components/attachment-strip.d.ts +21 -0
  223. package/dist/modes/interactive/components/attachment-strip.d.ts.map +1 -0
  224. package/dist/modes/interactive/components/attachment-strip.js +136 -0
  225. package/dist/modes/interactive/components/attachment-strip.js.map +1 -0
  226. package/dist/modes/interactive/components/footer.d.ts +6 -0
  227. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  228. package/dist/modes/interactive/components/footer.js +26 -1
  229. package/dist/modes/interactive/components/footer.js.map +1 -1
  230. package/dist/modes/interactive/components/status-sidebar.d.ts.map +1 -1
  231. package/dist/modes/interactive/components/status-sidebar.js +12 -1
  232. package/dist/modes/interactive/components/status-sidebar.js.map +1 -1
  233. package/dist/modes/interactive/interactive-mode.d.ts +27 -4
  234. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  235. package/dist/modes/interactive/interactive-mode.js +255 -53
  236. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  237. package/docs/compaction.md +1 -1
  238. package/docs/extensions.md +12 -3
  239. package/docs/index.md +4 -1
  240. package/docs/metrics.md +74 -2
  241. package/docs/provider-resilience.md +11 -0
  242. package/docs/providers.md +1 -1
  243. package/docs/sdk.md +4 -0
  244. package/docs/settings.md +64 -2
  245. package/docs/skills.md +16 -0
  246. package/docs/usage.md +1 -1
  247. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  248. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  249. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  250. package/examples/extensions/gondolin/package-lock.json +2 -2
  251. package/examples/extensions/gondolin/package.json +1 -1
  252. package/examples/extensions/sandbox/package-lock.json +2 -2
  253. package/examples/extensions/sandbox/package.json +1 -1
  254. package/examples/extensions/subagent/agent-capability-router.test.ts +7 -0
  255. package/examples/extensions/subagent/domain-profiles.ts +0 -1
  256. package/examples/extensions/with-deps/package-lock.json +2 -2
  257. package/examples/extensions/with-deps/package.json +1 -1
  258. package/npm-shrinkwrap.json +18 -18
  259. package/package.json +8 -10
  260. package/dist/core/tools/omp-pure-seams.d.ts +0 -41
  261. package/dist/core/tools/omp-pure-seams.d.ts.map +0 -1
  262. package/dist/core/tools/omp-pure-seams.js +0 -142
  263. package/dist/core/tools/omp-pure-seams.js.map +0 -1
  264. package/dist/core/tools/omp-seam-runtime.d.ts +0 -100
  265. package/dist/core/tools/omp-seam-runtime.d.ts.map +0 -1
  266. package/dist/core/tools/omp-seam-runtime.js +0 -40
  267. package/dist/core/tools/omp-seam-runtime.js.map +0 -1
@@ -44,6 +44,7 @@ import { ExtensionRunner, wrapRegisteredTools, } from "./extensions/index.js";
44
44
  import { emitSessionShutdownEvent } from "./extensions/runner.js";
45
45
  import { assertTextChatModelForCompletion } from "./grok-harness.js";
46
46
  import { grokPlaybookAppendForProvider } from "./grok-playbook.js";
47
+ import { captureHostResourceSnapshot } from "./host-resource-snapshot.js";
47
48
  import { decideLoadoutAccess } from "./loadout-access-policy.js";
48
49
  import { loadMcpServerConfigs } from "./mcp/config.js";
49
50
  import { McpManager } from "./mcp/manager.js";
@@ -53,6 +54,7 @@ import { TurnMetricsSink } from "./turn-metrics.js";
53
54
  const DISABLED_ENV_VALUES = new Set(["0", "false", "off", "disable", "disabled"]);
54
55
  const MAX_OVERFLOW_RECOVERY_ATTEMPTS = 2;
55
56
  const OVERFLOW_RECOVERY_EMERGENCY_TOKENS = 4_096;
57
+ const MAX_RESOURCE_OBSERVATION_JOURNALS = 32;
56
58
  function isDisabledEnvValue(value) {
57
59
  return value !== undefined && DISABLED_ENV_VALUES.has(value.trim().toLowerCase());
58
60
  }
@@ -75,15 +77,21 @@ import { selectContextFilesForModel } from "./model-prompt-policy.js";
75
77
  import { findExactModelReferenceMatch } from "./model-resolver.js";
76
78
  import { computePromptTokenBudget } from "./prompt-budget.js";
77
79
  import { classifyPromptCacheTransition } from "./prompt-cache.js";
80
+ import { createPromptSettlementState, reducePromptSettlement, settlePromptIfReady, } from "./prompt-settlement.js";
78
81
  import { expandPromptTemplate } from "./prompt-templates.js";
79
- import { isQuotaExhaustionMessage, isStickySafetyModel, pickFailoverCandidate, resolveProviderResilience, shouldEjectStickySafetyModel, shouldHonorSafetyFailover, stickySafetyBlockMessage, } from "./provider-resilience.js";
82
+ import { isQuotaExhaustionMessage, isStickySafetyModel, isUpstreamUnavailableMessage, pickFailoverCandidate, resolveFailoverCandidates, resolveProviderResilience, sameModelRouteCandidates, shouldEjectStickySafetyModel, shouldHonorSafetyFailover, stickySafetyBlockMessage, } from "./provider-resilience.js";
80
83
  import { computeRetryDelayMs, failoverModelKey, isFailoverTriggerError, isRetryableAssistantError, nextRetryAttempt, retryBudgetForAssistantError, } from "./provider-retry.js";
81
84
  import { getBiasStepsForCell, parseRouterBiasSnapshot } from "./reasoning-router-bias.js";
82
85
  import { classifyTaskV4, deriveRouterFeedbackFeaturesV4, resolveThinkingLevelV4WithUncertainty, TASK_CLASS_THINKING_LEVELS_V4, } from "./reasoning-router-v4.js";
83
86
  import { redactSensitiveText, redactSensitiveTextForced } from "./redaction.js";
84
87
  import { getRepositoryRouterLearningPaths } from "./repository-learning-scope.js";
88
+ import { decideResourceAdmission } from "./resource-admission.js";
89
+ import { resolveResourceGovernorSettings } from "./resource-governor-settings.js";
90
+ import { admissionObservationFacts, classificationObservationFacts, ResourceObservationJournal, settledObservationFacts, snapshotObservationFacts, soundObservationFacts, } from "./resource-observation-journal.js";
91
+ import { decideResourceSafetyGate, RESOURCE_PRESSURE_REQUIRED_ACTION } from "./resource-safety-gate.js";
85
92
  import { appendRouterFeedbackRecord, ROUTER_FEEDBACK_LEVELS, } from "./router-feedback-collector.js";
86
93
  import { RunJournalStore } from "./run-journal-store.js";
94
+ import { RunResourceLeaseController } from "./run-resource-lease.js";
87
95
  import { SessionBashRuntime } from "./session-bash-runtime.js";
88
96
  import { SessionBashService } from "./session-bash-service.js";
89
97
  import { SessionCompactionService } from "./session-compaction-service.js";
@@ -98,6 +106,8 @@ import { createAllToolDefinitions } from "./tools/index.js";
98
106
  import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
99
107
  import { createVerifiedBashOperations } from "./verified-bash-adapter.js";
100
108
  import { resolveSessionWorkspaceScope } from "./verified-bash-runtime.js";
109
+ import { classifyWorkloadCommand } from "./workload-classifier.js";
110
+ import { WorkloadPermitError, WorkloadPermitPool } from "./workload-permit-pool.js";
101
111
  /**
102
112
  * Parse a skill block from message text.
103
113
  * Returns null if the text doesn't contain a skill block.
@@ -356,6 +366,14 @@ export class AgentSession {
356
366
  _runJournalStore;
357
367
  _ownedSessionOwnerLease;
358
368
  _activeRunId = null;
369
+ // Resource governor (roadmap M2): lease controller owns the temporary tool
370
+ // cap for governed runs; the last decision backs the §19.4 read-only API.
371
+ _resourceLeaseController;
372
+ _resourceObservations = null;
373
+ _resourceObservationJournals = new Map();
374
+ _latestResourcePromptRunId = null;
375
+ _lastResourceAdmission = null;
376
+ _workloadPermitPool;
359
377
  _pendingRuntimeTerminationCause;
360
378
  _activeRunToolTermination;
361
379
  _lastTermination;
@@ -436,6 +454,7 @@ export class AgentSession {
436
454
  isStreaming: () => this.isStreaming,
437
455
  pushMessage: (message) => this.agent.state.messages.push(message),
438
456
  appendMessage: (message) => this.sessionManager.appendMessage(message),
457
+ acquireResourcePermit: (command, signal) => this._acquireBashResourcePermit(command, signal),
439
458
  });
440
459
  this._compactionService = new SessionCompactionService({
441
460
  sessionManager: this.sessionManager,
@@ -605,6 +624,12 @@ export class AgentSession {
605
624
  if (/auth|unauthori[sz]ed|forbidden|invalid.?api.?key|no api key|401|403|\/login/i.test(text)) {
606
625
  return { area: "provider", code: "auth" };
607
626
  }
627
+ // Gateway/upstream 5xx and dropped streams are transport failures, not
628
+ // transcript-shape problems. Classify as network so the guidance points to
629
+ // retry/model-switch instead of the protocol sanitize path.
630
+ if (isUpstreamUnavailableMessage(text)) {
631
+ return { area: "provider", code: "network" };
632
+ }
608
633
  // Kimi/K3 + OpenAI-compat: orphan tool results after dropped error assistants.
609
634
  // Sanitize-and-retry (transform-messages drops orphans), not a hard tool fatal.
610
635
  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)) {
@@ -629,6 +654,9 @@ export class AgentSession {
629
654
  if (/auth|api key|unauthori[sz]ed|forbidden|401|403|\/login/i.test(message)) {
630
655
  return { area: "provider", code: "auth" };
631
656
  }
657
+ if (isUpstreamUnavailableMessage(message)) {
658
+ return { area: "provider", code: "network" };
659
+ }
632
660
  if (/context.+overflow|context window|too many tokens/i.test(message)) {
633
661
  return { area: "provider", code: "context_overflow" };
634
662
  }
@@ -1111,6 +1139,11 @@ export class AgentSession {
1111
1139
  }
1112
1140
  else if (event.type === "agent_end") {
1113
1141
  await this._extensionRunner.emit({ type: "agent_end", messages: event.messages });
1142
+ // `agent_end` fires once per attempt. Extensions that release per-run
1143
+ // state need the boundary no retry follows, so mark it explicitly.
1144
+ if (!this._willRetryAfterAgentEnd(event)) {
1145
+ await this._extensionRunner.emit({ type: "agent_settled", messages: event.messages });
1146
+ }
1114
1147
  }
1115
1148
  else if (event.type === "turn_start") {
1116
1149
  this._turnMetricsState = {
@@ -1627,7 +1660,15 @@ export class AgentSession {
1627
1660
  this._promptCacheKey = nextKey;
1628
1661
  this._promptCacheStablePrefixCharacters = boundary ?? 0;
1629
1662
  }
1663
+ _getDefaultActiveSkills() {
1664
+ const trustedSkillNames = new Set(this._resourceLoader
1665
+ .getSkills()
1666
+ .skills.filter((skill) => skill.sourceInfo.scope === "user")
1667
+ .map((skill) => skill.name));
1668
+ return this.settingsManager.getDefaultActiveSkills().filter((name) => trustedSkillNames.has(name));
1669
+ }
1630
1670
  _rebuildSystemPrompt(toolNames, model = this.model) {
1671
+ const defaultActiveSkills = this._getDefaultActiveSkills();
1631
1672
  const assembled = assembleSessionSystemPrompt({
1632
1673
  cwd: this._cwd,
1633
1674
  toolNames,
@@ -1638,6 +1679,8 @@ export class AgentSession {
1638
1679
  appendSystemPrompt: this._resourceLoader.getAppendSystemPrompt(),
1639
1680
  providerAppend: grokPlaybookAppendForProvider(model?.provider),
1640
1681
  skills: this._resourceLoader.getSkills().skills,
1682
+ activeSkillNames: defaultActiveSkills,
1683
+ activeSkillSource: defaultActiveSkills.length > 0 ? "settings" : undefined,
1641
1684
  contextFiles: selectContextFilesForModel(this._resourceLoader.getAgentsFiles().agentsFiles, model),
1642
1685
  contextBudget: this._getContextBudgetOptions(undefined, model),
1643
1686
  });
@@ -1649,6 +1692,15 @@ export class AgentSession {
1649
1692
  // Prompting
1650
1693
  // =========================================================================
1651
1694
  async _runAgentPrompt(messages) {
1695
+ // One identity per top-level run (§16.2): internal retries and
1696
+ // continuations inside this call share it, as does the resource lease.
1697
+ const promptRunId = `prompt-run-${randomUUID()}`;
1698
+ const startedAtEpochMs = Date.now();
1699
+ // Roadmap M2: the lease spans the whole run including internal retries
1700
+ // and continuations, so they share one admission decision (§8.3).
1701
+ const resourceLease = await this._beginResourceGovernedRun(promptRunId);
1702
+ const resourceObservations = this._resourceObservationJournals.get(promptRunId) ?? null;
1703
+ let outcome = "completed";
1652
1704
  try {
1653
1705
  await this.agent.prompt(messages);
1654
1706
  while (await this._handlePostAgentRun()) {
@@ -1656,11 +1708,251 @@ export class AgentSession {
1656
1708
  }
1657
1709
  }
1658
1710
  catch (error) {
1711
+ outcome =
1712
+ this._userAbortRequested || (error instanceof Error && /abort/i.test(error.message)) ? "aborted" : "failed";
1659
1713
  this._publishRuntimeFailure(error);
1660
1714
  throw error;
1661
1715
  }
1662
1716
  finally {
1717
+ if (resourceLease !== null) {
1718
+ // Generation-safe: a stale release after another run acquired the
1719
+ // lease is a no-op instead of clobbering the newer cap (§8.1).
1720
+ this._resourceLeaseController?.release(resourceLease);
1721
+ resourceObservations?.record("resource_lease_released_v1", { promptRunId });
1722
+ }
1663
1723
  this._flushPendingBashMessages();
1724
+ // §16.4 (M4): settle only after every in-run continuation drained.
1725
+ this._emitPromptSettledIfReady({ promptRunId, startedAtEpochMs, outcome, resourceObservations });
1726
+ }
1727
+ }
1728
+ /**
1729
+ * Emit `prompt_settled` when the §16.4 conditions hold. Called exactly
1730
+ * once per promptRunId (from `_runAgentPrompt`'s finally), so the same
1731
+ * run can never emit twice; late-queued messages block emission
1732
+ * conservatively — the continuation run settles under its own id.
1733
+ * Settlement is a UX signal only and must never affect the run (§16.1).
1734
+ */
1735
+ _emitPromptSettledIfReady(input) {
1736
+ try {
1737
+ let state = createPromptSettlementState(input.promptRunId, input.startedAtEpochMs);
1738
+ state = reducePromptSettlement(state, { kind: "terminal", outcome: input.outcome });
1739
+ if (this.isStreaming) {
1740
+ state = reducePromptSettlement(state, { kind: "tool", delta: 1 });
1741
+ }
1742
+ if (this.agent.hasQueuedMessages()) {
1743
+ state = reducePromptSettlement(state, { kind: "continuation", delta: 1 });
1744
+ }
1745
+ const settled = settlePromptIfReady(state, Date.now());
1746
+ if (settled.event !== null) {
1747
+ input.resourceObservations?.record("prompt_settled_v1", settledObservationFacts(settled.event));
1748
+ this._emit(settled.event);
1749
+ }
1750
+ }
1751
+ catch {
1752
+ // Never let settlement bookkeeping break a run.
1753
+ }
1754
+ }
1755
+ /**
1756
+ * Resource governor preflight for one top-level run (roadmap §8, §25.3 M2).
1757
+ *
1758
+ * - `off`: nothing; v0.96.1 observable behavior is preserved.
1759
+ * - `observe` (default): fire-and-forget probe records a decision for the
1760
+ * §19.4 read-only API; the cap never changes and the prompt is never
1761
+ * delayed. The pending probe is bounded (~cpuSampleMs) and never rejects.
1762
+ * - `adaptive`/`strict`: bounded blocking probe (default 300 ms deadline),
1763
+ * then a generation-safe lease applies the effective tool cap for this
1764
+ * run. The caller's `finally` releases it exactly once.
1765
+ *
1766
+ * Never throws: any probe or policy failure leaves this run ungoverned
1767
+ * (§2.1: probe failure must not crash or block the prompt).
1768
+ */
1769
+ async _beginResourceGovernedRun(promptRunId) {
1770
+ try {
1771
+ const resolved = resolveResourceGovernorSettings(this.settingsManager.getResourceGovernorSettings());
1772
+ if (resolved.mode === "off") {
1773
+ return null;
1774
+ }
1775
+ const resourceObservations = this._openResourceObservations(promptRunId);
1776
+ const probeOptions = {
1777
+ cwd: this._cwd,
1778
+ maxProbeMs: resolved.maxProbeMs,
1779
+ cpuSampleMs: resolved.cpuSampleMs,
1780
+ };
1781
+ // §7.3: Agent.maxToolConcurrency uses undefined for "unlimited"; the
1782
+ // admission contract uses 0 for the same meaning.
1783
+ const configuredCaps = { maxToolConcurrency: this.agent.maxToolConcurrency ?? 0 };
1784
+ if (resolved.mode === "observe") {
1785
+ void captureHostResourceSnapshot(probeOptions)
1786
+ .then((snapshot) => {
1787
+ const decision = decideResourceAdmission({
1788
+ snapshot,
1789
+ config: resolved.admission,
1790
+ configuredCaps,
1791
+ });
1792
+ if (this._latestResourcePromptRunId === promptRunId) {
1793
+ this._lastResourceAdmission = decision;
1794
+ }
1795
+ resourceObservations?.record("resource_snapshot_v1", snapshotObservationFacts(snapshot));
1796
+ resourceObservations?.record("resource_admission_v1", admissionObservationFacts(decision));
1797
+ })
1798
+ .catch(() => { });
1799
+ return null;
1800
+ }
1801
+ const snapshot = await captureHostResourceSnapshot(probeOptions);
1802
+ const decision = decideResourceAdmission({ snapshot, config: resolved.admission, configuredCaps });
1803
+ if (this._latestResourcePromptRunId === promptRunId) {
1804
+ this._lastResourceAdmission = decision;
1805
+ }
1806
+ resourceObservations?.record("resource_snapshot_v1", snapshotObservationFacts(snapshot));
1807
+ resourceObservations?.record("resource_admission_v1", admissionObservationFacts(decision));
1808
+ this._resourceLeaseController ??= new RunResourceLeaseController({
1809
+ getCap: () => this.agent.maxToolConcurrency,
1810
+ setCap: (cap) => {
1811
+ this.agent.maxToolConcurrency = cap;
1812
+ },
1813
+ });
1814
+ const lease = this._resourceLeaseController.acquire({ promptRunId, decision });
1815
+ resourceObservations?.record("resource_lease_acquired_v1", {
1816
+ decisionId: decision.decisionId,
1817
+ appliedToolCap: decision.maxToolConcurrency,
1818
+ });
1819
+ return lease;
1820
+ }
1821
+ catch {
1822
+ return null;
1823
+ }
1824
+ }
1825
+ _openResourceObservations(promptRunId) {
1826
+ this._latestResourcePromptRunId = promptRunId;
1827
+ try {
1828
+ const journal = ResourceObservationJournal.open(this._cwd, promptRunId);
1829
+ this._resourceObservations = journal;
1830
+ this._resourceObservationJournals.set(promptRunId, journal);
1831
+ while (this._resourceObservationJournals.size > MAX_RESOURCE_OBSERVATION_JOURNALS) {
1832
+ const oldestPromptRunId = this._resourceObservationJournals.keys().next().value;
1833
+ if (oldestPromptRunId === undefined)
1834
+ break;
1835
+ this._resourceObservationJournals.delete(oldestPromptRunId);
1836
+ }
1837
+ return journal;
1838
+ }
1839
+ catch {
1840
+ this._resourceObservations = null;
1841
+ this._resourceObservationJournals.delete(promptRunId);
1842
+ return null;
1843
+ }
1844
+ }
1845
+ /** §20.2: record the TUI-side completion sound result in its originating run journal. */
1846
+ recordCompletionSoundResult(promptRunId, result) {
1847
+ const journal = this._resourceObservationJournals.get(promptRunId);
1848
+ journal?.record("completion_sound_result_v1", soundObservationFacts(result));
1849
+ this._resourceObservationJournals.delete(promptRunId);
1850
+ if (this._latestResourcePromptRunId === promptRunId) {
1851
+ this._latestResourcePromptRunId = null;
1852
+ if (this._resourceObservations === journal)
1853
+ this._resourceObservations = null;
1854
+ }
1855
+ }
1856
+ /** §19.4 read-only SDK surface: the most recent resource admission decision, if any. */
1857
+ getCurrentResourceAdmission() {
1858
+ return this._lastResourceAdmission;
1859
+ }
1860
+ /** §19.4 read-only SDK surface: capture a fresh host resource snapshot. */
1861
+ async getHostResourceSnapshot() {
1862
+ const resolved = resolveResourceGovernorSettings(this.settingsManager.getResourceGovernorSettings());
1863
+ return captureHostResourceSnapshot({
1864
+ cwd: this._cwd,
1865
+ maxProbeMs: resolved.maxProbeMs,
1866
+ cpuSampleMs: resolved.cpuSampleMs,
1867
+ });
1868
+ }
1869
+ /** §19.4 read-only SDK surface: shared workload permit pool state, if any. */
1870
+ getWorkloadPermitSnapshot() {
1871
+ return this._workloadPermitPool?.snapshot() ?? null;
1872
+ }
1873
+ /**
1874
+ * §14.1 shared-budget seam: child subagents must reuse this instance
1875
+ * instead of creating a private pool (host oversubscription). The live
1876
+ * child launcher wiring lands in M6.
1877
+ */
1878
+ get workloadPermitPool() {
1879
+ this._workloadPermitPool ??= new WorkloadPermitPool();
1880
+ return this._workloadPermitPool;
1881
+ }
1882
+ /**
1883
+ * Resource safety gate for the bash boundary (roadmap §9.4/§11, M3).
1884
+ * Runs only in adaptive/strict mode with a recorded admission decision;
1885
+ * observe/off keep v0.96.1 behavior. Never throws: gate machinery
1886
+ * failures leave the command ungated (§2.1), while computed block
1887
+ * verdicts and permit rejections return a bounded §11.3 payload.
1888
+ */
1889
+ async _acquireBashResourcePermit(command, signal) {
1890
+ try {
1891
+ const resolved = resolveResourceGovernorSettings(this.settingsManager.getResourceGovernorSettings());
1892
+ if (resolved.mode !== "adaptive" && resolved.mode !== "strict") {
1893
+ return {};
1894
+ }
1895
+ const decision = this._lastResourceAdmission;
1896
+ if (decision === null) {
1897
+ return {};
1898
+ }
1899
+ const classification = classifyWorkloadCommand(command);
1900
+ this._resourceObservations?.record("workload_classification_v1", classificationObservationFacts(classification));
1901
+ const verdict = decideResourceSafetyGate({ commandSafety: "allowed", classification, decision });
1902
+ if (verdict.kind === "allow") {
1903
+ return {};
1904
+ }
1905
+ if (verdict.kind === "block") {
1906
+ return { blocked: JSON.stringify(verdict.block) };
1907
+ }
1908
+ this._workloadPermitPool ??= new WorkloadPermitPool({ capacity: decision.maxHeavyProcesses });
1909
+ const pool = this._workloadPermitPool;
1910
+ pool.setCapacity(decision.maxHeavyProcesses);
1911
+ const waitStartedMs = Date.now();
1912
+ this._resourceObservations?.record("workload_permit_wait_v1", {
1913
+ workloadClass: classification.workloadClass,
1914
+ weight: verdict.weight,
1915
+ });
1916
+ try {
1917
+ const permit = await pool.acquire({
1918
+ requestId: `bash-${randomUUID()}`,
1919
+ promptRunId: this._resourceLeaseController?.activeLease?.promptRunId ?? "unleased",
1920
+ workloadClass: classification.workloadClass,
1921
+ weight: verdict.weight,
1922
+ signal,
1923
+ // Bounded §10.3 timeout-aware wait; a settings knob can follow later.
1924
+ timeoutMs: 60_000,
1925
+ });
1926
+ this._resourceObservations?.record("workload_permit_acquired_v1", {
1927
+ workloadClass: classification.workloadClass,
1928
+ weight: verdict.weight,
1929
+ waitMs: Date.now() - waitStartedMs,
1930
+ });
1931
+ return {
1932
+ release: () => {
1933
+ permit.release();
1934
+ this._resourceObservations?.record("workload_permit_released_v1", {
1935
+ workloadClass: classification.workloadClass,
1936
+ weight: verdict.weight,
1937
+ });
1938
+ },
1939
+ };
1940
+ }
1941
+ catch (error) {
1942
+ const code = error instanceof WorkloadPermitError ? error.code : "queue_overflow";
1943
+ return {
1944
+ blocked: JSON.stringify({
1945
+ kind: "resource_pressure",
1946
+ pressure: decision.pressure,
1947
+ action: "defer-heavy",
1948
+ reasonCodes: [...decision.reasons, `resource.permit.${code}`],
1949
+ requiredAction: RESOURCE_PRESSURE_REQUIRED_ACTION,
1950
+ }),
1951
+ };
1952
+ }
1953
+ }
1954
+ catch {
1955
+ return {};
1664
1956
  }
1665
1957
  }
1666
1958
  async _handlePostAgentRun() {
@@ -1702,8 +1994,13 @@ export class AgentSession {
1702
1994
  const expandPromptTemplates = options?.expandPromptTemplates ?? true;
1703
1995
  const preflightResult = options?.preflightResult;
1704
1996
  let currentText = redactSensitiveText(text);
1705
- let promptActiveSkillNames = [...(options?.activeSkillNames ?? [])];
1706
- let promptActiveSkillSource = options?.activeSkillSource;
1997
+ const defaultActiveSkills = this._getDefaultActiveSkills();
1998
+ let promptActiveSkillNames = mergePromptActiveSkillNames(defaultActiveSkills, options?.activeSkillNames ?? []);
1999
+ let promptActiveSkillSource = defaultActiveSkills.length > 0
2000
+ ? options?.activeSkillSource
2001
+ ? `settings+${options.activeSkillSource}`
2002
+ : "settings"
2003
+ : options?.activeSkillSource;
1707
2004
  let isBangSkillInvocation = false;
1708
2005
  if (expandPromptTemplates) {
1709
2006
  const bangInvocation = parseBangInvocation(text, {
@@ -1715,7 +2012,8 @@ export class AgentSession {
1715
2012
  ? `/skill:${bangInvocation.skillName} ${bangInvocation.prompt}`
1716
2013
  : `/skill:${bangInvocation.skillName}`;
1717
2014
  promptActiveSkillNames = mergePromptActiveSkillNames(promptActiveSkillNames, bangInvocation.activeSkillNames);
1718
- promptActiveSkillSource = bangInvocation.source;
2015
+ promptActiveSkillSource =
2016
+ defaultActiveSkills.length > 0 ? `settings+${bangInvocation.source}` : bangInvocation.source;
1719
2017
  }
1720
2018
  }
1721
2019
  let messages;
@@ -1819,8 +2117,8 @@ export class AgentSession {
1819
2117
  const turnSystemPromptOptions = {
1820
2118
  ...this._baseSystemPromptOptions,
1821
2119
  contextBudget: this._getContextBudgetOptions(expandedText),
1822
- ...(promptActiveSkillNames.length > 0 ? { activeSkillNames: promptActiveSkillNames } : {}),
1823
- ...(promptActiveSkillSource ? { activeSkillSource: promptActiveSkillSource } : {}),
2120
+ activeSkillNames: promptActiveSkillNames,
2121
+ activeSkillSource: promptActiveSkillSource,
1824
2122
  };
1825
2123
  const turnSystemPrompt = buildSystemPromptPlan(turnSystemPromptOptions);
1826
2124
  // Emit before_agent_start extension event
@@ -2702,6 +3000,25 @@ export class AgentSession {
2702
3000
  });
2703
3001
  return model;
2704
3002
  }
3003
+ /**
3004
+ * Authenticated failover candidates for compaction summarization, in
3005
+ * resilience-chain order, excluding the primary compaction model itself.
3006
+ * Used when the primary model's quota is exhausted (same-model retry is
3007
+ * useless until reset; another provider can still save the compaction).
3008
+ */
3009
+ _compactionFailoverModels(primary) {
3010
+ const candidates = resolveFailoverCandidates(this.settingsManager.getProviderResilienceSettings());
3011
+ const available = this._modelRegistry.getAvailable();
3012
+ const models = [];
3013
+ for (const candidate of candidates) {
3014
+ if (candidate.provider === primary.provider && candidate.id === primary.id)
3015
+ continue;
3016
+ const resolved = available.find((m) => m.provider === candidate.provider && m.id === candidate.id);
3017
+ if (resolved)
3018
+ models.push(resolved);
3019
+ }
3020
+ return models;
3021
+ }
2705
3022
  /**
2706
3023
  * Manually compact the session context.
2707
3024
  * Aborts current agent operation first.
@@ -2762,7 +3079,7 @@ export class AgentSession {
2762
3079
  }
2763
3080
  else {
2764
3081
  // Generate compaction result
2765
- const result = await compact(preparation, compactionModel, apiKey, headers, customInstructions, this._compactionAbortController.signal, this.thinkingLevel, this.agent.streamFn, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason: "manual" }));
3082
+ const result = await compact(preparation, compactionModel, apiKey, headers, customInstructions, this._compactionAbortController.signal, this.thinkingLevel, this.agent.streamFn, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason: "manual" }), this._compactionFailoverModels(compactionModel));
2766
3083
  summary = result.summary;
2767
3084
  firstKeptEntryId = result.firstKeptEntryId;
2768
3085
  tokensBefore = result.tokensBefore;
@@ -2805,6 +3122,8 @@ export class AgentSession {
2805
3122
  compactionCode = "aborted";
2806
3123
  else if (stale)
2807
3124
  compactionCode = "stale";
3125
+ else if (isQuotaExhaustionMessage(message))
3126
+ compactionCode = "quota_exhausted";
2808
3127
  const timestamp = new Date().toISOString();
2809
3128
  this._publishTermination(classifySessionTermination({
2810
3129
  sessionId: this.sessionId,
@@ -2858,7 +3177,7 @@ export class AgentSession {
2858
3177
  /**
2859
3178
  * Effective context window for the upcoming turn.
2860
3179
  * Image-bearing turns with a text-only session model are auto-routed to the
2861
- * vision model (gpt-5.6-luna, 400K) — that window is the real limit. Text-only
3180
+ * vision model (gpt-5.6-luna, 1M) — that window is the real limit. Text-only
2862
3181
  * turns keep the session model's (typically much larger) window.
2863
3182
  */
2864
3183
  _effectiveTurnContextWindow(pendingMessages, sessionWindow) {
@@ -3093,7 +3412,7 @@ export class AgentSession {
3093
3412
  }
3094
3413
  else {
3095
3414
  // Generate compaction result
3096
- const compactResult = await compact(preparation, compactionModel, apiKey, headers, undefined, this._autoCompactionAbortController.signal, this.thinkingLevel, this.agent.streamFn, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }));
3415
+ const compactResult = await compact(preparation, compactionModel, apiKey, headers, undefined, this._autoCompactionAbortController.signal, this.thinkingLevel, this.agent.streamFn, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }), this._compactionFailoverModels(compactionModel));
3097
3416
  summary = compactResult.summary;
3098
3417
  firstKeptEntryId = compactResult.firstKeptEntryId;
3099
3418
  tokensBefore = compactResult.tokensBefore;
@@ -3708,6 +4027,41 @@ export class AgentSession {
3708
4027
  // setModel itself blocks sticky targets, not sources — safe.
3709
4028
  await this.setModel(next);
3710
4029
  }
4030
+ /**
4031
+ * Rotate to another authenticated provider route serving the SAME model
4032
+ * family (e.g. openrouter/stealth/ox-alpha ↔ opencode-go/ox-alpha-free ↔
4033
+ * opencode/x-preview-f-free). Used when the current route fails with an
4034
+ * upstream availability error — same-model retry would hammer a dead
4035
+ * endpoint while the identical model is one route away. Visited and
4036
+ * unauthenticated routes are marked in {@link _refusedModels} so repeat
4037
+ * failures advance the rotation within the turn.
4038
+ */
4039
+ async _maybeRotateModelRoutes() {
4040
+ const current = this.model;
4041
+ if (!current)
4042
+ return undefined;
4043
+ const candidates = sameModelRouteCandidates(current, this._modelRegistry.getAvailable());
4044
+ for (const candidate of candidates) {
4045
+ const key = failoverModelKey(candidate.provider, candidate.id);
4046
+ if (this._refusedModels.has(key))
4047
+ continue;
4048
+ const next = this._modelRegistry.find(candidate.provider, candidate.id);
4049
+ if (!next || !this._modelRegistry.hasConfiguredAuth(next)) {
4050
+ this._refusedModels.add(key);
4051
+ continue;
4052
+ }
4053
+ try {
4054
+ await this.setModel(next);
4055
+ // The old route just failed upstream — keep it out of this turn's retries.
4056
+ this._refusedModels.add(failoverModelKey(current.provider, current.id));
4057
+ return `${next.provider}/${next.id}`;
4058
+ }
4059
+ catch {
4060
+ this._refusedModels.add(key);
4061
+ }
4062
+ }
4063
+ return undefined;
4064
+ }
3711
4065
  /**
3712
4066
  * Content/safety stops (stop_reason=refusal) are often false positives on coding turns
3713
4067
  * for Fable AND Claude Opus/Sonnet. Retrying the same model usually reprints the refusal —
@@ -3769,12 +4123,19 @@ export class AgentSession {
3769
4123
  if (attempt === undefined)
3770
4124
  return false;
3771
4125
  this._retryAttempt = attempt;
4126
+ // Upstream-unavailable (gateway 5xx / dropped stream): rotate to another
4127
+ // authenticated route of the SAME model family so the retry lands on a
4128
+ // live endpoint instead of hammering the dead one.
4129
+ const rotatedTo = isUpstreamUnavailableMessage(message.errorMessage)
4130
+ ? await this._maybeRotateModelRoutes()
4131
+ : undefined;
3772
4132
  // Content/safety stop (Fable/Opus/Sonnet): switch model BEFORE delay so retry is not same-model refusal.
3773
4133
  const failoverTo = await this._maybeFailoverFromSafetyStop(message);
4134
+ const switchedVia = failoverTo ? `failover ${failoverTo}` : rotatedTo ? `route ${rotatedTo}` : undefined;
3774
4135
  // Safety stops are usually immediate false positives — short delay after failover, full backoff otherwise.
3775
- const delayMs = computeRetryDelayMs(settings.baseDelayMs, attempt, failoverTo !== undefined);
3776
- const errorMessage = failoverTo
3777
- ? `${message.errorMessage || "content/safety stop"} → failover ${failoverTo}`
4136
+ const delayMs = computeRetryDelayMs(settings.baseDelayMs, attempt, switchedVia !== undefined);
4137
+ const errorMessage = switchedVia
4138
+ ? `${message.errorMessage || "content/safety stop"} → ${switchedVia}`
3778
4139
  : message.errorMessage || "Unknown error";
3779
4140
  this._emit({
3780
4141
  type: "auto_retry_start",