Repository navigation
[REA-6854] Apply a session's starting input, sent by a system client - #233
Conversation
tempusfrangit
left a comment
There was a problem hiding this comment.
Requesting changes because generation can start before the starting input is applied. With a delayed starting upload and steps: 1 in the full stack, I reproduced generation on default state followed by session closure before the upload command ran. Please gate generation until setup is applied and add a delayed-upload regression test.
3061225 to
2bb381b
Compare
|
@tempusfrangit thanks, reproduced on |
0f92c35 to
63884e1
Compare
A session that no client drives gets its setup on the start request. Each key of starting_input.state becomes the set_<key> command the model's state generates, followed by starting_input.commands, and the runtime submits them right after the session starts, on the same path a client's commands take: contract validation, upload resolution, the journal, the handler. The commands are sent by a system client, a connection the runtime opens itself on connection id 0 for a session that has a starting input or a step count. A handler therefore addresses a real client, whose ClientInfo.system is true, and while it is connected the session has an audience, so a session with a step count generates with no viewer and no change to the model's code. It carries no media and drops anything sent to it. It leaves once the starting input is submitted, unless the session has a step count; then it stays until the session ends. Its connection moves carry "system": true on the journal and stay out of the runtime's connection metrics. A rejected starting command is journalled as an error and the rest still run. A command that names an upload waits up to the orphan timeout for its bytes. Client commands wait until the list is submitted, the list runs once per session, and it stops if the session ends. The descriptor reports how many commands the list applies. The step loop takes its first step only once the starting input has landed. SessionStarted says whether a starting input follows, and the runner posts StartingInputApplied after the last starting command, on the same ordered queue, once any upload it names has resolved or timed out. Until then the loop's gate stays shut, though connected is set, so no step runs on the defaults the starting input replaces and a session cannot reach its steps before its setup arrives. In a session without a step count the system client leaves first, so a live session waits for a viewer as it always has. ReactorPipeline's loop waits on the same gate; a model with its own run() is unaffected. Signed-off-by: Dere-Wah <derexcontact@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
…put applies The session state is the runtime's occupancy signal: streaming means a client is being served. In a session without steps the system client only sends the starting commands, so its open and close are self-loops on the current state and leave the live-connection count alone. The session waits for a client of its own, and its orphan timer keeps running from the start. In a session with steps the system client is the session's only client, so it still occupies the session and holds it streaming until the session ends. Signed-off-by: Dere-Wah <derexcontact@gmail.com>
A start now refuses a starting command whose arguments fail the contract, so the commands a session applies have passed that check. What can still fail as the list runs is what only running a command shows, such as an upload whose bytes never arrive; that command is journalled as an error and the rest still run, as before. The tests follow: the failure case is an unresolvable upload, and the system-client contract test applies only valid commands. Signed-off-by: Dere-Wah <derexcontact@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
A starting command is submitted on the system client's connection id, so every InboundCommand names the connection it arrived on. The field goes back to ConnId, and its docstring says a starting command arrives on the system client's connection. Signed-off-by: Dere-Wah <derexcontact@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
6ca74ab to
37d36e1
Compare

Why
A caller that starts a session without a client still needs the model set up: the prompt, the seed, an image to start from. Those arrive as
starting_inputon the start request, and they should go through the same path a client's commands take, so validation, upload resolution, and the journal behave the same and the model needs no change.Two things stood in the way. A handler that takes
clienthad no client to receive. And a model only generates while someone is connected: the default step loop waits for a client, and hand-writtenrun()loops gate onself.connected. Both are solved by giving such a session a client the runtime owns.That client must not change what the session state means.
streamingis the runtime's occupancy signal: it says a client is being served, and anything that reads/eventsuses the state pair on each connection move to tell when a session gained or lost its clients. A live session is not served while the runtime applies its starting input, so the runtime's own client must not make it look occupied.What Changed
The starting input.
StartingInput.as_commands()turns eachstatekey into theset_<key>command the model's state generates, followed bycommandsin order. Right after the session starts, the runner submits them one at a time through_submit_command. Each command's arguments were already checked against the contract when the session started (#232), so what can still fail here is what only running a command shows, such as an upload whose bytes never arrive: that command is journalled as an error and the rest still run. A command that names an upload waits up to the orphan timeout for its bytes, since the caller can only seed them once the session exists. Client commands wait until the list is submitted, the list runs once per session, and it stops if the session ends. The descriptor reports"starting_input": {"applied": N}, the number of commands the list expanded to.The system client.
runner/system_client.pyaddsSystemConnection, a connection with no wire on connection id0, which no transport mints. It advertises no media, so the media fan-out skips it and a model is never held to a playout rate nobody is watching. Anything sent to it is dropped. The runner registers it for a session that has a starting input orsteps, right after the start, so the model seesSessionStarted, thenClientConnected(system=True), then the starting commands, which it sends. It leaves once the list is submitted, unless the session hassteps; then it stays until teardown. Because it is a real connection,self.connectedis set while it is there, so existing models generate with no change.The system client occupies only a session with
steps. In a session withstepsit is the session's only client, so it counts like any other connection: the session streams, and the orphan timeout cannot close it before its steps are done. In a session withoutstepsit only sends the starting commands, so it does not count.SessionStateMachinegains a second entry point for that case. It applies a connection open or close as a self-loop on the current state and leaves the live count alone:ConnectionManager.register(conn, system=True, occupies=False)records the connection as not occupying the session, so its laterdrop()closes it the same way it opened. A client that joins while the system client is still applying the list opens the session as usual (waiting -> streaming), the system client's close is then astreaming -> streamingself-loop, and the client's own close leaves the session orphaned. Because a session withoutstepsstayswaiting, its orphan timer keeps running from the start, as it does for any session no client has joined yet.The journal for a live session with a starting input:
And for a session with
steps, where the system client holds the session until it ends:A session with
stepsanswers/start_sessionwith"state": "streaming", because the system client has already connected. A session with only a starting input, and an older body with neither key, answer"waiting".The first step waits for the starting input. Without a viewer the system client paces nothing, so the default loop steps as fast as the model renders, and it began stepping the moment the system client connected, ahead of the starting commands. On
examples/starter, a session withsteps: 40finished every step and closed in the 200 ms before the caller seeded the image its starting input named; the upload was then refused and the command rejected. SoSessionStartednow says whether a starting input follows, and the runner posts an internalStartingInputAppliedafter the last starting command, on the same ordered queue, once any upload it names has resolved or timed out. Until it arrives the loop's gate stays shut, thoughself.connectedis set and starting handlers still receive the system client asclient.ReactorPipeline's loop waits on the same gate; a model with its ownrun()is unaffected. In a session withoutstepsthe system client leaves before the event, so a live session still takes its first step only once a viewer joins.SessionStarted.starting_inputdefaults toFalse, so anything else that posts it keeps today's behaviour.Rerun on the starter with the fix: no step ran before the image arrived, every saved step showed the uploaded image, and a step's state carried the starting
spin_speedrather than the default.The system client's connection moves carry
"system": truein the journal detail; other connections' detail is unchanged. The runtime's own metrics leave it out: it does not count in the connection series or inruntime_session_time_to_first_client_seconds. A model that needs to tell it apart reads the newClientInfo.system, whichClientConnectedcarries.Checked against
examples/starter: a startingstateof{spin_speed: 2.0, paused: true}and aset_static_intervalof4left the model's live state at those values before any client connected, a999was rejected by the field'sle=300and journalled, and the next session started from the defaults.