Files
claude-mode/docs/live-sessions.md
smoidoandClaude Opus 5 b514e00745 README down to what a user needs; the rest into docs/
The README had grown to 1,010 lines of user docs and design notes in one file,
and had gone stale: it still showed the old numbered menu, set up only three
providers, and listed preflight checks and a file layout that predate the last
three releases. It now carries install, first run, the full command
reference, the providers at a glance, troubleshooting and a docs index.

docs/:
- providers.md    presets, defaults, the model cache, context windows, each
                  provider (OpenRouter's cost guard and guardrail check are
                  written up for the first time), adding a provider
- live-sessions.md  what a switch does to running sessions, and repair
- design.md       why settings.json, why keys stay out of it (and the vault
                  per platform), the preflight checks as they are now
- bar-widget.md   the widget as it is now: providers from health.json, every
                  server provider's settings, the restart after an upgrade
- architecture.md the pieces, every file on disk and who writes it, the
                  contracts between them, where to change what
- development.md  running the tests, the conventions the code follows,
                  working on the widget, releasing
CONTRIBUTING.md points at it.

Also:
- The per-project session listing used awk, which the CLI avoids because it
  is missing from minimal images; it uses the script's own TSV helpers now,
  and tests/static.sh fails on any awk in the CLI.
- tests/static.sh checks every relative Markdown link and #anchor.
- A unit test pins the managed env keys between cm-json.py and
  claude-mode.ps1, which only a comment kept in step before.
- test_sessions covers the per-project listing, which nothing ran.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 01:52:03 +03:00

7.8 KiB

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.

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 CodeCtrl+Shift+PDeveloper: 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-<epoch>-<rand> 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:

claude-mode repair-session                 # transcripts for this project
claude-mode repair-session --all           # every project, problems only
claude-mode repair-session <session-id>    # show what it would cut
claude-mode repair-session <session-id> --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:

claude-mode repair-session --ignore <session-id>     # stop counting it
claude-mode repair-session --ignored                 # what is dismissed, and whether it is still broken
claude-mode repair-session --unignore <session-id>   # 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 <session>.jsonl.pre-repair-backup-<stamp>.
  2. Writes the dropped turns out as <session>.recovered-<stamp>.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/<pid>/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.