Skip to content

bug(tui): session picker refuses saved sessions whose runtime store exists but is not the running host's own #6207

Description

@7jrxt42BxFZo4iAnN4CX

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

  1. Launch Codewhale in a workspace; sessions created there are bound to the host's store,
    e.g. ~/.codewhale/sessions/<host-session-id>/runtime.
  2. Lose the host session record while its store directory stays on disk. A conversation created in
    that host keeps the durable binding to it.
  3. Start a new Codewhale process (new host, its own store) and open the saved session (picker or
    --resume from the TUI).
  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    • Status
      Backlog

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions