1jehuang / 1jehuang/jcode

Cross-provider failover never runs in remote (jcode serve) sessions: countdown is gated on !is_remote

Open
#734 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

autonomous: no bug priority: high triage: needs-decision triage: reproducible
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
  1. Start a shared daemon: jcode serve --server-name test
  2. Attach a client over the socket: jcode --socket "$XDG_RUNTIME_DIR/jcode.sock" (the default socket lives in JCODE_RUNTIME_DIR / XDG_RUNTIME_DIR, e.g. /run/user/1000/jcode.sock)
  3. Confirm the client is in remote mode: run /cache stats and observe - is_remote: true
  4. Have at least two providers configured (jcode auth status shows e.g. claude and openai)
  5. Leave provider.cross_provider_failover at its default (countdown)
  6. 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:

  1. The countdown is never advanced. maybe_progress_provider_failover_countdown is called only from app/local.rs:109 (verified at v0.65.0), so a remote session that armed a countdown has no loop that progresses it. It needs driving from remote::handle_tick too.
  2. A staged route switch is never flushed. The only consumer of pending_route_selection on the remote side is remote.rs:394, inside apply_terminal_event (verified at v0.65.0), so even once a countdown fires, the SetRoute is not sent until the user happens to press a key.
  3. Route matching needs to cover every provider. Route selection for a remote failover has to map the prompt's to_provider (a provider_key() value) onto a server-offered route. Matching only the claude and openai api_method families makes copilot / cursor / bedrock / openrouter / gemini / antigravity targets dead-end with "no available <X> route is offered by the server" unless the catalog provider name happens to equal the provider_key. All eight provider_key() values need handling. (Note that antigravity's route vocabulary spells its api_method plain "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.2 and v0.65.0 (verified both tags contain the guard)
  • Linux 6.18.33.2-microsoft-standard-WSL2 x86_64
  • Config: provider.cross_provider_failover at 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.