feat(plugins): add model failover and terminal failure hooks#70990
feat(plugins): add model failover and terminal failure hooks#70990mantisai-bot wants to merge 34 commits into
Conversation
Greptile SummaryAdds two fire-and-forget plugin hooks — Confidence Score: 5/5Safe to merge — both hooks are fire-and-forget with fail-open semantics and no changes to existing failover control flow. All remaining findings are P2: a minor duplicate type-guard call and an optional field in the attempts type that is never populated. Neither affects correctness or runtime behavior. No files require special attention.
|
|
Codex review: needs changes before merge. Reviewed July 3, 2026, 3:05 AM ET / 07:05 UTC. Summary Reproducibility: yes. Source inspection reproduces the review blockers: returned Review metrics: 3 noteworthy metrics.
Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review findings
Review detailsBest possible solution: Land a maintainer-approved Plugin SDK hook contract only after the terminal hook covers both thrown and returned exhausted failures, CI is clean, and broader fallback notification/context policy remains on the existing fallback tracker. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection reproduces the review blockers: returned Is this the best way to solve the issue? No, not yet. The hook surface is a plausible owner-boundary solution, but the implementation should share terminal-hook emission across thrown and returned exhausted failures and get maintainer signoff on the public API/data boundary. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 8ed6c78b7891. Label changesLabel justifications:
Evidence reviewedSecurity concerns:
Acceptance criteria:
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
This directly addresses the hook/event gap from #65824 item 1 — glad to see it moving. A few notes from running our own local mitigation (canon-guardian, shared in #75750) that might be useful for the implementation: On hook timing: @kinthaiofficial noted in #65824 that On what operators actually need in the event payload: From our production logs, the most actionable fields are: which model failed, which fallback was selected, whether it was a rate-limit/quota failure vs. a hard error (404/invalid config), and whether the primary is expected to recover. The last one is the difference between 'alert now' and 'just log it.' Scope boundary looks right — keeping context re-injection out of this PR is the right call. That's a bigger design question that belongs in the #65824 thread. Thanks for picking this up. Running 2026.5.3-1 and will test against the final version when it lands. |
4755b57 to
ef986eb
Compare
|
Quick follow-up after the recent interest here: I re-checked the current head The current
The timing boundary is intentionally observation-only: docs now call out that I also re-ran the requested focused validation locally:
All passed, current checks are green, and merge state is |
ef986eb to
2d82dfe
Compare
2d82dfe to
da7929f
Compare
da7929f to
ab573be
Compare
ab573be to
95dc679
Compare
95dc679 to
c74ecfc
Compare
|
@clawsweeper re-review Rebased onto current |
|
🦞🧹 I asked ClawSweeper to review this item again. |
…nto feat/70976-model-failover-hooks # Conflicts: # docs/.generated/config-baseline.sha256 # docs/.generated/plugin-sdk-api-baseline.sha256 # src/auto-reply/reply/agent-runner-execution.test.ts # src/auto-reply/reply/agent-runner-execution.ts
…over-hooks # Conflicts: # docs/.generated/config-baseline.sha256 # docs/.generated/plugin-sdk-api-baseline.sha256
…over-hooks # Conflicts: # docs/.generated/plugin-sdk-api-baseline.sha256 # src/auto-reply/reply/agent-runner-execution.ts # src/auto-reply/reply/followup-runner.ts
|
@clawsweeper re-review Current head |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
…over-hooks # Conflicts: # docs/.generated/config-baseline.sha256 # docs/.generated/plugin-sdk-api-baseline.sha256 # extensions/memory-core/src/memory/manager-fts-state.test.ts # scripts/plugin-sdk-surface-report.mjs # src/agents/embedded-agent-runner/run/failover-observation.ts # src/agents/embedded-agent-runner/run/llm-idle-timeout.test.ts # src/commands/migrate/skill-selection-prompt.ts # src/gateway/session-utils.ts # src/proxy-capture/store.sqlite.test.ts # ui/src/ui/views/workboard.ts
…over-hooks # Conflicts: # docs/.generated/config-baseline.sha256 # docs/.generated/plugin-sdk-api-baseline.sha256 # extensions/slack/src/monitor/send.runtime.ts # scripts/plugin-sdk-surface-report.mjs # src/agents/agent-tools.schema.test.ts # src/agents/model-fallback.ts # src/commands/onboard-channels.e2e.test.ts
…over-hooks # Conflicts: # scripts/plugin-sdk-surface-report.mjs
…over-hooks # Conflicts: # docs/.generated/config-baseline.sha256 # docs/.generated/plugin-sdk-api-baseline.sha256 # extensions/codex/src/app-server/app-inventory-cache.test.ts # extensions/codex/src/app-server/plugin-app-cache-key.ts # extensions/msteams/src/monitor-handler.ts # extensions/telegram/src/lane-delivery-text-deliverer.ts # scripts/plugin-sdk-surface-report.mjs # test/scripts/check-workflows.test.ts
Summary
Adds two plugin observability hooks for model failure handling:
model_failover: observes embedded-runner failover decisions, including failed source refs, selected target refs, stage, decision, reason metadata, recoverability, timeout/abort/status flags, and fallback availability.model_failure_terminal: observes terminal before-reply model failures after all configured attempts are exhausted.This PR does not change fallback policy, provider retry behavior, context re-injection behavior, command/tool execution, network access, or default user-facing failure copy.
Current implementation
model_failoverfrom the embedded-runner failover decision path.model_failure_terminalafter resolving the actual delivered failure reply, sofinalMessagematches user-visible failure copy.Security / compatibility
model_failover,model_failure_terminal.model_failure_terminalis conversation-access gated for non-bundled plugins because it can expose delivered failure copy.Real behavior proof
model_failoverwhen the primary model fails and the runtime selects the configured fallback target. A separate exhausted-model run should emitmodel_failure_terminalafter all configured model attempts fail, withfinalMessagematching the user-visible failure copy andattemptssummarizing provider/model/reason metadata.v25.6.0, pnpm11.2.2, PR head5cd9b674c3b9.openclaw.plugin.json. OpenClaw loaded that plugin through the real plugin loader; the plugin registeredapi.on("model_failover", ...)andapi.on("model_failure_terminal", ...)and wrote hook payloads to a redacted JSONL artifact. Model providers were deterministic local OpenAI-compatible Responses endpoints. Scenario 1 returned HTTP 429 from the primary endpoint and a successful SSE canary from fallback. Scenario 2 returned HTTP 429 from both primary and fallback to exhaust all configured model attempts. No external provider token was used.OPENCLAW_MODEL_FAILOVER_PROOF_HEAD=$(git rev-parse --short=12 HEAD) node --import tsx .artifacts/model-failover-real-proof.mjsmodel_failoverfired afterproof-primary/proof-primary-modelreturned HTTP 429, selectedproof-fallback/proof-fallback-model, and then deliveredPROOF_FALLBACK_CANARY_OK. The same proof run confirmed the exhausted-model scenario emittedmodel_failure_terminalafter both configured attempts failed.model_failoverreported selected targetproof-fallback/proof-fallback-model, failed sourceproof-primary/proof-primary-model,decision: fallback_model,failoverReason: rate_limit,status: 429, andsourceRecoverable: true.PROOF_FALLBACK_CANARY_OK.proof-terminal-primary/proof-terminal-primary-modelandproof-terminal-fallback/proof-terminal-fallback-model, exhausted all attempts, and emittedmodel_failure_terminalwithkind: all_models_failed.model_failure_terminal.finalMessagewas exactly the delivered terminal reply:⚠️ All models are temporarily rate-limited. Please try again in a few minutes.attemptscontained both failed provider/model summaries withrate_limitreasons.Regression coverage for the latest review findings
New source tests cover the ClawSweeper findings directly:
src/auto-reply/reply/agent-runner-execution.test.ts:passes the selected next fallback candidate into embedded auto-reply runssrc/auto-reply/reply/agent-runner-execution.test.ts:emits model_failure_terminal with the delivered terminal failure copysrc/auto-reply/reply/followup-runner.test.ts:passes selected next fallback candidates to embedded follow-up runsThe unrelated Anthropic config-default change is no longer part of this PR diff.
Validation
Commands run on head
5cd9b674c3b9:Observed results:
tsgo:test:srcpassed.pnpm check:architecturepassed.git diff --check origin/main...HEAD && git diff --checkpassed.git merge-tree --write-tree HEAD origin/mainpassed.oxfmt --checkpassed.5cd9b674c3b9, with the runtime-loaded plugin observingmodel_failover, selected fallback targetproof-fallback/proof-fallback-model, delivered fallback canaryPROOF_FALLBACK_CANARY_OK, and terminalmodel_failure_terminal.finalMessagematching the delivered all-models-failed reply.Latest rebase/proof refresh (2026-06-16)
Current head:
566a985e2773e7890782801386191837ee102f90Merged current
origin/main(395a0823484f) into the PR branch. Conflict resolution kept both current-main lifecycle terminal error emission and this PR'smodel_failure_terminalplugin hook emission. Queued follow-up fallback execution now keeps bothexposeNextCandidateToRun: trueand current-main exhausted-result merging. The Plugin SDK API baseline hash was regenerated from the merged tree. A follow-up commit also bumps the plugin SDK public export budget from 10,270 to 10,274 for the intentional hook surface expansion, fixing the post-pushchecks-node-core-fastfailure intest/scripts/plugin-sdk-surface-report.test.ts.Validation on the current head:
Observed results:
--checkpassed with the exact deprecated-export override used by the failing CI test.git diff --check origin/main...HEAD && git diff --checkpassed.oxfmt --checkpassed.566a985e2773, with the runtime-loaded plugin observingmodel_failover, selected fallback targetproof-fallback/proof-fallback-model, delivered fallback canaryPROOF_FALLBACK_CANARY_OK, and terminalmodel_failure_terminal.finalMessagematching the delivered all-models-failed reply.Notes:
pnpm plugin-sdk:api:gengot stuck in pnpm's dependency-status install wrapper on this host, so the same generator was run directly withnode --max-old-space-size=8192 --import tsx scripts/generate-plugin-sdk-api-baseline.ts --write.pnpm config:docs:check,pnpm tsgo:test:src, orpnpm check:architectureon this refresh.Latest head refresh (2026-06-22)
Current head:
6a1b11fb758bb1c7ed0f45a9e840ef54e05ff8fcMerged current
origin/main(3ff59df960) into the PR branch, resolved the Plugin SDK surface-budget conflict by keeping this PR's intentional higher hook-surface budgets, and aligned the slash-command browser-import expectation with the current shared-registry import order.Validation on the current head:
Observed results:
6a1b11fb758b: the runtime-loaded proof plugin observedmodel_failover, the local primary provider returned HTTP 429, the fallback provider deliveredPROOF_FALLBACK_CANARY_OK, and the exhausted terminal run emittedmodel_failure_terminalwithfinalMessagematching the delivered all-models-failed reply.Limitations: provider failures were deterministic local HTTP endpoints, not real third-party outages; secret-bearing headers were redacted in proof output. I did not rerun full repository CI locally on this refresh.
Latest head refresh (2026-06-30)
Current head:
0ffca7e11f6fMerged current
origin/main(738b2be4b4) into the PR branch. Conflict resolution kept this PR'smodel_failover/model_failure_terminalhook API additions, accepted current-main Codex app-inventory/cache-key and channel-delivery fixes, regenerated config and Plugin SDK API baselines, raised the Plugin SDK public export budget to10404for the intentional hook surface, and removed a duplicatenormalizeFastModeimport introduced by the merge.Validation on the current head:
Observed results:
git diff --checkpassed.oxfmt --checkpassed.0ffca7e11f6f: the runtime-loaded proof plugin observedmodel_failover, the local primary provider returned HTTP 429, the fallback provider deliveredPROOF_FALLBACK_CANARY_OK, and the exhausted terminal run emittedmodel_failure_terminalwithfinalMessagematching the delivered all-models-failed reply.Limitations: provider failures were deterministic local HTTP endpoints, not real third-party outages; secret-bearing headers were redacted in proof output. I did not rerun full repository CI locally on this refresh.