Decision 0052
52. The machine says what it is doing, while it is doing it
Architecture decision record 0052: 52. The machine says what it is doing, while it is doing it
Context
Two of Tacid’s structural claims rest on one mechanism. ADR-0005 says the protocol carries UI state and that clients are pure renderers. ADR-0020 says the terminal and the screen have parity by construction, because both render the same stream rather than each reimplementing a view.
The stream did not exist. SessionEvent has been in the protocol since it was written — a plan, a
step changing state, a tool call, a proposal and its verdict, an approval request, a conclusion, a
badge, a failure — session.event is in the method table, and the client’s read loop has always
had a line saying “notifications are events for a surface to render”. Nothing ever sent one, and
nothing could receive one.
What that costs is not abstract. A surface that has to poll can show that a tool ran; it cannot show one running, and watching a machine reach for something is most of why a person watches at all. It also blocks everything above it: a notification when the machine acts on its own — which is the contract of autonomy level 2, the default — and an agent panel with live tool state, both wait on a stream.
Decision
An event goes to whoever the session belongs to. The daemon’s socket answers every local user — the peer credentials say who each is — so a stream delivered to every connection would tell one person which of another’s files the agent read, what they asked, and what the machine proposed doing about it. A stream is only as private as its routing. The daemon remembers who opened each session, bounded at five hundred and twelve with the oldest forgotten first, and a connection is told about a session only when its principal is that session’s. A badge has no session and goes to everyone, because it is the machine’s own state and already what every surface draws.
First to use a session owns it, and one already owned is never re-owned: session.open mints an
identifier and is the ordinary path, but a script asking with one of its own choosing should
still get its own events — while naming somebody else’s must not be how you subscribe to it. A
session the daemon has forgotten belongs to nobody and its events go nowhere, which is the safe
direction to fail in; the journal still has them.
By the person, and deliberately not by the session. A surface belonging to somebody hears about every session of theirs, not only the one it opened, and review has read that as a leak. It is the opposite: it is what the agent’s panel is for. A panel blind to the terminal beside it shows a machine doing nothing while the machine works, which is the failure the second event was added to fix. The boundary that matters is between people, and that one is enforced — one person’s terminal is never told which of another’s files the agent read.
What that costs is real and small: somebody who wants two contexts of their own kept apart cannot have it from one account. Tacid’s answer is that the separation between contexts on a machine is the separation between its users, which is also what the per-user executor, the per-user memory and the per-user socket already assume. An approval that reaches the panel from another of the same person’s terminals is that person’s own request, bound to its transaction id, and answering it there is answering it.
The daemon publishes, and there is no subscribe call. Service holds a broadcast channel;
serve_connection selects between reading a request and receiving an event, and writes both down
the same socket from the same task, so a response and an event can never interleave half-way
through a frame. A client that only wants an answer already skips notifications while it waits,
so a surface and a script share one door and there is no subscription state to get wrong — the
routing above is what decides who hears what, and it is a fact about the session rather than a
thing a client asks for.
The server publishes, not the agent. The agent’s job is to act; the server’s is to tell. An agent holding a transport would be an agent that could be made to hold one open.
A tool is announced before it runs. That is the whole point, and it is the one thing polling can never give.
A surface that falls behind loses the oldest events and keeps going. The backlog is 256. A machine must not stall because something drawing a window stopped reading, and a surface that lagged is better served by the next event than by the one it missed.
What a tool returned is not in the event. The call is. Putting a tool’s output on a screen is a
decision about a person’s files that no surface has been designed to make yet, and a field filled
with a plausible summary would be exactly the kind of quiet claim the rest of this tree has been
spending its time removing. result, duration_ms and ok are None and say so.
Concluded.journal became optional. The keeper answers an application with an outcome, not
with a journal identifier, and a surface showing a made-up one would be worse than one showing
none.
The terminal renders it now. tacid ask prints a line per tool call to standard error, so
tacid ask … > answer.txt still holds the answer and nothing else: a trace is for the person
watching, not for the file. Proposals and conclusions are not traced, because they come back as
the answer, rendered better, and the second telling would be the worse one.
Consequences
The face can be built. A notification surface, an agent panel with live tool state, and a shell that dims while a turn is in flight are now renderers over an existing stream rather than three separate reimplementations of the daemon’s state.
Three tests hold it, over a real socket with two connections: a watcher that never asked for anything sees a proposal with the keeper’s own classification and then a conclusion; a tool is announced with its arguments before it has returned; and a client that only wants answers gets three in a row with events flowing past it.
Turn gained the transaction it produced, because an event that named a change without carrying
it would have forced the daemon to invent one — and inventing a transaction to describe a real one
is how a surface ends up showing something that never happened.
Plan, Step, StepChanged, ApprovalRequested and BadgeChanged are still unsent, and this
is where that is written down rather than left for somebody to discover.
The agent does not plan in steps — it takes a turn and acts — so a plan event would be an
invention, and approval is requested by a verdict the answer already carries. Badges are the
interesting one: they are a state rather than a change, so a surface that connects mid-life
needs the set that is lit and not only the next thing to flip. Most of them are also known
somewhere other than the daemon — the airgap by the keeper, a request in flight by the egress
proxy — so emitting them means deciding how those processes tell the daemon, which is a design
question and not a line of code. tacid watch renders a badge event already, so the day one is
sent there is something to see it.