Description
In the TUI, opening a saved session fails with:
Failed to restore session: This session belongs to another Runtime host. Resume it in a new
Codewhale process to reopen its saved store.
The session's runtime store directory exists, its ownership validates, and no other Codewhale
process is running — so nothing else holds the store. The refusal sits in the session-switch guard,
which treats "the bound directory exists" as "another live host owns it".
Steps to reproduce
- Launch Codewhale in a workspace; sessions created there are bound to the host's store,
e.g. ~/.codewhale/sessions/<host-session-id>/runtime.
- Lose the host session record while its store directory stays on disk. A conversation created in
that host keeps the durable binding to it.
- Start a new Codewhale process (new host, its own store) and open the saved session (picker or
--resume from the TUI).
- The refusal above.
Expected behavior
Opening the session should succeed, because the bound store both validates and is free:
RuntimeStoreBinding::validate_existing_store() (crates/tui/src/runtime_threads.rs:2918) accepts
it — the owner record parses and the recomputed scope
sha256(owner_id ‖ 0x00 ‖ <store>/events.lock) equals the scope saved in the session;
- the store's process-owner lock (
runtime-process.owner.lock) is free — no live host holds it;
RuntimeThreadManager::open_for_session (crates/tui/src/runtime_threads.rs:4636-4668) already
takes this exact path when a process opens a session with a binding: existing store + passing
validation ⇒ adopt it.
So either adopt the store the same way in the switch path, or fall back to transcript recovery,
instead of refusing because the running process happens to be bound to a different store. At
minimum, the message should name the real condition and the remedy that works.
Actual behavior
Fail-closed, in apply_loaded_session_with_goal (crates/tui/src/tui/ui/apply.rs:3561-3573):
if let Some(binding) = session.metadata.runtime_store.as_ref()
&& let Some(tasks) = app.runtime_services.task_manager.as_ref()
&& tasks.session_store_binding().as_ref() != Some(binding)
{
if binding
.is_missing_session_store() // apply.rs:3566-3567
.map_err(|error| error.to_string())?
{
recovered_binding = tasks.session_store_binding();
}
if recovered_binding.is_none() {
return Err("This session belongs to another Runtime host. …".into()); // apply.rs:3573
}
}
is_missing_session_store() (crates/tui/src/runtime_threads.rs:2891) returns true only when
symlink_metadata yields NotFound; an existing directory returns false no matter who — if
anyone — holds it. The recovery branch added for missing stores (commit 1de7d138, issue #6102)
therefore never covers "directory exists, its host is gone", and the guard's message asserts a live
rival host that does not exist.
Evidence from this machine
~/.codewhale/sessions held 57 saved sessions:
Concrete case:
- session
c035ff00-ea47-4109-bee4-154b98f67514, 921 messages, workspace
/home/f2/Documents/Shared/ProL/Rust/pipeline-orchestrator;
- binding
data_dir: …/sessions/e27ea9fe-75ab-47d1-b443-794ef523f638/runtime,
execution_scope: 27271aff…;
- that directory exists and holds
owner.json (owner_4acdc4dd…), state.json
({"schema_version":2,"next_seq":1}) and two lock files — 4 files, zero events;
- no session record
e27ea9fe-….json exists: the host that created the store is gone, the
conversation it hosted is not;
- recomputing the scope from
owner_id and …/runtime/events.lock reproduces 27271aff… exactly —
this is the session's own scope, not a foreign one;
ps showed no running codewhale process.
Second case: session 6b57f767-… (0 messages) is bound to …/6b57f767-…/runtime-recovered-session
— a store minted by the #6102 recovery path itself (runtime_threads.rs:4657-4658). If the next
launch refuses an existing runtime-recovered-session directory the same way, recovery hands the
session a binding the following launch rejects. Worth confirming: it looks self-inflicted, but I did
not run a second launch against it.
Caveat on this report
The failure is the TUI path (Failed to restore session: …, tui/ui/apply.rs). I did not run a live
codewhale resume <id>, so I cannot say from observation that a fresh process adopting the binding
succeeds — but read as code, open_for_session validates this same store and adopts it, which is why
the guard's advice ("resume in a new Codewhale process") looks plausible while the operator, opening
the session from the picker, only sees the refusal. I also did not mutate live session state to test
repairs.
Impact
Blocks opening older sessions from the picker with an error that points at a rival process that is
not there, and offers no in-product way to reach the session. 22 of 57 sessions on this machine are
in the triggering state.
Related: #6102 (missing-store recovery — the case this one falls outside), #6014 (session-picker UX
around empty sessions).
Environment
- OS: Arch Linux
- codewhale version: 0.9.13 (dev)
- Install method: source build
- Model/provider: DeepSeek
- Terminal app: tmux (
TERM=tmux-256color)
- Shell: fish
Logs, screenshots, or recordings
No crash log — the failure is a UI message on session restore. The on-disk state above is the
reproduction artifact.
Description
In the TUI, opening a saved session fails with:
The session's runtime store directory exists, its ownership validates, and no other Codewhale
process is running — so nothing else holds the store. The refusal sits in the session-switch guard,
which treats "the bound directory exists" as "another live host owns it".
Steps to reproduce
e.g.
~/.codewhale/sessions/<host-session-id>/runtime.that host keeps the durable binding to it.
--resumefrom the TUI).Expected behavior
Opening the session should succeed, because the bound store both validates and is free:
RuntimeStoreBinding::validate_existing_store()(crates/tui/src/runtime_threads.rs:2918) acceptsit — the owner record parses and the recomputed scope
sha256(owner_id ‖ 0x00 ‖ <store>/events.lock)equals the scope saved in the session;runtime-process.owner.lock) is free — no live host holds it;RuntimeThreadManager::open_for_session(crates/tui/src/runtime_threads.rs:4636-4668) alreadytakes this exact path when a process opens a session with a binding: existing store + passing
validation ⇒ adopt it.
So either adopt the store the same way in the switch path, or fall back to transcript recovery,
instead of refusing because the running process happens to be bound to a different store. At
minimum, the message should name the real condition and the remedy that works.
Actual behavior
Fail-closed, in
apply_loaded_session_with_goal(crates/tui/src/tui/ui/apply.rs:3561-3573):is_missing_session_store()(crates/tui/src/runtime_threads.rs:2891) returnstrueonly whensymlink_metadatayieldsNotFound; an existing directory returnsfalseno matter who — ifanyone — holds it. The recovery branch added for missing stores (commit
1de7d138, issue #6102)therefore never covers "directory exists, its host is gone", and the guard's message asserts a live
rival host that does not exist.
Evidence from this machine
~/.codewhale/sessionsheld 57 saved sessions:Concrete case:
c035ff00-ea47-4109-bee4-154b98f67514, 921 messages, workspace/home/f2/Documents/Shared/ProL/Rust/pipeline-orchestrator;data_dir:…/sessions/e27ea9fe-75ab-47d1-b443-794ef523f638/runtime,execution_scope:27271aff…;owner.json(owner_4acdc4dd…),state.json(
{"schema_version":2,"next_seq":1}) and two lock files — 4 files, zero events;e27ea9fe-….jsonexists: the host that created the store is gone, theconversation it hosted is not;
owner_idand…/runtime/events.lockreproduces27271aff…exactly —this is the session's own scope, not a foreign one;
psshowed no running codewhale process.Second case: session
6b57f767-…(0 messages) is bound to…/6b57f767-…/runtime-recovered-session— a store minted by the #6102 recovery path itself (
runtime_threads.rs:4657-4658). If the nextlaunch refuses an existing
runtime-recovered-sessiondirectory the same way, recovery hands thesession a binding the following launch rejects. Worth confirming: it looks self-inflicted, but I did
not run a second launch against it.
Caveat on this report
The failure is the TUI path (
Failed to restore session: …,tui/ui/apply.rs). I did not run a livecodewhale resume <id>, so I cannot say from observation that a fresh process adopting the bindingsucceeds — but read as code,
open_for_sessionvalidates this same store and adopts it, which is whythe guard's advice ("resume in a new Codewhale process") looks plausible while the operator, opening
the session from the picker, only sees the refusal. I also did not mutate live session state to test
repairs.
Impact
Blocks opening older sessions from the picker with an error that points at a rival process that is
not there, and offers no in-product way to reach the session. 22 of 57 sessions on this machine are
in the triggering state.
Related: #6102 (missing-store recovery — the case this one falls outside), #6014 (session-picker UX
around empty sessions).
Environment
TERM=tmux-256color)Logs, screenshots, or recordings
No crash log — the failure is a UI message on session restore. The on-disk state above is the
reproduction artifact.