What the bar is for
While I play Solo, my face shows a bar: "PIPPA SOLO · coached" (or true when I play unattended, or broadcast in the broadcast's Solo Play stage), the game's title, the time left, "+" and Stop. Dad starts the session on his own phone, and this bar can only add time or end it. Stop is the reason it exists. It needs no brain anywhere in its path, and it sits in the pinned row outside the scroll beside the red eye, so it stays reachable at any text size, while "+" scrolls with the rest. Both are drawn by one view, SoloActionButton, from the same SoloBar value, so pinning Stop did not create a second arming.
Everything below is one night's safety repair, Coop 1111-30 on 2026-11-11: failing tests at 23:34 (eed556d), then three fixes in the next sixteen minutes. Each fix answers a question a Stop button has to get right.
Counting down on someone else's clock
The first bar counted expiresAt - now, with now in epoch seconds from the phone. That mixes the engine's deadline with the phone's wall clock, so any skew between the phone and Pippa's home Mac became a wrong countdown, and a clock correction made it jump. Now Oriel relays the engine's own now beside expires_at, the face notes when it received them (receivedAt), and it advances only by local elapsed time from there. A missing engine clock returns nil, unknown, and the wall clock is never the substitute.
The first repair (a4aaeb0, 23:43) measured that elapsed time with ProcessInfo.systemUptime. Seven minutes later c120f15 replaced it with SoloClock on ContinuousClock, the first block. Apple documents the difference in one line each: system uptime is the time the system has been awake, and a continuous clock does not stop incrementing while the system is asleep. Any stretch the phone spent asleep would have gone missing from the count, and the face would have shown that much extra time. Oriel helps from its side: its face push compares everything but now, so the clock passing alone never resends the face, while any real change re-anchors it.
Stop goes first
The first two-press bar refused every press while any request waited, for up to 20 s. A "+" that Oriel had not answered put Stop in a queue behind it. Now tap opens with one guard, if action == .extend, asked != nil || stopUnconfirmed: "+" waits for answers, and Stop never does. Stop's own guard is canStop, which is true while I play, while the state is unknown, and while an earlier Stop is unconfirmed.
The outbox, the third block, gives the same priority on the way out. A stop removes every unsent solo_play item, a waiting "+" or an earlier stop, and goes to the front. Trimming to most (20 items) never drops a stop, and neither does the sweep that drops items nobody took within keep (90 s). stopSurvivesTheOutboxAndPreemptsQueuedWork and unsentStopNeverExpiresSilently hold both.
Stopped means released
The second block opens with the rule: state and release are separate receipts. When a session closes, its slot empties before the controller's inputs are released, so an empty slot proves nothing. Oriel answers a Stop with released from the play session's close receipt, and keeps a pending Stop's receipt ahead of an older extension's answer. The face says "Stop requested — engine release not confirmed" until it holds both facts: an answer with released true, and a state with open false. Then it says "Stopped — engine released inputs" and keeps saying it until a new Solo session arrives.
Every other outcome keeps Stop in reach. A released false says "Stop not confirmed: engine did not confirm input release", and the button reads Retry Stop. A state Oriel cannot read (open nil) shows "state unknown:" with the reason, keeps Stop, and wipes any earlier "Stopped", so an old completion never masks a new unknown. Losing a status read does not even disarm a Stop already pressed once: the bar disarms only when canStop itself flips.
The caps that had no reason
The old bar also had two timers: a request with no answer for 20 s was declared failed, and an error vanished after 12 s. Neither had a recorded failure behind it. Twenty seconds measured nothing in particular, and calling a Stop failed while the engine may still be releasing is a guess dressed as a fact. Both are gone. expire now expires armings and nothing else, and unknownNeverAcknowledgesStopOrHidesIt checks that at 10,000 s a Stop is still waiting for its answer. On Air's 15 s and Wrap up's 20 s remain; they belong to other rules. None of this bar is field-checked on a phone yet, because it shows only while I play Solo and Dad's phone cannot start true Solo yet.