# Live sessions What a switch does to the Claude Code sessions already running, and how claude-mode keeps that from costing you work. The incident that led to all of this is written up in [incident-mode-switch-corrupts-live-sessions.md](incident-mode-switch-corrupts-live-sessions.md). ## Why a switch breaks a running session The two halves of the config behave differently. The static half — base URL, model ids, the env block — really is read once at startup, and a running session keeps what it started with. The credential is not. It comes from running `apiKeyHelper`, which Claude Code re-invokes on a timer (`CLAUDE_CODE_API_KEY_HELPER_TTL_MS`), and the helper answers for whatever `state.json` says *at that moment*. So a switch reaches into a live session through the one thing that was never cached: | switching to | what the running session gets | |---|---| | `anthropic` | nothing — the helper returns no credential in that mode, by design | | another provider | the new provider's key, sent to the old base URL, which rejects it | | another preset of the *same* provider, same key | same key, same endpoint — this one survives, on the model ids it started with | Either of the first two starts failing calls whenever the timer next fires — mid-turn as easily as between turns. So restart the affected sessions, or let `claude-mode sessions --restart` do it: - **CLI** — exit and relaunch `claude` - **VS Code** — `Ctrl+Shift+P` → *Developer: Reload Window* - **Desktop app** — quit and reopen `claude-mode status` shows what the *next* launch will use. ## The part that is not just an inconvenience A failed call is recoverable. A *successful* one may not be. If a running session takes even one completion from the new provider before anything notices, that provider's message-id format lands in its transcript. OpenRouter issues `gen--` where Anthropic issues `msg_…`, and native Anthropic then refuses to resume the session at all: ``` API Error: 400 diagnostics.previous_message_id: must be the `id` from a prior /v1/messages response (starts with `msg_`) ``` There is no supported way back from that. The transcript has to be rolled back to the last message Anthropic issued: ```bash claude-mode repair-session # transcripts for this project claude-mode repair-session --all # every project, problems only claude-mode repair-session # show what it would cut claude-mode repair-session --apply ``` A bare listing covers only the project you are standing in (walking up from the current directory to find it), while a **named session id is looked up across every project** — you rarely remember which project a session you cannot resume belonged to. `--all` drops the scoping entirely. `--all` reports only what is actually actionable. Of 59 transcripts here it once flagged 16; 5 had simply never received a reply, and 10 had run start to finish on a gateway, so every id in them is that provider's by design — they resume fine under the provider they were born on. Neither is damage, so neither is listed. Only a transcript with a genuine `msg_` message *and* another provider's output after it is something this can or should touch. ### Dismissing a session Not every broken session is worth repairing — a throwaway, or one whose work was finished some other way — and one that can never be cleared keeps the bar's warning dot lit for good. So a session can be dismissed, and one untouched for more than 7 days is hidden on its own: ```bash claude-mode repair-session --ignore # stop counting it claude-mode repair-session --ignored # what is dismissed, and whether it is still broken claude-mode repair-session --unignore # or --unignore-all claude-mode repair-session --all --max-age 0 # include the age-hidden ones ``` `CM_IGNORE_AGE_DAYS` changes the default age. Neither touches the transcript. Hidden sessions are always counted — `--all` ends with a line like `2 hidden: 1 ignored, 1 older than 7 days` — so age-hiding never looks like damage disappearing. Dismissals live in `~/.claude-mode/ignored-sessions.json`; an entry is dropped when its transcript is deleted and when the session is repaired, so a session that breaks again later is not silently hidden. ## The cut turns are not thrown away Truncating is the mechanical fix, but the turns being cut are the work itself — and a session that resumes with a hole in its memory is barely resumed at all. So `--apply` does three things before it deletes anything: 1. **Backs up** the original as `.jsonl.pre-repair-backup-`. 2. **Writes the dropped turns out** as `.recovered-.md` — what was asked, what was answered, what was run. Tool *results* are left out; they are most of a transcript by volume and the least useful part of a summary. 3. **Hands them back to the session** as one appended note, so the agent that resumes knows what it just did. That note is a `user` entry marked `isMeta` — the marker Claude Code uses for its own local-command caveats, meaning "context, not something to answer". It carries **no `message.id`**, so it cannot re-create the very condition being repaired. `--no-reinject` writes the Markdown but leaves the session untouched. It refuses to touch a transcript written to in the last 90 seconds, since that belongs to a session still alive. ## The switch asks first While any sessions are running, a switch stops and asks: restart them (the only answer that ends with everything on the new mode), close them, proceed anyway, or abort — and abort is the default. Non-interactively it refuses outright unless given `--yes`. ``` claude-mode sessions running sessions (2) 562250 pts/4 /home/you/Work working (this session - never touched) 631644 pts/1 /home/you/Projects/api ``` Sessions are found through `/proc//exe`, which resolves to the real `claude` binary — a process-name match would sweep up every shell that merely mentions claude on its command line. Two things are then filtered out: - **The calling session.** It is listed, and never signalled. - **Forks of a session.** A busy session spawns children off its own binary, and they inherit the same `exe`; without excluding anything whose parent is itself claude, the count climbed and fell with load (2, 5, 11 and 40 on the same two sessions). A real session's parent is a terminal. `working` is a sampled-CPU heuristic — two reads of `utime + stime` 300ms apart — so it is a good guess about which session is mid-turn, not a promise. `--stop` sends SIGTERM, never SIGKILL: Claude Code writes out its transcript on the way down. `--restart` stops each session and reopens it, re-running the parent terminal's own command line where there is one, so the same terminal, flags and directory come back. Interactive runs confirm first; `--yes` is for callers that have already asked; `--dry-run` prints the plan and touches nothing. All of this needs `/proc`, so it is Linux-only. ## The bar notices for you Nothing tells you a session is unresumable until you try to resume it, by which point you have usually forgotten which one it was. So the bar widget scans every project on a timer (and whenever the panel opens) and puts a dot on its icon when there is something to fix. The panel lists the affected sessions, with **Repair** (after saying what it will drop and keep) and **Ignore** beside each, and the hidden ones under a collapsed `hidden (N)` row with **Restore**. The scan is affordable because it reads the *tail* of each transcript first: if the last message is Anthropic's, the transcript is healthy and the rest of the file is never opened. That is the overwhelmingly common case, so a sweep of 59 transcripts costs about 60ms, against about 5s for reading every byte.