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>
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 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-<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:
- Backs up the original as
<session>.jsonl.pre-repair-backup-<stamp>. - 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. - 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.