Let somebody talk to a Bot that is already working (#16)
* Let somebody talk to a Bot that is already working Typing into a channel while the Bot had the turn did nothing. The composer took the keystrokes and refused the send, so a person watching their coworker head off in the wrong direction had two ways out: stop the turn and lose whatever it had done, or wait for it to finish being wrong. Neither is what they wanted, which was to say "no, the other one" while it was working and have that land. A message typed mid-turn is now parked instead of dropped. It appears in the transcript straight away as their own bubble, faded, saying Queued underneath, with a Remove next to it; and when the turn ends everything parked runs as one follow-up turn with the lines joined by newlines. A burst of three corrections costs one turn, not three. The drain is keyed on the turn being over and never asks how it ended, which is what makes Stop a way of steering rather than a way of giving up: park a correction, press Stop, and the correction is what runs next. There is no stop path in the code to forget about. The rule is one pure reducer in composer/queue.ts and is tested as one. The state is held by ConversationView, which is the nearest thing that owns both the composer that parks a message and the transcript that has to show it. Its docblock says plainly what the state is worth: memory in one tab, gone on reload, not an outbox. The affordance is drawn only while a turn is in flight, so a reload finds no queue and shows none rather than promising to send words it will never send. The compose screen does not get this. It creates the channel on send and then navigates away, so anything parked there would go down with the unmount, which is worse than a send button that visibly will not go. Queueing is therefore an opt-in prop and only the channel view asks for it. * Stop letting the run decide when the turn is over The queue drained in the middle of answers. It waits for the turn to end and read the end of the turn off the agent, and the agent does not know: it reports the run on the wire, and a turn that touches the browser is several runs in a row. The Bot asks for a click, the run ends so the browser can answer it, and another run starts carrying the answer. In every one of those gaps the agent says it is idle. OpenBot registers every computer tool as a frontend tool, so the gaps open on ordinary work. The view patched half of it by also watching its own send, which covered turns typed into the composer and nothing else. A channel starts turns by two other routes — the first message of a new channel, and a button inside a rendered component — and for those the queue drained on the Bot's first browser action. That posted a correction as a second turn while the first was still going, two runs racing on one thread, and took the unanswered tool call along with it: the history repair stitches a fabricated "produced no result" over a call that is still executing and about to produce a real one, which is two results for one call, which providers refuse. So the fact is now kept where every turn passes through it. `say` is the one funnel in the channel, and it counts what it is holding: a turn from the moment somebody asks for it until the whole thing has come back, browser actions included. That is what the composer is told, and what the queue waits for. Stop is counted separately, and separately on purpose. It reaches a run through the core's abort controller, and that controller does not exist until `say` has finished waiting for the runtime agent — as much as a second and a half on a channel that is still joining. Drawn from the turn, a Stop button appeared in that window, aborted nothing, and let the message go anyway; the one control the whole affordance leans on was quietly lying. It is drawn from the run instead, and the composer takes the two facts as two props. Three smaller things came out of the same reading. A queue no longer drains into a conversation that has been disabled, because a coworker deleted mid-turn takes the channel with it and one more user turn posted into it helps nobody; the cost is that parked words stay on screen unrun, under the notice that says why. Each parked line's Remove button now names the sentence it would delete, so three of them in a row are three different buttons to somebody reading by name. And the reducer no longer leans on an argument about two components' timing to rule out an idle send meeting a non-empty queue: it runs them in the order they were typed instead of letting the new one jump the line.
J
Jerel Velarde committed
f1670fd53f6e07c9ff30fd3e241b346b62da9fcd
Parent: 93e357f
Committed by GitHub <noreply@github.com>
on 8/19/2026, 11:48:44 PM