One Word for Every Wait
The last lessons kept noise out of the room. This one is about the opposite failure: silence that looks like something. On 2026-10-18, using voice mode on a trip abroad, Dad noticed that between his last word and her first sound the screen said one word, "Thinking". Transcribing, uploading, the speaker check, the memory search, the model's silence and its thinking all looked the same, and a stalled step pulsed exactly like a busy one. What made him uneasy was not the wait. It was a line that did not move.
"이걸 실시간으로 뭘 하고 있는지 계속 보여주면 응답이 늦어지더라도 불안하지는 않다고."
— Dad ("If it keeps showing what it is doing in real time, I am not anxious even when the answer is slow.")
In a conversation that never ends this matters more than anywhere. A Master Chief turn can wait behind another doorway's turn, a voice check, a compaction or a long tool call, and from the outside every one of those used to look like the model being slow.
The Board
Office now publishes where each turn is (services/turn_stage.py, todo #531). It is an in-memory board with one entry per conversation, and it is deliberately not a timing system of its own. The two clocks that already split a turn for the log, [turn-prep] and [turn-provider], publish to it as they mark, so the stage a screen reads is the step the log will name. In order: received, voice_check, preparing, memory, model (asked, nothing back yet), thinking and writing (the first of each), tool with the tool's name, compacting, and done.
A screen reads it with a long poll, GET /api/chat/stage/{conversation_id} with the request id and the last sequence number it saw: office answers the moment the stage moves past that number, or after at most 25 seconds. An entry belonging to another request reads as no stage at all, so a screen never shows a neighbor's turn as its own. Each entry also records when it first entered every stage, and a finished turn stays findable by its request id among the last 64 finished turns (a done entry itself leaves the board after ten minutes), because the voice turn's timeline reads its server half from the same board: the record of a turn and the line on the screen are one source. A private conversation's entry is never logged or stored.
Compaction Is a Stage
Dad added one requirement the same evening: a slow turn must never be mistaken for a compaction, or the other way round. So every compaction publishes compacting and its end with a kind that says whose it was: before_reply (the emergency compaction in front of an ordinary reply), background (Master Chief's pass between turns), manual (Dad's own, from the chat menu), and the vendors' own inside a turn: claude, read from the status the Claude CLI reports, and codex, read from the compaction items in the Responses stream the Codex adapter consumes. A background pass never masks a turn that is already running. The conversation's metadata carries compacting, last_compacted_at and the kind. The board itself lives in memory, so after a restart the metadata falls back to the newest summary row: that keeps when our own last compaction happened, but not its original kind, and not a vendor's compaction inside a turn. The phone's composer line now always says it: a compaction running, or "Last compacted" with the time and who did it (Claude, Codex, between turns, or you).
Stalled Is a Diagnosis, Not a Timer
The first version of the line still lied in one place. Out walking on a cellular connection, two spoken turns with pasted photos read "Uploading your recording · stalled, nothing for 70s" while what was crossing the link was the photos: that build called every upload a recording, and the next one counted progress only when a whole photo landed, so a 1.5 MB photo at 40 KB/s would still have read stalled while its bytes moved. Dad's rule from that walk had three parts: no step sits as one frozen word, "stalled" is a diagnosis, and the watch shows what the phone shows.
- Evidence, not a word. Uploads report bytes and measured rate ("Uploading photo 2 of 4 · 1.1 of 3.6 MB · 40 KB/s"), naming the leg with more left to send. Thinking and writing show characters arrived. Steps that happen only on office show when office last answered the board's long poll. New evidence is progress; the same evidence is not.
- A check before the verdict. Past a step's limit the phone checks its own link, then office (the board's answer if it is under fifteen seconds old, otherwise the health route), again every fifteen seconds, and the line says what it found: "no bytes out for 47s; office answers, the cellular link is not carrying the bytes", or that office answers and the model has not started, or that the phone has no network. Orange is for trouble only.
- One wording. The watch receives the phone's own line for its turn, at most once a second while the watch app is in front, and ages it ("iPhone quiet 9s"). There is no second copy of the wording on the wrist, and the web voice screen reaches the same diagnosis in the same words.