open-multi-agent-kit 0.90.9 → 0.91.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 (80) hide show
  1. package/CHANGELOG.md +22 -3
  2. package/README.md +14 -1
  3. package/dist/core/agent-session.d.ts +27 -0
  4. package/dist/core/agent-session.d.ts.map +1 -1
  5. package/dist/core/agent-session.js +188 -25
  6. package/dist/core/agent-session.js.map +1 -1
  7. package/dist/core/command-safety.d.ts.map +1 -1
  8. package/dist/core/command-safety.js +0 -8
  9. package/dist/core/command-safety.js.map +1 -1
  10. package/dist/core/footer-data-provider.d.ts +7 -1
  11. package/dist/core/footer-data-provider.d.ts.map +1 -1
  12. package/dist/core/footer-data-provider.js +12 -0
  13. package/dist/core/footer-data-provider.js.map +1 -1
  14. package/dist/core/index.d.ts +1 -0
  15. package/dist/core/index.d.ts.map +1 -1
  16. package/dist/core/index.js +1 -0
  17. package/dist/core/index.js.map +1 -1
  18. package/dist/core/metrics-sampler.d.ts +20 -0
  19. package/dist/core/metrics-sampler.d.ts.map +1 -1
  20. package/dist/core/metrics-sampler.js +52 -0
  21. package/dist/core/metrics-sampler.js.map +1 -1
  22. package/dist/core/model-registry.d.ts.map +1 -1
  23. package/dist/core/model-registry.js +4 -1
  24. package/dist/core/model-registry.js.map +1 -1
  25. package/dist/core/model-resolver.d.ts.map +1 -1
  26. package/dist/core/model-resolver.js +15 -6
  27. package/dist/core/model-resolver.js.map +1 -1
  28. package/dist/core/provider-resilience.d.ts +51 -0
  29. package/dist/core/provider-resilience.d.ts.map +1 -0
  30. package/dist/core/provider-resilience.js +86 -0
  31. package/dist/core/provider-resilience.js.map +1 -0
  32. package/dist/core/run-journal.d.ts.map +1 -1
  33. package/dist/core/run-journal.js +14 -22
  34. package/dist/core/run-journal.js.map +1 -1
  35. package/dist/core/session-termination.d.ts +8 -2
  36. package/dist/core/session-termination.d.ts.map +1 -1
  37. package/dist/core/session-termination.js +45 -3
  38. package/dist/core/session-termination.js.map +1 -1
  39. package/dist/core/settings-manager.d.ts +30 -0
  40. package/dist/core/settings-manager.d.ts.map +1 -1
  41. package/dist/core/settings-manager.js +17 -0
  42. package/dist/core/settings-manager.js.map +1 -1
  43. package/dist/core/tools/grep.d.ts.map +1 -1
  44. package/dist/core/tools/grep.js +47 -0
  45. package/dist/core/tools/grep.js.map +1 -1
  46. package/dist/core/tools/omp-pure-seams.d.ts +41 -0
  47. package/dist/core/tools/omp-pure-seams.d.ts.map +1 -0
  48. package/dist/core/tools/omp-pure-seams.js +142 -0
  49. package/dist/core/tools/omp-pure-seams.js.map +1 -0
  50. package/dist/core/tools/omp-seam-runtime.d.ts +100 -0
  51. package/dist/core/tools/omp-seam-runtime.d.ts.map +1 -0
  52. package/dist/core/tools/omp-seam-runtime.js +40 -0
  53. package/dist/core/tools/omp-seam-runtime.js.map +1 -0
  54. package/dist/core/tools/read.d.ts.map +1 -1
  55. package/dist/core/tools/read.js +34 -1
  56. package/dist/core/tools/read.js.map +1 -1
  57. package/dist/modes/interactive/components/footer.d.ts +1 -1
  58. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  59. package/dist/modes/interactive/components/footer.js +11 -8
  60. package/dist/modes/interactive/components/footer.js.map +1 -1
  61. package/dist/modes/interactive/theme/omk-aurora-dark.json +80 -0
  62. package/dist/modes/interactive/theme/omk-aurora-light.json +80 -0
  63. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  64. package/dist/modes/interactive/theme/theme.js +5 -0
  65. package/dist/modes/interactive/theme/theme.js.map +1 -1
  66. package/docs/adaptorch-preview.md +1 -1
  67. package/docs/provider-resilience.md +87 -0
  68. package/docs/settings.md +30 -0
  69. package/docs/themes.md +2 -0
  70. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  71. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  72. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  73. package/examples/extensions/gondolin/package-lock.json +2 -2
  74. package/examples/extensions/gondolin/package.json +1 -1
  75. package/examples/extensions/sandbox/package-lock.json +2 -2
  76. package/examples/extensions/sandbox/package.json +1 -1
  77. package/examples/extensions/with-deps/package-lock.json +2 -2
  78. package/examples/extensions/with-deps/package.json +1 -1
  79. package/npm-shrinkwrap.json +22 -13
  80. package/package.json +6 -5
@@ -23,6 +23,7 @@ import { stripFrontmatter } from "../utils/frontmatter.js";
23
23
  import { resolvePath } from "../utils/paths.js";
24
24
  import { sanitizeBinaryOutput } from "../utils/shell.js";
25
25
  import { sleep } from "../utils/sleep.js";
26
+ import { createAdaptorchBridge } from "./adaptorch-bridge.js";
26
27
  import { applyCategoryTimeoutDefaults, resolveAgentToolSettings, resolveToolTimeoutCategory, } from "./agent-tool-settings.js";
27
28
  import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
28
29
  import { parseBangInvocation } from "./bang-skill-invocation.js";
@@ -47,6 +48,7 @@ import { grokPlaybookAppendForProvider } from "./grok-playbook.js";
47
48
  import { assertLoadoutAccess, decideLoadoutAccess } from "./loadout-access-policy.js";
48
49
  import { findExactModelReferenceMatch } from "./model-resolver.js";
49
50
  import { expandPromptTemplate } from "./prompt-templates.js";
51
+ import { isContentSafetyStopMessage, isStickySafetyModel, isTransientProviderErrorMessage, pickFailoverCandidate, resolveProviderResilience, stickySafetyBlockMessage, } from "./provider-resilience.js";
50
52
  import { getBiasStepsForCell, getDefaultRouterBiasSnapshotPath, parseRouterBiasSnapshot, } from "./reasoning-router-bias.js";
51
53
  import { classifyTaskV4, resolveThinkingLevelV4WithUncertainty } from "./reasoning-router-v4.js";
52
54
  import { redactSensitiveText } from "./redaction.js";
@@ -254,6 +256,15 @@ export class AgentSession {
254
256
  */
255
257
  _reasoningRouterBiasSnapshot = null;
256
258
  _reasoningRouterBiasSnapshotLoaded = false;
259
+ /**
260
+ * AdaptOrch advisory bridge (default-off, global-only opt-in via
261
+ * `adaptorchBridge.enabled` in ~/.omk/agent/settings.json). Lazily
262
+ * constructed on the first v4 auto-turn when enabled; stays `null`
263
+ * otherwise. The bridge is advisory-only: its hint is fused into the
264
+ * resolver as a bounded ±2 step nudge, never as an override.
265
+ */
266
+ _adaptorchBridge = null;
267
+ _adaptorchBridgeInitAttempted = false;
257
268
  // Event subscription state
258
269
  _unsubscribeAgent;
259
270
  _eventListeners = [];
@@ -472,17 +483,27 @@ export class AgentSession {
472
483
  if (isContextOverflow(message, this.model?.contextWindow ?? 0)) {
473
484
  return { area: "provider", code: "context_overflow" };
474
485
  }
486
+ // Fable/Claude often emit stop_reason=refusal on benign turns (false positive).
487
+ // Match broadly so we never mislabel these as provider.protocol.
488
+ if (/stop_reason\s*=\s*(refusal|sensitive)|content\/safety stop|safety stop|provider\.refusal|kind=provider_refusal/i.test(text)) {
489
+ return { area: "provider", code: "refusal" };
490
+ }
475
491
  if (/auth|unauthori[sz]ed|forbidden|invalid.?api.?key|no api key|401|403|\/login/i.test(text)) {
476
492
  return { area: "provider", code: "auth" };
477
493
  }
478
494
  if (/rate.?limit|too many requests|429|quota|available balance|billing/i.test(text)) {
479
495
  return { area: "provider", code: "rate_limit" };
480
496
  }
497
+ // Kimi/K3 + OpenAI-compat: orphan tool results after dropped error assistants.
498
+ // Sanitize-and-retry (transform-messages drops orphans), not a hard tool fatal.
499
+ 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)) {
500
+ return { area: "provider", code: "protocol" };
501
+ }
481
502
  if (/tool.+timed? out|tool.+timeout/i.test(text))
482
503
  return { area: "tool", code: "timeout" };
483
- if (/tool/i.test(text))
504
+ if (/\btool\b/i.test(text) && !/tool_call_id/i.test(text))
484
505
  return { area: "tool", code: "fatal" };
485
- if (/network|fetch failed|connection|socket|websocket|timed? out|timeout|dns|econn/i.test(text)) {
506
+ if (/network|fetch failed|connection|socket|websocket|timed? out|timeout|dns|econn|^terminated$/i.test(text)) {
486
507
  return { area: "provider", code: "network" };
487
508
  }
488
509
  return { area: "provider", code: "protocol" };
@@ -1531,6 +1552,8 @@ export class AgentSession {
1531
1552
  if (!this.model) {
1532
1553
  throw new Error(formatNoModelSelectedMessage());
1533
1554
  }
1555
+ // Root-level: eject sticky safety models (Fable) before the turn leaves the machine.
1556
+ await this._ejectStickySafetyModelIfNeeded();
1534
1557
  // Grok OAuth: refuse Imagine ids on the chat/completions path (tool-only).
1535
1558
  assertTextChatModelForCompletion(this.model.id, this.model.provider);
1536
1559
  if (!this._modelRegistry.hasConfiguredAuth(this.model)) {
@@ -1935,6 +1958,11 @@ export class AgentSession {
1935
1958
  if (!this._modelRegistry.hasConfiguredAuth(model)) {
1936
1959
  throw new Error(`No API key for ${model.provider}/${model.id}`);
1937
1960
  }
1961
+ // Root-level: block sticky safety models (Fable) unless explicitly allowed in settings.
1962
+ const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
1963
+ if (resilience.blockStickySafetyModels && isStickySafetyModel(model.id, model.provider)) {
1964
+ throw new Error(stickySafetyBlockMessage(model.id, model.provider));
1965
+ }
1938
1966
  // Grok OAuth: block selecting Imagine models as the session chat model.
1939
1967
  assertTextChatModelForCompletion(model.id, model.provider);
1940
1968
  const previousModel = this.model;
@@ -1959,7 +1987,15 @@ export class AgentSession {
1959
1987
  return this._cycleAvailableModel(direction);
1960
1988
  }
1961
1989
  async _cycleScopedModel(direction) {
1962
- const scopedModels = this._scopedModels.filter((scoped) => this._modelRegistry.hasConfiguredAuth(scoped.model));
1990
+ const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
1991
+ const scopedModels = this._scopedModels.filter((scoped) => {
1992
+ if (!this._modelRegistry.hasConfiguredAuth(scoped.model))
1993
+ return false;
1994
+ if (resilience.blockStickySafetyModels && isStickySafetyModel(scoped.model.id, scoped.model.provider)) {
1995
+ return false;
1996
+ }
1997
+ return true;
1998
+ });
1963
1999
  if (scopedModels.length <= 1)
1964
2000
  return undefined;
1965
2001
  const currentModel = this.model;
@@ -1970,20 +2006,19 @@ export class AgentSession {
1970
2006
  const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
1971
2007
  const next = scopedModels[nextIndex];
1972
2008
  const thinkingLevel = this._getThinkingLevelForModelSwitch(next.thinkingLevel);
1973
- // Apply model
1974
- this.agent.state.model = next.model;
1975
- this.sessionManager.appendModelChange(next.model.provider, next.model.id);
1976
- this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
1977
- // Apply thinking level.
1978
- // - Explicit scoped model thinking level overrides current session level
1979
- // - Undefined scoped model thinking level inherits the current session preference
1980
- // setThinkingLevel clamps to model capabilities.
2009
+ // Single path through setModel (sticky block + auth + Imagine guards).
2010
+ await this.setModel(next.model);
2011
+ // Explicit scoped thinking level still applies after switch.
1981
2012
  this.setThinkingLevel(thinkingLevel);
1982
- await this._emitModelSelect(next.model, currentModel, "cycle");
1983
2013
  return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };
1984
2014
  }
1985
2015
  async _cycleAvailableModel(direction) {
1986
- const availableModels = await this._modelRegistry.getAvailable();
2016
+ const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
2017
+ const raw = await this._modelRegistry.getAvailable();
2018
+ // Hard-kill sticky safety models from the cycle list (Fable must not re-enter via hotkey).
2019
+ const availableModels = resilience.blockStickySafetyModels
2020
+ ? raw.filter((m) => !isStickySafetyModel(m.id, m.provider))
2021
+ : raw;
1987
2022
  if (availableModels.length <= 1)
1988
2023
  return undefined;
1989
2024
  const currentModel = this.model;
@@ -1993,13 +2028,8 @@ export class AgentSession {
1993
2028
  const len = availableModels.length;
1994
2029
  const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
1995
2030
  const nextModel = availableModels[nextIndex];
1996
- const thinkingLevel = this._getThinkingLevelForModelSwitch();
1997
- this.agent.state.model = nextModel;
1998
- this.sessionManager.appendModelChange(nextModel.provider, nextModel.id);
1999
- this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
2000
- // Re-clamp thinking level for new model's capabilities
2001
- this.setThinkingLevel(thinkingLevel);
2002
- await this._emitModelSelect(nextModel, currentModel, "cycle");
2031
+ // Always go through setModel so sticky blocks / auth / Imagine guards stay single-path.
2032
+ await this.setModel(nextModel);
2003
2033
  return { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false };
2004
2034
  }
2005
2035
  // =========================================================================
@@ -2099,7 +2129,7 @@ export class AgentSession {
2099
2129
  hadFence: features.hadFence,
2100
2130
  hadDiff: features.hadDiff,
2101
2131
  });
2102
- const resolved = resolveThinkingLevelV4WithUncertainty(verdict, availableLevels, undefined, bias, null);
2132
+ const resolved = resolveThinkingLevelV4WithUncertainty(verdict, availableLevels, undefined, bias, this._getAdaptorchHint(verdict, features));
2103
2133
  if (learningEnabled && resolved !== "off") {
2104
2134
  const record = {
2105
2135
  routerVersion: "v4",
@@ -2193,6 +2223,76 @@ export class AgentSession {
2193
2223
  return 1;
2194
2224
  return 0;
2195
2225
  }
2226
+ /**
2227
+ * AdaptOrch advisory bridge hint accessor (default-off, global-only).
2228
+ * Lazily constructs the bridge on first call when enabled; returns `null`
2229
+ * (no hint) otherwise. The bridge's `getFreshHint` is synchronous and
2230
+ * cache-read-only, so this never blocks the turn-start path. A fire-and-
2231
+ * forget `requestRefresh` is issued for the NEXT turn's benefit.
2232
+ *
2233
+ * The returned hint is shaped as `{ level, confidence }` for the resolver's
2234
+ * hint-fusion slot: confidence is mapped from the bridge's closed
2235
+ * `confidenceBand` enum (low=0.5, medium=0.75, high=0.95) and the level is
2236
+ * derived from the bridge's `taskClass` via the same static rule table the
2237
+ * resolver uses. The resolver's own HINT_CONFIDENCE_THRESHOLD (0.7) gates
2238
+ * whether the hint actually fuses, so a "low" band hint is structurally
2239
+ * inert.
2240
+ */
2241
+ _getAdaptorchHint(verdict, features) {
2242
+ if (!this.settingsManager.getAdaptorchBridgeEnabled())
2243
+ return null;
2244
+ // Lazy init (at most once per session)
2245
+ if (!this._adaptorchBridgeInitAttempted) {
2246
+ this._adaptorchBridgeInitAttempted = true;
2247
+ const bridgeSettings = this.settingsManager.getAdaptorchBridgeSettings();
2248
+ this._adaptorchBridge = createAdaptorchBridge({
2249
+ // The advisory function is a no-op stub until a real MCP transport
2250
+ // is wired by a future lane. The bridge's circuit breaker and budget
2251
+ // counter ensure this stub can never cause harm.
2252
+ advisoryFn: async () => null,
2253
+ ttlMs: bridgeSettings?.ttlMs,
2254
+ timeoutMs: bridgeSettings?.timeoutMs,
2255
+ maxConsultsPerSession: bridgeSettings?.maxConsultsPerSession,
2256
+ failureThreshold: bridgeSettings?.failureThreshold,
2257
+ });
2258
+ }
2259
+ const bridge = this._adaptorchBridge;
2260
+ if (bridge === null)
2261
+ return null;
2262
+ const payload = {
2263
+ schemaVersion: 1,
2264
+ taskClass: verdict.taskClass,
2265
+ runnerUp: verdict.taskClass,
2266
+ marginBucket: "high",
2267
+ lenBucket: features.lenBucket,
2268
+ hadFence: features.hadFence,
2269
+ hadDiff: features.hadDiff,
2270
+ pressureBucket: this._computePressureBucket(),
2271
+ };
2272
+ // Fire-and-forget refresh for next turn
2273
+ bridge.requestRefresh(payload);
2274
+ // Synchronous cache read for this turn
2275
+ const hint = bridge.getFreshHint(payload);
2276
+ if (hint === null)
2277
+ return null;
2278
+ // Map confidenceBand -> numeric confidence for the resolver's threshold gate
2279
+ const confidenceMap = { low: 0.5, medium: 0.75, high: 0.95 };
2280
+ const confidence = confidenceMap[hint.confidenceBand] ?? 0.5;
2281
+ // Map taskClass -> ThinkingLevel via the same rule table the resolver uses
2282
+ const levelMap = {
2283
+ trivial: "minimal",
2284
+ "simple-edit": "low",
2285
+ "code-gen": "medium",
2286
+ debug: "high",
2287
+ refactor: "high",
2288
+ review: "high",
2289
+ plan: "xhigh",
2290
+ };
2291
+ const level = levelMap[hint.taskClass];
2292
+ if (level === undefined)
2293
+ return null;
2294
+ return { level, confidence };
2295
+ }
2196
2296
  /**
2197
2297
  * Cycle to next thinking level.
2198
2298
  * @returns New level, or undefined if model doesn't support thinking
@@ -3371,13 +3471,68 @@ export class AgentSession {
3371
3471
  const err = message.errorMessage;
3372
3472
  if (this._isNonRetryableProviderLimitError(err))
3373
3473
  return false;
3374
- // Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504, service unavailable, network/connection errors (including connection lost), WebSocket transport closes/errors, fetch failed, premature stream endings, HTTP/2 closed before response, terminated, retry delay exceeded
3375
- return /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i.test(err);
3474
+ // Shared contract with provider-resilience.ts (Fable safety stop, K3 orphan tool_call_id, terminated).
3475
+ return isTransientProviderErrorMessage(err);
3376
3476
  }
3377
3477
  /**
3378
3478
  * Prepare a retryable error for continuation with exponential backoff.
3379
3479
  * @returns true if the caller should continue the agent, false otherwise
3380
3480
  */
3481
+ /** Eject sticky safety model at prompt boundary (session resume / leftover default). */
3482
+ async _ejectStickySafetyModelIfNeeded() {
3483
+ const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
3484
+ if (!resilience.blockStickySafetyModels)
3485
+ return;
3486
+ const current = this.model;
3487
+ if (!current || !isStickySafetyModel(current.id, current.provider))
3488
+ return;
3489
+ const pick = pickFailoverCandidate(resilience.failoverCandidates, { provider: current.provider, id: current.id }, (c) => {
3490
+ const next = this._modelRegistry.find(c.provider, c.id);
3491
+ return !!next && this._modelRegistry.hasConfiguredAuth(next);
3492
+ });
3493
+ if (!pick) {
3494
+ throw new Error(stickySafetyBlockMessage(current.id, current.provider));
3495
+ }
3496
+ const next = this._modelRegistry.find(pick.provider, pick.id);
3497
+ if (!next) {
3498
+ throw new Error(stickySafetyBlockMessage(current.id, current.provider));
3499
+ }
3500
+ // Bypass setModel sticky check by temporarily allowing via direct state switch path:
3501
+ // setModel itself blocks sticky targets, not sources — safe.
3502
+ await this.setModel(next);
3503
+ }
3504
+ /**
3505
+ * Sticky safety models (e.g. claude-fable-5) often false-positive on coding turns.
3506
+ * Retrying the same model repeats the refusal — switch via provider-resilience chain first.
3507
+ */
3508
+ async _maybeFailoverFromSafetyStop(message) {
3509
+ const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
3510
+ if (!resilience.autoFailoverOnSafetyStop)
3511
+ return undefined;
3512
+ if (!isContentSafetyStopMessage(message.errorMessage))
3513
+ return undefined;
3514
+ const current = this.model;
3515
+ const currentId = message.model || current?.id || "";
3516
+ const currentProvider = message.provider || current?.provider || "";
3517
+ if (!isStickySafetyModel(currentId, currentProvider))
3518
+ return undefined;
3519
+ const pick = pickFailoverCandidate(resilience.failoverCandidates, current ? { provider: current.provider, id: current.id } : undefined, (c) => {
3520
+ const next = this._modelRegistry.find(c.provider, c.id);
3521
+ return !!next && this._modelRegistry.hasConfiguredAuth(next);
3522
+ });
3523
+ if (!pick)
3524
+ return undefined;
3525
+ const next = this._modelRegistry.find(pick.provider, pick.id);
3526
+ if (!next)
3527
+ return undefined;
3528
+ try {
3529
+ await this.setModel(next);
3530
+ return `${next.provider}/${next.id}`;
3531
+ }
3532
+ catch {
3533
+ return undefined;
3534
+ }
3535
+ }
3381
3536
  async _prepareRetry(message) {
3382
3537
  const settings = this.settingsManager.getRetrySettings();
3383
3538
  if (!settings.enabled) {
@@ -3389,13 +3544,21 @@ export class AgentSession {
3389
3544
  this._retryAttempt--;
3390
3545
  return false;
3391
3546
  }
3392
- const delayMs = settings.baseDelayMs * 2 ** (this._retryAttempt - 1);
3547
+ // Content/safety stop on Fable: switch model BEFORE delay so the retry is not another Fable refusal.
3548
+ const failoverTo = await this._maybeFailoverFromSafetyStop(message);
3549
+ // Safety stops are usually immediate false positives — short delay after failover, full backoff otherwise.
3550
+ const delayMs = failoverTo
3551
+ ? Math.min(400, settings.baseDelayMs)
3552
+ : settings.baseDelayMs * 2 ** (this._retryAttempt - 1);
3553
+ const errorMessage = failoverTo
3554
+ ? `${message.errorMessage || "content/safety stop"} → failover ${failoverTo}`
3555
+ : message.errorMessage || "Unknown error";
3393
3556
  this._emit({
3394
3557
  type: "auto_retry_start",
3395
3558
  attempt: this._retryAttempt,
3396
3559
  maxAttempts: settings.maxRetries,
3397
3560
  delayMs,
3398
- errorMessage: message.errorMessage || "Unknown error",
3561
+ errorMessage,
3399
3562
  });
3400
3563
  // Remove error message from agent state (keep in session for history)
3401
3564
  const messages = this.agent.state.messages;