d8570f4d5accc3c66653688ae69f367dd5848210
`ClaudeUsage` read `~/.claude/.credentials.json` on the machine running the backend and asked the API about that account, once, globally. But a session runs wherever its setup says, so the numbers on the usage screen belonged to the backend's account rather than to the account that spent the tokens. That is not a rounding error in the layout this project is aiming at. `ai-server` belongs on the host; the host has no `claude` CLI at all and the VM is a remote. So the screen would have reported "is Claude Code logged in on this machine?" while every session ran fine on a machine whose limits nobody could see. It looked correct only because backend and CLI happen to be the same box today. Usage is now per machine, asked through the same `Transport` the sessions use -- `ssh host sh -c 'cat $HOME/...'` for a remote, unchanged for the local one. `$HOME` is left for the far shell to expand, since a path built here is this machine's home directory and over ssh that is somebody else's. Machines with no Claude provider are not asked and get no row: they have no Claude limits, and a row about them would be a fact about nothing. The snapshot gains the states it could not say. `available` plus an `error` string made three different situations look identical, and the one that suffered was the harmless one: a machine nobody has logged in on is a decision somebody made, with nothing to fix, and it read as broken. `notLoggedIn`, `unreachable` and `failed` are now distinct, and which one a failed read is gets decided in `why_no_credentials` rather than at the call site. Supporting changes: `ssh::command` builds a `std::process::Command` that tokio converts from, so a blocking caller can use the one place that knows what a correct ssh invocation is; `Transport::capture_blocking` is that caller's door. The cache is keyed by machine and service rather than by position, since the set is no longer fixed at startup -- a positional cache would hand one machine's numbers to another the moment a setup was added. A cached snapshot still picks up a rename immediately, because the name has nothing to do with the poll interval. Verified against a real ssh setup (loopback, per AGENTS.md) with five machines, all four states seen: local `ok`, loopback-over-ssh `ok`, unreachable host `unreachable` carrying ssh's own message, a claude-less machine correctly absent, and -- with the backend's HOME emptied -- local `notLoggedIn` while the ssh machine still reported real windows, which is the production shape. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VETa8afmpWaYezLCqJhDB8
Languages
Rust
54%
Kotlin
43.6%
Shell
2.4%