A Second Review, Three Weeks Later
The last lesson's review asked whether Coop's activity had any effect, and found 74 wakes that changed nothing. Three weeks later a second review (todo #519) asked a different question: what does Coop cost when nothing is happening? With eight adapters following eight quiet chats, Coop's processes used 29% of a CPU core. By the end of the day they used 4.5%. The trigger was not the bill. It was Dad, on a laptop in Spain, watching Coop's work page say Loading work details… and never finish.
Nothing in the review changed what Coop decides. Every finding was a place where the system paid, again and again, for something that had not changed. Read them as one list, because they are one mistake in seven costumes.
Fifty-Five Processes per Glance
Track 4's first lesson said that reading Claude's session registry is not free: to tell a live chat from an exited one, the helper checks each registered process's birth time with ps. It ran ps once per registration. On office at the time of the review there were 55 registrations, so one registry read spawned 55 processes and took about 200 ms. And reads were not rare: every adapter read the registry at least twice per poll, the activity sweep every minute, the assignment form on every request. py-spy, a sampling profiler, found 87% of two adapters' samples inside that one function.
The fix asks ps once for every pid (ps -p a,b,c -o pid=,lstart=) and returns the same normalized strings, so nothing downstream changed. A registry read went from 193–218 ms to 36–40 ms. One detail is the kind you only learn by running it: ps refuses the whole call if one pid in the list is out of range, so the helper never asks for a pid of 0 or of 100000 and above.
A Connection Is Not Free Either
The engine opened a new SQLite connection for every read and write and closed it afterwards. That sounds harmless until you remember Track 2: the store carries 160 change and fence triggers, and opening a connection parses the whole schema. Measured: 2.2 ms per open, against 0.01 ms on a kept connection. That cost rode on every adapter poll, every chief tick (five seconds), every coordinator tick (three), the notifier (fifteen) and every page read. It also had a quieter side effect: when the last connection closed, SQLite checkpointed and deleted the write-ahead log, and a read-only opener that arrived in between failed.
Now connection() lends one from a pool that keeps at most eight open connections; a burst beyond that opens extra ones and closes them when they come back. The pool is small on purpose, because launchd gives the runtime 256 file descriptors. The rules keep the old guarantees: a nested block gets its own connection, a transaction never outlives the block that began it (it is rolled back on exit, as a close would have done), and a connection that broke is dropped instead of lent again. Track 2's fence still holds, because every pooled connection is one the engine opened and registered its functions on.
Two more costs sat beside it. The lookups made every few seconds (an adapter's own envelopes and bindings, the notifier's queue, pending reviews) scanned whole tables, and eleven indexes fixed that. And the chief runner, deciding every five seconds whether a review was due, read its role through the full review context: every pending obligation's work, reports and deliveries, 17 ms and 59 KB, on the one event loop that also serves the workspace and the adapters. Chief.role() now reads the one row it needed, and store work runs off that loop. A worker snapshot went from 3.3 ms to under 0.1 ms; the chief's context from 17.4 ms to 0.32 ms.
Polling a Chat That Will Never Answer
Track 4 said an adapter reads its chat every two seconds while a delivery is in flight. In flight was the problem. One Codex chat's adapter had polled every two seconds for six days, waiting on two reply reviews the Codex app never showed; another had polled for seventeen hours on a review of work that had finished the day before. "Something is owed" had quietly become "poll fast forever".
Now the fast pace is earned by the chat, not by the queue: two seconds while the chat shows life (a turn running, or a native write in the last two minutes), fifteen seconds when the wait is on a chat that has gone silent. The reading already carried both facts for every receiver, so a worker's answer is still picked up as quickly as before. An unanswered invitation, once looked for four times a second for up to ninety seconds per pass, is now looked for every two seconds like any other delivery. And a refused idle retirement, which had been retried and logged every fifteen seconds (one chat's refusals filled 19,747 log lines in four days), is now logged once per distinct reason and tried again ten minutes later.
Envelopes for Work That Was Over
Why was anything owed to a chat whose work had finished? Track 4's revival lesson met the first half of this: a reply review sent for a binding that later resolved, or a work envelope sent for a dispatch that later finished, could never be acknowledged, and sat as sent, receipt uncertain for ever. The first repair taught owed() to ignore such rows. The adapter still saw them as in flight, which is the six-day poll above.
The real repair is one rule in one place. _settle_finished cancels, with the receipt reason work-over, every envelope nothing can answer any more: a reply review whose binding is no longer active, and an invitation or work envelope whose dispatch resolved, was declined or was cancelled, whether it is still pending or already sent. It runs in the same transaction that ends a binding or a dispatch, and once at engine start for older rows. owed() lost its special cases, because the rows they excused no longer exist in an open state.
The same commit found the mirror image. Closing work through the control facade cancelled an assignment still in setup, but left an invitation that had already been sent; a close from the CLI withdrew nothing at all. A worker could still answer COOP_ACCEPT for closed work, and Dad later heard that his invitation "was not accepted". _withdraw_offers is now the one rule for every door that ends work: an assignment that never started is cancelled quietly, an invitation nobody accepted is cancelled and its chat told the offer is withdrawn, and a chat already being connected is left to the coordinator, which tells it why.
A Page That Never Finished Loading
From Spain, office was 302 ms away at roughly 100 to 500 KB/s over the tailnet. Every page of the workspace waited first on one read, the operations view, which by then weighed 2.8 MB: every recent outbox payload and receipt, 200 reports with their evidence, 245 sessions, 200 dispatches, 100 assignments with their setup prompts. The page refetched it every 30 seconds, on every focus and visibility change, and every three seconds while an assignment ran, and each refresh cancelled the read still in flight. Over that link the read never finished, so neither did the small detail read queued behind it.
The engine now serves views cut to what each page shows. The attention view carries only what is open or owed, bounded and without payloads, prompts or evidence: 2.8 MB became 114 KB, 30 KB gzipped. One item's assignment story is its own 3 to 10 KB read, the only thing polled every three seconds, and only while an attempt is under way. JSON responses are gzipped. And the page's reader, usePolled, never cancels a read to refresh it: a refresh asked for during a read becomes one more read after it, and the last data stays on screen meanwhile.
One regression is worth keeping as a footnote. The slim view first listed every failed notification however old, where the old one had shown terminal rows from the last seven days only. Three week-old failures reappeared, and the badge on Dad's sidebar rose from 8 to 11 with nothing new to decide. Within the hour the week's window was back. When you shrink a view, write down every filter the old one applied, including the ones nobody remembers choosing.
Receipts Nobody Could Use
The last finding is Track 2's oldest one, wearing new clothes. Every activity reading was stored with an idempotency receipt, under a key made fresh for each reading, so no receipt could ever answer a retry: 8,805 of the 14,521 rows in engine_commands when the review counted, about 1,250 a day. A reading now commits in its own transaction without one (Track 2's third lesson has the details). And the compatibility client, which had fsynced a receipt file for every call, now writes one only for writes: of the 19 receipts kept on office, 15 had been reads.