`ended` and `failed` say how a run finished but not why: a stop its owner
asked for, a free weekend running out, an idle box and a game quitting on
its own all left the same row behind. A session now carries `end_reason`,
one word from a fixed list, written by whoever ends the run, and
`time_claimed`, stamped when a host takes the run, so bring-up can be
measured from the claim rather than from the request.
The control plane writes the two endings it decides itself: `user` when
the owner stops a run nobody had claimed yet, and `left_queue` when a
waiting run is abandoned. The write keeps a reason only on a terminal
state, the same way an error message is kept only for a failure, so no
row can say it ended while it is still going. Both fields are returned
wherever a session is.
The column is text rather than a database enum so a reason added later
is a code change and not a migration.
A host holds only so many runs at once, and until now every waiting run
was handed to it regardless; the ones it could not hold failed there.
The host now says on its heartbeat how many runs it holds, and is handed
only as many waiting runs as it has free. The rest wait in line: paid
runs before free-weekend runs, then first come first served. Reading a
waiting run returns its place in line, and says when a paid run that
asked later has gone ahead, so a free player is never reordered quietly.
A waiting run nobody has asked after for a minute was abandoned, and
leaves the line rather than holding up everyone behind it. A host that
does not report its slots is handed every run, as before.