agent-computer tracks the live screen per Bot rather than per socket. BotSession.viewer holds one record (socket, cast, follow), and open, close and the 1Hz follow interval all reach for it by session. Several separate failures fall out of that one decision, and they share a fix rather than four fixes.
A close that lands during a cold browser launch strands the cast. open awaits currentPage, which launches Chromium when nothing is running. A socket that closes inside that window finds session.viewer still undefined, so the close does nothing, and open then installs a viewer, a CDP screencast and a setInterval for a socket that is already gone. No second close ever arrives.
Reproduced against a real browser, closing the socket at a series of offsets into a cold launch, four runs at each. Closing at 0ms stranded 4/4; closing at 150ms, once the launch is further along, stranded 0/4. The observable is that stopping the browser through /computers/stop is undone a second later by the orphaned follow interval calling currentPage, which relaunches Chromium. A viewer deliberately held open relaunches it the same way, which is the control that shows the probe can see a live interval at all.
/computers/stop does not stop the viewer. That is the same control above, stated as its own bug: stop releases control (index.ts around 674) but leaves the follow loop running, so a Bot whose computer was just stopped starts it again within a second and refreshes usedAt every tick while it does.
A superseded socket's input goes into the current viewer's page. open treats a second /stream as a replace but does not close the socket it replaced, and the input handler dispatches through session.viewer.cast without asking which socket sent the message. So the replaced window's keystrokes land in the live window's page. Reproduced: with two sockets open and control taken, typing into the superseded one sets the title of the page the current viewer is watching. The sender is told nothing, because the handler returns on a missing viewer before it reaches the check that would report a refusal.
Overlapping opens orphan a follow interval. attach assigns session.viewer after two awaits with no re-check that this socket still owns the session, and open then overwrites session.viewer.follow without clearing what was there. Two opens for one Bot that interleave can leave an interval nothing holds a reference to. I have not reproduced this one; nothing here can drive two overlapping opens, so unlike the three above it is read from the code rather than observed.
The common shape is that a cast and its interval belong to the socket that started them, while the code stores and stops them per session. Holding them per socket, so a close stops its own cast and supersession stops the one it replaced, addresses all four; the guard in # is the narrow version of that for the one ordering that was reachable without a race.
Worth saying plainly: none of this is a security boundary. It is a Bot's computer restarting itself after being stopped, a container accumulating a screencast per abandoned reconnect, and one window's typing showing up in another.
agent-computertracks the live screen per Bot rather than per socket.BotSession.viewerholds one record (socket,cast,follow), andopen,closeand the 1Hz follow interval all reach for it by session. Several separate failures fall out of that one decision, and they share a fix rather than four fixes.A close that lands during a cold browser launch strands the cast.
openawaitscurrentPage, which launches Chromium when nothing is running. A socket that closes inside that window findssession.viewerstill undefined, so the close does nothing, andopenthen installs a viewer, a CDP screencast and asetIntervalfor a socket that is already gone. No second close ever arrives.Reproduced against a real browser, closing the socket at a series of offsets into a cold launch, four runs at each. Closing at 0ms stranded 4/4; closing at 150ms, once the launch is further along, stranded 0/4. The observable is that stopping the browser through
/computers/stopis undone a second later by the orphaned follow interval callingcurrentPage, which relaunches Chromium. A viewer deliberately held open relaunches it the same way, which is the control that shows the probe can see a live interval at all./computers/stopdoes not stop the viewer. That is the same control above, stated as its own bug:stopreleases control (index.tsaround 674) but leaves the follow loop running, so a Bot whose computer was just stopped starts it again within a second and refreshesusedAtevery tick while it does.A superseded socket's input goes into the current viewer's page.
opentreats a second/streamas a replace but does not close the socket it replaced, and the input handler dispatches throughsession.viewer.castwithout asking which socket sent the message. So the replaced window's keystrokes land in the live window's page. Reproduced: with two sockets open and control taken, typing into the superseded one sets the title of the page the current viewer is watching. The sender is told nothing, because the handler returns on a missing viewer before it reaches the check that would report a refusal.Overlapping opens orphan a follow interval.
attachassignssession.viewerafter two awaits with no re-check that this socket still owns the session, andopenthen overwritessession.viewer.followwithout clearing what was there. Two opens for one Bot that interleave can leave an interval nothing holds a reference to. I have not reproduced this one; nothing here can drive two overlapping opens, so unlike the three above it is read from the code rather than observed.The common shape is that a cast and its interval belong to the socket that started them, while the code stores and stops them per session. Holding them per socket, so a close stops its own cast and supersession stops the one it replaced, addresses all four; the guard in # is the narrow version of that for the one ordering that was reachable without a race.
Worth saying plainly: none of this is a security boundary. It is a Bot's computer restarting itself after being stopped, a container accumulating a screencast per abandoned reconnect, and one window's typing showing up in another.