open-multi-agent-kit 0.90.9 → 0.92.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 (131) hide show
  1. package/CHANGELOG.md +39 -4
  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 +196 -32
  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 -58
  9. package/dist/core/command-safety.js.map +1 -1
  10. package/dist/core/compaction/transaction.d.ts.map +1 -1
  11. package/dist/core/compaction/transaction.js +7 -3
  12. package/dist/core/compaction/transaction.js.map +1 -1
  13. package/dist/core/context-budget-headroom-candidates.d.ts.map +1 -1
  14. package/dist/core/context-budget-headroom-candidates.js +21 -11
  15. package/dist/core/context-budget-headroom-candidates.js.map +1 -1
  16. package/dist/core/context-budget-system-prompt-items.d.ts +1 -0
  17. package/dist/core/context-budget-system-prompt-items.d.ts.map +1 -1
  18. package/dist/core/context-budget-system-prompt-items.js +7 -8
  19. package/dist/core/context-budget-system-prompt-items.js.map +1 -1
  20. package/dist/core/footer-data-provider.d.ts +7 -1
  21. package/dist/core/footer-data-provider.d.ts.map +1 -1
  22. package/dist/core/footer-data-provider.js +12 -0
  23. package/dist/core/footer-data-provider.js.map +1 -1
  24. package/dist/core/index.d.ts +1 -0
  25. package/dist/core/index.d.ts.map +1 -1
  26. package/dist/core/index.js +1 -0
  27. package/dist/core/index.js.map +1 -1
  28. package/dist/core/loadouts.d.ts.map +1 -1
  29. package/dist/core/loadouts.js +27 -48
  30. package/dist/core/loadouts.js.map +1 -1
  31. package/dist/core/metrics-sampler.d.ts +20 -0
  32. package/dist/core/metrics-sampler.d.ts.map +1 -1
  33. package/dist/core/metrics-sampler.js +52 -0
  34. package/dist/core/metrics-sampler.js.map +1 -1
  35. package/dist/core/model-registry.d.ts.map +1 -1
  36. package/dist/core/model-registry.js +4 -1
  37. package/dist/core/model-registry.js.map +1 -1
  38. package/dist/core/model-resolver.d.ts.map +1 -1
  39. package/dist/core/model-resolver.js +15 -6
  40. package/dist/core/model-resolver.js.map +1 -1
  41. package/dist/core/provider-resilience.d.ts +51 -0
  42. package/dist/core/provider-resilience.d.ts.map +1 -0
  43. package/dist/core/provider-resilience.js +86 -0
  44. package/dist/core/provider-resilience.js.map +1 -0
  45. package/dist/core/run-journal.d.ts.map +1 -1
  46. package/dist/core/run-journal.js +14 -22
  47. package/dist/core/run-journal.js.map +1 -1
  48. package/dist/core/session-termination.d.ts +8 -2
  49. package/dist/core/session-termination.d.ts.map +1 -1
  50. package/dist/core/session-termination.js +45 -3
  51. package/dist/core/session-termination.js.map +1 -1
  52. package/dist/core/settings-manager.d.ts +30 -0
  53. package/dist/core/settings-manager.d.ts.map +1 -1
  54. package/dist/core/settings-manager.js +17 -0
  55. package/dist/core/settings-manager.js.map +1 -1
  56. package/dist/core/system-prompt.d.ts.map +1 -1
  57. package/dist/core/system-prompt.js +35 -30
  58. package/dist/core/system-prompt.js.map +1 -1
  59. package/dist/core/tools/grep.d.ts.map +1 -1
  60. package/dist/core/tools/grep.js +47 -0
  61. package/dist/core/tools/grep.js.map +1 -1
  62. package/dist/core/tools/omp-pure-seams.d.ts +41 -0
  63. package/dist/core/tools/omp-pure-seams.d.ts.map +1 -0
  64. package/dist/core/tools/omp-pure-seams.js +142 -0
  65. package/dist/core/tools/omp-pure-seams.js.map +1 -0
  66. package/dist/core/tools/omp-seam-runtime.d.ts +100 -0
  67. package/dist/core/tools/omp-seam-runtime.d.ts.map +1 -0
  68. package/dist/core/tools/omp-seam-runtime.js +40 -0
  69. package/dist/core/tools/omp-seam-runtime.js.map +1 -0
  70. package/dist/core/tools/read.d.ts.map +1 -1
  71. package/dist/core/tools/read.js +34 -1
  72. package/dist/core/tools/read.js.map +1 -1
  73. package/dist/main.d.ts.map +1 -1
  74. package/dist/main.js +112 -11
  75. package/dist/main.js.map +1 -1
  76. package/dist/modes/interactive/components/footer.d.ts +1 -1
  77. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  78. package/dist/modes/interactive/components/footer.js +11 -8
  79. package/dist/modes/interactive/components/footer.js.map +1 -1
  80. package/dist/modes/interactive/theme/omk-aurora-dark.json +80 -0
  81. package/dist/modes/interactive/theme/omk-aurora-light.json +80 -0
  82. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  83. package/dist/modes/interactive/theme/theme.js +5 -0
  84. package/dist/modes/interactive/theme/theme.js.map +1 -1
  85. package/dist/modes/print-mode.d.ts.map +1 -1
  86. package/dist/modes/print-mode.js +9 -0
  87. package/dist/modes/print-mode.js.map +1 -1
  88. package/docs/adaptorch-preview-spec.md +174 -0
  89. package/docs/adaptorch-preview.md +2 -2
  90. package/docs/compaction.md +13 -13
  91. package/docs/correctness-wall.md +13 -5
  92. package/docs/custom-provider.md +8 -8
  93. package/docs/development.md +2 -2
  94. package/docs/extensions.md +8 -8
  95. package/docs/json.md +4 -4
  96. package/docs/provider-resilience.md +87 -0
  97. package/docs/providers.md +1 -1
  98. package/docs/session-format.md +5 -5
  99. package/docs/settings.md +30 -0
  100. package/docs/skills.md +2 -0
  101. package/docs/themes.md +4 -2
  102. package/docs/tui.md +4 -4
  103. package/docs/usage.md +4 -0
  104. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  105. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  106. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  107. package/examples/extensions/gondolin/package-lock.json +2 -2
  108. package/examples/extensions/gondolin/package.json +1 -1
  109. package/examples/extensions/sandbox/package-lock.json +2 -2
  110. package/examples/extensions/sandbox/package.json +1 -1
  111. package/examples/extensions/subagent/README.md +35 -10
  112. package/examples/extensions/subagent/adaptive-agent-runtime.test.ts +234 -0
  113. package/examples/extensions/subagent/adaptive-agent-runtime.ts +240 -0
  114. package/examples/extensions/subagent/adaptive-result.ts +112 -0
  115. package/examples/extensions/subagent/checkpoint-runtime.test.ts +125 -0
  116. package/examples/extensions/subagent/checkpoint-runtime.ts +198 -0
  117. package/examples/extensions/subagent/deadline-budget.test.ts +135 -0
  118. package/examples/extensions/subagent/deadline-budget.ts +242 -0
  119. package/examples/extensions/subagent/deadline-profile-store.test.ts +72 -0
  120. package/examples/extensions/subagent/deadline-profile-store.ts +172 -0
  121. package/examples/extensions/subagent/fixtures/fake-omk-json.mjs +13 -0
  122. package/examples/extensions/subagent/fixtures/hanging-process-tree.mjs +40 -0
  123. package/examples/extensions/subagent/index.ts +253 -137
  124. package/examples/extensions/subagent/managed-process.test.ts +178 -0
  125. package/examples/extensions/subagent/managed-process.ts +236 -0
  126. package/examples/extensions/subagent/subagent-extension-smoke.test.ts +112 -0
  127. package/examples/extensions/subagent/subagent-runtime-types.ts +76 -0
  128. package/examples/extensions/with-deps/package-lock.json +2 -2
  129. package/examples/extensions/with-deps/package.json +1 -1
  130. package/npm-shrinkwrap.json +28 -19
  131. 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
@@ -2309,7 +2409,9 @@ export class AgentSession {
2309
2409
  const message = capture.report.activeMessages[index];
2310
2410
  if (message?.role !== "user")
2311
2411
  continue;
2312
- const candidate = sanitizeBinaryOutput(redactSensitiveText(this._getUserMessageText(message)).trim()).slice(0, 16_384);
2412
+ const candidate = sanitizeBinaryOutput(redactSensitiveText(this._getUserMessageText(message)).trim())
2413
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, "")
2414
+ .slice(0, 16_384);
2313
2415
  if (candidate.length > 0)
2314
2416
  latestIntent = candidate;
2315
2417
  break;
@@ -2342,7 +2444,7 @@ export class AgentSession {
2342
2444
  branch: null,
2343
2445
  worktree: this._cwd,
2344
2446
  modelHistory,
2345
- nextAction: latestIntent,
2447
+ nextAction: latestIntent.slice(0, 4096) || "Continue the current session",
2346
2448
  };
2347
2449
  }
2348
2450
  _barrierError(barrier) {
@@ -2651,7 +2753,7 @@ export class AgentSession {
2651
2753
  throw new Error("Compaction cancelled");
2652
2754
  }
2653
2755
  const compactionResult = {
2654
- summary,
2756
+ summary: redactSensitiveText(summary),
2655
2757
  firstKeptEntryId,
2656
2758
  tokensBefore,
2657
2759
  details,
@@ -2934,7 +3036,7 @@ export class AgentSession {
2934
3036
  return false;
2935
3037
  }
2936
3038
  const result = {
2937
- summary,
3039
+ summary: redactSensitiveText(summary),
2938
3040
  firstKeptEntryId,
2939
3041
  tokensBefore,
2940
3042
  details,
@@ -3371,13 +3473,68 @@ export class AgentSession {
3371
3473
  const err = message.errorMessage;
3372
3474
  if (this._isNonRetryableProviderLimitError(err))
3373
3475
  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);
3476
+ // Shared contract with provider-resilience.ts (Fable safety stop, K3 orphan tool_call_id, terminated).
3477
+ return isTransientProviderErrorMessage(err);
3376
3478
  }
3377
3479
  /**
3378
3480
  * Prepare a retryable error for continuation with exponential backoff.
3379
3481
  * @returns true if the caller should continue the agent, false otherwise
3380
3482
  */
3483
+ /** Eject sticky safety model at prompt boundary (session resume / leftover default). */
3484
+ async _ejectStickySafetyModelIfNeeded() {
3485
+ const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
3486
+ if (!resilience.blockStickySafetyModels)
3487
+ return;
3488
+ const current = this.model;
3489
+ if (!current || !isStickySafetyModel(current.id, current.provider))
3490
+ return;
3491
+ const pick = pickFailoverCandidate(resilience.failoverCandidates, { provider: current.provider, id: current.id }, (c) => {
3492
+ const next = this._modelRegistry.find(c.provider, c.id);
3493
+ return !!next && this._modelRegistry.hasConfiguredAuth(next);
3494
+ });
3495
+ if (!pick) {
3496
+ throw new Error(stickySafetyBlockMessage(current.id, current.provider));
3497
+ }
3498
+ const next = this._modelRegistry.find(pick.provider, pick.id);
3499
+ if (!next) {
3500
+ throw new Error(stickySafetyBlockMessage(current.id, current.provider));
3501
+ }
3502
+ // Bypass setModel sticky check by temporarily allowing via direct state switch path:
3503
+ // setModel itself blocks sticky targets, not sources — safe.
3504
+ await this.setModel(next);
3505
+ }
3506
+ /**
3507
+ * Sticky safety models (e.g. claude-fable-5) often false-positive on coding turns.
3508
+ * Retrying the same model repeats the refusal — switch via provider-resilience chain first.
3509
+ */
3510
+ async _maybeFailoverFromSafetyStop(message) {
3511
+ const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
3512
+ if (!resilience.autoFailoverOnSafetyStop)
3513
+ return undefined;
3514
+ if (!isContentSafetyStopMessage(message.errorMessage))
3515
+ return undefined;
3516
+ const current = this.model;
3517
+ const currentId = message.model || current?.id || "";
3518
+ const currentProvider = message.provider || current?.provider || "";
3519
+ if (!isStickySafetyModel(currentId, currentProvider))
3520
+ return undefined;
3521
+ const pick = pickFailoverCandidate(resilience.failoverCandidates, current ? { provider: current.provider, id: current.id } : undefined, (c) => {
3522
+ const next = this._modelRegistry.find(c.provider, c.id);
3523
+ return !!next && this._modelRegistry.hasConfiguredAuth(next);
3524
+ });
3525
+ if (!pick)
3526
+ return undefined;
3527
+ const next = this._modelRegistry.find(pick.provider, pick.id);
3528
+ if (!next)
3529
+ return undefined;
3530
+ try {
3531
+ await this.setModel(next);
3532
+ return `${next.provider}/${next.id}`;
3533
+ }
3534
+ catch {
3535
+ return undefined;
3536
+ }
3537
+ }
3381
3538
  async _prepareRetry(message) {
3382
3539
  const settings = this.settingsManager.getRetrySettings();
3383
3540
  if (!settings.enabled) {
@@ -3389,13 +3546,21 @@ export class AgentSession {
3389
3546
  this._retryAttempt--;
3390
3547
  return false;
3391
3548
  }
3392
- const delayMs = settings.baseDelayMs * 2 ** (this._retryAttempt - 1);
3549
+ // Content/safety stop on Fable: switch model BEFORE delay so the retry is not another Fable refusal.
3550
+ const failoverTo = await this._maybeFailoverFromSafetyStop(message);
3551
+ // Safety stops are usually immediate false positives — short delay after failover, full backoff otherwise.
3552
+ const delayMs = failoverTo
3553
+ ? Math.min(400, settings.baseDelayMs)
3554
+ : settings.baseDelayMs * 2 ** (this._retryAttempt - 1);
3555
+ const errorMessage = failoverTo
3556
+ ? `${message.errorMessage || "content/safety stop"} → failover ${failoverTo}`
3557
+ : message.errorMessage || "Unknown error";
3393
3558
  this._emit({
3394
3559
  type: "auto_retry_start",
3395
3560
  attempt: this._retryAttempt,
3396
3561
  maxAttempts: settings.maxRetries,
3397
3562
  delayMs,
3398
- errorMessage: message.errorMessage || "Unknown error",
3563
+ errorMessage,
3399
3564
  });
3400
3565
  // Remove error message from agent state (keep in session for history)
3401
3566
  const messages = this.agent.state.messages;
@@ -3466,11 +3631,10 @@ export class AgentSession {
3466
3631
  assertLoadoutAccess((request) => decideLoadoutAccess(this._loadoutAccessPolicy, request), { operation: "execute", toolName: "bash", command: resolvedCommand });
3467
3632
  }
3468
3633
  // Non-negotiable safety floor for headless callers (RPC bash): hard-deny
3469
- // block-tier commands and credential/secret file access before any shell is
3470
- // spawned. This mirrors the §0.1 freedom safety floor behavior in omakit.
3634
+ // block-tier commands before any shell is spawned.
3471
3635
  if (options?.safetyGate === "headless") {
3472
3636
  const floorVerdict = classifyShellCommand(command);
3473
- if (floorVerdict.risk === "block" || floorVerdict.rule.startsWith("secret.")) {
3637
+ if (floorVerdict.risk === "block") {
3474
3638
  throw new Error(`OMK §0.1 safety floor blocked bash: [${floorVerdict.rule}] ${floorVerdict.reason}`);
3475
3639
  }
3476
3640
  }