From 2ebbef696b0771b6eed4c243753286bdb3efa423 Mon Sep 17 00:00:00 2001 From: CodeWhale Bot Date: Tue, 15 Sep 2026 12:14:25 -0700 Subject: [PATCH] fix(tui): tell the user the resume route that actually works (#6225) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The session-switch refusal said: This session belongs to another Runtime host. Resume it in a new Codewhale process to reopen its saved store. #6225 reports hitting this from the plainest possible path on a clean install — start `codewhale`, `/quit`, start `codewhale`, `/resume`, pick the session — and the advice is impossible to follow, because they *were* in a new process. Starting a new process and then picking the session from `/resume` returns to this same switch path. The route that works is opening the session **at launch**: `TaskManager::start` passes the saved binding through to `open_for_session`, which calls `validate_existing_store()`, points the runtime at that store and opens it. That is adoption; the picker inside a running session is not. So the message was true and unactionable at once. It now names the real condition and the command, with the session id interpolated so it can be pasted: This session's saved Runtime store belongs to a different host. Switching to it from inside a running session cannot carry that store's queued work across, but opening it directly can: run `codewhale resume ` from your shell. This is only the message. The underlying refusal is #6207 and is still wrong for the case #6225 reports — an existing, ownerless, empty store — but a user should not have to wait for that to reach their session. The binding test asserted the old string; it now asserts the refusal points at the direct-open path and names the session, which is the property that matters rather than the wording. Checks: `./scripts/dev-test.sh tui` — 12707 tests run, 12707 passed, 19 skipped. `./scripts/dev-test.sh tui runtime_store_binding` — 7 passed. CI clippy gate exits 0; `cargo fmt --check` clean on both files. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NWzjx9Q7Mw2G7K8rpiJy9p --- crates/tui/src/tui/ui/apply.rs | 17 ++++++++++++++++- .../src/tui/ui/tests/runtime_store_binding.rs | 13 ++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/crates/tui/src/tui/ui/apply.rs b/crates/tui/src/tui/ui/apply.rs index a41d58aa89..1de043a2f2 100644 --- a/crates/tui/src/tui/ui/apply.rs +++ b/crates/tui/src/tui/ui/apply.rs @@ -3626,7 +3626,22 @@ pub(crate) fn apply_loaded_session_with_goal( recovered_binding = tasks.session_store_binding(); } if recovered_binding.is_none() { - return Err("This session belongs to another Runtime host. Resume it in a new Codewhale process to reopen its saved store.".into()); + // Name the real condition and the path that actually works. The + // old wording ("resume it in a new Codewhale process") sent users + // in circles: starting a new process and then picking the session + // from `/resume` lands here again, because that is this same + // switch path. Opening the session *at launch* is a different + // route — `TaskManager::start` passes the saved binding through to + // `open_for_session`, which validates the existing store and + // adopts it (runtime_threads.rs, `validate_existing_store` then + // `open_inner`). So the advice has to say which one (#6207, #6225). + return Err(format!( + "This session's saved Runtime store belongs to a different host. \ + Switching to it from inside a running session cannot carry that \ + store's queued work across, but opening it directly can: run \ + `codewhale resume {}` from your shell.", + session.metadata.id + )); } } if app.session_transition_blocked() { diff --git a/crates/tui/src/tui/ui/tests/runtime_store_binding.rs b/crates/tui/src/tui/ui/tests/runtime_store_binding.rs index fac744477b..e947176ae0 100644 --- a/crates/tui/src/tui/ui/tests/runtime_store_binding.rs +++ b/crates/tui/src/tui/ui/tests/runtime_store_binding.rs @@ -281,7 +281,18 @@ async fn runtime_store_binding_survives_launch_snapshot_and_resume() -> anyhow:: let old_id = other_app.current_session_id.clone(); let error = apply_loaded_session_with_goal(&mut other_app, &mut resumed_config, &loaded, None) .unwrap_err(); - assert!(error.contains("Resume it in a new Codewhale process")); + // The refusal must name the route that actually works. "Resume it in a new + // Codewhale process" was true but unactionable: starting a new process and + // then picking the session from `/resume` returns here, because that is + // this same switch path (#6207, #6225). + assert!( + error.contains("codewhale resume"), + "the refusal must point at the direct-open path: {error}" + ); + assert!( + error.contains(&loaded.metadata.id), + "the refusal must name the session to open: {error}" + ); assert_eq!(other_app.current_session_id, old_id); assert_eq!(other_app.input, "preserve pending input"); foreign.shutdown_and_wait().await?;