Cross-provider failover never runs in remote (jcode serve) sessions: countdown is gated on !is_remote
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 19.9k
- Forks
- 2.3k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 30
Description
Summary
Cross-provider failover (added in #29) never runs in remote sessions (any client attached to a jcode serve daemon over a socket, e.g. jcode --socket ...). The countdown and manual-hint paths are both gated on !self.is_remote, so a remote client falls through to a generic "only available in local sessions right now" message and the prompt is never resent, even though cross_provider_failover = "countdown" is the default.
Local sessions are unaffected.
Reproduction steps
Deterministic repro (no real outage needed)
Appending this test to crates/jcode-tui/src/tui/app/tests/support_failover/part_02.rs at tag v0.65.0 fails. It is the existing local-session test with one line added to mark the client as remote:
#[test]
fn repro_remote_session_never_arms_failover_countdown() {
with_temp_jcode_home(|| {
write_test_config("[provider]\ncross_provider_failover = \"countdown\"\n");
let (mut app, _active_provider) = create_switchable_test_app("claude");
// The only difference from the passing local test: this client is
// attached to a `jcode serve` daemon.
app.set_remote_server_identity_for_tests(
Some("workhorse"), None, Some("0.65.0"), Some("session_remote_repro"),
);
let prompt = crate::provider::ProviderFailoverPrompt {
from_provider: "claude".to_string(),
from_label: "Anthropic".to_string(),
to_provider: "openai".to_string(),
to_label: "OpenAI".to_string(),
reason: "OAuth usage exhausted".to_string(),
estimated_input_chars: 16_000,
estimated_input_tokens: 4_000,
};
app.handle_turn_error(failover_error_message(&prompt));
assert!(
app.pending_provider_failover.is_some(),
"EXPECTED a countdown for a remote session, got: {}",
app.display_messages.last().unwrap().content
);
});
}
Run with:
cargo test -p jcode-tui --lib -- --test-threads=1 repro_remote_session
Result on v0.65.0:
test result: FAILED. 0 passed; 1 failed
EXPECTED a countdown to be armed for a remote session, but none was. Message was:
⚠ Anthropic became unavailable - jcode did not resend your prompt to OpenAI automatically.
...
Automatic countdown switching is only available in local sessions right now.
The equivalent local test (same file, no set_remote_server_identity_for_tests call) passes.
Manual repro
- Start a shared daemon:
jcode serve --server-name test - Attach a client over the socket:
jcode --socket "$XDG_RUNTIME_DIR/jcode.sock"(the default socket lives inJCODE_RUNTIME_DIR/XDG_RUNTIME_DIR, e.g./run/user/1000/jcode.sock) - Confirm the client is in remote mode: run
/cache statsand observe- is_remote: true - Have at least two providers configured (
jcode auth statusshows e.g.claudeandopenai) - Leave
provider.cross_provider_failoverat its default (countdown) - Drive the active provider into a failover-triggering error (rate limit / provider unavailable) while a prompt is in flight
Expected behavior
Same as a local session: a 3-second cancelable countdown, then the provider switches and the prompt is resent automatically.
Actual behavior
No countdown, no switch, no resend. The client prints the manual message plus:
Automatic countdown switching is only available in local sessions right now.
and the status line shows <provider> unavailable; manual switch suggested. The user must run /model, pick another provider, and retype/resend by hand.
Root cause
crates/jcode-tui/src/tui/app/model_context.rs, in handle_provider_failover_prompt (line numbers from tag v0.65.0):
CrossProviderFailoverMode::Manual if !self.is_remote => { // line 143
CrossProviderFailoverMode::Countdown if !self.is_remote => { // line 150
_ => {
// remote sessions always land here
"...Automatic countdown switching is only available in local sessions right now."
}
Both guards exclude remote sessions, so every remote client hits the _ arm.
Worth noting there are three further links, which is why simply deleting the guards is not sufficient:
- The countdown is never advanced.
maybe_progress_provider_failover_countdownis called only fromapp/local.rs:109(verified atv0.65.0), so a remote session that armed a countdown has no loop that progresses it. It needs driving fromremote::handle_ticktoo. - A staged route switch is never flushed. The only consumer of
pending_route_selectionon the remote side isremote.rs:394, insideapply_terminal_event(verified atv0.65.0), so even once a countdown fires, theSetRouteis not sent until the user happens to press a key. - Route matching needs to cover every provider. Route selection for a remote failover has to map the prompt's
to_provider(aprovider_key()value) onto a server-offered route. Matching only the claude and openaiapi_methodfamilies makes copilot / cursor / bedrock / openrouter / gemini / antigravity targets dead-end with "no available<X>route is offered by the server" unless the catalogprovidername happens to equal theprovider_key. All eightprovider_key()values need handling. (Note that antigravity's route vocabulary spells itsapi_methodplain"https".)
Unit tests can pass while the feature is still inert end to end, because they call the countdown helper directly rather than going through remote::handle_tick.
Environment
- jcode
v0.64.2andv0.65.0(verified both tags contain the guard) - Linux 6.18.33.2-microsoft-standard-WSL2 x86_64
- Config:
provider.cross_provider_failoverat default (countdown)
Notes
I have a working patch on a fork: https://github.com/toddmok/jcode/tree/fix/remote-cross-provider-failover (rebased onto current master, 0 behind). It removes the guards, drives the countdown and flushes the staged route from remote::handle_tick, and covers all eight provider_key() values. The new tests were each verified to fail with the wiring removed, so they are regression detectors rather than only-green tests. Serial suite: 2129 passed, 0 failed.
The branch also happens to fix two lock-order inversions that deadlock cargo test -p jcode-tui --lib: on current master the lib suite returned no verdict in 2/2 runs under a 300s cap (and hung ~28 min once before I killed it), while the branch finishes in ~18s. Those are separate commits and could be taken independently if you'd prefer them split.
I couldn't open a PR because this repo's pull_request_creation_policy is collaborators_only, so I'm filing this as an issue per the contributing note. Happy to split it up, restructure it, or have it pulled from the fork, whichever is easiest for you.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with handle_provider_failover_prompt in crates/jcode-tui/src/tui/app/model_context.rs, then trace maybe_progress_provider_failover_countdown through local.rs and remote::handle_tick in remote.rs. Run the repro in crates/jcode-tui/src/tui/app/tests/support_failover/part_02.rs and verify remote sessions advance the countdown, flush staged route changes, and resolve all eight provider keys before the relevant cargo tests pass.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- cli
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 48/100