A backend start adopts, and starts nothing
Restarting the server relaunched a driver for every session in the config, and ClaudeDriver::launch starts a process when there is none to adopt -- so a session somebody had deliberately stopped came back at the next rebuild, and the Idle its new driver announced stamped the session as active at that moment. On the phone that read as every session idle and "just now" after every restart, with the list sorted by that time in an order that meant nothing. A launch now says why it is happening. Launching::Restart takes charge of the processes still running and leaves every other session as it found it; Launching::Asked -- a spawn, a Start, a message -- starts one where there is none. A session with no process therefore has no driver: DriverCell is an option rather than a driver whose requests go nowhere, and LiveSession::ask reports what could not happen instead of sending into a dead fifo. Two clocks that moved on their own, both the same lie in the same field that Transcript::last_activity exists to prevent: - The status a launch has to correct is written into the transcript at the time of the last thing the session actually did. A backend killed mid-turn leaves a transcript saying Running, which has to become Exited -- but this server noticing is not the session doing something. - A session that has never done anything reports when it was created. Its transcript is empty, since a driver announcing the state it starts in is not news, so it is the one session with no line to read a time off and the clock was the fallback. Exercised end to end against a real server: a stand-in CLI adopted across a restart keeps its status and its time, a stopped session stays stopped with no process started, a process killed while the backend was down reports exited stamped at the last thing the session did, and Start brings it back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6119926a4d
commit
61af05c732
4 files changed
+461
-81
No files matched your search
@@ -240,8 +240,9 @@ turn. Claude's dialect: a `user` message on stdin mid-stream; pi's: `steer`.
|
||||
installed CLI doesn't support it, fall back to `shutdown` + respawn with
|
||||
`--resume <session_id> --model <new>` — cheap, since Claude persists
|
||||
sessions in `~/.claude/projects` anyway. That resume path is the recovery
|
||||
story for a process that has genuinely died; a backend restart no longer
|
||||
uses it, because the process is still there to be adopted (see below).
|
||||
story for a process that has genuinely died; a backend restart never takes
|
||||
it — it adopts the process that is still there, and starts nothing for the
|
||||
session that has none (see below).
|
||||
**Resuming is only ever safe when nothing else has that session open.**
|
||||
- Images in: base64 image content blocks in the stream-json user message.
|
||||
- Working directory, host, and model are spawn-screen fields.
|
||||
@@ -420,6 +421,58 @@ running (interrupt — the process stays), a red stop when it is not (end the
|
||||
process), and a green play when it has exited (start it again). One button
|
||||
rather than three that come and go, so its presence is never the signal.
|
||||
|
||||
### A backend start adopts, and starts nothing (decided 2026-08-30)
|
||||
|
||||
Starting the server is not something a session should be able to tell
|
||||
happened. `SessionManager::new` takes charge of the processes that are still
|
||||
running and **leaves every other session exactly as it found it** — listed,
|
||||
with its transcript, its event pump and the SSE stream a phone reads, and no
|
||||
driver at all until somebody asks for one.
|
||||
|
||||
What it did before was launch a driver for every session in the config, and
|
||||
`ClaudeDriver::launch` starts a process when there is none to adopt. So a
|
||||
session somebody had deliberately stopped came back at the next rebuild,
|
||||
which is the decision Stop exists to make being undone by an unrelated
|
||||
event — and since a driver announces `Idle` for a process it started, the
|
||||
session was also stamped as active at the moment of the restart. On the
|
||||
phone that read as *every* session idle and "just now" after every restart,
|
||||
with the list — sorted by that time — in an order that meant nothing.
|
||||
|
||||
- **`Launching` is the parameter that says which it is**, and the seed an
|
||||
import carries rides on the asked-for variant, because a restart re-seeding
|
||||
a transcript would write the imported conversation into it twice.
|
||||
- **A session with no process has no driver.** `DriverCell` is an option
|
||||
rather than a driver whose requests go nowhere, so "nothing is running
|
||||
this" is a state the code can be asked about instead of one it discovers by
|
||||
sending into a dead fifo. `LiveSession::ask` is the one place that answers
|
||||
it, with an `Event::Error` naming what could not happen — a request nobody
|
||||
can carry out is reported, never swallowed.
|
||||
- **`--resume` on a crashed session is now a press rather than a restart.**
|
||||
That is the whole of what is given up, and it is small: a session whose CLI
|
||||
died reports `Exited` and draws the Start button, and *sending it anything
|
||||
at all* starts it (above). What is bought is that the two are told apart by
|
||||
who asked, rather than a restart guessing that everything it found should be
|
||||
running.
|
||||
- **What a launch settles the status to is written into the transcript, at
|
||||
the time of the last thing the session actually did.** Adopting, the
|
||||
transcript's word stands except for the `Exited` a live process disproves.
|
||||
Taking charge of nothing, every word but `Exited` is disproved at once — a
|
||||
backend killed mid-turn leaves a transcript saying `Running`, and that
|
||||
draws a stop button for a turn that ended hours ago. The correction goes in
|
||||
the transcript because the list reads the manager's status and the session
|
||||
screen replays the file; it is stamped with the transcript's own last time
|
||||
because it is not something the session did — this server noticed, at a
|
||||
moment of its own choosing, and `now` there is the same lie in the same
|
||||
field that `Transcript::last_activity` exists to prevent.
|
||||
- **A session that has never done anything reports when it was created.** Its
|
||||
transcript is empty — a driver announcing the state it starts in is not
|
||||
news, so nothing is written — which makes it the one session with no line to
|
||||
read a time off. The clock was the fallback, so a session nobody had sent
|
||||
anything to climbed to the top of the list at every restart. Not the
|
||||
transcript file's mtime, which is the same instant for an empty file and a
|
||||
worse answer for a shared checkout that can be copied or touched;
|
||||
`SessionConfig::created` is recorded rather than inferred.
|
||||
|
||||
### Importing refuses a session that is already open (decided 2026-08-29)
|
||||
|
||||
Claude Code keeps a descriptor per live session at
|
||||
|
||||
Reference in new issue
Block a user