diff --git a/README.md b/README.md index 70f8aab..defd865 100644 --- a/README.md +++ b/README.md @@ -300,9 +300,27 @@ If the profile doesn't load: `Set-ExecutionPolicy -Scope CurrentUser RemoteSigne ## Restarting sessions -Claude Code reads all of this **once at startup**. A switch does not affect a -running session — that's how the process loads config, not something scripting -can change. +A switch **breaks** running sessions. This originally said it did not affect +them; that was wrong, and the difference matters, because the two halves of the +config behave differently. + +The static half — base URL, model ids, the env block — genuinely 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`, present in 2.1.251), +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` | the helper returns nothing, by design — no credential at all | +| another provider | the new provider's key, sent to the old base URL, which rejects it | +| another preset of the *same* provider, same `keyRef` | same key, same endpoint — this one survives, on the model ids it started with | + +Either of the first two starts failing calls whenever the TTL happens to expire: +mid-turn as easily as between turns. So restart afterwards — and on Linux, +`claude-mode sessions --restart` will do it for you: - **CLI** — exit and relaunch `claude` - **VS Code** — `Ctrl+Shift+P` → *Developer: Reload Window* @@ -446,12 +464,11 @@ stays exactly as it was. ## Sessions still on the old provider -Claude Code reads all of this once, at startup. A switch therefore does nothing -to a session already running: it keeps talking to the old provider on the old -key until it is restarted, which is the confusing part — the bar says one thing -and the session in front of you is doing another. A session mid-request is worse -off, because anthropic mode *deletes* the helper, so an in-flight turn can fail -on its next tool call rather than stopping cleanly. +A switch does not leave running sessions on the old provider — it breaks them. +See [Restarting sessions](#restarting-sessions) for the mechanism: their +endpoint is fixed at startup but their credential is re-fetched on a timer, so +it switches under them and the endpoint they are still pointed at refuses it. +Which is why this is a decision rather than a notification. ``` claude-mode sessions diff --git a/linux/claude-mode b/linux/claude-mode index 7fc77ea..fc3d917 100755 --- a/linux/claude-mode +++ b/linux/claude-mode @@ -539,14 +539,31 @@ cmd_preflight() { # --------------------------------------------------------------------------- # Running sessions # -# Claude Code reads settings.json once, at startup. A switch therefore does -# nothing to a session already running - it keeps talking to the old provider on -# the old key until it is restarted, which is the confusing part: the bar says -# one thing and the session in front of you is doing another. +# A switch breaks running sessions. Not "leaves them on the old provider" - +# breaks them, and it is worth being exact about why, because the two halves of +# the config behave differently. # -# Worse for a session mid-request. The key it is using can be pulled out from -# under it (anthropic mode deletes the helper outright), so an in-flight turn -# can fail on the next tool call rather than at a clean boundary. +# The static half - base URL, model ids, the env block - really is read once at +# startup, and a running session keeps the values it started with. +# +# The credential is not. It is fetched by running apiKeyHelper, which Claude +# Code re-invokes on a timer (CLAUDE_CODE_API_KEY_HELPER_TTL_MS, present in +# 2.1.251), and the helper answers for whatever state.json says *now*. So a +# switch reaches into a live session through the one thing that was never +# cached: +# +# -> anthropic the helper returns nothing at all, by design, and the +# session's next refresh comes back with no credential +# -> another provider +# the helper hands over the new provider's key while the +# session is still pointed at the old base URL, which rejects +# it +# +# Either way the session starts failing its calls, at whatever moment the TTL +# happens to expire - mid-turn as easily as between turns. The one case that +# does survive is a switch between two presets of the same provider sharing a +# keyRef: same key, same endpoint, and the session simply carries on with the +# model ids it started with. # # Sessions are found through /proc//exe rather than by matching process # names. `claude` is a real ELF binary here, so the symlink resolves to it @@ -690,7 +707,7 @@ $rows EOF_ROWS if [ "$action" = "list" ]; then - printf '\n %sthese keep the provider they started with until restarted%s\n' "$C_DIM" "$C_RESET" + printf '\n %stheir credential is re-fetched on a timer, so a switch breaks them%s\n' "$C_DIM" "$C_RESET" printf ' %sclaude-mode sessions --stop close them%s\n' "$C_DIM" "$C_RESET" printf ' %sclaude-mode sessions --restart close and reopen each in its own directory%s\n' "$C_DIM" "$C_RESET" printf ' %s--dry-run shows what either would do, and does nothing%s\n' "$C_DIM" "$C_RESET" @@ -803,9 +820,11 @@ cm_report_live_sessions() { busy="$(printf '%s' "$rows" | cut -f4 | grep -c '^yes$' || true)" printf '\n' - warn "$n Claude Code session(s) still running on the previous provider" + warn "$n Claude Code session(s) are running and will start failing their calls" + say 'their key is re-fetched on a timer and now resolves to the new mode, which' + say 'the endpoint they are still pointed at will not accept. Restart them.' if [ "${busy:-0}" -gt 0 ]; then - warn "$busy of them is mid-request - it may fail on its next call rather than at a clean stop" + warn "$busy of them is mid-request and will break wherever it happens to be" fi printf ' %sclaude-mode sessions what is running%s\n' "$C_DIM" "$C_RESET" printf ' %sclaude-mode sessions --restart close and reopen them on the new provider%s\n' "$C_DIM" "$C_RESET" diff --git a/omarchy/smoido.claude-mode/Panel.qml b/omarchy/smoido.claude-mode/Panel.qml index 3dc32ea..7f26cc8 100644 --- a/omarchy/smoido.claude-mode/Panel.qml +++ b/omarchy/smoido.claude-mode/Panel.qml @@ -48,10 +48,12 @@ Panel { // ---- Switch flow // - // A switch is not one action. It rewrites settings.json, which every future - // session reads and no running session re-reads, so two things have to be - // settled before the write: whether the target can actually serve requests, - // and what should happen to the sessions still talking to the old one. + // A switch is not one action. It rewrites settings.json, and two things have + // to be settled before the write: whether the target can actually serve + // requests, and what should happen to the sessions already running - which a + // switch breaks rather than leaves alone, because their credential is + // re-fetched on a timer and resolves to the new mode while their endpoint + // stays the old one. // // list -> preflight -> blocked // -> sessions -> confirm -> switch -> list @@ -848,10 +850,15 @@ Panel { font.bold: true } + // Not "they will keep using the old provider". Their endpoint and model + // ids are fixed at startup, but the key is re-fetched on a timer and + // will resolve to the new mode, which the old endpoint refuses - so + // they fail rather than carry on. Text { width: parent.width - text: "Claude Code reads its settings once, at startup, so these keep " - + Modes.title(root.mode) + " until they are restarted." + text: "They keep pointing at " + Modes.title(root.mode) + ", but their key is " + + "re-fetched on a timer and will switch under them. Their calls start " + + "failing from that moment, not at a clean stop." color: Color.muted wrapMode: Text.WordWrap lineHeight: 1.2 @@ -863,8 +870,8 @@ Panel { width: parent.width visible: parent.busyCount > 0 text: parent.busyCount === 1 - ? "One is working right now and will lose that turn." - : parent.busyCount + " are working right now and will lose those turns." + ? "One is working right now and will break mid-turn." + : parent.busyCount + " are working right now and will break mid-turn." color: root.urgentColor wrapMode: Text.WordWrap font.family: root.bar ? root.bar.fontFamily : Style.font.family