Skip to content

feat(cli): wire the provider failover chain into every face via TOLE_FALLBACK_* (#209, 3/3) - #407

Merged
ajianaz merged 7 commits into
developfrom
feat/provider-failover-wiring
Oct 21, 2026
Merged

ajianaz merged 7 commits into
developfrom
feat/provider-failover-wiring

Conversation

@ajianaz

@ajianaz ajianaz commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

What

Wires the core FailoverProvider from #404 (#209, 1/3) into every session face (run, chat, mission, serve, ACP and MCP) through TOLE_FALLBACK_BASE_URL / TOLE_FALLBACK_API_KEY / TOLE_FALLBACK_MODEL. Also fixes three failover edge cases that a cora 0.19 agent-mode scan found in the core chain.

Closes #209. There was no separate 2/3 PR; this PR finishes the issue.

Why

#209 asks for a reserve provider so missions keep going when the primary is unavailable. #404 added the chain to core but nothing used it yet. The scan also turned up three bugs in the chain:

  • Retry misclassification: if the primary timed out and the reserve then returned a fail-loud 401, the combined error still contained "timeout". The turn loop then retried it as transient.
  • Duplicated live output: a stream that broke after live tokens had been shown failed over, and the reserve streamed a whole new answer after the partial one.
  • No fallback during cooldown: a sticky reserve that failed before the cooldown ended never tried the primary again, even if the primary had recovered.

Changes

  • tole-cli/src/provider_chain.rs (new): builds the chain from env. A reserve without its own key is refused, so the primary's key is never sent to another host. The model defaults to the primary's.
  • main.rs, session_host.rs, mission.rs: every face builds its provider through provider_chain::build.
  • tole-core/src/provider.rs: adds a last_route passthrough so tole_route lands in the usage row.
  • tole-core/src/failover.rs:
    • Adds FAILOVER_MARKER and leading_error().
    • The chain now walks from the active link and wraps round.
    • Adds the STREAM_CUT_AFTER_OUTPUT class, which never fails over.
  • tole-core/src/openai.rs: a stream read error after a sink has emitted output now reports stream cut after output instead of stream read.
  • tole-core/src/turn.rs: transient retry classification goes through is_transient_provider_error, which reads only the last provider's error.
  • README env table and CHANGELOG are updated in this PR.

Testing

  • tests/provider_failover_e2e.rs: with the primary down, the reserve completes the turn and tole_route is recorded. Auth errors do not fail over.
  • New unit tests:
    • failover: a fail-loud reserve error is not transient; a stream cut after output never reaches the reserve; a failing sticky reserve wraps back to the primary.
    • openai: a stream cut after output is classified as not-failover, and a stream cut before any output is still failover.
    • turn: only the last error decides whether to retry.
  • cargo fmt --all, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace and cora review --staged (✅ no issues) all pass.
  • In one parallel workspace run, openai::streaming_tests::wire_stats_are_per_call_not_carried_over failed once. It passed 3 times on its own and in 4 full tole-core lib runs. It uses the shared-port SSE mock, not the changed error path, so it looks like an existing flaky test.

ajianaz and others added 4 commits October 16, 2026 16:43
- A stream cut after a live sink showed output now reports
  "stream cut after output" and never fails over, so the reserve cannot
  re-stream a full answer after the partial one.
- The chain walks from the active link and wraps round, so a sticky
  reserve that fails during the cooldown falls back to the primary.
- The turn loop's transient retry reads only the last provider's error
  (failover::leading_error), so an earlier "timeout"/"429" no longer
  makes a fail-loud 401 from the reserve look retryable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

🔍 Cora AI Code Review

✅ No issues found. Code looks good!


Review powered by cora-code · BYOK · MIT

Resolve mission.rs: keep the failover chain build and apply the pinned
system prompt (from #410) to every link via configure_mission_provider.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ajianaz added a commit that referenced this pull request Oct 21, 2026
)

Diffs of a few hundred lines already exceed the 10-minute limit: the job
is cancelled mid-review and no verdict is posted, which blocks the merge
gate (seen three times on #407). Sync the stated limit in AGENTS.md and
CONTRIBUTING.md.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
@ajianaz
ajianaz merged commit a7a9872 into develop Oct 21, 2026
17 checks passed
@ajianaz
ajianaz deleted the feat/provider-failover-wiring branch October 21, 2026 01:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

provider failover chain — reserve provider for mission resilience

1 participant