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>
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
# 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-<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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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.
|
||||
Reference in New Issue
Block a user