events_url | string | Use this exact URL, including its origin and embedded controlling desktop credential, in a WebSocket client. A REST Bearer API key does not authenticate this socket; Bearer-only requests receive JSON 400 guidance naming events_url, equally for unknown IDs. Watch-only credentials cannot open it. A websocket that streams what this computer does, without being asked — so an agent can wait for something to happen instead of paying for a screenshot to find out that nothing has. Text frames of JSON, one event each; nothing is ever sent to it. The first frame is {"type":"hello", ...} and lists the event types this computer can emit — not everything the platform knows how to emit. A guest with nowhere to run a watcher (a Windows one, or a Linux one whose hardware carries no terminal channel) never produces the guest-reported half, and events says so rather than leaving you waiting for something that cannot arrive. The two halves of that are not the same requirement: file.changed needs only the terminal channel, so it is available on every Linux computer that has one — including images too old to carry the X bindings, which emit no window events at all. A guest that turns out not to be able to run one — an image built without the X bindings the watcher needs — is not always known at that first frame, and when the answer arrives afterwards a {"type":"capabilities", "events": [...], "detail": "..."} frame revises the list. It goes both ways: a computer stopped and started under an open socket can ACQUIRE the channel its watcher runs over, and the same frame says so. Treat it as replacing what hello advertised: the rest of the stream carries on. Ignore any frame whose type you do not recognise; the vocabulary grows. Nominate a tree with &watch= and hello carries watching: the paths this stream will report file changes under, as this host has NORMALISED them. A trailing slash and a . segment are accepted and cleaned away, and the cleaned form is what every file.changed carries in watch — so match on what hello gives back rather than on what you sent. The field is absent when you nominated nothing, which means no file.changed can arrive at all; a path this host cannot honour is a 400 on the upgrade rather than a socket that opens and then says nothing. Each entry is {path, armed} — path as this host normalised it, and armed for whether that tree is ALREADY being watched. "armed": false means not live yet: wait for its {watch, armed: true} before reading silence as "nothing has changed". "armed": true means live now, and no event is coming to tell you so — somebody else nominated it first, the guest answers a nomination once, and this field is how you are told. The same split as ready above: state in hello, transitions on the stream. A computer watches at most 32 distinct trees across every stream open on it. A nomination that would take it past that is a 409 carrying reason: "unavailable" and naming the limit; close another stream, or nominate paths it is already watching, which cost nothing extra. Connect without &since= and hello also carries windows — the desktop as this host last saw it, each entry the same shape GET /computers/{id}/windows returns. It is what you are joining rather than what has just happened, so a client attached to a machine somebody else is already using starts with the screen it is looking at, and every window.closed after it names something you have been told about. The field is present and empty when nothing is open, and ABSENT when you resume from a cursor that could be honoured, because you already hold those windows — absent and empty are different answers, so test for the field rather than for its length. A gap counts as no continuity, so a gapped reconnect carries it too. It rides in hello rather than arriving as a frame after it so that it cannot come apart from the cursor that implies it: a client which stored the cursor and dropped before a second frame would resume with continuity it had, over a desktop it did not. Last SEEN, not guaranteed live. A window whose close happened while this host had lost its link to the guest is reported into a dead pipe and stays in this picture, so an entry here can name a window that is already gone. GET /computers/{id}/windows asks the machine and is the authority on the present; this is what makes a later window.closed correlatable, and what to reconcile against that listing if it matters. hello also carries ready, and it is the difference between waiting and waiting forever. computer.ready is announced once per desktop SESSION, so if the desktop was already up when you attached — somebody else got there first, or you reconnected — the event has happened for that session and will not happen again for it. "ready": true means it already has; wait for the event only when it is false.
Per session, not per running period, and the difference is a real one: restarting the display manager inside a guest destroys the desktop and brings up a new one without the computer ever leaving running. That is a new session, so computer.ready fires again and the windows you were told about belong to the desktop that is gone. Treat a second computer.ready as what it is — a desktop you have not seen before — rather than as a duplicate to discard. Mind the ORDER when you do, because the replacement announces itself before it says it is ready: the new desktop’s windows arrive as window.opened FIRST, and computer.ready comes after them. The windows of the desktop that is gone are not closed one by one — there is nothing left to report their closes — so a client that empties its map when the second computer.ready arrives throws away the openings it was just handed, and is left describing an empty screen that has windows on it. The stream does not mark where the replacement begins. Nothing between the last event of the old desktop and the first window.opened of the new one distinguishes them, so there is no rule over the openings you have already received that separates the two — an opening a moment before the replacement and an opening a moment after it look the same on the wire. Do not try to sort them. When a second computer.ready tells you the desktop was replaced, ask GET /computers/{id}/windows: it asks the machine, and it is the authority on the present here as everywhere else on this stream. Keep serving your existing map until the answer comes back — it is stale in a known way, which is better than empty. After that each event carries type, at, computer, seq, cursor, a source of daemon or guest, and a data object whose shape is the type’s own: - window.opened — a window EXISTS, which is not always the same as it having just appeared: a watcher starting on a desktop announces what is already open, so the first client to connect after a computer boots is told about the panel and the desktop as openings. What is already there when YOU attach comes in hello’s windows instead. The listing stays the authority on the present — it asks the guest — and this stream is the authority on changes.
- window.opened / window.focused — data is a window, the same shape GET /computers/{id}/windows returns, with the same fields, and the same meanings for all but one. Its position and size are as they were at that event: moving or resizing a window does not itself produce an event, so read the listing if you need a window’s geometry right now.
The exception is type, which this stream reports more LITERALLY than the listing does — in two ways, both set out in the field’s own description. As everywhere else on this stream, the listing is the authority when the difference matters.
- window.blurred — data is {id} and nothing else: the window that HAD the keyboard, reported when focus leaves it and no window at all takes it. Read that literally, because on a desktop that draws its own background — XFCE, which is what the Linux templates run — clicking the background or minimising the last window gives the keyboard to the desktop window itself, and what you get is an ordinary window.focused naming a window whose type is desktop. That is the common case and it is not a blur. A blur is for when the machine genuinely reports no active window, which a bare window manager and some lock screens do. Without it window.focused could only ever move focus and never retract it, so a desktop with nothing focused went on naming whichever window last held it. The window is otherwise unchanged, so it is named rather than described; it is still open and you will still get its window.closed. You are only sent this for a window you were told about, and only when something was focused — a desktop that already had nothing focused sends none.
It CAN reach you twice with no window.focused between, and that is not a bug to work around: if the link to the guest drops and comes back, the desktop is re-described to this host rather than replayed to you, so focus that returned while the link was down is not an event you were sent. Treat a blur as the idempotent statement it is — nothing has the keyboard now — rather than as a transition from a window you were told was focused. If you need to be certain which window has it, GET /computers/{id}/windows asks the machine.
- window.closed — data is {id} and nothing else. The window is gone, so there is no position or size to report and none is invented; match the id against a window you were told about earlier — hello’s windows, or a window.opened or window.focused this connection was sent. You are only sent closes for those, so an id you do not recognise is not a close whose opening you missed.
Gone from the DESKTOP, with one exception that only a desktop of more than 200 windows can reach. This stream describes at most 200 windows plus the one that has the keyboard: a window.focused can therefore name a window you were not told about, and it carries the whole window so that you need nothing earlier to place it. When focus then moves to ANOTHER window outside those 200, you are sent window.closed for the previous one before the window.focused that names the new one — and that window is still on the screen. Below 200 windows a close means what it says; on a desktop that size, read it as "this stream has stopped describing that window". Nothing on this platform will tell you more: GET /computers/{id}/windows refuses a desktop of more than 200 windows rather than enumerate part of it, so the listing cannot settle whether that window is still there, and this reference does not pretend otherwise. Said here because the alternative was worse: a focus onto a window outside the picture used to be dropped, so on a desktop that size every subscriber was told the keyboard had stayed wherever it was when the machine was first watched.
- process.exited — data is {execution_id, pid, exit_code} for an observed exit of a command started with background: true. Match execution_id to the ID returned in the 202 response: a PID can be reused, but the execution ID never resolves to a replacement command. Read daemon-only metadata at GET /computers/{id}/executions/{executionId} and independent guest output at GET /computers/{id}/executions/{executionId}/output?stdout_offset=0&stderr_offset=0. Each reader owns its offsets; neither route automatically resumes the computer. The legacy GET /computers/{id}/exec/{pid} consumes shared output and can address a newer command after PID reuse. The lost variant is {execution_id, pid, lost: true}, with no exit_code: the guest agent no longer knows about the command, as can happen after a guest restart. This host cannot say whether it finished, so no outcome is invented. Metadata may briefly report lost before the handle is removed; output is unavailable, and removed or expired IDs return 404. The event tells a waiting caller to stop waiting. Guest output remains volatile and mutable, not suitable for passive Activities/history. This event transport is unchanged: connecting can start a guest watcher/broker even without nominated file watches, so it is not a passive history transport either.
- clipboard.changed — data is {selection}, either clipboard or primary. The contents are deliberately not here; read them at GET /computers/{id}/clipboard if you want them.
- file.changed — something changed under a directory YOU nominated. This is the one event type that never arrives unasked: pass &watch=<absolute path> when you connect (repeat it for up to four trees) and you are sent changes under those and nothing else. Without it, no file.changed can reach this socket at all. data is {watch, path, kind, dir} — watch is the tree you nominated, path is the absolute path that changed and is always inside it, kind is created, modified or deleted, and dir is present when the thing that changed is a directory.
The tree is watched all the way down, and directories created inside it are picked up as they appear. Nothing is announced about what is ALREADY in the tree when you nominate it — those are not changes, and listing the directory yourself is the answer to "what is in there now". A rename inside the tree arrives as a deleted for the old path and a created for the new one rather than as a move: inotify reports the two ends separately and one of them is often outside the tree, so each event is true about the path it names.
Writes are coalesced, which is why this is usable at all: an editor save is several kernel events and a compiler writing one object file is more, so changes to one path inside a short window arrive as one event. A file created and then written reads as created; one written and then removed reads as deleted. What you get is the truth about that path when the window closed, not a transcript of every write.
Wait for {watch, armed: true} before you act on silence. Arming is not instant: the nomination is accepted the moment you connect, but the guest has to be asked, and on a computer nobody has opened a terminal on this host has to install the watcher into the guest first — seconds, not milliseconds. inotify reports changes and not state, so anything that happens to the tree before the watch is armed is never reported and never will be. armed is what closes that: until it arrives, silence means "not watching yet". It arrives again after anything that re-arms the watch — a stop and a start, a guest reboot, a broker replaced — and a second one means what the first did: reporting starts HERE, so re-read the tree if what happened during the interruption matters. computer.ready says the same thing about a desktop session.
One shape carries no path and no kind, and a lost instead: {watch, lost} says the stream under that tree is incomplete, and treat any non-empty lost as "my picture of this tree is wrong". "flood" means the tree changed faster than the cap allows it to be reported — transient, so re-read the tree and keep listening; a build under a watched path costs you one of these rather than thousands of events. "budget" means the tree is bigger than the directory budget one watch gets, so part of it is not being watched at all — permanent for this watch, and the fix is a narrower path. "unwatchable" means the directory is not there yet, is not a directory, cannot be read, or is a SYMLINK — those are refused rather than followed, because inotify pins whatever the link resolved to when the watch was added and repointing it afterwards produces no event at all, so a followed link would report one tree under another tree’s name; nominate the real path. This reason recovers on its own where it can — nominating the directory a job is about to create is a supported thing to do, and the watch starts by itself when it appears. That recovery is announced by {watch, armed: true} and by nothing else: there is no synthetic event for the directory’s own creation.
Nominate the narrowest tree you can. A whole home directory under a build is thousands of changes a second, and the replay history this stream keeps is per COMPUTER and shared with every other subscriber to it — so a broad watch spends the history that a client resuming with &since= needs. The cap above is what stops that being unbounded, but a watch that is permanently flooding tells you very little either way.
- computer.ready — the guest’s desktop session is up and accepting input. This is the one to wait for after creating a computer, in place of screenshotting until something appears.
- computer.idle — nobody has touched this machine for its whole idle window. data carries idle_seconds. Listening is not using: holding this socket open is not activity, or the event could never fire — and it does not hold off an automatic suspend either, so on a computer configured to suspend when idle expect computer.suspended to follow. Setting idle_suspend_min: 0 stops the suspend without stopping the event, subject to the plan’s never-suspend limit.
- computer.started / computer.stopped / computer.suspended — data carries status, and previous where there was one. It is absent on the first transition a host reports for a computer after the daemon restarts, which has no earlier status to have moved from — so read it as optional rather than assuming a string is always there. Reconnecting. Every event has an opaque cursor, and so does the opening hello — that one is where the stream is at the moment you attach, to store if you disconnect before seeing an event. Pass the last one you have as &since=<cursor> and you get what you missed. If this host can no longer replay that far you get a {"type":"gap"} frame instead, naming the oldest cursor it still holds — which is your signal to reconcile with a listing rather than assume nothing happened. It carries no seq: a gap is a statement about the stream rather than a position in it, so a client that skips anything not newer than the last sequence it saw must not skip this. A cursor from before a daemon restart always reads as a gap, which is correct: the numbering it belongs to is gone. When this host ends the stream it says so first, with {"type":"closed", "detail": "..."}, and the sentence is the difference between a socket worth reopening at once and one that is not. A subscriber that stopped reading for long enough is put down deliberately rather than quietly skipped — dropping events into the floor would be a second, silent way to miss one — so reconnect with the last cursor you hold and you get what you missed, or an honest gap. A computer this host no longer holds says so instead, and that one is not worth retrying against the same place. A socket that simply dies carries no such frame, which is how you tell a network from a decision. source is worth reading. daemon means this platform observed it. guest means the machine reported it about itself — every window.* event, clipboard.changed, file.changed and computer.ready — and anyone with root inside the guest can make those say anything. They are your machine describing itself, which is exactly as much as they are worth.
Carries the same controlling credential as url; absent for a viewer, because a window title is content and a watch-only credential must not read it. Absent on Windows guests. A suspended computer is refused with 409 and resume_required: true, and a stopped one with a 409 carrying reason: "unavailable". |